1.自定义View总结

[[自写动画自定义View]]

基本流程

  1. 明确需求,确定你想实现的效果。
  2. 确定是使用组合控件的形式还是全新自定义的形式,组合控件即使用多个系统控件来合成一个新控件,你比如 titilebar,这种形式相对简单。
  3. 如果是完全自定义一个 view 的话,你首先需要考虑继承哪个类,是 View 呢,还是 ImageView 等子类。
  4. 根据需要去复写 View 的 onDraw 、onMeasure 、onLayout 方法。
  5. 根据需要去复写 dispatchTouchEvent、onTouchEvent 方法。
  6. 根据需要为你的自定义 view 提供自定义属性,即编写 attr. xml, 然后在代码中通过 TypedArray 等类获取到自定义属性值。
  7. 需要处理滑动冲突、像素转换等问题。
  8. 在要在 onDraw 或是 onLayout 中去创建对象,因为 onDraw 方法可能会被频繁调用,可以在 view 的构造函数中进行创建对象

问题

父 View 调用了子 View 的 measure () / layout (),而子 View 自己重写了 onMeasure () / onLayout (),到底哪个为准?

onMeasure:谁决定大小?
流程:
父 View 调用 child.measure (widthSpec, heightSpec)。
子 View 内部调用 onMeasure () → 计算出自己的 measuredWidth / measuredHeight。
系统最终会保存结果到 child.getMeasuredWidth ()/getMeasuredHeight ()。

结论:
父 View 只能提供约束(MeasureSpec),比如 AT_MOST、EXACTLY、UNSPECIFIED。
子 View 的 onMeasure () 负责决定实际的测量值。
所以最终大小取决于子 View 的 onMeasure 实现,但必须在父 View 提供的约束范围内

onLayout:谁决定位置?
流程:
父 View 调用 child.layout (l, t, r, b)。
系统会触发 child.onLayout (changed, l, t, r, b)。
子 View 的 onLayout 里一般不会改变自己的位置(因为位置由父 View 决定),它更多是用来安放子 View(孙子级)。

结论:
父 View 决定子 View 的位置(left/top/right/bottom)。
子 View 的 onLayout 只能决定自己的子 View 的位置,不能改变自己在父 View 里的位置。

谁说了算总结:
大小(width/height) → 子 View 的 onMeasure 为准(父只能限制,子决定最终值)。
位置(left/top/right/bottom) → 父 View 的 layout 为准(子不能改)。

绘制流程

  • onMeasure 测量 view 的大小,设置自己显示在屏幕上的宽高。
  • onLayout 确定 view 的位置,父 view 会根据子 view 的需求,和自身的情况,来综合确定子 view 的位置 (确定他的大小)。
  • onDraw (Canvas)绘制 view 的内容。
  • 在主线程中拿到 view 调用 Invalide ()方法,刷新当前视图,导致执行 onDraw 执行,如果是在子线程用 postinvalidate,或者不需要一直刷新用 postinvalidateDelayed (300),每隔300毫秒刷新一次。
  • 如果希望视图的绘制流程 (三步)可以完完整整地重新走一遍,就不能使用 invalidate ()方法,而应该调用 requestLayout ()了。

Invalidate 和 postInvalidate 的区别及使用
View invalidate : 层层上传到父级,直到传递到 ViewRootImpl 后触发了 scheduleTraversals 0)然后整个 View 树开始重新按照 View 绘制流程进行重绘任务。
Invalidate: 在 ui 线程刷新 view
PostInvalidate: 在工作线程刷新 view (底层还是 handler) 其实它的原理就是 invalidate+handler

onMeasure

onMeasure 测量 view 的大小。设置自己显示在屏幕上的宽高。
5edce9c0581e4ecf7e5a0565e2d788c8

MeasureSpec 有 SpecMode 和 SpecSize 俩个属性。

