重学Android:自定义View基础(一)

前言

作为一名安卓开发,也被称为大前端,做一个美观的界面,是我们必备的基础技能,可能在开发中我们最常用的是系统自带的View ,因为他能满足绝大部分需求,难一点的我们也可以上Github上找个三方库使用,少数情况下会让我们进行自定义View,当然这不代表着我们可以不去掌握其原理,因为它是通往中高级程序员的必经之路,也是大厂面试的热门知识,只有熟练掌握其核心原理,才能让我们在后续的开发中游刃有余。

由于这是开篇文章,说的有点多,笔者是想借着写博客的机会,把那些最不经意的基础打牢一下,并且加上自己的拙见与大家分享,共同进步。

自定义View简介

自定义View是Android开发中的一种常见需求,它允许开发者创建复杂的用户界面组件,以满足特定的设计需求。自定义View 的好处在于可以完全控制View的外观和行为。常见的是 extend Viewextend ViewGroup 以及系统自带的View

1. onMeasure


onMeasure方法用于测量View的尺寸。它的主要任务是决定View 的宽度和高度。以下是一个简单的自定义View 示例,它在onMeasure中实现了固定大小的测量逻辑。

示例代码

java 复制代码
public class CustomView extends View {
    public CustomView(Context context) {
        super(context);
    }

    public CustomView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 期望的宽高
        int desiredWidth = 200;
        int desiredHeight = 200;

        // 获取父View提供的宽高
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);

        // 测量宽度
        width = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY ?
                width : desiredWidth;

        // 测量高度
        height = MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY ?
                height : desiredHeight;

        // 设置测量后的宽高
        setMeasuredDimension(width, height);
    }
}

2. onDraw


onDraw方法用于绘制View的内容。在此方法中,使用Canvas绘制图形或文字。

示例代码

Java 复制代码
@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    Paint paint = new Paint(); // 创建画笔
    paint.setColor(Color.BLUE); // 设置颜色为蓝色

    // 在中心绘制一个半径为100的圆
    canvas.drawCircle(getWidth() / 2, getHeight() / 2, 100, paint);
}

3. onTouch


onTouch方法用于处理触摸事件,使View能够响应用户的触摸操作。

示例代码

java 复制代码
@Override
public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            // 处理按下事件
            // 可以在这里改变View的状态或外观
            break;
        case MotionEvent.ACTION_MOVE:
            // 处理移动事件
            // 例如,移动View或改变某些属性
            break;
        case MotionEvent.ACTION_UP:
            // 处理抬起事件
            // 可以在这里完成某个操作,比如动画结束
            break;
    }
    return true; // 返回true表示事件已被处理
}

4. 自定义属性


通过自定义属性,可以使自定义View 在XML中更加灵活。如果你想写一个自定义View的三方库,自定义属性是必须掌握的。

定义

res/values/attrs.xml中添加自定义属性:

java 复制代码
<declare-styleable name="CustomView">
    <attr name="customColor" format="color" />
    <attr name="customSize" format="dimension" />
</declare-styleable>

使用

在自定义View的构造函数中读取这些属性:

java 复制代码
public CustomView(Context context, AttributeSet attrs) {
    super(context, attrs);
    
    TypedArray a = context.getTheme().obtainStyledAttributes(
            attrs,
            R.styleable.CustomView,
            0, 0);

    try {
        // 读取自定义颜色属性,默认为黑色
        int customColor = a.getColor(R.styleable.CustomView_customColor, Color.BLACK);
        // 读取自定义尺寸属性,默认为50dp
        float customSize = a.getDimension(R.styleable.CustomView_customSize, 50);
        
        // 使用customColor和customSize进行后续逻辑
    } finally {
        a.recycle(); // 释放TypedArray资源!!!
    }
}

5. 测量模式


在Android中,自定义View 的测量过程由三种测量模式决定:EXACTLYAT_MOSTUNSPECIFIED

