【Android】图解View的工作流程原理

在这里插入图片描述

入口

DecorView如何加载到Window中

在这里插入图片描述


MeasureSpec

该类是View的内部类,封装View的规格尺寸。
在这里插入图片描述
他就是一个32位的int值,高2为代表 specMode(测量模式),低30位代表specSize(测量大小)
specMode:UNSPECIFIED AT_MOST EXACTLY

对于每个View都有对应的MeasureSpec,在测量流程中,通过makeMeasureSpec() 来保存宽和高,通过
getMode()getSize() 得到模式和宽高
MeasureSpec自身的布局参数和父容器的测量规格共同影响

那么顶层View的DecorView没有父容器,怎么得到测量规格呢?
通过getRootMeasureSpec()

/**
 * 根据窗口大小和根视图尺寸,获取根视图的MeasureSpec
 *
 * @param windowSize    窗口大小
 * @param rootDimension 根视图尺寸
 * @return 根视图的MeasureSpec
 */
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
    int measureSpec;
    switch (rootDimension) {
        case ViewGroup.LayoutParams.MATCH_PARENT:
            // 如果根视图尺寸为MATCH_PARENT(即填充父窗口),窗口无法调整大小。
            // 强制根视图尺寸为窗口大小,使用MeasureSpec.EXACTLY模式。
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // 如果根视图尺寸为WRAP_CONTENT(即自适应内容),窗口可以调整大小。
            // 设置根视图最大尺寸为窗口大小,使用MeasureSpec.AT_MOST模式。
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // 如果根视图尺寸为具体的数值,窗口希望有确定的大小。
            // 强制根视图尺寸为指定的大小,使用MeasureSpec.EXACTLY模式。
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
    }
    return measureSpec;
}


Measure

在某些极端情况下,系统可能需要多次measure才能确定最终的测量宽/高

在这里插入图片描述


View的测量

在这里插入图片描述

ViewGroup的测量

ViewGroup没有onMeasure(),用measureChildren()去递归调用子元素的测量方法measureChild()
在这里插入图片描述


Layout

View的layout()

在这里插入图片描述

在这里插入图片描述


Draw

在这里插入图片描述

官方文档阐述为:

  1. 如果需要,则绘制背景
  2. 保存当前canvas层(可以不执行)
  3. 绘制View的内容
  4. 绘制子View
  5. 如果需要,则绘制View的褪色边缘,类似于阴影效果(可以不执行)
  6. 绘制装饰,例如滚动条
  7. 如果有必要,绘制默认的焦点高亮显示(可以不执行)

1、绘制背景

调用View的drawBackground()来执行

/**
 * Draws the background onto the specified canvas.
 *
 * @param canvas Canvas on which to draw the background
 */
@UnsupportedAppUsage
private void drawBackground(Canvas canvas) {
    final Drawable background = mBackground; // 获取背景Drawable对象
    if (background == null) { // 如果背景Drawable为null
        return; // 直接返回,不进行绘制
    }
    setBackgroundBounds(); // 设置背景Drawable的边界矩形
    ...
    final int scrollX = mScrollX; // 获取View的当前水平滚动偏移量
    final int scrollY = mScrollY; // 获取View的当前垂直滚动偏移量
    if ((scrollX | scrollY) == 0) { // 如果水平和垂直滚动偏移量都为0
        background.draw(canvas); // 直接绘制背景Drawable在画布上
    } else { // 如果有滚动偏移量
        canvas.translate(scrollX, scrollY); // 将画布平移至滚动偏移量的位置
        background.draw(canvas); // 绘制背景Drawable在平移后的画布上
        canvas.translate(-scrollX, -scrollY); // 恢复画布的原始位置
    }
}

3、绘制View内容

onDraw() 需要去自己进行重写实现

4、绘制子View

dispathchDraw() 需要去自己进行重写实现

ViewGroup进行了重写:

@Override
protected void dispatchDraw(Canvas canvas) {
    ...
    for (int i = 0; i < childrenCount; i++) { // 遍历子View
        while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) { // 如果当前索引为临时索引
        
            final View transientChild = mTransientViews.get(transientIndex);
            
            if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE || 
                transientChild.getAnimation() != null) { // 如果临时子View可见或者临时子View有动画
                more |= drawChild(canvas, transientChild, drawingTime); // 在画布上绘制临时子View,并返回是否还有更多绘制
            }
            
            transientIndex++; // 增加临时索引
            
            if (transientIndex >= transientCount) { // 如果临时索引超过了临时子View的数量
                transientIndex = -1; // 重置临时索引
            }
        }

        final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder); // 获取并验证预排序的子View索引
        final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex); // 根据索引找到对应View
        
        if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) { // 如果子View可见或者有动画
            more |= drawChild(canvas, child, drawingTime); // 在画布上绘制子View,并返回是否还有更多绘制
        }
    }
    ...
}

最后调用了drawChild()方法,而该方法其实返回的是child的draw()方法,即View的draw():

/**
 * This method is called by ViewGroup.drawChild() to have each child view draw itself.
 *
 * This is where the View specializes rendering behavior based on layer type,
 * and hardware acceleration.
 */
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
        ...
    if (!drawingWithDrawingCache) { // 1. 没有使用绘制缓存
        if (drawingWithRenderNode) { // 使用RenderNode进行绘制
            mPrivateFlags &= ~PFLAG_DIRTY_MASK;
            ((RecordingCanvas) canvas).drawRenderNode(renderNode);
        } else {
            // 对于没有背景的布局,快速路径
            if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) { // 子View标记为不需要被绘制
                mPrivateFlags &= ~PFLAG_DIRTY_MASK;
                dispatchDraw(canvas); // 调用dispatchDraw()方法进行绘制
            } else {
                draw(canvas); // 调用draw()方法进行绘制
            }
        }
    } else if (cache != null) { // 2. 存在绘制缓存
        mPrivateFlags &= ~PFLAG_DIRTY_MASK;
        if (layerType == LAYER_TYPE_NONE || mLayerPaint == null) {
            // 没有图层画笔,使用临时画笔绘制位图
            ...
        } else {
            // 使用图层画笔绘制位图,合并两个透明度,并恢复
            ...
        }
    }
        ...
    return more; // 返回是否还有更多需要绘制的内容
}

在这里插入图片描述

6、绘制装饰

View的DrawForeground()

/**
 * 绘制视图的前景内容。
 *
 * <p>前景内容可以包括滚动条、前景绘制或其他视图特定的装饰。前景绘制在主视图内容之上。</p>
 *
 * @param canvas 用于绘制的画布
 */
public void onDrawForeground(Canvas canvas) {
    onDrawScrollIndicators(canvas); // 调用绘制滚动指示器的方法
    onDrawScrollBars(canvas); // 调用绘制滚动条的方法

    final Drawable foreground = mForegroundInfo != null ? mForegroundInfo.mDrawable : null;
    if (foreground != null) {
        // 如果存在前景就绘制
        ...
        foreground.draw(canvas);
    }
}

  • 14
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

xoliu1

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值