引言
在Android开发中,我们经常需要在视图绘制完成后执行一些操作,比如动态调整控件尺寸、获取视图的实际宽高等。传统的做法是使用post()
方法,但这种方式存在一些局限性。本文将介绍几种更优的解决方案,结合源码解读对比,并通过实际案例进行对比分析。
问题背景
在开发中,需要为标签设置宽度约束:如果内容宽度超过76dp则设置为76dp,否则保持自适应宽度。
旧有方案:使用post()
// 设置宽度约束:如果内容宽度超过76dp则设置为76dp,否则自适应
test.post {
val maxWidth = 76.dp
val contentWidth = test.paint.measureText(test.text.toString()) +
test.paddingLeft + test.paddingRight
if (contentWidth > maxWidth) {
test.layoutParams.width = maxWidth
} else {
test.layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT
}
test.requestLayout()
}
问题分析:
post()
只是简单地延迟执行,不保证在正确的时机执行- 可能因为消息队列延迟导致时序问题
- 无法精确控制执行时机
优化方案:使用ViewTreeObserver回调
方案1:OnPreDrawListener
test.viewTreeObserver.addOnPreDrawListener(object : ViewTreeObserver.OnPreDrawListener {
override fun onPreDraw(): Boolean {
// 移除监听器,避免重复执行
test.viewTreeObserver.removeOnPreDrawListener(this)
val maxWidth = 76.dp
val contentWidth = test.paint.measureText(test.text.toString()) +
test.paddingLeft + test.paddingRight
if (contentWidth > maxWidth) {
test.layoutParams.width = maxWidth
} else {
test.layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT
}
test.requestLayout()
return true
}
})
特点:
- 在视图即将绘制前执行
- 可以修改布局参数,然后调用
requestLayout()
重新布局 - 适合在绘制前进行布局调整
方案2:OnGlobalLayoutListener(推荐)
test.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
// 移除监听器,避免重复执行
test.viewTreeObserver.removeOnGlobalLayoutListener(this)
val maxWidth = 76.dp
val contentWidth = test.paint.measureText(test.text.toString()) +
test.paddingLeft + test.paddingRight
// 如果内容宽度超过最大宽度,则设置为最大宽度
if (contentWidth > maxWidth) {
test.layoutParams.width = maxWidth
test.requestLayout()
} else {
// 如果内容宽度不超过最大宽度,保持 wrap_content
test.layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT
test.requestLayout()
}
}
})
特点:
- 在布局完成后执行
- 此时视图已经有了确定的尺寸和位置
- 适合获取视图的最终尺寸
- 性能更好,避免了在绘制前进行布局调整
方案3:OnLayoutChangeListener
test.addOnLayoutChangeListener(object : View.OnLayoutChangeListener {
override fun onLayoutChange(v: View?, left: Int, top: Int, right: Int, bottom: Int,
oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int) {
// 移除监听器,避免重复执行
test.removeOnLayoutChangeListener(this)
// 可以比较新旧布局信息
val newWidth = right - left
val oldWidth = oldRight - oldLeft
// 执行宽度设置逻辑
// ...
}
})
特点:
- 在布局参数或尺寸发生变化时执行
- 可以获取变化前后的布局信息
- 适合监听布局变化并做出响应
源码深度分析:三种回调机制的对比
1. 绘制流程中的OnPreDrawListener
在Android的绘制流程中,OnPreDrawListener
的调用时机非常精确,它位于View.draw()
方法的最开始:
// View.java - draw()方法的核心流程
public void draw(Canvas canvas) {
// 1. 首先调用OnPreDrawListener
if (!dirtyOpaque) {
drawBackground(canvas);
}
// 2. 保存canvas状态
final int saveCount = canvas.getSaveCount();
// 3. 执行OnPreDrawListener回调
if (!verticalEdges && !horizontalEdges) {
// 如果没有裁剪,直接绘制
if (!dirtyOpaque) onDraw(canvas);
dispatchDraw(canvas);
drawAutofilledHighlight(canvas);
if (overlay != null && !overlay.isEmpty()) {
overlay.getOverlayView().dispatchDraw(canvas);
}
onDrawForeground(canvas);
drawDefaultFocusHighlight(canvas);
if (debugDraw()) {
debugDrawFocus(canvas);
}
} else {
// 如果有裁剪,需要特殊处理
// ...
}
// 4. 恢复canvas状态
canvas.restoreToCount(saveCount);
}
2. 布局流程中的OnGlobalLayoutListener
OnGlobalLayoutListener
在布局流程完成后被调用,具体在ViewGroup.layout()
方法中:
// ViewGroup.java - layout()方法的核心流程
@Override
public final void layout(int l, int t, int r, int b) {
if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
if (mTransition != null) {
mTransition.layoutChange(this);
}
super.layout(l, t, r, b);
} else {
// 记录延迟布局
mLayoutCalledWhileSuppressed = true;
}
}
// View.java - layout()方法
@SuppressWarnings({"unchecked"})
public void layout(int l, int t, int r, int b) {
// 1. 检查布局参数是否发生变化
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
// 2. 执行布局
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
// 3. 如果布局发生变化,调用onLayout()
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
// 4. 通知OnGlobalLayoutListener
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
// 5. 清除布局标志
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}
3. ViewTreeObserver中的回调管理
// ViewTreeObserver.java
public class ViewTreeObserver {
private CopyOnWriteArray<OnPreDrawListener> mOnPreDrawListeners;
private CopyOnWriteArray<OnGlobalLayoutListener> mOnGlobalLayoutListeners;
// 通知所有OnPreDrawListener
final boolean dispatchOnPreDraw() {
boolean cancelDraw = false;
final CopyOnWriteArray<OnPreDrawListener> listeners = mOnPreDrawListeners;
if (listeners != null && listeners.size() > 0) {
CopyOnWriteArray.Access<OnPreDrawListener> access = listeners.start();
try {
int count = access.size();
for (int i = 0; i < count; i++) {
cancelDraw |= access.get(i).onPreDraw();
}
} finally {
listeners.end();
}
}
return cancelDraw;
}
// 通知所有OnGlobalLayoutListener
final void dispatchOnGlobalLayout() {
final CopyOnWriteArray<OnGlobalLayoutListener> listeners = mOnGlobalLayoutListeners;
if (listeners != null && listeners.size() > 0) {
CopyOnWriteArray.Access<OnGlobalLayoutListener> access = listeners.start();
try {
int count = access.size();
for (int i = 0; i < count; i++) {
access.get(i).onGlobalLayout();
}
} finally {
listeners.end();
}
}
}
}
4. 布局变化监听中的OnLayoutChangeListener
OnLayoutChangeListener
是 View
的直接监听器,在布局发生变化时被调用。与 ViewTreeObserver
的回调不同,它直接绑定到具体的 View
实例上:
// View.java - OnLayoutChangeListener 接口定义
public interface OnLayoutChangeListener {
void onLayoutChange(View v, int left, int top, int right, int bottom,
int oldLeft, int oldTop, int oldRight, int oldBottom);
}
4.1 OnLayoutChangeListener 的注册机制
// View.java - 添加布局变化监听器
public void addOnLayoutChangeListener(OnLayoutChangeListener listener) {
if (mListenerInfo == null) {
mListenerInfo = new ListenerInfo();
}
if (mListenerInfo.mOnLayoutChangeListeners == null) {
mListenerInfo.mOnLayoutChangeListeners = new ArrayList<OnLayoutChangeListener>();
}
mListenerInfo.mOnLayoutChangeListeners.add(listener);
}
// View.java - 移除布局变化监听器
public void removeOnLayoutChangeListener(OnLayoutChangeListener listener) {
if (mListenerInfo != null && mListenerInfo.mOnLayoutChangeListeners != null) {
mListenerInfo.mOnLayoutChangeListeners.remove(listener);
}
}
4.2 OnLayoutChangeListener 的触发时机
OnLayoutChangeListener
在 View.layout()
方法中被触发,具体在布局发生变化后:
// View.java - layout() 方法中的布局变化检测
public void layout(int l, int t, int r, int b) {
// 1. 检查布局参数是否发生变化
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
// 2. 执行布局
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
// 3. 如果布局发生变化,调用onLayout()并通知监听器
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
// 4. 通知OnLayoutChangeListener
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
// 5. 清除布局标志
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}
4.3 OnLayoutChangeListener 的执行机制
与 OnGlobalLayoutListener
类似,OnLayoutChangeListener
也是同步执行的,但它的执行时机更加精确:
// View.java - setFrame() 方法中的变化检测
protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;
// 1. 检查边界是否发生变化
if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
changed = true;
// 2. 更新边界值
int oldWidth = mRight - mLeft;
int oldHeight = mBottom - mTop;
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
// 3. 如果尺寸发生变化,标记需要重新测量
if (oldWidth != (right - left) || oldHeight != (bottom - top)) {
sizeChange(oldWidth, oldHeight, right - left, bottom - top);
}
}
return changed;
}
// View.java - sizeChange() 方法
protected void sizeChange(int w, int h, int oldw, int oldh) {
// 1. 调用 onSizeChanged() 回调
onSizeChanged(w, h, oldw, oldh);
// 2. 标记需要重新布局
if (mOverlay != null) {
mOverlay.getOverlayView().setRight(w);
mOverlay.getOverlayView().setBottom(h);
}
}
4.4 OnLayoutChangeListener 的性能特点
优势:
- 精确的时机控制:只在布局真正发生变化时才触发
- 详细的变更信息:提供新旧布局参数的对比
- 直接绑定:不需要通过 ViewTreeObserver 管理
注意事项:
- 同步执行:在 layout() 方法中同步执行,需要注意性能
- 避免循环:在回调中调用 requestLayout() 可能导致无限循环
// 性能优化示例:避免在 OnLayoutChangeListener 中调用 requestLayout()
view.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom,
int oldLeft, int oldTop, int oldRight, int oldBottom) {
// ❌ 避免:直接调用 requestLayout() 可能导致循环
// view.requestLayout();
// ✅ 推荐:使用 post() 延迟执行
view.post(new Runnable() {
@Override
public void run() {
// 在下一个消息循环中安全地修改布局
if (view.getWidth() != targetWidth) {
view.getLayoutParams().width = targetWidth;
view.requestLayout();
}
}
});
}
});
4.5 OnLayoutChangeListener 的实际应用场景
场景1:监听 RecyclerView 的布局变化
recyclerView.addOnLayoutChangeListener(object : View.OnLayoutChangeListener {
override fun onLayoutChange(v: View?, left: Int, top: Int, right: Int, bottom: Int,
oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int) {
val newWidth = right - left
val oldWidth = oldRight - oldLeft
if (newWidth != oldWidth) {
// 宽度发生变化,可能需要调整列数
val spanCount = if (newWidth > 600.dp) 3 else 2
(recyclerView.layoutManager as GridLayoutManager).spanCount = spanCount
}
}
})
场景2:监听键盘弹出导致的布局变化
rootView.addOnLayoutChangeListener(object : View.OnLayoutChangeListener {
override fun onLayoutChange(v: View?, left: Int, top: Int, right: Int, bottom: Int,
oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int) {
val heightDiff = oldBottom - bottom
if (heightDiff > 100) {
// 键盘弹出,调整UI布局
adjustLayoutForKeyboard(true)
} else if (heightDiff < -100) {
// 键盘收起,恢复UI布局
adjustLayoutForKeyboard(false)
}
}
})
场景3:监听 ViewPager 的页面切换
viewPager.addOnLayoutChangeListener(object : View.OnLayoutChangeListener {
override fun onLayoutChange(v: View?, left: Int, top: Int, right: Int, bottom: Int,
oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int) {
val newWidth = right - left
val oldWidth = oldRight - oldLeft
if (newWidth != oldWidth) {
// 宽度变化,可能需要重新计算页面位置
viewPager.setCurrentItem(currentItem, false)
}
}
})
5. 为什么OnGlobalLayoutListener时机最准确?
时机准确的原因:
- 布局完成后执行:
// 在View.layout()完成后,所有子视图都已经完成布局
// 此时视图的尺寸和位置都是确定的
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
// 布局完成后,通知OnGlobalLayoutListener
dispatchOnGlobalLayout();
}
- 测量和布局都已完成:
// 在layout()方法中,measure()已经完成
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
- 视图状态稳定:
// 布局完成后,视图的边界已经确定
boolean changed = setFrame(l, t, r, b);
// 此时可以安全地获取视图的尺寸
int width = right - left;
int height = bottom - top;
6. 为什么OnGlobalLayoutListener性能最好?
性能优势的原因:
- 不阻塞绘制流程:
// OnGlobalLayoutListener在布局完成后执行,不影响绘制
// 而OnPreDrawListener在绘制流程中执行,会阻塞绘制
public void draw(Canvas canvas) {
// OnPreDrawListener在这里执行,会阻塞绘制
boolean cancelDraw = dispatchOnPreDraw();
if (cancelDraw) return;
// 绘制逻辑...
}
- 避免重新布局循环:
// OnGlobalLayoutListener中调用requestLayout()是安全的
test.viewTreeObserver.addOnGlobalLayoutListener {
// 布局已经完成,此时修改布局参数不会造成循环
if (contentWidth > maxWidth) {
test.layoutParams.width = maxWidth
test.requestLayout() // 安全,会触发下一次布局
}
}
- 异步执行机制:
// ViewTreeObserver使用CopyOnWriteArray,支持并发访问
private CopyOnWriteArray<OnGlobalLayoutListener> mOnGlobalLayoutListeners;
// 回调执行不会阻塞主线程的其他操作
final void dispatchOnGlobalLayout() {
// 在布局完成后异步执行,不影响UI响应
// ...
}
7. 绘制流程与回调时机深度分析
Android视图绘制流程:
// 完整的视图绘制流程
ViewRootImpl.performTraversals() {
// 1. 测量阶段
performMeasure();
// 调用 View.onMeasure()
// 2. 布局阶段
performLayout();
// 调用 View.onLayout()
// 3. 绘制阶段
performDraw();
// 调用 View.onDraw()
}
关键差异:同步 vs 异步执行
OnPreDrawListener - 同步执行(影响绘制):
// View.java - draw()方法中的同步执行
public void draw(Canvas canvas) {
// 1. 在绘制流程中同步调用OnPreDrawListener
boolean cancelDraw = dispatchOnPreDraw(); // 同步执行,阻塞绘制
if (cancelDraw) {
return; // 如果返回true,直接取消本次绘制
}
// 2. 继续执行绘制逻辑
if (!dirtyOpaque) {
drawBackground(canvas);
}
// ... 其他绘制代码
}
// ViewTreeObserver.java - 同步通知
final boolean dispatchOnPreDraw() {
boolean cancelDraw = false;
final CopyOnWriteArray<OnPreDrawListener> listeners = mOnPreDrawListeners;
if (listeners != null && listeners.size() > 0) {
CopyOnWriteArray.Access<OnPreDrawListener> access = listeners.start();
try {
int count = access.size();
for (int i = 0; i < count; i++) {
// 同步执行每个监听器,阻塞绘制流程
cancelDraw |= access.get(i).onPreDraw();
}
} finally {
listeners.end();
}
}
return cancelDraw;
}
OnGlobalLayoutListener - 异步通知(不影响绘制):
// View.java - layout()方法中的异步通知
public void layout(int l, int t, int r, int b) {
// 1. 完成布局逻辑
boolean changed = setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
// 2. 布局完成后,异步通知OnGlobalLayoutListener
// 注意:这里不是同步调用,而是通过消息队列异步执行
if (mAttachInfo != null) {
mAttachInfo.mViewTreeObserver.dispatchOnGlobalLayout();
}
}
}
// ViewTreeObserver.java - 异步通知机制
final void dispatchOnGlobalLayout() {
final CopyOnWriteArray<OnGlobalLayoutListener> listeners = mOnGlobalLayoutListeners;
if (listeners != null && listeners.size() > 0) {
// 通过Handler异步执行,不阻塞当前线程
if (mHandler != null) {
mHandler.post(new Runnable() {
@Override
public void run() {
CopyOnWriteArray.Access<OnGlobalLayoutListener> access = listeners.start();
try {
int count = access.size();
for (int i = 0; i < count; i++) {
// 在下一个消息循环中执行,不阻塞绘制
access.get(i).onGlobalLayout();
}
} finally {
listeners.end();
}
}
});
}
}
}
为什么OnPreDrawListener会影响绘制流程?
- 同步执行机制:
// OnPreDrawListener在draw()方法中同步执行
public void draw(Canvas canvas) {
// 这里会阻塞绘制流程
boolean cancelDraw = dispatchOnPreDraw();
if (cancelDraw) return;
// 只有OnPreDrawListener执行完成后,才会继续绘制
// 如果OnPreDrawListener中有复杂计算或requestLayout(),会严重影响性能
}
- 可能触发重新布局循环:
// 在OnPreDrawListener中调用requestLayout()会导致问题
test.viewTreeObserver.addOnPreDrawListener {
test.requestLayout() // 立即触发重新布局
return true
}
问题分析:
- 当前正在执行
draw()
方法 OnPreDrawListener
中调用requestLayout()
requestLayout()
会标记需要重新测量和布局- 系统会立即触发新的布局和绘制流程
- 可能导致无限循环:
draw()
→OnPreDrawListener
→requestLayout()
→layout()
→draw()
→ …
为什么OnGlobalLayoutListener不会影响绘制流程?
- 异步执行机制:
// OnGlobalLayoutListener通过消息队列异步执行
final void dispatchOnGlobalLayout() {
// 不阻塞当前线程,在下一个消息循环中执行
mHandler.post(new Runnable() {
@Override
public void run() {
// 异步执行监听器回调
access.get(i).onGlobalLayout();
}
});
}
- 安全的重新布局:
// 在OnGlobalLayoutListener中调用requestLayout()是安全的
test.viewTreeObserver.addOnGlobalLayoutListener {
test.requestLayout() // 在下一个布局周期执行,安全
}
优势分析:
- 当前布局已经完成,
layout()
方法即将结束 OnGlobalLayoutListener
异步执行,不阻塞当前线程requestLayout()
会在下一个布局周期执行,不会造成循环- 绘制流程不受影响,可以正常进行
执行时序对比
OnPreDrawListener的执行时序:
View.draw() 开始
↓
dispatchOnPreDraw() 同步执行 ← 阻塞绘制流程
↓
OnPreDrawListener.onPreDraw() 执行 ← 可能包含复杂操作
↓
View.draw() 继续 ← 如果OnPreDrawListener耗时,会影响帧率
OnGlobalLayoutListener的执行时序:
View.layout() 开始
↓
onLayout() 执行
↓
dispatchOnGlobalLayout() 异步通知 ← 不阻塞布局流程
↓
View.layout() 结束
↓
View.draw() 开始 ← 绘制流程不受影响
↓
OnGlobalLayoutListener.onGlobalLayout() 异步执行 ← 在下一个消息循环中
实际性能影响对比
OnPreDrawListener的性能问题:
// 问题示例:在绘制前进行复杂计算
textView.viewTreeObserver.addOnPreDrawListener {
// 复杂计算会阻塞绘制流程
val textWidth = textView.paint.measureText(longText)
val complexResult = performComplexCalculation()
// 修改布局会立即触发重新布局
textView.layoutParams.width = complexResult
textView.requestLayout() // 危险!可能导致循环
return true
}
OnGlobalLayoutListener的性能优势:
// 优势示例:在布局完成后安全操作
textView.viewTreeObserver.addOnGlobalLayoutListener {
// 复杂计算不会阻塞绘制流程
val textWidth = textView.paint.measureText(longText)
val complexResult = performComplexCalculation()
// 修改布局是安全的
textView.layoutParams.width = complexResult
textView.requestLayout() // 安全,在下一个布局周期执行
}
8. 实际性能测试对比
测试场景:动态调整TextView宽度
// 使用OnPreDrawListener(性能较差)
textView.viewTreeObserver.addOnPreDrawListener {
val textWidth = textView.paint.measureText(textView.text.toString())
if (textWidth > maxWidth) {
textView.layoutParams.width = maxWidth
textView.requestLayout() // 立即触发重新布局,可能造成循环
}
return true
}
// 使用OnGlobalLayoutListener(性能较好)
textView.viewTreeObserver.addOnGlobalLayoutListener {
val textWidth = textView.paint.measureText(textView.text.toString())
if (textWidth > maxWidth) {
textView.layoutParams.width = maxWidth
textView.requestLayout() // 在下一个布局周期执行,安全
}
}
性能差异:
- OnPreDrawListener:可能造成绘制阻塞,影响帧率
- OnGlobalLayoutListener:不影响绘制流程,性能稳定
9. 最佳实践建议
选择OnGlobalLayoutListener的场景:
// ✅ 推荐:获取视图尺寸
imageView.viewTreeObserver.addOnGlobalLayoutListener {
val actualWidth = imageView.width
val actualHeight = imageView.height
// 根据实际尺寸进行后续处理
}
// ✅ 推荐:调整布局参数
textView.viewTreeObserver.addOnGlobalLayoutListener {
if (textView.width > maxWidth) {
textView.layoutParams.width = maxWidth
textView.requestLayout() // 安全
}
}
// ✅ 推荐:初始化依赖尺寸的操作
recyclerView.viewTreeObserver.addOnGlobalLayoutListener {
// RecyclerView布局完成后,可以安全地设置适配器
recyclerView.adapter = adapter
}
避免使用OnPreDrawListener的场景:
// ❌ 避免:在绘制前修改布局
view.viewTreeObserver.addOnPreDrawListener {
view.requestLayout() // 会阻塞绘制流程
return true
}
// ❌ 避免:复杂计算
view.viewTreeObserver.addOnPreDrawListener {
performComplexCalculation() // 会阻塞绘制
return true
}
方案对比分析
方案 | 触发时机 | 适用场景 | 性能影响 | 推荐度 | 特点 |
---|---|---|---|---|---|
post() |
消息队列延迟 | 简单延迟执行 | 中等 | ⭐⭐ | 简单但不够精确 |
OnPreDrawListener |
绘制前 | 需要在绘制前调整布局 | 中等 | ⭐⭐⭐ | 时机较精确,但可能影响绘制性能 |
OnGlobalLayoutListener |
布局完成后 | 获取视图尺寸,调整布局 | 较低 | ⭐⭐⭐⭐⭐ | 时机最准确,性能最好 |
OnLayoutChangeListener |
布局变化时 | 监听布局变化 | 较低 | ⭐⭐⭐⭐ | 提供详细的布局变化信息 |
最佳实践建议
1. 选择合适的时间点
- 布局调整:使用
OnGlobalLayoutListener
- 绘制前处理:使用
OnPreDrawListener
- 变化监听:使用
OnLayoutChangeListener
2. 避免内存泄漏
// 重要:在回调中移除监听器
view.viewTreeObserver.removeOnGlobalLayoutListener(this)
3. 性能优化
// 只在需要时才调用requestLayout()
if (contentWidth > maxWidth) {
test.layoutParams.width = maxWidth
test.requestLayout() // 只在必要时调用
}
4. 错误处理
test.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
try {
// 检查视图是否仍然有效
if (!test.isAttachedToWindow) {
return
}
// 移除监听器
test.viewTreeObserver.removeOnGlobalLayoutListener(this)
// 执行逻辑
// ...
} catch (e: Exception) {
// 异常处理
}
}
})
实际应用场景
场景1:动态调整文本宽度
// 根据文本内容动态调整TextView宽度
textView.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
textView.viewTreeObserver.removeOnGlobalLayoutListener(this)
val maxWidth = 200.dp
val textWidth = textView.paint.measureText(textView.text.toString())
if (textWidth > maxWidth) {
textView.layoutParams.width = maxWidth
textView.requestLayout()
}
}
})
场景2:获取视图实际尺寸
// 获取ImageView的实际显示尺寸
imageView.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
imageView.viewTreeObserver.removeOnGlobalLayoutListener(this)
val actualWidth = imageView.width
val actualHeight = imageView.height
// 根据实际尺寸进行后续处理
// ...
}
})
场景3:监听布局变化
// 监听RecyclerView的布局变化
recyclerView.addOnLayoutChangeListener(object : View.OnLayoutChangeListener {
override fun onLayoutChange(v: View?, left: Int, top: Int, right: Int, bottom: Int,
oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int) {
val newWidth = right - left
val oldWidth = oldRight - oldLeft
if (newWidth != oldWidth) {
// 宽度发生变化,进行相应处理
// ...
}
}
})
参考资料: