Android框架思考--界面View封装

前言

Android项目不管使用什么框架结构,承载界面的必然少不了Activity或者Fragment,而对于一个用户界面来说,有一些业务逻辑的处理是通用的,比如请求网络时需要有loading框,比如网络错误时需要界面有对应提示,比如通用的导航栏,比如每个界面都用activity就需要在Manifest.xml文件中配置等等,这些能否做一些封装,可以让开发者只关注具体界面的具体逻辑,快速实现一个界面?

思考方向

基于以上的问题,我们要封装的内容需要满足以下需求:

  • 不用每一个用户界面都在Manifest.xml文件中配置
  • 通用的导航栏处理
  • 通过简单的继承就可以自动实现loading(这个后续还会和网络请求关联)、异常界面(无数据、无网络等)显示等

用户界面的承载选择

如果想不在Manifest.xml中配置很多用到的用户界面,那么使用Fragment就是我们的必然选择了,使用Fragment有两种方式,一种是所有的Fragment都有一个公用的Activity来承载,每一次用户切换界面其实还是切换Activity,第二个就是只启动一个Activity,在Activity中切换Fragment以达到界面的切换。第二种看起来更合理一点,但是对于界面生命周期的管理以及一些公用的参数用不好就会出现混乱的情况,所以我们采用第一种方案。

方案思路

先定义一个接口,封装基本的界面操作方法(loading、toast、显示错误信息等),然后用一个BaseFragment来实现该接口方法,再用一个Activity来承载这个实现了BaseFragment的具体业务的Fragment,传递的参数中告诉Activity需要加载的Fragment名字,通过这样,只需要注册一个承载Acitivty就可以实现显示不同的用户界面。如下图:


灵魂画手画的流程

具体实现

按照上面思路首先要定义好一个用户界面基本的方法

  • IView.java 在MVP模式中也会复用到
package com.kotlin.anonyper.testapplication.base;

/**
 * 普通view的操作接口
 * TestApplication
 * Created by anonyper on 2018/12/17.
 */

public interface IView {
    /**
     * 弹出通知
     *
     * @param message
     */
    void showToast(String message);

    /**
     * 隐藏loading条
     */
    void hideLoading();


    /**
     * 控制显示loading
     *
     * @param message    loading内容
     * @param cancelAble 是否可取消
     */
    void showLoading(String message, boolean cancelAble);

    /**
     * 显示内容部分view
     */
    void showContentView();

   /**
     * 显示异常部分view
     *
     * @param imageRes 显示的资源图片
     * @param message 显示的信息
     */
    void showExcptionView(int imageRes, String message);
}

然后用Fragment来实现IView接口,实现其中的方法


/**
 * 基本的fragment
 * TestApplication
 * Created by anonyper on 2018/12/18.
 */

public abstract class BaseFragment extends Fragment implements IView {

    private ProgressDialog progressDialog;

    @Override
    public void showToast(String message) {
        Toast.makeText(this.getContext(), message, Toast.LENGTH_LONG).show();
    }

    @Override
    public void hideLoading() {
        if (progressDialog != null && progressDialog.isShowing()) {
            progressDialog.dismiss();
        }
    }

    @Override
    public void showLoading(String message, boolean cancelAble) {
        if (TextUtils.isEmpty(message)) {
            message = "";
        }
        if (progressDialog == null) {
            progressDialog = new ProgressDialog(this.getActivity());
        }

        if (this.getActivity().isFinishing()) {
            return;
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
            if (this.getActivity().isDestroyed()) {
                return;
            }
        }
        progressDialog.setMessage(message);
        progressDialog.setCanceledOnTouchOutside(true);
        progressDialog.setCancelable(cancelAble);
        if (!progressDialog.isShowing()) {
            progressDialog.show();
        }

    }

    @Override
    public void showContentView() {

    }

    @Override
    public void showExcptionView(int imageRes,String message) {

    }
}

上述中showContentViewshowExcptionView没有具体的实现,这个会放到BaseFragment的子类中来实现。
针对大多数用户界面(带有网络请求),有一个同样式的title导航、异常界面以及数据界面切换显示逻辑。针对这种情况,我们封装一个SimpleBaseFragmen,将显示内容和异常界面用FrameLayout容器并列存放,然后和导航栏的view通过LinearLayout容器竖直排列。先看公用的title类:


