简介
想让App有丝滑般的切换速度和顺畅的体验么?那就放开Activity,使用Fragment来展示UI页面吧
Github futurice/android-best-practices上列举了一些列安卓开发最佳实践建议,其中对Fragment使用做了一些陈述(传送门),我表示赞同。
最近项目需要新增了一个功能模块,我引入了一个Activity,多个fragment的方式来组织UI,这个过程有了一些收获。
对fragment的操作,我使用support v4包下的,FragmentManager和FragmentTransaction这两个类。
正文
如何启动一个Fragment
replace方式
这里我没有用Activity下的getSupportFragmentManager得到了FragmentManager来替换Fragment,而是选择使用fragment下的getChildFragmentManager()获取FragmentManager。
放在基类BasicActivity里,
/**
* 替换当前Fragment里的某个FrameLayout布局
* @param resId 被替换的布局ID
* @param fragmentTab 新的Fragment名
* @param arguments 传入新的Fragment的Bundle
* @param isAddToBack 是否加入回退栈
*/
private void replaceOneFragment(@IdRes int resId, String fragmentTab, Bundle arguments, boolean isAddToBack) {
int childrenFragmentContainerResID = ((BasicFragment) mCurrentFragment).getChildrenFragmentContainerResID();
int layoutId = resId <= 0 ? childrenFragmentContainerResID : resId;
if (layoutId == -1) {
throw new IllegalStateException("You should overwrite getChildrenFragmentContainerResID from BasicFragment");
}
FragmentManager manager = mCurrentFragment.getChildFragmentManager();
if (manager != null) {
FragmentTransaction transaction = manager.beginTransaction();
transaction
.setCustomAnimations(R.anim.right_enter, R.anim.left_exit, R.anim.left_enter, R.anim.right_exit)
.replace(layoutId, fragmentProvider(fragmentTab, arguments), fragmentTab);
if (isAddToBack) {
transaction.addToBackStack(fragmentTab);
}
transaction.commitAllowingStateLoss();
}
}
说明:
- getChildrenFragmentContainerResID(),该方法在BasicFragment里,用来获取要替换的布局ID
- BasicActivity里,fragmentProvider(),Fragment提供者
add/show/hide显示Fragment(支持SingleTask启动Fragment)
/**
* 显示特定Tag的Fragment,如果是第一次显示,则新建并添加该Fragment
*
* @param fragmentTab Fragment标签名
* @param arguments 传入Fragment的参数
* @param isAddBackStack 是否加入FragmentManager回退栈
* @param launchMode 启动模式 分为: STANDARD,SINGLE,SINGLE_ENHANCEMENT
*/
private void showOneFragment(String fragmentTab, Bundle arguments, boolean isAddBackStack, LaunchMode launchMode) {
FragmentManager manager = getSupportFragmentManager();
if (manager == null) {
return;
}
Fragment fragmentByTag = manager.findFragmentByTag(fragmentTab);
if (fragmentByTag != null && launchMode == LaunchMode.SINGLE_ENHANCEMENT) {
popMultipleBackStack(fragmentTab, arguments);
return;
}
FragmentTransaction transaction = manager.beginTransaction();
//设置过渡动画
transaction.setCustomAnimations(R.anim.right_enter, R.anim.left_exit, 0, 0);
//隐藏当前所有fragment
List<Fragment> fragments = manager.getFragments();
if (fragments != null && fragments.size() > 0) {
for (Fragment f : fragments) {
if (f != null) {
transaction.hide(f);
}
}
}
//第一次添加该Fragment
if (fragmentByTag == null) {
mCurrentFragment = fragmentProvider(fragmentTab, arguments);
mFragmentBackDeque.push(fragmentTab);
transaction.add(getFragmentContainerResID(), mCurrentFragment, fragmentTab);
if (isAddBackStack) {
transaction.addToBackStack(fragmentTab);
}
transaction.commitAllowingStateLoss();
return;
}
if (!(fragmentByTag instanceof BasicFragment)) {
throw new ClassCastException("fragment must extends BasicFragment");
}
//更新Arguments,按后退键时Fragment里的后退方法里使用
if (arguments != null) {
setSupportBackStackArguments(arguments);
}
//根据启动模式类型,采取不同的方式维护后退栈
switch (launchMode) {
case STANDARD:
mFragmentBackDeque.push(fragmentTab);
break;
case SINGLE:
synchronizeFragmentBackDequeWhenSingleLaunchMode(fragmentTab);
break;
}
BasicFragment basicFragment = (BasicFragment) fragmentByTag;
mCurrentFragment = fragmentByTag;
basicFragment.setSupportArguments(arguments);
transaction.show(fragmentByTag);
transaction.commitAllowingStateLoss();
}
/**
* fragment 启动模式
*/
public enum LaunchMode {
/**
* 标准模式
*/
STANDARD,
/**
* 单例模式,其他Fragment从自维护的mFragmentBackDeque栈里退出
*/
SINGLE,
/**
* 强化版单例模式,其他Fragment从FragmentManager栈和自维护的mFragmentBackDeque栈里退出
*/
SINGLE_ENHANCEMENT,
}
说明:
-
popMultipleBackStack()实现一次弹出多个Fragment
-
在隐藏当前所有fragment操作,特别需要在遍历时,做个非空判断,
for (Fragment f : fragments) { if (f != null) { transaction.hide(f); } }
这样做,是因为Fragment出栈后,会出现栈内顺序不正确的bug,详看Fragment全解析系列(一):那些年踩过的坑,一文中关于多个Fragment同时出栈的深坑BUG这一部分的内容。
- synchronizeFragmentBackDequeWhenSingleLaunchMode()单例模式下,管理自维护的Fragment后退栈
- mFragmentBackDeque是自维护回退管理队列
后退键监听管理
使用Fragment组织UI后,返回上一个页面的逻辑有了变化。如果遇到之前replace替换了,则先从该fragment的FragmentManager里恢复原来的被替换的fragment,没有,则把之前hide状态的Fragment重新show显示出来,这个过程需要用了队列自己来维护回退
@Override
public void onBackPressed() {
if (mFragmentBackDeque == null || mCurrentFragment == null) {
return;
}
//检查当前Fragment的ChildFragmentManager回退栈是否需要回退
int childStackEntryCount = mCurrentFragment.getChildFragmentManager().getBackStackEntryCount();
if (childStackEntryCount > 0) {
mCurrentFragment.getChildFragmentManager().popBackStackImmediate();
return;
}
//检查当前Fragment的自维护的回退栈是否需要回退
if (mFragmentBackDeque.size() >= 2) {
showOneFragmentOnBackPressed();
return;
}
finish();
}
-
showOneFragmentOnBackPressed()实现返回键显示特定Tag的Fragment
-
同时,如果需要,我们可以给FragmentManager添加OnBackStackChangedListener,监听FragmentManager回退栈成员数量的变化,具体使用见文末的代码
-
还有一种思路,View提供了setOnKeyListener(OnKeyListener onKeyListener),用OnKeyListener来监听后退健。
Fragment入参管理
第一次启动Fragment,走的是fragment生命周期方法,之后启动fragment从hide状态重新show时,不走Fragment生命周期方法,而是调用onHiddenChanged(boolean hidden)方法。因此,在这两种场景下支持给fragment传入参数,并且做到每次显示fragment,都能拿到最新的入参bundle。
我在BasicFragment设计如下:
/**
* Fragment Argument解析
* @param arguments
*/
protected void parseArguments(Bundle arguments){
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
parseArguments(getArguments());
}
@Override
public void onHiddenChanged(boolean hidden) {
super.onHiddenChanged(hidden);
if(!hidden){
parseArguments(getSupportArguments());
}
}
这样,新的Fragmnet在继承BasicFragment时,只需要重新parseArguments()即可
Toolbar管理
项目中使用Toolbar,用BasicFragmentWithToolbar来负责Toolbar的设置逻辑
BasicFragmentWithToolbar类里的核心代码
@Override
public void onHiddenChanged(boolean hidden) {
super.onHiddenChanged(hidden);
if (!hidden) {
configureToolbar();
}
}
@Override
protected void configureToolbar() {
super.configureToolbar();
int stackEntryCount = getChildFragmentManager().getBackStackEntryCount();
if (stackEntryCount > 0) {
FragmentManager.BackStackEntry stackEntry = getChildFragmentManager().getBackStackEntryAt(stackEntryCount - 1);
//Get the name that was supplied to FragmentTransaction.addToBackStack(String) when creating this entry
String fragmentTab = stackEntry.getName();
BasicFragmentWithToolbar fragmentByTag = (BasicFragmentWithToolbar) getChildFragmentManager().findFragmentByTag(fragmentTab);
if (fragmentByTag != null) {
fragmentByTag.setupToolbar();
}
} else {
setupToolbar();
}
}
/**
* 设置Toolbar的显示内容
*/
protected void setupToolbar() {
mToolbar.setNavigationIcon(R.drawable.icon_header_left);
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
getAttachActivity().onBackPressed();
}
});
}
如何使用上述的Fragment封装
我举个例子,我建一个BillContainerActivity,来作为Bill相关的Fragment容器,代码如下
public class BillContainerActivity extends BasicActivity {
private static final String TAG = "BillContainerActivity";
@Bind(R.id.frame_activity_order_body)
FrameLayout mOrderBody;
public static Intent getCallingIntent(Activity activity){
Intent intent = new Intent(activity,BillContainerActivity.class);
return intent;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initializeShow();
}
private void initializeShow() {
showOneFragment(BillHomeFragment.class.getSimpleName(),true);
}
@Override
protected BasicFragment fragmentProvider(String fragmentTab, Bundle arguments) {
BasicFragment currentFragment;
if(BillHomeFragment.class.getSimpleName().equals(fragmentTab)){
currentFragment = BillHomeFragment.newInstance();
}
else if(BillProcessFragment.class.getSimpleName().equals(fragmentTab)){
currentFragment = BillProcessFragment.newInstance(arguments);
}
else if(BillStructureFragment.class.getSimpleName().equals(fragmentTab)){
currentFragment = BillStructureFragment.newInstance(arguments);
}
else if(AdjustBillDetailFragment.class.getSimpleName().equals(fragmentTab)){
currentFragment = AdjustBillDetailFragment.newInstance(arguments);
}
else if(BrokerageDetailFragment.class.getSimpleName().equals(fragmentTab)){
currentFragment = BrokerageDetailFragment.newInstance(arguments);
}
else if(InvoiceDetailFragment.class.getSimpleName().equals(fragmentTab)){
currentFragment = InvoiceDetailFragment.newInstance(arguments);
}
else if(InvoiceListFragment.class.getSimpleName().equals(fragmentTab)){
currentFragment = InvoiceListFragment.newInstance(arguments);
}
else{
currentFragment = BillHomeFragment.newInstance();
}
return currentFragment;
}
@Override
protected int getFragmentContainerResID() {
return R.id.frame_activity_order_body;
}
@Override
protected int getLayoutResID() {
return R.layout.activity_bill_home;
}
}
说明
会发现作为Fragment容器的BillContainerActivity,代码量很少,主要做两件事
- 初始化显示Fragment
- 实现fragmentProvider方法,该方法把这个容器所需的Fragment构造出来
小结
完整封装Fragment的代码随着项目需求在进化和改动,还是决定放到Github上完整代码传送门,欢迎star
欢迎关注CodeThings相关阅读资料
特别鸣谢:YoKey系列
What the Fragment?-Google I/O 2016(需自备梯子)
网友评论
回到我的封装体系中,我会调用showOneFragment(SubmitOrderFragmentA.class.getSimpleName,bundle,true)启动SubmitOrderFragmentA页面,同样的启动SubmitOrderFragmentB页面,然后showOneFragment(OrderListFragment.class.getSimpleName,bundle,true,LaunchMode.SINGLE_ENHANCEMENT), 这时候bundle里可以存放下单成功的商品数据和跳转到订单详情的标示,我在OrderListFragment的onSupportBackPressed()回调方法里,在启动订单详情的Fragment。
这样就能实现了