又解一个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不参与任何刷新才是正解。

相关推荐
哆啦A梦的口袋呀23 分钟前
Android 底层实现基础
android
闻道且行之32 分钟前
Android Studio下载及安装配置
android·ide·android studio
alexhilton1 小时前
初探Compose中的着色器RuntimeShader
android·kotlin·android jetpack
小墙程序员1 小时前
kotlin元编程(二)使用 Kotlin 来生成源代码
android·kotlin·android studio
小墙程序员1 小时前
kotlin元编程(一)一文理解 Kotlin 反射
android·kotlin·android studio
fatiaozhang95272 小时前
创维智能融合终端DT741_移动版_S905L3芯片_安卓9_线刷固件包
android·电视盒子·刷机固件·机顶盒刷机
小林学Android4 小时前
Android四大组件之Activity详解
android
搬砖不得颈椎病5 小时前
Jetpack DataStore vs SharedPreferences:现代Android数据存储方案对比
android
auxor7 小时前
Android 窗口管理 - 窗口添加过程分析Client端
android
雨白8 小时前
HTTP协议详解(一):工作原理、请求方法与状态码
android·http