/**
 * 公共标题
 */
public class TitleBar extends RelativeLayout {


    Context mContext;
    View titleView;
    @BindView(R.id.imgv_titleleft)
    ImageView imgvTitleleft;
    @BindView(R.id.rlt_titleleft)
    RelativeLayout rltTitleLeft;
    @BindView(R.id.tv_title)
    TextView tvTitle;
    @BindView(R.id.imgv_titleright)
    ImageView imgvTitleright;
    @BindView(R.id.tv_titleright)
    TextView tvTitleright;
    @BindView(R.id.rlt_titleright)
    RelativeLayout rltTitleright;
    @BindView(R.id.tv_titleline)
    View tvTitleline;
    @BindView(R.id.rlt_title)
    RelativeLayout rltTitle;


    public TitleBar(Context context) {
        super(context);
        this.mContext = context;
        initView();
    }

    public TitleBar(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.mContext = context;
        initView();
    }

    public TitleBar(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.mContext = context;
        initView();
    }

    private void initView() {
        titleView = View.inflate(getContext(), R.layout.view_title_bar, this);
        ButterKnife.bind(this, titleView);
    }

    public View getTitleView() {
        return titleView;
    }

    public void setTitle(String title) {
        if (tvTitle != null) {
            tvTitle.setText(title);
        }
    }

    public void setTitle(int res) {
        if (res > 0 && mContext != null)
            setTitle(mContext.getResources().getString(res));
    }

    public View getLeftView() {
        return rltTitleLeft;
    }

    public View getRightView() {
        return rltTitleright;
    }

    public void setLeftImage(int res) {
        if (imgvTitleleft != null && res > 0) {
            imgvTitleleft.setImageResource(res);
        }
    }

    public void setRightImage(int res) {
        if (imgvTitleright != null && res > 0) {
            imgvTitleright.setImageResource(res);
        }
    }

    public void setRightText(String rightText) {
        if (tvTitleright != null) {
            tvTitleright.setText(rightText);
        }
    }

    public void setRightText(int rightText) {
        if (tvTitleright != null && rightText > 0) {
            tvTitleright.setText(rightText);
        }
    }

    public void hideTitle() {
        if (titleView != null) {
            titleView.setVisibility(View.GONE);
        }
    }

    public void showTitle() {
        if (titleView != null) {
            titleView.setVisibility(View.VISIBLE);
        }
    }

    public void hideLeftView() {
        if (rltTitleLeft != null) {
            rltTitleLeft.setVisibility(View.INVISIBLE);
        }
    }

    public void showLeftView() {
        if (rltTitleLeft != null) {
            rltTitleLeft.setVisibility(View.VISIBLE);
        }
    }

    public void hideLeftImage() {
        if (imgvTitleleft != null) {
            imgvTitleleft.setVisibility(View.GONE);
            rltTitleLeft.setVisibility(View.INVISIBLE);
        }
    }

    public void showLeftImage() {
        if (imgvTitleleft != null) {
            imgvTitleleft.setVisibility(View.VISIBLE);
            rltTitleLeft.setVisibility(View.VISIBLE);
        }
    }

    public void showRightText() {
        if (tvTitleright != null) {
            tvTitleright.setVisibility(View.VISIBLE);
            rltTitleright.setVisibility(View.VISIBLE);
        }

    }

    public void hideRightText() {
        if (tvTitleright != null) {
            tvTitleright.setVisibility(View.GONE);
        }

    }

    public void showRightImage() {
        if (imgvTitleright != null) {
            imgvTitleright.setVisibility(View.VISIBLE);
            rltTitleright.setVisibility(View.VISIBLE);
        }

    }

    public void hideRightImage() {
        if (imgvTitleright != null) {
            imgvTitleright.setVisibility(View.GONE);
        }

    }

    public void hideRightView() {
        if (rltTitleright != null) {
            rltTitleright.setVisibility(View.INVISIBLE);
        }
    }

    public void showRightView() {
        if (rltTitleright != null) {
            rltTitleright.setVisibility(View.VISIBLE);
        }
    }

