×

打造安卓App丝滑的操作体验--Fragment深入使用和封装之道

96
sugaryaruan
2017.03.10 14:29* 字数 956

简介

想让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();
    }
}

说明:

  1. getChildrenFragmentContainerResID(),该方法在BasicFragment里,用来获取要替换的布局ID
  2. 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,
}

说明:

  1. popMultipleBackStack()实现一次弹出多个Fragment

  2. 在隐藏当前所有fragment操作,特别需要在遍历时,做个非空判断,

     for (Fragment f : fragments) {
             if (f != null) {
                 transaction.hide(f);
             }
     }
    

这样做,是因为Fragment出栈后,会出现栈内顺序不正确的bug,详看Fragment全解析系列(一):那些年踩过的坑,一文中关于多个Fragment同时出栈的深坑BUG这一部分的内容。

  1. synchronizeFragmentBackDequeWhenSingleLaunchMode()单例模式下,管理自维护的Fragment后退栈
  2. 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();
}
  1. showOneFragmentOnBackPressed()实现返回键显示特定Tag的Fragment

  2. 同时,如果需要,我们可以给FragmentManager添加OnBackStackChangedListener,监听FragmentManager回退栈成员数量的变化,具体使用见文末的代码

  3. 还有一种思路,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,代码量很少,主要做两件事

  1. 初始化显示Fragment
  2. 实现fragmentProvider方法,该方法把这个容器所需的Fragment构造出来

小结

完整封装Fragment的代码随着项目需求在进化和改动,还是决定放到Github上完整代码传送门,欢迎star

欢迎关注CodeThings

相关阅读资料

特别鸣谢:YoKey系列

  1. Fragment全解析系列(一):那些年踩过的坑
  2. Fragment全解析系列(二):正确的使用姿势
  3. Fragment之我的解决方案:Fragmentation

Android开发之Fragment最佳实践

What the Fragment?-Google I/O 2016(需自备梯子)

知乎:关于 Android,用多个 activity,还是单 activity 配合 fragment?

Android multiple fragment transaction ordering

Android进阶
Web note ad 1