SpecMode

  1. unspecified: 父 View 不对子 View 做任何限制,需要多大给多大,一般不关心这个模式
  2. exactly: view 的大小就是 SpecSize(父 view 的大小)指定的大小。相当于mach_parents,就是根据这个获取的测量模式
  3. at_most: 父容器指定了一个 specsize,view 不能大于这个值。具体的值看 view, 相当于wrap_content
  • 常开发中我们接触最多的不是 MeasureSpec 而是 LayoutParams,在 View 测量的时候,LayoutParams 会和父 View 的 MeasureSpec 相结合被换算成 View 的 MeasureSpec,进而决定 View 的大小。
  • LayoutParams 类是用于子 view 向父 view 传达自己的意愿的一个东西(孩子想变成什么样向其父亲说明)

测量流程:从根 View 递归调用每一级子 View 的 measure () 方法,对它们进行测量
布局流程:从根 View 递归调用每一级子 View 的 layout () 方法,把测量过程得出的子 View 的位置和尺寸传给子 View,子 View 保存

  • onMeasure = 算法(你来计算宽高逻辑)
  • setMeasuredDimension = 存储(告诉系统最终大小)
  • setMeasuredDimension(int measuredWidth, int measuredHeight) 是真正保存测量结果的唯一方法。所有 onMeasure 的计算结果,最终必须调用它才能生效。

示例一

对自定义 View 完全进行自定义尺寸计算:重写 onMeasure ():CircleView

  1. 重写 onMeasure ()
  2. 计算出自⼰的尺⼨
  3. resolveSize() 或者 resolveSizeAndState () 修正结果
    (根据自己的尺寸和父 view 的模式和尺寸,综合计算自己的大小)
    size 是自己的
    unknown_filename.1
  • ⾸先用 MeasureSpec.getMode (measureSpec) 和 MeasureSpec.getSize (measureSpec)取出父对自己的尺寸 限制类型和具体限制尺⼨;
  • 如果 MeasureSpec 的 mode 是 EXACTLY,表示⽗ View 对子 View 的尺⼨寸做出了精确限制,所以就放弃计算出的 size,直接选用 MeasureSpec 的 size;
  • 如果 MeasureSpec 的 mode 是 UNSPECIFIED,表示父 View 对子 View 没有任何尺寸限制,所以直接选用计算出的 size,忽略 spec 中的 size。
  • at_most,最大不能超过父

示例二

  • 如果写的自定义 View 是继承现有控件的,而且写了 super.measure (),则会默认使用那个现有控件的测量宽高,你可以在这个已经测量好的宽高上做修改,当然也可以全部重新测过再改掉。
  • 如果我们的 View 直接继承 ImageView,ImageView 已经运行了一大堆已经写好的代码测出了相应的宽高。我们可以在它基础上更改即可。比如我们的 Image2View 是一个自定义的正方形的 ImageView:
1
2
3
4
5
6
7
8
9
10
11
12
@Override
protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) {

//这里已经帮我们测好了 ImageView 的规则下的宽高,并且通过了 setMeasuredDimension 方法赋值进去了。
super.onMeasure (widthMeasureSpec, heightMeasureSpec);

int measuredWidth = getMeasuredWidth ();
int measuredHeight = getMeasuredHeight ();
int size = Math.min (measuredWidth, measuredHeight);
setMeasuredDimension (size, size);
}

  • setMeasuredDimension 后才能 getmeasure 宽高,super 里做了这步,因为这方法是用来设置 view 测量的宽和高。
  • 完全自定义 view:重写 onMeasure 方法的目的是为了能够给 view 一个 warp_content 属性下的默认大小,因为不重写 onMeasure,那么系统就不知道该使用默认多大的尺寸。如果不处理,那 wrap_content 就相当于 match_parent。所以自定义控件需要支持 warp_content 属性就重写 onMeasure。
  • 可以自己尝试一下自定义一个 View,然后不重写 onMeasure ()方法,你会发现只有设置 match_parent 和 wrap_content 效果是一样的,事实上 TextView、ImageView 等系统组件都在 wrap_content 上有自己的处理。

getMeasuredHeight

