前言
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布局文件以及对饮的业务逻辑,其他的就自动完成。当然,这个配合网络数据加载过程使用才会更有意义,后续会在这个基础上,加上网络请求的使用过程,让两者联动起来!
作者:AnonyPer
链接:https://www.jianshu.com/p/36e25602c5dd