三种布局模式

在 Android 布局中,父 View 可以通过不同的模式给子 View 传递尺寸限制,常见的有以下三种模式:

1. EXACTLY(确切模式)
  • 定义:父 View 给子 View 传递了一个确切的尺寸,子 View 应该使用这个尺寸。
  • 适用场景 :一般用于设置为 match_parent 或具体的尺寸值时。

例如,设置子 View 的宽度为父 View 的 100dp,子 View 必须遵循这一具体尺寸。


2. AT_MOST(最多模式)
  • 定义:父 View 给子 View 传递了一个最大尺寸,子 View 可以选择小于或等于这个尺寸。
  • 适用场景 :一般用于设置为 wrap_content,子 View 根据内容大小自适应,但不能超过父 View 的最大限制。

例如,当子 View 选择包裹内容时,它会根据内容大小自适应,但不能超过父 View 给定的最大限制。


3. UNSPECIFIED(未指定模式)
  • 定义:父 View 没有给子 View 限制尺寸,子 View 可以根据自身需求决定尺寸。
  • 适用场景 :一般用于需要自由尺寸的情况,例如 ListViewScrollView 中的子 View。

这种模式一般在自定义控件或特定场景下使用,较少应用于常规布局。

源码解析

在自定义View的onMeasure方法中,我们可以通过MeasureSpec类来解析这三种模式。MeasureSpec包含两个主要信息:Mode(模式)Size(大小)

MeasureSpec的源代码

java 复制代码
public static final int UNSPECIFIED = 0;
public static final int EXACTLY = 1;
public static final int AT_MOST = 2;

private static final int MODE_SHIFT = 30;
private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

//使用掩码(mask)来提取高2位。
public static int getMode(int measureSpec) {
    return (measureSpec & MODE_MASK);  // MODE_MASK = 0x3
}

//通过掩码去除高2位,获取低30位的值。
public static int getSize(int measureSpec) {
    return (measureSpec & ~MODE_MASK);  // ~MODE_MASK = 0xFFFFFFFC
}

测量模式存储形式

MeasureSpec是一个32位的整数(int 值 4个字节,32bit),其中包含模式和大小信息。

  • 高位(位31到位30):用于存储模式。
  • 低位(位29到位0):用于存储大小。

二进制表示

EXACTLY0b01000000000000000000000000000000(只关心高两位)

java 复制代码
public static final int EXACTLY     = 1 << MODE_SHIFT;

AT_MOST0b10000000000000000000000000000000

java 复制代码
public static final int AT_MOST     = 2 << MODE_SHIFT;

UNSPECIFIED0b00000000000000000000000000000000

java 复制代码
public static final int UNSPECIFIED = 0 << MODE_SHIFT;

各模式示例代码

  1. EXACTLY(确切模式)
java 复制代码
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // 获取父View提供的确切宽高
    int width = MeasureSpec.getSize(widthMeasureSpec);
    int height = MeasureSpec.getSize(heightMeasureSpec);

    // 使用父View提供的尺寸
    setMeasuredDimension(width, height);
}

应用场景 :当父布局设置为match_parent时,子View的宽高将完全匹配父布局。

  1. AT_MOST(最多模式)
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;
    int height;

    // 处理宽度
    if (widthMode == MeasureSpec.AT_MOST) {
        // 计算子View的宽度,最大不超过widthSize
        width = Math.min(desiredWidth, widthSize);
    } else {
        width = desiredWidth; // 使用期望宽度
    }

    // 处理高度
    if (heightMode == MeasureSpec.AT_MOST) {
        // 计算子View的高度,最大不超过heightSize
        height = Math.min(desiredHeight, heightSize);
    } else {
        height = desiredHeight; // 使用期望高度
    }

    setMeasuredDimension(width, height);
}

应用场景 :父布局使用wrap_content,子View可以根据内容自适应,但不会超过父布局的最大值。

  1. UNSPECIFIED(未指定模式)
