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

相关推荐
Jackilina_Stone2 小时前
【faiss】用于高效相似性搜索和聚类的C++库 | 源码详解与编译安装
android·linux·c++·编译·faiss
棒棒AIT3 小时前
mac 苹果电脑 Intel 芯片(Mac X86) 安卓虚拟机 Android模拟器 的救命稻草(下载安装指南)
android·游戏·macos·安卓·mac
fishwheel3 小时前
Android:Reverse 实战 part 2 番外 IDA python
android·python·安全
消失的旧时光-19436 小时前
Android网络框架封装 ---> Retrofit + OkHttp + 协程 + LiveData + 断点续传 + 多线程下载 + 进度框交互
android·网络·retrofit
zcychong7 小时前
Handler(二):Java层源码分析
android
Chef_Chen8 小时前
从0开始学习R语言--Day58--竞争风险模型
android·开发语言·kotlin
用户2018792831679 小时前
演员的智能衣橱系统之Selector选择器
android
CYRUS_STUDIO9 小时前
OLLVM 混淆 + VMP 壳照样破!绕过加壳 SDK 的核心检测逻辑
android·逆向·汇编语言
Kapaseker9 小时前
憋了一周了,12000字深入浅出Android的Context机制
android