getHeight ()和 getMeasuredHeight ()的区别
有俩种方法可以获得控件的宽高

  1. getMeasuredHeight (): 控件实际的大小。获取测量完的高度,只要在 onMeasure 方法执行完,就可以用它获取到宽高,在自定义 view 内使用 view. measure (0,0)方法可以主动通知系统去测量,然后就可以直接使用它获取宽高。measure 里调用的 onmeasure。
  2. getHeight ():控件显示的大小, 必须在 onLayout 方法执行完后,才能获得宽高。
1
2
3
4
5
6
7
8
view.getViewTreeObserver (). addOnGlobalLayoutListener (new OnGlobalLayoutListener () {
@Override
public void onGlobalLayout () {
headerView.getViewTreeObserver (). removeGlobalOnLayoutListener (this);
int headerViewHeight = headerView.getHeight ();
//直接可以获取宽高
}
});

getMeasuredHeight() view 的实际高度,getHeight() 反映父容器允许的高度,它是 View 在父布局中实际分配到的显示区域高度
getMeasuredHeight() >= getHeight() // 当父容器限制高度时成立
![[Pasted image 20250702195530.png]]

onLayout

  • onLayout 设置自己显示在屏幕上的位置 (只有在自定义 ViewGroup 中才用到),这个坐标是相对于当前视图的父视图而言的。view 自身有一些建议权,决定权在父 view 手中。
  • 调用场景:在 view 需要给其孩子设置尺寸和位置时被调用。子 view,包括孩子在内,必须重写 onLayout (boolean, int, int, int, int)方法,并且调用各自的 layout (int, int, int, int)方法。
1
2
3
4
5
6
7
8
9
10
11
protected void onLayout (boolean changed, int l, int t, int r, int b) {
for (int i = 0; i < getChildCount (); i++) {
View view = getChildAt (i);
/**
* 父 view 会根据子 view 的需求,和自身的情况,来综合确定子 view 的位置, (确定他的大小)
*/
//指定子 view 的位置 , 左,上,右,下,是指在 viewGround 坐标系中的位置
view.layout (0+i*getWidth (), 0, getWidth ()+i*getWidth (), getHeight ());
}
}

onDraw

onDraw (Canvas)绘制 view 的内容。控制显示在屏幕上的样子 (自定义 viewgroup 时不需要这个)

1
2
3
4
5
6
7
/*
* backgroundBitmap 要绘制的图片
* left 图片的左边界
* top 图片的上边界
* paint 绘制图片要使用的画笔
*/
canvas.drawBitmap (backgroundBitmap, 0, 0, paint);

其他

在 Android 中,给 View 设置圆角通常涉及以下几个步骤:

  1. **设置 outlineProvider**:为 View 设置一个 Outline,它定义了 View 的形状和圆角。
  2. **调用 invalidateOutline()**:通知系统 View 的轮廓已经改变,需要重新绘制。
  3. **设置 clipToOutline**:决定是否根据 View 的 Outline 裁剪其内容。

View 和 ViewGroup 的区别

  • ViewGroup 需要控制子 view 如何摆放的时候需要实现 onLayout。
  • View 没有子 view,所以不需要 onLayout 方法,需要的话实现 onDraw
  • 继承系统已有控件或容器, 比如 FrameLayou,它会帮我们去实现 onMeasure 方法中,不需要去实现 onMeasure, 如果继承 View 或者 ViewGroup 的话需要 warp_content 属性的话,需要实现 onMeasure 方法
  • 自定义 ViewGroup 大多时候是控制子 view 如何摆放,并且做相应的变化(滑动页面、切换页面等)。
  • 自定义 view 主要是通过 onDraw 画出一些形状,然后通过触摸事件去决定如何变化

scrollTo ()和 scrollBy ()

  • scrollTo 将当前视图的基准点移动到某个点(坐标点);
  • ScrollBy 移动当前 view 内容移动一段距离。
  • 为正时,图片向左移动,为负时,图片向右移动 (跟下面的坐标轴是反的)

722549|700

三⻆函数的计算横向的位移是 cos,纵向的位移是 sin

drawChild

