美文网首页安卓集中营程序猿葵花宝典待学习
打造安卓App丝滑的操作体验--Fragment深入使用和封装之

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

作者: sugaryaruan | 来源:发表于2017-03-10 14:29 被阅读4793次

    简介

    想让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

    相关文章

      网友评论

      • 勤息嘻嘻嘻:过来给大神点赞
        sugaryaruan:@勤息嘻嘻嘻 :smile: :smile: thanks
      • 50c8dc99c5fa:感谢感谢...我还是挺喜欢用 Fragment 的, 顺道去看了知乎那个,对应用场景确实以后要思考一下了.
        sugaryaruan:@kingtami 是这样,fragment虽好,也不可滥用
      • 2505ffecdcc0:看完了 不错 深有启发
      • Jlanglang:我觉得自带的fragment管理就挺好的,我目前还没碰到回退不了的问题,清fragment栈也挺方便的啊。
      • 风舞尘起:能实现一次回退多个fragment吗?比如下订单,成功后就清除前面几个,然后跳到订单详情
        风舞尘起: @sugaryaruan 晕乎了😅
        Jlanglang: @风舞尘起 为什么不考虑详情页用activity呢。
        sugaryaruan:有一个思路,比如最初在OrderListFragment页,下单后要清除的是SubmitOrderFragmentA和SubmitOrderFragmentB,(这些Fragment都添加到FragmentManager回退栈),在SubmitOrderFragmentB页中下单操作成功后,从回退栈弹出SubmitOrderFragmentA,SubmitOrderFragmentB,显示OrderListFragment,传给OrderListFragment下单成功后的商品信息数据,在回退栈回调里,显示订单详情页。

        回到我的封装体系中,我会调用showOneFragment(SubmitOrderFragmentA.class.getSimpleName,bundle,true)启动SubmitOrderFragmentA页面,同样的启动SubmitOrderFragmentB页面,然后showOneFragment(OrderListFragment.class.getSimpleName,bundle,true,LaunchMode.SINGLE_ENHANCEMENT), 这时候bundle里可以存放下单成功的商品数据和跳转到订单详情的标示,我在OrderListFragment的onSupportBackPressed()回调方法里,在启动订单详情的Fragment。

        这样就能实现了

      • 辉RSH:程序本身还是挺好的,就是说明的文字有些讲的不够细致。挺一下。
        sugaryaruan:Talk is cheap, show you my code

      本文标题:打造安卓App丝滑的操作体验--Fragment深入使用和封装之

      本文链接:https://www.haomeiwen.com/subject/qbongttx.html