Android视图回调机制:从post到ViewTreeObserver,从源码分析到最佳实践

发布于:2025-08-11 ⋅ 阅读:(18) ⋅ 点赞:(0)

引言

在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

OnLayoutChangeListenerView 的直接监听器,在布局发生变化时被调用。与 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 的触发时机

OnLayoutChangeListenerView.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 的性能特点

优势:

  1. 精确的时机控制:只在布局真正发生变化时才触发
  2. 详细的变更信息:提供新旧布局参数的对比
  3. 直接绑定:不需要通过 ViewTreeObserver 管理

注意事项:

  1. 同步执行:在 layout() 方法中同步执行,需要注意性能
  2. 避免循环:在回调中调用 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时机最准确?

时机准确的原因:
  1. 布局完成后执行
// 在View.layout()完成后,所有子视图都已经完成布局
// 此时视图的尺寸和位置都是确定的
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
    onLayout(changed, l, t, r, b);
    // 布局完成后,通知OnGlobalLayoutListener
    dispatchOnGlobalLayout();
}
  1. 测量和布局都已完成
// 在layout()方法中,measure()已经完成
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
    onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
    mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
  1. 视图状态稳定
// 布局完成后,视图的边界已经确定
boolean changed = setFrame(l, t, r, b);
// 此时可以安全地获取视图的尺寸
int width = right - left;
int height = bottom - top;

6. 为什么OnGlobalLayoutListener性能最好?

性能优势的原因:
  1. 不阻塞绘制流程
// OnGlobalLayoutListener在布局完成后执行,不影响绘制
// 而OnPreDrawListener在绘制流程中执行,会阻塞绘制
public void draw(Canvas canvas) {
    // OnPreDrawListener在这里执行,会阻塞绘制
    boolean cancelDraw = dispatchOnPreDraw();
    if (cancelDraw) return;

    // 绘制逻辑...
}
  1. 避免重新布局循环
// OnGlobalLayoutListener中调用requestLayout()是安全的
test.viewTreeObserver.addOnGlobalLayoutListener {
    // 布局已经完成,此时修改布局参数不会造成循环
    if (contentWidth > maxWidth) {
        test.layoutParams.width = maxWidth
        test.requestLayout() // 安全,会触发下一次布局
    }
}
  1. 异步执行机制
// 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会影响绘制流程?
  1. 同步执行机制
// OnPreDrawListener在draw()方法中同步执行
public void draw(Canvas canvas) {
    // 这里会阻塞绘制流程
    boolean cancelDraw = dispatchOnPreDraw();
    if (cancelDraw) return;

    // 只有OnPreDrawListener执行完成后,才会继续绘制
    // 如果OnPreDrawListener中有复杂计算或requestLayout(),会严重影响性能
}
  1. 可能触发重新布局循环
// 在OnPreDrawListener中调用requestLayout()会导致问题
test.viewTreeObserver.addOnPreDrawListener {
    test.requestLayout() // 立即触发重新布局
    return true
}

问题分析:

  • 当前正在执行draw()方法
  • OnPreDrawListener中调用requestLayout()
  • requestLayout()会标记需要重新测量和布局
  • 系统会立即触发新的布局和绘制流程
  • 可能导致无限循环:draw()OnPreDrawListenerrequestLayout()layout()draw() → …
为什么OnGlobalLayoutListener不会影响绘制流程?
  1. 异步执行机制
// OnGlobalLayoutListener通过消息队列异步执行
final void dispatchOnGlobalLayout() {
    // 不阻塞当前线程,在下一个消息循环中执行
    mHandler.post(new Runnable() {
        @Override
        public void run() {
            // 异步执行监听器回调
            access.get(i).onGlobalLayout();
        }
    });
}
  1. 安全的重新布局
// 在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) {
            // 宽度发生变化,进行相应处理
            // ...
        }
    }
})

参考资料:


网站公告

今日签到

点亮在社区的每一天
去签到