![[Pasted image 20250702163133.png]]

  • 触发条件
    • View 首次附加到窗口时
    • View 的可见性发生变化时
    • 调用 invalidate() 或 postInvalidate() 时
    • 视图布局发生变化时(调用 requestLayout()
    • 动画执行期间(每帧重绘)

dispatchDraw

dispatchDraw (Canvas canvas) 方法是 ViewGroup 类的一个受保护的方法,用于处理视图组的绘制流程。它会在以下几种情况下被调用:

  1. 当视图树中的某个视图或视图组需要重新绘制时。
  2. 当视图树的布局发生变化时。
    当调用了 invalidate () 或 requestLayout () 方法后,导致视图需要重新布局和绘制时。

dispatchDraw (Canvas canvas) 的主要作用是将绘制任务分发给它的子视图。当 dispatchDraw 被调用时,它会间接导致子视图的 onDraw (Canvas canvas) 方法被调用。这是通过以下步骤完成的:

  1. dispatchDraw 通常会调用 drawChild (Canvas canvas, View child, long drawingTime) 方法来绘制每个子视图。
  2. drawChild 方法内部会调用子视图的 draw (Canvas canvas) 方法。
    子视图的 draw (Canvas canvas) 方法会进一步调用 onDraw (Canvas canvas) 方法来进行实际的绘制工作。因此,dispatchDraw 通过一系列调用链最终导致子视图的 onDraw 方法被执行,从而完成子视图的绘制

onFinishInflate 获取子View

当 xml 被填充完毕时调用,在自定义 viewgroup 中,可以通过这个方法获得子 view 对象

1
2
3
4
5
6
7
8
9
10
protected void onFinishInflate () {
super.onFinishInflate ();
// 容错性检查 (至少有俩子 View, 子 View 必须是 ViewGroup 的子类)
if (getChildCount () < 2){
throw new IllegalStateException ("布局至少有俩孩子. Your ViewGroup must have 2 children at least.");
}

mLeftContent = (ViewGroup) getChildAt (0);
mMainContent = (ViewGroup) getChildAt (1);
}

获得 View 宽高的几种方式

获取 View 宽高的几种方法

  1. OnGlobalLayoutListener 获取
  2. OnPreDrawListener 获取
  3. OnLayoutChangeListener 获取
  4. 重写 View 的 onSizeChanged
  5. 使用 View.post 方法
  • onWindowFocusChanged:View 已经初始化完毕,宽高已经有了,需要注意 onWindowFocusChanged 会被调用多次,Activity 得到焦点和失去焦点都会执行这个回调
1
2
3
4
5
6
7
8
9
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus) {
//获得宽度
int width = view.getMeasuredWidth();
}
}

  • view.post (runnable)
    通过 post 可以将一个 runnable 投递到消息队列的尾部,等待 Looper 调用此 runnable 的时候,View 也已经初始化好了
  • ViewTreeObserver
1
2
3
4
5
6
7
8
9
10
@Overrideprotected void onStart() {
super.onStart();
ViewTreeObserver observer=view.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
int width=view.getMeasuredWidth();
}
});}

padding

padding 是属于本 View 的属性,不同于 margin (不需要自定义时做处理系统就能很好的使用 margin),所以要在测量绘图时考虑它
测量时, desireSize=实际所需 size+相应方向的 padding。

View 的绘制过程

a.绘制背景 background.draw (canvas)
b.绘制自己 (onDraw)
c.绘制 children (dispatchDraw)
d.绘制装饰 (onDrawScrollBars)

坐标系

6ffa7e36-0587-450e-946f-3f0cd60c6b82

  • view 的位置参数有 left、right、top、bottom(可以 getXX 获得),3.0后又增加了几个参数:x、y、translationX 和 translationY,其中 x 和 y 是 view 左上角的坐标,而translationX 和 translationY 是 view 左上角相对于父容器的偏移量。这些参数都是相对于父容器的坐标,并且 translationX 和 translationY 的默认值是0,他们的换算关系是:x=left+translationX   y=top+ translationY。需要注意的是,view 在平移的过程中,top 和 left 表示的是原始左上角的位置信息,其值并不会发生改变,此时发生改变的是 x、y、translationX 和 translationY 这四个参数
  • touchslop 是系统所能识别出的被认为是滑动的最小距离,比如当俩次滑动事件的滑动距离小于这个值,我们就可以认为未达到滑动距离的临界值
  • Rect 成员变量为 int 类型,RectF 为 float 类型