    public void hideTitleLine() {
        if (tvTitleline != null) {
            tvTitleline.setVisibility(View.GONE);
        }
    }

    public void showTitleLine() {
        if (tvTitleline != null) {
            tvTitleline.setVisibility(View.VISIBLE);
        }
    }

    public void setRightClickListener(View.OnClickListener listener) {
        if (listener != null) {
            getRightView().setOnClickListener(listener);
        }
    }

}

写一个SimpleBaseFragment继承BaseFragment类,实现title、内容view(由具体业务的fragment提供)、异常view的控制逻辑:有几个重点方法:

  • onAttach 用来回去上下文content
@Override
    public void onAttach(Context context) {
        super.onAttach(context);
        this.context = context;
    }
  • getExcptionView 获取异常的view,如果异常view不满足使用,可以重写该方法
public View getExcptionView() {
    View excptionView = View.inflate(this.getContext(), R.layout.view_excption_empty, null);
    excptionView.setVisibility(View.GONE);
    return excptionView;
}
  • initTitle 处理title,添加左上角返回
public void initTitle() {
        if (mTitleBar != null) {
            mTitleBar.showLeftView();
            mTitleBar.getLeftView().setOnClickListener(v -> {
                try {
                    hideSoftInput();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                getActivity().finish();
            });
        }

    }
  • 控制内容view和异常的显示
@Override
    public void showContentView() {
        if (mainContentView != null) {
            mainContentView.setVisibility(View.VISIBLE);
        }
        if (excptionView != null) {
            excptionView.setVisibility(View.GONE);
        }
    }

    @Override
    public void showExcptionView(int imageRes, String message) {
        if (mainContentView != null) {
            mainContentView.setVisibility(View.GONE);
        }
        if (excptionView != null) {
            excptionView.setVisibility(View.VISIBLE);
            if (imageRes > 0) {
                ImageView image = excptionView.findViewById(R.id.error_imageview);
                if (image != null) {
                    image.setImageResource(imageRes);
                }
            }
            if (!TextUtils.isEmpty(message)) {
                TextView descView = excptionView.findViewById(R.id.desc);
                if (descView != null) {
                    descView.setText(message);
                }
            }
            excptionView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    onExcptionViewClick();
                }
            });
        }
    }
  • onCreateView方法的实现(重点)
    在这个方法中,控制Title View、业务内容View、异常View的加载:
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        super.onCreateView(inflater, container, savedInstanceState);
        int viewId = getLayoutId();
        if (viewId > 0) {
            contentView = inflater.inflate(viewId, null, false);//
            if (contentView != null)
                unbinder = ButterKnife.bind(this, contentView);
        }
        mTitleBar = new TitleBar(context);//标题栏
        mRootView = new LinearLayout(context);//根view  里面包含title和一个包含其他内容的view
        mRootView.setBackgroundColor(getResources().getColor(R.color.app_bg_theme));
        mRootView.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
        mRootView.setOrientation(LinearLayout.VERTICAL);
        mRootView.addView(mTitleBar);
        FrameLayout frameLayout = new FrameLayout(context);//将除了title之外的view放进该frameLayout
        frameLayout.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
        mainContentView = new LinearLayout(context);
        mainContentView.setLayoutParams(new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
        mainContentView.setOrientation(LinearLayout.VERTICAL);
        if (contentView != null) {
            contentView.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 0, 1));
            mainContentView.addView(contentView);
        }

        frameLayout.addView(mainContentView);
        excptionView = getExcptionView();

        if (excptionView != null) {
            frameLayout.addView(excptionView);
        }
        mRootView.addView(frameLayout);
        //设置Toolbar相关
        initTitle();
        //初始化控件
        initView(contentView, getArguments());
        return mRootView;

    }

/**
     * 获取要展示的资源view,由业务的Fragment来具体提供
     *
     * @return 展示view的layout资源
     */
    public abstract int getLayoutId();

通过以上方法,我们封装了一个基本的常用的Fragment,在具体使用的过程中,我们通过继承该SampleBaseFragment,实现public abstract int getLayoutId();方法,就会自动的添加title、异常view等逻辑。

具体用法:

MainFragment.java

/**
 * TestApplication
 * Created by anonyper on 2018/12/18.
 */

