又解一个bug - Fragment异常显示问题

最近维护了一段时间公司自研的移动端跨平台框架,发现了不少早期设计上的问题,其中有一个Fragment相关的显示隐藏案例还是比较有意思的,可以分享给大家避免后续踩到同样的坑

案例构造

为了方便演示,这里我们构造一个简单的案例来进行,在Activity里面有一个容器(FrameLayout),根据变量isOneShow的不同显示对应的Fragment并隐藏其他Fragment

scss 复制代码
假设有两个frgamnt
val one = FragmentOne()
val two = FragmentTwo()

var isOneShow = true

在Activity 里面
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    省略...

    val btn = this.findViewById<TextView>(R.id.btn)
    btn.setOnClickListener {
val sf = supportFragmentManager.beginTransaction()
        if (isOneShow) {
            sf.show(two)
            sf.hide(one)

        } else {
            sf.show(one)
            sf.hide(two)
        }

        isOneShow = !isOneShow
        sf.commitAllowingStateLoss()

    }

val sf = supportFragmentManager.beginTransaction()
    sf.add(R.id.container_fg, one)
    sf.add(R.id.container_fg, two)
    sf.show(one)
    sf.hide(two)
    sf.commitAllowingStateLoss()
}

这里有个场景是,One假设是我们的原生页面Fragment,Two是跨平台渲染的Fragment(采取的方案是类RN,这里不重要),在显示OneFragment时,由于内部的一些异步渲染逻辑并没有判断TwoFragment当前是否可见就执行了刷新操作,导致最终的UI展示异常(类似Fragment视图叠加),如右下图

kotlin 复制代码
OneFragment 展示时isHidden = false,TwoFragment isHidden 为true
执行了以下代码
two.view?.visibility = View.VISIBLE

为了方便演示,我们在onBackPressed修改
override fun onBackPressed() {

    two.view?.visibility = View.VISIBLE

}

出现这种情况一开始还觉得挺违反直觉的,因为TwoFragment当前的确属于hidden状态,但是里面的视图却展示出来了,但是看了一下Fragment是如何管理内部视图之后,就焕然开朗了。

FragmentTransaction的hide 究竟做了什么?

回到上面代码,我们再重新看一下hide方法

less 复制代码
public FragmentTransaction hide(@NonNull Fragment fragment) {
    addOp(new Op(OP_HIDE, fragment));

    return this;
}

可以看到,最终hide方法只是构造了一个OP_HIDE的指令放入了一个List当中,最终它这个操作是可以异步执行的,如果我们采取的是类似commitxxx这种方式的话

最终FragmentTransaction包装的事务处理会在executeOps方法中,根据不同的指令执行对应的方法

ini 复制代码
void executeOps() {
    final int numOps = mOps.size();
    for (int opNum = 0; opNum < numOps; opNum++) {
        final Op op = mOps.get(opNum);
        final Fragment f = op.mFragment;
        ....
        switch (op.mCmd) {
           ....
            case OP_HIDE:
                f.setAnimations(op.mEnterAnim, op.mExitAnim, op.mPopEnterAnim, op.mPopExitAnim);
                mManager.hideFragment(f);
                break;
            case OP_SHOW:
                f.setAnimations(op.mEnterAnim, op.mExitAnim, op.mPopEnterAnim, op.mPopExitAnim);
                mManager.setExitAnimationOrder(f, false);
                mManager.showFragment(f);
                break;
            case OP_DETACH:
                f.setAnimations(op.mEnterAnim, op.mExitAnim, op.mPopEnterAnim, op.mPopExitAnim);
                mManager.detachFragment(f);
                break;
           ....
        }
        if (!mReorderingAllowed && op.mCmd != OP_ADD && f != null) {
            if (!FragmentManager.USE_STATE_MANAGER) {
                mManager.moveFragmentToExpectedState(f);
            }
        }
    }
    if (!mReorderingAllowed && !FragmentManager.USE_STATE_MANAGER) {
        //这里是关键,如何显示与隐藏视图
        mManager.moveToState(mManager.mCurState, true);
    }
}

那么我们在Fragment添加的View,是什么时候被设置为隐藏的呢?其实在下面的mManager.moveToState方法moveToState最终会调用moveFragmentToExpectedState方法,把当前的Fragment表现设置为预期的行为