MotionEvent 中 getRawX、getRawY 与 getX、getY 以及 View 中的 getScrollX、getScrollY

  • getRawX ()、getRawY ()返回的是触摸点相对于屏幕的位置,而 getX ()、getY ()返回的则是触摸点相对于 View 的位置。
  • getScrollX ()与 getScrollY ()的值由调用 View 的 scrollTo (int x, int y)或者 scrollBy (int x, int y)产生,其中 scrollTo 是将 View 中的内容移动到指定的坐标 x、y 处,此 x、y 是相对于 View 的左上角,而不是屏幕的左上角。scrollBy (int x, int y)则是改变 View 中的相对位置,参数 x、y 为距离上一次的相对位置。
  • 当 View 中的内容向右移动时,getScrollX ()的值为负数,同理,向 scrollTo 与 scrollBy 的 x 中传入负数,view 中的内容向右移动,反之向左。
  • 当 View 中的内容向下移动时,getScrollY ()的值为负数,同理,向 scrollTo 与 scrollBy 的 y 中传入负数,view 中的内容向下移动,反之向上。
  • 把 getScrollx 的值看成坐标。比如 view 向右边移动20px。那么得到的值就是 view. getScrollx ()的值就是-20

c82ecb89-00f7-476b-bbe9-fba0ac818be1|600

四种滑动的方法

  1. 使用 scrollTo ()或 scrollBy (),坐标是反的
  2. 动画
  3. 实时改变 layoutparams,重新布局
  4. layout()方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
    public boolean onTouchEvent (MotionEvent event) {
//获取到手指处的横坐标和纵坐标
int x = (int) event.getX ();
int y = (int) event.getY ();

switch (event.getAction ()) {
case MotionEvent. ACTION_DOWN:
lastX = x;
lastY = y;
break;
case MotionEvent. ACTION_MOVE:
//计算移动的距离
int offX = x - lastX;
int offY = y - lastY;
/**
* 第一种
* 调用 layout 方法来重新放置它的位置
*/
layout (getLeft () + offX, getTop () + offY,
getRight () + offX, getBottom () + offY);
// /**
// * 第二种
// * 这两个方法分别是对左右移动和上下移动的封装,传入的就是偏移量
// */
// offsetLeftAndRight (offX);
// offsetTopAndBottom (offY);
// /**
// * 第三种
// */
// ViewGroup. MarginLayoutParams mlp =
// (ViewGroup. MarginLayoutParams) getLayoutParams ();
// mlp. leftMargin = getLeft () + offX;
// mlp. topMargin = getTop () + offY;
// setLayoutParams (mlp);
// /**
// * 第四种
// * sceollTo (x, y)传入的应该是移动的终点坐标
// * scrollBy (dx, dy)传入的是移动的增量。
// * 通过 scrollBy 传入的值应该是你需要的那个增量的相反数
// */
// ((View) getParent ()). scrollBy (-offX, -offY);
break;
}
return true;
}

如果让 view 在一段时间内移动到某个位置 (不是快速滑动,弹性,有动画)方法:

  1. 使用自定义动画 (让 view 在一段时间内做某件事),extends Animation,
    总要修改的是 translationx. y 这俩个值 (相对于父容器移动的距离)
  2. 使用 Scoller,OverScroller。用于自动计算滑动的偏移
    模板 (固定代码):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
	 * @param startX	开始时的 X 坐标
