Android-高级 UI-04- 自定义View

186 阅读10分钟

1 自定义View分类

image.png

1.1. 自定义View

在没有现成的View,需要自己实现的时候,就使用自定义View,一般继承自View,SurfaceView或其他的View

1.2. 自定义ViewGroup

自定义ViewGroup一般是利用现有的组件根据特定的布局方式来组成新的组件,大多继承自ViewGroup或各种Layout

2 自定义View的绘制流程

image.png

  • 自定义View主要是实现 onMeasure + onDraw
  • 自定义ViewGroup主要是实现onMeasure + onLayout

1.自定义View的开发

image.png
image.png

在 Android 中,自定义 View 的三个核心流程是 测量(Measure)布局(Layout)绘制(Draw) 。以下是每个流程的详细解析:


1. 测量(Measure)

测量阶段的主要目的是计算 View 的宽高,结果存储在 getMeasuredWidth()getMeasuredHeight() 中。

核心方法
  • onMeasure(int widthMeasureSpec, int heightMeasureSpec)

    • 父视图通过 MeasureSpec 将宽高限制传递给子视图。
    • 子视图根据这些限制自行计算尺寸,并通过 setMeasuredDimension() 方法设置自身的宽高。
MeasureSpec 介绍

MeasureSpec 是父容器传递给子视图的宽高规格,包含两部分信息:

  • 模式(Mode)

    • UNSPECIFIED:父容器不对视图的大小做任何限制(少见)。
    • EXACTLY:父容器指定了确切的宽高,子视图必须遵守。
    • AT_MOST:父容器指定了一个最大宽高,子视图不能超过这个范围。
  • 尺寸(Size) :模式对应的具体数值。

自定义 View 示例
java
复制代码
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);

    int width = 100; // 默认宽度
    int height = 100; // 默认高度

    if (widthMode == MeasureSpec.EXACTLY) {
        width = widthSize; // 父容器指定确切宽度
    } else if (widthMode == MeasureSpec.AT_MOST) {
        width = Math.min(width, widthSize); // 限制最大宽度
    }

    if (heightMode == MeasureSpec.EXACTLY) {
        height = heightSize;
    } else if (heightMode == MeasureSpec.AT_MOST) {
        height = Math.min(height, heightSize);
    }

    setMeasuredDimension(width, height);
}

2. 布局(Layout)

布局阶段的主要任务是确定 View 的位置(lefttoprightbottom),即视图在父容器中的具体位置。

核心方法
  • onLayout(boolean changed, int left, int top, int right, int bottom)

    • 通过参数指定 View 在屏幕中的位置。
    • 在 ViewGroup 中,会循环对子视图调用 layout() 方法。
自定义 ViewGroup 示例
java
复制代码
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    int childCount = getChildCount();
    int curLeft = 0;

    for (int i = 0; i < childCount; i++) {
        View child = getChildAt(i);
        int childWidth = child.getMeasuredWidth();
        int childHeight = child.getMeasuredHeight();
        child.layout(curLeft, 0, curLeft + childWidth, childHeight);
        curLeft += childWidth;
    }
}

3. 绘制(Draw)

绘制阶段负责将 View 绘制到屏幕上,通常用来自定义内容的呈现。

核心方法
  • onDraw(Canvas canvas)

    • 使用 Canvas 对象绘制内容。

    • 常用 API:

      • drawRect():绘制矩形。
      • drawCircle():绘制圆形。
      • drawText():绘制文本。
      • drawBitmap():绘制位图。
自定义 View 示例
java
复制代码
@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    Paint paint = new Paint();
    paint.setColor(Color.RED);
    paint.setStyle(Paint.Style.FILL);

    // 绘制一个红色的圆
    canvas.drawCircle(getWidth() / 2, getHeight() / 2, Math.min(getWidth(), getHeight()) / 2, paint);
}

三者的联系

  1. 测量阶段:确定宽高,用于后续布局和绘制。
  2. 布局阶段:确定位置,决定子视图如何排列。
  3. 绘制阶段:根据宽高和位置,最终渲染内容。

注意点

  1. 重写 onMeasure() 时必须调用 setMeasuredDimension() ,否则会抛出异常。
  2. onLayout() 只在 ViewGroup 中重写,普通 View 不需要。
  3. onDraw() 不要在方法中创建对象或执行耗时操作,避免性能问题。

通过以上三个流程的配合,可以高效地实现自定义 View 的功能并满足复杂布局和绘制需求。

2.Paint常用方法

image.png
image.png

3.Canvas绘制