java 复制代码
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // 在此模式下,子View可以自由选择尺寸
    setMeasuredDimension(desiredWidth, desiredHeight);
}

应用场景 :适用于需要灵活大小的场景,例如在ScrollView 中,子View的尺寸可以根据内容进行扩展。

6. 安卓自定义View刷新调用顺序图

scss 复制代码
    ┌────────────────────┐
    │    调用invalidate() │
    └────────────────────┘
              ↓
    ┌────────────────────────────┐
    │  调用View.invalidate()     │
    └────────────────────────────┘
              ↓
   ┌──────────────────────────────┐
   │   调用ViewParent.invalidate() │ (若有父视图,向上请求刷新)
   └──────────────────────────────┘
              ↓
   ┌──────────────────────────────┐
   │   调用请求重绘机制:UI线程刷新 │
   └──────────────────────────────┘
              ↓
     ┌────────────────────────┐
     │ 调用requestLayout()    │  (如果布局变化,调用此方法会触发onMeasure)
     └────────────────────────┘
              ↓
    ┌────────────────────────────┐
    │  调用onMeasure()           │
    └────────────────────────────┘
              ↓
    ┌────────────────────────────┐
    │   调用setMeasuredDimension │  (设置最终宽高)
    └────────────────────────────┘
              ↓
     ┌────────────────────────┐
     │ 调用onLayout()         │ (进行视图的布局)
     └────────────────────────┘
              ↓
   ┌──────────────────────────────┐
   │ 调用onDraw()                 │  (进行绘制操作)
   └──────────────────────────────┘
              ↓
   ┌──────────────────────────────┐
   │    更新显示,重新渲染视图      │
   └──────────────────────────────┘

简单来说就是三步走,onMeasureonLayoutonDraw,其中onLayout一般情况下,普通视图不需要重写此方法,除非视图具有子视图并需要自己进行布局。比如你如果想自定义一个某东搜索框下面的历史搜索记录布局的时候,就必须重写onLayout了。

总结

方法 作用 何时重写
onMeasure() 测量视图的大小 当视图的尺寸依赖于父视图的MeasureSpec或动态计算时
onLayout() 布局子视图的位置 当视图是布局容器或需要动态布局子视图时
onDraw() 绘制视图的内容 当需要自定义视图内容的绘制时,几乎所有自定义视图都需要重写

7. 最后


基础只是理论概念,要想牢记,还得在实战中运用,当然上面都会了,就能和面试官吹牛逼了。再会!

另外给喜欢记笔记的同学安利一款好用的云笔记软件,对比大部分国内的这个算还不错的,免费好用:wolai
_ 蓝 橙

相关推荐
长亭外的少年1 小时前
Kotlin 编译失败问题及解决方案:从守护进程到 Gradle 配置
android·开发语言·kotlin
建群新人小猿4 小时前
会员等级经验问题
android·开发语言·前端·javascript·php
1024小神5 小时前
tauri2.0版本开发苹果ios和安卓android应用,环境搭建和最后编译为apk
android·ios·tauri
兰琛5 小时前
20241121 android中树结构列表(使用recyclerView实现)
android·gitee
Y多了个想法6 小时前
RK3568 android11 适配敦泰触摸屏 FocalTech-ft5526
android·rk3568·触摸屏·tp·敦泰·focaltech·ft5526
NotesChapter7 小时前
Android吸顶效果,并有着ViewPager左右切换
android
_祝你今天愉快8 小时前
分析android :The binary version of its metadata is 1.8.0, expected version is 1.5.
android
暮志未晚Webgl8 小时前
109. UE5 GAS RPG 实现检查点的存档功能
android·java·ue5
麦田里的守望者江9 小时前
KMP 中的 expect 和 actual 声明
android·ios·kotlin
Dnelic-9 小时前
解决 Android 单元测试 No tests found for given includes:
android·junit·单元测试·问题记录·自学笔记