自定义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 和自己都调了,onLayout onMeasure 哪个为准,以 onLayout 为准,自己想什么样什么样设置

绘制流程

  • 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
View postInvalidate 最终会调用 ViewRootImpl. DispatchInvalidateDelayed 方法 mHandler 是 ViewRootHandler 实例,在该 Handler 的 handleMessage 方法中调用了 view. Invalidate方法。

onMeasure

  1. 实现构造方法。(三个构造方法)第二个是创建布局文件调用的构造函数

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

  • LayoutParams 类是用于子 view 向父 view 传达自己的意愿的一个东西(孩子想变成什么样向其父亲说明)
  • 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 的大小。

测量流程:从根 View 递归调用每一级子 View 的 measure () 方法,对它们进行测量

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

unknown_filename.3
通过 setMeasuredDimension ()方法

示例一

对自定义 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 ();
//直接可以获取宽高
}
});
  • 当屏幕可以包裹内容的时候,他们的值相等;当 view 的高度超出屏幕时,getMeasuredHeight ()是实际 View 的大小,与屏幕无关,getHeight 的大小此时则是屏幕的大小。此时,getMeasuredHeight () = getHeight+超出部分。
  • 这俩个一般情况是一样的,但是在 viewgroup 里 getWidth 是父类给子 view 分配的空间:右边-左边。系统可能需要多次 measure 才能确定最终的测量宽高,很可能是不准确的,好习惯是在 onlayout 里获得测量宽高或最终宽高。
  • 还有一种获得控件宽高的方法:onSizeChanged:当该组件的大小被改变时回调此方法
  • getWidth 是右-左,在 onMeasure 里还没值了,所以在 onMeasure 里需要使用 getMeasuredHeight

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
12
13
protected void onLayout (boolean changed, int l, int t, int r, int b) {

for (int i = 0; i < getChildCount (); i++) {
View view = getChildAt (i); // 取得下标为 I 的子 view
/**
* 父 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);

其他

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

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);
}


自定义View总结
http://peiniwan.github.io/2024/04/29acc0f47ad5.html
作者
六月的雨
发布于
2024年4月6日
许可协议