3.1 Canvas基本图形的绘制

image.png

3.2Canvas的变换操作

image.png

3.3Canvas的保存与回滚

image.png
image.png
image.png
image.png

3.4屏幕显示与Canvas的关系

image.png
image.png
image.png

3.5文字绘制

image.png
image.png
image.png

3.6圆形头像绘制

image.png
image.png
image.png

3.7 Matrix矩阵操作

image.png

  • 平移矩阵推导
    image.png
  • 缩放矩阵推导
    image.png
  • 旋转矩阵的推导
    image.png
    image.png
    image.png
    image.png
    image.png
    image.png
  • 操作矩阵
    image.png
    image.png
    image.png
    image.png
    image.png
    image.png
    image.png

4.View矩阵的原理

image.png
image.png
image.png
image.png
image.png

5自定义drawable

image.png
image.png
image.png

3 View的层级结构

image.png

4 面试题

4.1 LayoutParams 是什么?与MeasureSpec有关系吗?

1. LayoutParams

定义
  • LayoutParams 是 Android 中用于定义视图布局规则的一个类,属于 View 的父容器(ViewGroup)的一个内部类。
  • 它描述了视图在其父容器中的 宽高、位置和边距 等信息。
作用
  • 每个 ViewGroup 都会定义自己的 LayoutParams 子类(如 LinearLayout.LayoutParams, RelativeLayout.LayoutParams),用来解析子视图在布局中的排列规则。
  • LayoutParams 在布局过程中会被 ViewGroup 使用来布局子视图。
常见参数
  • widthheight:定义视图的宽和高,常见值包括 match_parent, wrap_content, 或具体尺寸(如 100dp)。
  • margin:用于定义视图之间的间距。
  • gravity(在某些布局中使用):子视图对齐方式。
代码示例
java
复制代码
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
        LinearLayout.LayoutParams.MATCH_PARENT,
        LinearLayout.LayoutParams.WRAP_CONTENT
);
params.setMargins(16, 16, 16, 16);
view.setLayoutParams(params);

2. MeasureSpec

定义
  • MeasureSpec 是 Android 中用来描述视图测量要求的一个工具类。
  • 它是一种 压缩的整数值,将测量模式和尺寸合并在一起,用来告诉子视图如何测量自己。
作用
  • MeasureSpec 是布局过程中 ViewonMeasure() 方法的关键参数。
  • 它确定了视图的 测量模式(Mode)测量大小(Size)
测量模式
  • UNSPECIFIED:父容器对视图的大小没有任何限制,视图可以是任意大小。
  • EXACTLY:父容器指定了精确的大小,视图必须匹配这个尺寸。
  • AT_MOST:父容器指定了一个最大值,视图的大小不能超过这个值。
代码示例
java
复制代码
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(200, MeasureSpec.EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.AT_MOST);
view.measure(widthMeasureSpec, heightMeasureSpec);

3. LayoutParams 与 MeasureSpec 的关系

虽然两者是 Android 布局过程中不同阶段的工具,但它们在布局测量和排版的过程中紧密关联:

布局阶段
  1. 获取 LayoutParams

    • 父容器通过子视图的 LayoutParams 获取布局约束信息(如宽、高、margin)。
  2. 生成 MeasureSpec

    • 父容器根据自己的尺寸、子视图的 LayoutParams(如 wrap_contentmatch_parent),为子视图生成对应的 MeasureSpec
  3. 调用 onMeasure

    • 父容器将生成的 MeasureSpec 传递给子视图,子视图根据 MeasureSpec 计算自己的大小。
流程图
rust
复制代码
父容器 --> 获取子视图的 LayoutParams --> 根据父容器尺寸和 LayoutParams 生成 MeasureSpec --> 传递 MeasureSpec 给子视图 --> 子视图测量自己

4. 总结

  • LayoutParams 是父容器用来描述子视图布局规则的类,负责 布局阶段

  • MeasureSpec 是父容器用来告知子视图测量要求的工具,负责 测量阶段

  • 两者的联系在于:

    • 父容器通过 LayoutParams 理解子视图的布局规则。
    • 父容器基于 LayoutParams 和自己的约束条件,生成 MeasureSpec 传递给子视图完成测量。

4.2 为什么要measure

image.png

在 Android 中,measure 是布局过程中的第一步,其作用是测量视图的尺寸。视图的尺寸直接决定了它在屏幕上的布局效果,因此测量是整个视图系统工作的基础。下面详细分析为什么需要 measure