* @param startY 开始时的 Y 坐标
* @param disX X 方向要移动的距离
* @param disY Y 方向要移动的距离
myScroller.startScroll (getScrollX (), 0,distance,0,Math.abs (distance));//持续的时间
/**
* Scroller 不主动去调用这个方法
* 而 invalidate ()可以掉这个方法
* invalidate->draw->computeScroll
*/
@Override
public void computeScroll () {
super.computeScroll ();
if (scroller.computeScrollOffset ()){//返回 true, 表示动画没结束
scrollTo (scroller.getCurrX (), 0);
invalidate ();
}
}

scroller、OverScroller

scroller 本身并不能实现 view 的滑动,它需要配合view 的的 computeScroll 方法才能完成弹性滑动的效果,它不断的让 view 重绘,而每一次重绘距滑动起始时间会有一个时间间隔,通过这个时间间隔 srcoller 就可以得出 view 当前的滑动位置,知道了滑动位置就可以通过 scrollTo 方法来完成 view 的滑动,就这样,view 的每一次重绘就会导致 view 进行小幅度的滑动,而多次的小幅度滑动就组成了弹性动画。

scrollTo () 是瞬时方法,不会自动使用动画。如果要用动画,需要配合 View.computeScroll () 方法
computeScroll () 在 View 重绘时被自动调用,使用方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// onTouchEvent () 中:例如滑动等等
overScroller.startScroll (startX, startY, dx, dy);
postInvalidateOnAnimation ();
......
// onTouchEvent () 外:
@Override
public void computeScroll () {
if (overScroller.computeScrollOffset ()) { //
计算实时位置
scrollTo (overScroller.getCurrX (),
overScroller.getCurrY ()); // 更新⾯面
postInvalidateOnAnimation (); // 下一帧继续
}
}

VelocityTracker

如果 GestureDetector 不能满⾜足需求,或者觉得 GestureDetector 过于复杂,可以自己处理 onTouchEvent () 的事件。但需要使用 VelocityTracker 来计算手指移动速度。
unknown_filename.2

Canvas 使用

  • save:用来保存 Canvas 的状态。save 之后,可以调用 Canvas 的平移、放缩、旋转、错切、裁剪等操作。
  • restore:用来恢复 Canvas 之前保存的状态。防止 save 后对 Canvas 执行的操作对后续的绘制有影响。
  • save 和 restore 要配对使用 ( restore 可以比 save 少,但不能多),如果 restore 调用次数比 save 多,会引发 Error 。save 和 restore 之间,往往夹杂的是对 Canvas 的特殊操作。

unknown_filename.5

Canvas 还提供了一系列位置转换的方法:rorate、scale、translate、skew (扭曲)等

unknown_filename.4
unknown_filename.6
unknown_filename.7
unknown_filename.8

inflate 方法

View.inflate () 和 LayoutInflator.from (). inflate () 有啥区别?
调用 inflate () 方法的时候有时候传 null,有时候传 parent 是为啥?

View inflate 只是个简易的包装方法,实际上还是调用的 LayoutInflater inflate ;
当 root 传空时,会直接返回要加载的 layoutId,返回的 View 没有父布局且没有 LayoutParams
当 root 不传空时,又分为 attachToRoot 为真或者为假:

attachToRoot = true 会为传入的 layoutId 直接设置参数,并将其添加到 root 中,然后将传入的 root 返回
attachToRoot = false 会为传入的 layoutId 设置参数,但是不会添加到 root ,然后返回 layoutId 对应的 View;

当我们不为子 View 的展示负责时,attachToRoot 必须为 false;否则就会出现对应的负责人,比如上面说的 Rv 或者 FragmentManager,已经把布局 id 添加到 ViewGroup 了,我们还继续设置 attachToRoot 为 true,想要手动 addView,那必然会发生 child already has parent的错误。

1
2
3
4
5
6
7
8
9
10
11
12
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {  
LayoutInflater inflater = LayoutInflater.from(getActivity());
View view = inflater.inflate(R.layout.item, parent, false);
return new ViewHolder(view);
}


public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_layout, parentViewGroup, false);
}


1.自定义View总结
http://peiniwan.github.io/2025/12/618995c41398.html
作者
六月的雨
发布于
2025年12月16日
许可协议