public class MainFragment extends SimpleBaseFragment {

    //该界面的业务view
    @Override
    public int getLayoutId() {
        return R.layout.activity_main;
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
       //这个里面可以处理业务view或者参数,view可以通过butterknife来实例化具体的view。
    }
}

可以看出,通过继承SimpleBaseFragment,我们的MainFragment只需要关注具体的业务处理即可,不用再关注导航栏、错误界面等逻辑。

上面实现了基本的Fragment封装,接下来介绍承载Fragment的Activity:
先定义一个基本的Activity,这个是为了便于写统计、埋点等代码:BaseActivity

/**
 * 基础的activi 其他一些自定义的activity也需要继承该类 可以在这里面添加界面统计、埋点等基础代码
 * TestApplication
 * Created by anonyper on 2018/12/18.
 */

public class BaseActivity extends AppCompatActivity {
    public String TAG = this.getClass().getName();

    @Override
    protected void onResume() {
        super.onResume();
        //埋点代码
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        //埋点代码
    }
}

然后通过继承这个基本的Activity,同时添加对Fragment的承载:
SimpleBaseActivity.java

/**
 * 基本的activi实现类 项目的fragment都承载在这个类中
 * TestApplication
 * Created by anonyper on 2018/12/18.
 */

public class SimpleBaseActivity extends BaseActivity {

    BaseFragment baseFragment;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.view_sample_activity);
        if (getIntent().getStringExtra(BaseConfig.FRAGMENT_KEY) != null) {
            showFragment();
        }
    }

    void showFragment() {
        Intent intent = getIntent();
        if (intent == null) {
            return;
        }
        String targetFragment = intent.getStringExtra(BaseConfig.FRAGMENT_KEY);

        try {
            baseFragment = (BaseFragment) Class.forName(targetFragment).newInstance();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        if (baseFragment == null) {
            LogUtil.e(TAG, "targetFragment error");
            return;
        }
        Bundle bundle = intent.getExtras();
        if (bundle != null) {
            baseFragment.setArguments(bundle);
        }
        FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
        ft.replace(R.id.fragment_container, baseFragment, this.getClass().getName());
        ft.commitAllowingStateLoss();
    }

    /**
     * 权限的回调也通知fragment 防止部分fragment需要获取隐私权限
     * @param requestCode
     * @param permissions
     * @param grantResults
     */
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (baseFragment != null) {
            baseFragment.onRequestPermissionsResult(requestCode, permissions, grantResults);
        }
    }

    /**
     * 通知fragment
     * @param requestCode
     * @param resultCode
     * @param data
     */
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (baseFragment != null) {
            baseFragment.onActivityResult(requestCode, resultCode, data);
        }
    }
}

其中R.layout.view_sample_activity布局文件如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/app_bg_theme"
    android:orientation="vertical">

    <FrameLayout
        android:id="@+id/fragment_container"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" />

</LinearLayout>

Manifast.xml配置如下:

<activity
            android:name="com.kotlin.anonyper.testapplication.activity.SimpleBaseActivity"
            android:configChanges="orientation|screenSize|keyboardHidden"
            android:label="@string/app_name">
            <intent-filter>
                <action android:name="com.kotlin.anonyper.testapplication.sample.action" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </activity>

然后使用的地方,同时Intent发送com.kotlin.anonyper.testapplication.sample.action这个action来启动SimpleBaseActivity。具体如下:

Intent intent = new Intent();
        intent.putExtra(BaseConfig.FRAGMENT_KEY, MainFragment.class.getName());
        intent.setAction(BaseConfig.ACTION);
        startActivity(intent);

其中BaseConfig.FRAGMENT_KEYBaseConfig.ACTION是:

public static final String FRAGMENT_KEY = "base_fragment";
public static final String ACTION = "com.kotlin.anonyper.testapplication.sample.action";

以上代码,就完成了用户界面View的封装,这样以后在写代码的过程中,只需要关注layout布局文件以及对饮的业务逻辑,其他的就自动完成。当然,这个配合网络数据加载过程使用才会更有意义,后续会在这个基础上,加上网络请求的使用过程,让两者联动起来!
代码下载地址:https://download.csdn.net/download/she_cool_/10861278

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342

推荐阅读更多精彩内容