1. 为什么要进行测量?

  • 动态布局: Android 应用需要适配多种屏幕尺寸和分辨率,视图的大小不能提前固定,必须根据父容器和子视图的需求动态计算。
  • 层级结构复杂: Android 中的视图层级可能非常复杂,父容器和子视图需要协作来确定每个视图的尺寸。例如,LinearLayout 中每个子视图的宽高可能由其他子视图决定。
  • 资源约束: 在布局过程中,需要确保每个视图的大小符合父容器的空间限制,从而避免越界或浪费屏幕空间。

2. Measure 的目的是什么?

  • 确定每个视图的大小: 每个视图都需要通过测量确定自己的宽度和高度,才能决定如何绘制内容。

  • 满足布局规则: 不同的父容器(如 LinearLayout, ConstraintLayout)有不同的布局规则,这些规则影响子视图的测量结果。例如:

    • wrap_content:根据内容决定大小。
    • match_parent:占满父容器的剩余空间。
  • 为后续布局和绘制服务: 测量阶段确定了视图的大小,接下来的布局阶段会根据测量结果,决定视图的位置,最后在绘制阶段进行渲染。


3. Measure 的工作原理

测量通过调用视图的 measure() 方法触发,主要依赖以下两点:

  1. 父容器传递的测量要求(MeasureSpec)

    • 包括测量模式和尺寸限制。
  2. 视图的 LayoutParams

    • 定义视图的布局规则(如 wrap_content, match_parent)。

父容器根据自身尺寸和子视图的 LayoutParams 生成 MeasureSpec,再递归调用子视图的 measure() 方法,直到整棵视图树的测量完成。


4. 如果不进行测量会怎样?

  • 如果没有测量,视图的尺寸将无法确定,Android 系统就不知道视图占据的空间和位置。
  • 最终,视图无法正确绘制,用户将看不到期望的布局效果。

5. 总结:为什么要 Measure?

measure 的本质是解决 动态布局屏幕适配 问题:

  • 确定视图的大小,以适配不同的屏幕尺寸和分辨率。
  • 实现复杂的视图层级和布局规则。
  • 为布局阶段和绘制阶段提供必要的尺寸信息。

measure 是 Android 渲染管线中的关键步骤,没有测量,后续的布局和绘制工作都无法完成。

4.3 Android两种坐标系

image.png

image.png

4.4 getMeasureWidth与getWidth的区别

在 Android 中,getMeasuredWidth()getWidth() 都是用于获取视图宽度的,但它们有明显的区别,尤其是在视图的生命周期中:


1. getMeasuredWidth()

  • 定义:表示视图经过 测量阶段 后得到的宽度。

  • 来源:该值是 measure() 方法根据父容器提供的 MeasureSpec 计算出的值。

  • 特点

    • 仅在测量阶段之后才有意义。
    • 可能与最终视图的实际宽度不同(视图的布局可能会调整测量值)。
    • 在视图的 onMeasure() 方法中可以获取此值。
  • 使用场景

    • 用于了解测量阶段的结果,常用于自定义视图中,分析布局计算的宽度。

2. getWidth()

  • 定义:表示视图在 布局阶段 完成后,实际分配的宽度。

  • 来源:该值是在布局阶段后,由 layout() 方法确定的宽度。

  • 特点

    • 是视图在屏幕上显示的实际宽度。
    • 只有在视图布局完成后,getWidth() 才能返回有效值。
    • 如果在布局完成之前调用,可能返回 0
  • 使用场景

    • 用于实际的渲染或操作,确保视图已经布局好,并已在屏幕上占据指定的宽度。

3. 核心区别

特性getMeasuredWidth()getWidth()
阶段测量阶段后(measure布局阶段后(layout
含义测量的宽度实际绘制的宽度
修改可能性可被布局阶段调整已经固定,不可再修改
调用时机测量完成后即可调用布局完成后才能获取有效值
返回值稳定性可能与最终宽度不同是最终用于绘制的宽度

4. 示例

假设一个视图的 LayoutParamswrap_content,测量阶段会根据内容计算出宽度:

java
复制代码
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int measuredWidth = getMeasuredWidth(); // 测量的宽度
}

在布局阶段,可能会根据父容器的约束调整宽度:

java
复制代码
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    int width = getWidth(); // 实际分配的宽度
}

5. 注意事项

  • 如果只需要知道视图的最终显示宽度,请使用 getWidth()
  • 如果需要在自定义视图中了解测量结果,并在布局阶段前调整子视图布局,请使用 getMeasuredWidth()
  • 在视图未完成测量或布局之前,调用这两个方法都会返回 0