解决setText()触发requestLayout导致View位置频繁刷新的问题

在我们某个应用中,有一个功能需要显示时间的秒数、分钟数等。在正常情况下,我们每秒更新一次 TextViewsetText() 方法来显示数据。然而,当我们希望 TextView 能够移动时,却发现每次移动后,TextView 都会重置位置。

通过分析堆栈日志,我们发现每秒都会调用 TextViewrequestLayout() 方法。深入调查后,我们发现问题的根源在于每次调用 setText() 方法时,都会触发 requestLayout()

接下来,我们一起看看源码:

kotlin 复制代码
 @CallSuper
    public void requestLayout() {
        if (isRelayoutTracingEnabled()) {
            Trace.instantForTrack(TRACE_TAG_APP, "requestLayoutTracing",
                    mTracingStrings.classSimpleName);
            printStackStrace(mTracingStrings.requestLayoutStacktracePrefix);
        }

        if (mMeasureCache != null) mMeasureCache.clear();

        if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
            // Only trigger request-during-layout logic if this is the view requesting it,
            // not the views in its parent hierarchy
            ViewRootImpl viewRoot = getViewRootImpl();
            if (viewRoot != null && viewRoot.isInLayout()) {
                if (!viewRoot.requestLayoutDuringLayout(this)) {
                    return;
                }
            }
            mAttachInfo.mViewRequestingLayout = this;
        }

        mPrivateFlags |= PFLAG_FORCE_LAYOUT;
        mPrivateFlags |= PFLAG_INVALIDATED;

        if (mParent != null && !mParent.isLayoutRequested()) {
            mParent.requestLayout();
        }
        if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
            mAttachInfo.mViewRequestingLayout = null;
        }
    }

这里的关键有两个方面:首先,设置 mPrivateFlags 标记为需要重新布局,触发下一次视图刷新周期时,会依次调用 onMeasureonLayout 等方法进行布局计算;其次,调用父视图的 requestLayout 方法,这会引发父视图进行布局重计算。

通过这个分析,我们可以理解为什么 TextView 的位置会被重置。如果我们调用子视图的 requestLayout 进行布局重计算,它也会调用父视图的 requestLayout,这一过程会层层传递,直到根视图。

回到我们遇到的问题,我们并没有直接调用 requestLayout,而是调用了 setTextsetBackgroundDrawable 等方法。看起来,这些方法内部可能会间接调用 requestLayout,从而导致父视图也进行布局重计算。

因此,我查看了 setText 方法,并发现了之前在排查堆栈信息时注意到的关键代码:checkForRelayout 方法。

点进去可以看到:

scss 复制代码
 @UnsupportedAppUsage
    private void checkForRelayout() {
        // If we have a fixed width, we can just swap in a new text layout
        // if the text height stays the same or if the view height is fixed.

        if ((mLayoutParams.width != LayoutParams.WRAP_CONTENT
                || (mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth))
                && (mHint == null || mHintLayout != null)
                && (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)) {
            // Static width, so try making a new text layout.

            int oldht = mLayout.getHeight();
            int want = mLayout.getWidth();
            int hintWant = mHintLayout == null ? 0 : mHintLayout.getWidth();

            /*
             * No need to bring the text into view, since the size is not
             * changing (unless we do the requestLayout(), in which case it
             * will happen at measure).
             */
            makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING,
                          mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(),
                          false);

            if (mEllipsize != TextUtils.TruncateAt.MARQUEE) {
                // In a fixed-height view, so use our new text layout.
                if (mLayoutParams.height != LayoutParams.WRAP_CONTENT
                        && mLayoutParams.height != LayoutParams.MATCH_PARENT) {
                    autoSizeText();
                    invalidate();
                    return;
                }

                // Dynamic height, but height has stayed the same,
                // so use our new text layout.
                if (mLayout.getHeight() == oldht
                        && (mHintLayout == null || mHintLayout.getHeight() == oldht)) {
                    autoSizeText();
                    invalidate();
                    return;
                }
            }

            // We lose: the height has changed and we have a dynamic height.
            // Request a new view layout using our new text layout.
            requestLayout();
            invalidate();
        } else {
            // Dynamic width, so we have no choice but to request a new
            // view layout with a new text layout.
            nullLayouts();
            requestLayout();
            invalidate();
        }
    }

ViewLayoutParams 设置为 WRAP_CONTENT 时,TextView 会进入到 if 语句中,这涉及到 View 的宽高计算,最终会调用 invalidate()requestLayout() 方法,导致了我们遇到的问题。

在了解了这一点后,我开始考虑解决方法,提出了几个思路:

  1. requestLayout 调用后恢复 View 的位移信息
  2. 避免调用 requestLayout

第一个方法显然有堆积操作的嫌疑,在 TextView 异常加载的基础上再去处理位移信息,这样会变得复杂且不易维护。因此,我将重点放在了第二个方法上------如何避免调用 requestLayout。根据源码中的逻辑,关键是避免将 TextViewLayoutParams 设置为 WRAP_CONTENT

这样,就有了两种方案:

  1. 自定义 TextView,手动判断并设置 TextView 的宽高
  2. 修改布局,直接设置宽度为固定值或 MATCH_PARENT

在查看布局后,我选择了第二种方案,因为我使用的是约束布局。在这种布局中,当我的 TextView 只设置了 layout_constraintStart_toStartOflayout_constraintEnd_toEndOf 时,WRAP_CONTENTMATCH_PARENT 的效果其实是一样的。

而如果同时设置了 layout_constraintStart_toStartOflayout_constraintEnd_toEndOf,则可以通过外面嵌套一层线性布局,将 TextView 设置为 MATCH_PARENT,这样就可以有效避免 requestLayout 的调用,解决了问题。

原文地址:mp.weixin.qq.com/s/oQdV463kv...

相关推荐
JMchen1234 小时前
Android后台服务与网络保活:WorkManager的实战应用
android·java·网络·kotlin·php·android-studio
crmscs5 小时前
剪映永久解锁版/电脑版永久会员VIP/安卓SVIP手机永久版下载
android·智能手机·电脑
localbob5 小时前
杀戮尖塔 v6 MOD整合版(Slay the Spire)安卓+PC端免安装中文版分享 卡牌肉鸽神作!杀戮尖塔中文版,电脑和手机都能玩!杀戮尖塔.exe 杀戮尖塔.apk
android·杀戮尖塔apk·杀戮尖塔exe·游戏分享
机建狂魔5 小时前
手机秒变电影机:Blackmagic Camera + LUT滤镜包的专业级视频解决方案
android·拍照·摄影·lut滤镜·拍摄·摄像·录像
hudawei9965 小时前
flutter和Android动画的对比
android·flutter·动画
lxysbly7 小时前
md模拟器安卓版带金手指2026
android
儿歌八万首7 小时前
硬核春节:用 Compose 打造“赛博鞭炮”
android·kotlin·compose·春节
消失的旧时光-194310 小时前
从 Kotlin 到 Dart:为什么 sealed 是处理「多种返回结果」的最佳方式?
android·开发语言·flutter·架构·kotlin·sealed
Jinkxs10 小时前
Gradle - 与Groovy/Kotlin DSL对比 构建脚本语言选择指南
android·开发语言·kotlin
&有梦想的咸鱼&10 小时前
Kotlin委托机制的底层实现深度解析(74)
android·开发语言·kotlin