解决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...

相关推荐
二流小码农1 小时前
鸿蒙开发:DevEcoStudio中的代码提取
android·ios·harmonyos
江湖有缘1 小时前
使用obsutil工具在OBS上完成基本的数据存取【玩转华为云】
android·java·华为云
移动开发者1号3 小时前
Android 多 BaseUrl 动态切换策略(结合 ServiceManager 实现)
android·kotlin
移动开发者1号3 小时前
Kotlin实现文件上传进度监听:RequestBody封装详解
android·kotlin
AJi6 小时前
Android音视频框架探索(三):系统播放器MediaPlayer的创建流程
android·ffmpeg·音视频开发
柿蒂6 小时前
WorkManager 任务链详解:优雅处理云相册上传队列
android
alexhilton7 小时前
使用用例(Use Case)以让Android代码更简洁
android·kotlin·android jetpack
峥嵘life7 小时前
Android xml的Preference设置visibility=“gone“ 无效分析解决
android·xml
用户2018792831678 小时前
通俗故事:驱动二进制文件在AOSP中的角色
android
穷人小水滴8 小时前
在 Termux 中签名 apk 文件
android·linux·apk