框架需要解决的问题
- 自定义标题定制
- 数据的处理,包括刷新,加载数据,已经数据为空的处理机制
- 全局的功能实现,沉浸式状态栏,滑动退出等
- 提供便捷的工具类
框架的可移植性
为了便于framework库的移植,上层framework库不做具体应用场景的封装,仅只做接口的定义和调用。
框架中的Fragment概览
FrameworkFragment提供抽象标题操作相关的接口
DataFragment输入FrameworkFragment的子集,提供数据相关的操作接口,比如是刷新数据,数据为空的,数据异常的处理接口,但不包含分页相关操作
RecycleFragment属于DataFragment的子集,以Item的形式提供了RecycleView的实现。提供了列表,九宫格,瀑布流等形式的布局展现,同时提供了分页展示的功能
自定义标题的加载流程
标题是基于BaseActivity实现的,通过setContentView(int layout),我们实现了对布局生成的代理。
@Override
public void setContentView(int layoutResID) {
mTitleTool = DefaultTitleTool.createInstance();
if (hasTitleBar()) {
View rootView = mTitleTool.addTitleView(
getApplicationContext(),
getLayoutInflater().inflate(layoutResID,
getDecotatorView(), false), getDecotatorView(),isPinTitle());
mTitleTool.binding(this);
super.setContentView(rootView);
} else {
super.setContentView(layoutResID);
}
}
通过代理,我们在View的上层增加了RootLayout作为ContentView的父控件,(RootLayout继承自RelativeLayout,主要是为了解决引入沉浸式状态栏而引入的bug,具体是指,当界面为沉浸式显示时,点击输入法会导致界面的展示不符合预期)
public ViewGroup getDecotatorView() {
if (mDecoratorView == null) {
mDecoratorView = new RootLayout(this);
}
return mDecoratorView;
}
通过DefaultTitleTool的addTitleView(),生成带有标题的ContentView。
public View addTitleView(Context context, View contentView,
ViewGroup rootView, boolean isPinTitle) {
···
LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT);
if (!isPinTitle) {
params.addRule(RelativeLayout.BELOW, R.id.title_layout);
}
rootView.addView(contentView, params);
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) {
mStatusBar = new View(context);
mStatusBar.setId(R.id.view_status_bar);
mStatusBar.setBackgroundColor(context.getResources().getColor(R.color.ch1));
rootView.addView(mStatusBar,LayoutParams.MATCH_PARENT,getStatusBarHeight(context));
rootView.addView(mTitleView);
((LayoutParams) mTitleView.getLayoutParams()).addRule(RelativeLayout.BELOW, R.id.view_status_bar);
} else {
rootView.addView(mTitleView);
}
rootView.setFitsSystemWindows(true);
return rootView;
}
- 重写BaseActivity中的isPinTitle()方法可以设置标题跟传入的ContentView是上下排列还是标题栏覆盖在ContentView之上,默认标题栏是上下排列的,覆盖模式的效果可以参考个人主页标题栏的实现。
- 按照标题栏的加载流程,在setContentView()方法调用以后,会触发标题栏的初始化,之后如果需要对标题的值进行动态设置,需要调用 notifyTitleData() 方法来触发刷新
- 目前项目针对4.4及以上的系统提供了沉浸式状态栏,mStatusBar是一个和标题栏相同颜色,和状态栏等高的一个View,避免了对标题栏的每个控件都设置padding。
设置沉浸式状态栏的代码实现在BaseActivity当中
@Override
protected void onCreate(Bundle savedInstanceState) {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) {
setTransform();
}
}
@TargetApi(Build.VERSION_CODES.KITKAT)
private void setTransform(){
Window window = getWindow();
window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
}
Fragment的加载流程
在onActivityCreate()中实现是否根据序列化恢复Fragment的逻辑
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
boolean isInitState = false;
if (savedInstanceState != null) {
isInitState = onRestoreInstanceState(savedInstanceState);
}
if (!isInitState) {
onCreateInit(getInitRefreshMode());
}
}
根据isInitState,如果已经init过了,则调用onRestoreInstanceState()进行Fragment的数据的序列化恢复,如果没有init,则调用onCreateInit()。
我们可以看到,在ParcelRecycleFragment中,重写了onRestoreInstanceState()方法
@Override
public boolean onRestoreInstanceState(Bundle savedInstanceState) {
if (savedInstanceState != null) {
ArrayList<T> contentData = savedInstanceState.getParcelableArrayList(KEY_CONTENT_DATA);
isLoadingMore = false;
if (contentData != null && contentData.size() > 0) {
getContentData().clear();
for (T info : contentData) {
if (info == null) {
return super.onRestoreInstanceState(savedInstanceState);
}
getContentData().add(info);
}
getAdapter().notifyDataSetChanged();
return true;
}
}
return super.onRestoreInstanceState(savedInstanceState);
}
通过重写onRestoreInstanceState(),ParcelRecycleFragment实现了对Adapter中ContentData的数据保存(FooterData和HeaderData因为类型不确定,要保存的话,需要下层实现根据具体需求自己去onSaveInstance()中去保存)。
目前框架层对于序列化操作,只提供了ParcelRecycleFragment的默认实现,在做序列化实现是,需要注意的是,onSaveInstance()和onRestoreInstanceState()中,Parcel的数据存取时的顺序必须一一对应。
在序列化过程中,Fragment默认是会直接被序列化保存到FragmentManager里面的,我们只需要通过findFragmentByTag()来获取Fragment实例就可以了,如果这个时候再new一个新的Fragment去add的话,就会出现Fragment来回切换重叠的问题。下面是一个建议的写法
Fragment fragment = manager.findFragmentByTag(TAG);
if(fragment==null){
fragment = ChatSessionListFragment.getInstance(groupType,listType,eventType,false,groupSession);
}
transaction.replace(R.id.fragment_content,fragment,TAG);
transaction.commitAllowingStateLoss();
后期框架可能会演进成一个Activity来处理多个Fragment的架构,上层会对这一块做一些处理,可能就不需要再去做这个实现了。但是,原理和上面的实现原理会是一样。
在处理Fragment序列化的过程中,我们可能会遇到Fragment中嵌套有多个Fragment的情况(类似即刻约首页),这时候切换Fragment会有时候出现白屏的现象,根本原因在于,对于嵌套的Fragment,需要使用getChildFragmentManager(),这样的话,Fragment保存的时候会把它的子Fragment保存在ChildFragmentManager中
mServeManagement = (ServeManagementFragment) getChildFragmentManager().findFragmentByTag(FRAGMENT_TIME_UP_TO_TAG);
如果不需要序列化恢复,则调用onCreateInit(),紧接着会调用refreshUi(),FrameworkDataFragment重写了refreshUi()方法。
@Override
public void refreshUi(RefreshMode refreshMode,Bundle bundle) {
mRefreshMode=refreshMode;
sendRequest(refreshMode);
}
框架中的数据请求,网络请求均建议重写sendRequest()方法,目前不建议重写refreshUi(),逻辑不清晰的情况下,重写refreshUi()有可能导致请求的递归调用。
另外,需要注意的是,重写onActivityCreate()方法时,super.onActivityCreate()是会触发sendRequest(),界面会请求两次相同的接口,大部分都是因为没有想清楚需不需要框架去触发首次init操作。
通过isAutoPull()可以配置是否显示下拉刷新的动画,isPullAble()配置是否可以下拉。这两个接口都是针对DataFragment以及它的子类
自定义DataFragment和RecycleFragment的界面样式
- getPtrLayout() 定义Ptr控件内容的布局,即下拉刷新下面的布局部分
- getLayout() 定义整个Fragment的布局内容,需要注意的是,PtrFrameLayout的id必须是fragment_ptr_home_ptr_frame,
RecycleView的id必须是recycle_vertical否则会导致找不到控件,异常报错。
RecycleFragment可以通过重写getLayoutManager(),来获得列表,九宫格,瀑布流的能力。
一个九宫格的例子
@Override
protected RecyclerView.LayoutManager getLayoutManager() {
return new GridLayoutManager(getContext(),3);
}
一个瀑布流的例子
@Override
protected RecyclerView.LayoutManager getLayoutManager() {
return new StaggeredGridLayoutManager(3,StaggeredGridLayoutManager.VERTICAL);
}
DataFragment框架引入的目的是为了针对不需要翻页滑动的页面进行统一的数据为空处理,下拉刷新处理等逻辑,如果有这类需求的可以继承DataFragment
目前因为时间和需求量不大的缘故,DataFragment中的数据为空的处理还没有做具体实现,后期有计划实现数据为空的统一处理,以及对下拉刷新的浮层操作,提供框架支持,暂时这里就不展开讨论了。