scss 复制代码
void moveFragmentToExpectedState(@NonNull Fragment f) {
    if (!mFragmentStore.containsActiveFragment(f.mWho)) {
        if (isLoggingEnabled(Log.DEBUG)) {
            Log.d(TAG, "Ignoring moving " + f + " to state " + mCurState
                    + "since it is not added to " + this);
        }
        return;
    }
    moveToState(f);

    // 执行隐藏动画
    if (f.mView != null) {
        if (f.mIsNewlyAdded && f.mContainer != null) {
            // Make it visible and run the animations
            if (f.mPostponedAlpha > 0f) {
                f.mView.setAlpha(f.mPostponedAlpha);
            }
            f.mPostponedAlpha = 0f;
            f.mIsNewlyAdded = false;
            // run animations:
            FragmentAnim.AnimationOrAnimator anim = FragmentAnim.loadAnimation(
                    mHost.getContext(), f, true, f.getPopDirection());
            if (anim != null) {
                if (anim.animation != null) {
                    f.mView.startAnimation(anim.animation);
                } else {
                    anim.animator.setTarget(f.mView);
                    anim.animator.start();
                }
            }
        }
    }
    这里会隐藏hidefragment的视图
    if (f.mHiddenChanged) {
        completeShowHideFragment(f);
    }
}

最终是在completeShowHideFragment方法中,会判断fragment当前的mHidden标记,如果mHidden为true(即一开始展示,但是执行后需要隐藏),就会在动画结束时通过设置fragment.mView.setVisibility(View.GONE) 把当前Fragemnt的View隐藏

scss 复制代码
private void completeShowHideFragment(@NonNull final Fragment fragment) {
    if (fragment.mView != null) {
        FragmentAnim.AnimationOrAnimator anim = FragmentAnim.loadAnimation(
                mHost.getContext(), fragment, !fragment.mHidden, fragment.getPopDirection());
        if (anim != null && anim.animator != null) {
            anim.animator.setTarget(fragment.mView);
            if (fragment.mHidden) {
                if (fragment.isHideReplaced()) {
                    fragment.setHideReplaced(false);
                } else {
                    final ViewGroup container = fragment.mContainer;
                    final View animatingView = fragment.mView;
                    container.startViewTransition(animatingView);
                    // Delay the actual hide operation until the animation finishes,
                    // otherwise the fragment will just immediately disappear
                    anim.animator.addListener(new AnimatorListenerAdapter() {
                        @Override
                        public void onAnimationEnd(Animator animation) {
                            container.endViewTransition(animatingView);
                            animation.removeListener(this);
                            if (fragment.mView != null && fragment.mHidden) {
                                fragment.mView.setVisibility(View.GONE);
                            }
                        }
                    });
                }
            } else {
                fragment.mView.setVisibility(View.VISIBLE);
            }
            anim.animator.start();
        } else {
            if (anim != null) {
                fragment.mView.startAnimation(anim.animation);
                anim.animation.start();
            }
            final int visibility = fragment.mHidden && !fragment.isHideReplaced()
                    ? View.GONE
: View.VISIBLE;
            fragment.mView.setVisibility(visibility);
            if (fragment.isHideReplaced()) {
                fragment.setHideReplaced(false);
            }
        }
    }
    invalidateMenuForFragment(fragment);
    fragment.mHiddenChanged = false;
    fragment.onHiddenChanged(fragment.mHidden);
}

这里我们其实就明白了,其实Fragment本身并不是一个纯粹隔离的容器,因为最终View的隐藏其实还是通过fragment.mView.setVisibility的方式设置的,如果当前Fragment mHidden 即使为true(即隐藏),我们还是可以通过设置fragment.mView本身的可见性来达到视图展示的目的,一般这种情况都是属于未预期的行为

因此,我们如果直接操作fragment.mView本身的话,很容易造成非预期的行为,在我们的方案中,因为JS执行是异步的,当JS传输最终的json数据给到原生侧渲染视图时,并没有判断当前Fragemnt是否可见,而是直接操作了fragment.mView本身并设置了fragment.mView为可见,导致了一些视图混乱的bug出现

最后

Fragment本身并不是一个隔离的容器,其显示与隐藏最终操作的还是本身的View,这点需要注意。

最后,这个问题一直隐藏了3年多的时间,当我们在多Fragment页面时用这套跨平台方案时才把它暴露出来,实属有点小坑,不过这里也给了我们一个教训,当设计跨平台框架的RootView时(一般是一个根节点用于派发后续的渲染节点)采取异步刷新回调的方式时,需要刷新的只是RootView的子控件,RootView不参与任何刷新才是正解。

相关推荐
stevenzqzq8 小时前
android Hilt注解
android
望佑11 小时前
自定义Scrollbar的两种实现方式
android
望佑11 小时前
记录一次完整ANR日志及分析
android
Android小码家12 小时前
Live555+Windows+MSys2 编译Androidso库和运行使用
android·live555
水w12 小时前
【Android】基础架构(详细介绍)
android·android studio
望佑12 小时前
低成本实现媒体文件预览
android
_祝你今天愉快13 小时前
安卓源码学习之【开机向导定制 OOBE/Provision源码分析】
android·源码
技术野侠客13 小时前
Android端部署DeepSeek
android·人工智能
懋学的前端攻城狮13 小时前
Android 一些基础-05-导航与 Tab
android
Tee xm13 小时前
清晰易懂的 Kotlin 安装与配置教程
android·开发语言·kotlin