在我们某个应用中,有一个功能需要显示时间的秒数、分钟数等。在正常情况下,我们每秒更新一次 TextView
的 setText()
方法来显示数据。然而,当我们希望 TextView
能够移动时,却发现每次移动后,TextView
都会重置位置。
通过分析堆栈日志,我们发现每秒都会调用 TextView
的 requestLayout()
方法。深入调查后,我们发现问题的根源在于每次调用 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
标记为需要重新布局,触发下一次视图刷新周期时,会依次调用 onMeasure
和 onLayout
等方法进行布局计算;其次,调用父视图的 requestLayout
方法,这会引发父视图进行布局重计算。
通过这个分析,我们可以理解为什么 TextView
的位置会被重置。如果我们调用子视图的 requestLayout
进行布局重计算,它也会调用父视图的 requestLayout
,这一过程会层层传递,直到根视图。
回到我们遇到的问题,我们并没有直接调用 requestLayout
,而是调用了 setText
、setBackgroundDrawable
等方法。看起来,这些方法内部可能会间接调用 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();
}
}
当 View
的 LayoutParams
设置为 WRAP_CONTENT
时,TextView
会进入到 if
语句中,这涉及到 View
的宽高计算,最终会调用 invalidate()
和 requestLayout()
方法,导致了我们遇到的问题。
在了解了这一点后,我开始考虑解决方法,提出了几个思路:
- 在
requestLayout
调用后恢复View
的位移信息 - 避免调用
requestLayout
第一个方法显然有堆积操作的嫌疑,在 TextView
异常加载的基础上再去处理位移信息,这样会变得复杂且不易维护。因此,我将重点放在了第二个方法上------如何避免调用 requestLayout
。根据源码中的逻辑,关键是避免将 TextView
的 LayoutParams
设置为 WRAP_CONTENT
。
这样,就有了两种方案:
- 自定义
TextView
,手动判断并设置TextView
的宽高 - 修改布局,直接设置宽度为固定值或
MATCH_PARENT
在查看布局后,我选择了第二种方案,因为我使用的是约束布局。在这种布局中,当我的 TextView
只设置了 layout_constraintStart_toStartOf
或 layout_constraintEnd_toEndOf
时,WRAP_CONTENT
和 MATCH_PARENT
的效果其实是一样的。
而如果同时设置了 layout_constraintStart_toStartOf
和 layout_constraintEnd_toEndOf
,则可以通过外面嵌套一层线性布局,将 TextView
设置为 MATCH_PARENT
,这样就可以有效避免 requestLayout
的调用,解决了问题。