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

1,441 阅读20分钟
原文链接: mp.weixin.qq.com

code小生,一个专注 Android 领域的技术平台

公众号回复 Android 加入我的安卓技术群

作者:AnonyPer链接:https://www.jianshu.com/p/36e25602c5dd声明:本文已获AnonyPer授权发表,转发等请联系原作者授权

前言

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) {    }}

上述中 showContentView 和 showExcptionView 没有具体的实现,这个会放到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_KEY 和 BaseConfig.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

分享技术我是认真的