自定义View总结
[[自写动画自定义View]]
基本流程
- 明确需求,确定你想实现的效果。
- 确定是使用组合控件的形式还是全新自定义的形式,组合控件即使用多个系统控件来合成一个新控件,你比如 titilebar,这种形式相对简单。
- 如果是完全自定义一个 view 的话,你首先需要考虑继承哪个类,是 View 呢,还是 ImageView 等子类。
- 根据需要去复写 View 的 onDraw 、onMeasure 、onLayout 方法。
- 根据需要去复写 dispatchTouchEvent、onTouchEvent 方法。
- 根据需要为你的自定义 view 提供自定义属性,即编写 attr. xml, 然后在代码中通过 TypedArray 等类获取到自定义属性值。
- 需要处理滑动冲突、像素转换等问题。
- 在要在 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
- 实现构造方法。(三个构造方法)第二个是创建布局文件调用的构造函数
onMeasure 测量 view 的大小。设置自己显示在屏幕上的宽高。
- LayoutParams 类是用于子 view 向父 view 传达自己的意愿的一个东西(孩子想变成什么样向其父亲说明)
- MeasureSpec 有 SpecMode 和 SpecSize 俩个属性。
SpecMode
- unspecified: 父 View 不对子 View 做任何限制,需要多大给多大,一般不关心这个模式
- exactly: view 的大小就是 SpecSize(父 view 的大小)指定的大小。相当于mach_parents,就是根据这个获取的测量模式
- 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 保存
通过 setMeasuredDimension ()方法
示例一
对自定义 View 完全进行自定义尺寸计算:重写 onMeasure ():CircleView
- 重写 onMeasure ()
- 计算出自⼰的尺⼨
- 用 resolveSize() 或者 resolveSizeAndState () 修正结果
(根据自己的尺寸和父 view 的模式和尺寸,综合计算自己的大小)
size 是自己的
- ⾸先用 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 |
|
- 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 ()的区别
有俩种方法可以获得控件的宽高
- getMeasuredHeight (): 控件实际的大小。获取测量完的高度,只要在 onMeasure 方法执行完,就可以用它获取到宽高,在自定义 view 内使用 view. measure (0,0)方法可以主动通知系统去测量,然后就可以直接使用它获取宽高。measure 里调用的 onmeasure。
- getHeight ():控件显示的大小, 必须在 onLayout 方法执行完后,才能获得宽高。
1 |
|
- 当屏幕可以包裹内容的时候,他们的值相等;当 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 |
|
onDraw
onDraw (Canvas)绘制 view 的内容。控制显示在屏幕上的样子 (自定义 viewgroup 时不需要这个)
1 |
|
其他
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 内容移动一段距离。
- 为正时,图片向左移动,为负时,图片向右移动 (跟下面的坐标轴是反的)
三⻆函数的计算横向的位移是 cos,纵向的位移是 sin
onFinishInflate 获取子View
当 xml 被填充完毕时调用,在自定义 viewgroup 中,可以通过这个方法获得子 view 对象
1 |
|
获得 View 宽高的几种方式
获取 View 宽高的几种方法
- OnGlobalLayoutListener 获取
- OnPreDrawListener 获取
- OnLayoutChangeListener 获取
- 重写 View 的 onSizeChanged
- 使用 View.post 方法
- onWindowFocusChanged:View 已经初始化完毕,宽高已经有了,需要注意 onWindowFocusChanged 会被调用多次,Activity 得到焦点和失去焦点都会执行这个回调
1 |
|
- view.post (runnable)
通过 post 可以将一个 runnable 投递到消息队列的尾部,等待 Looper 调用此 runnable 的时候,View 也已经初始化好了 - ViewTreeObserver
1 |
|
padding
padding 是属于本 View 的属性,不同于 margin (不需要自定义时做处理系统就能很好的使用 margin),所以要在测量绘图时考虑它
测量时, desireSize=实际所需 size+相应方向的 padding。
View 的绘制过程
a.绘制背景 background.draw (canvas)
b.绘制自己 (onDraw)
c.绘制 children (dispatchDraw)
d.绘制装饰 (onDrawScrollBars)
坐标系
- 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
四种滑动的方法
- 使用 scrollTo ()或 scrollBy (),坐标是反的
- 动画
- 实时改变 layoutparams,重新布局
- layout()方法
1 |
|
如果让 view 在一段时间内移动到某个位置 (不是快速滑动,弹性,有动画)方法:
- 使用自定义动画 (让 view 在一段时间内做某件事),extends Animation,
总要修改的是 translationx. y 这俩个值 (相对于父容器移动的距离) - 使用 Scoller,OverScroller。用于自动计算滑动的偏移
模板 (固定代码):
1 |
|
scroller、OverScroller
scroller 本身并不能实现 view 的滑动,它需要配合view 的的 computeScroll 方法才能完成弹性滑动的效果
,它不断的让 view 重绘,而每一次重绘距滑动起始时间会有一个时间间隔,通过这个时间间隔 srcoller 就可以得出 view 当前的滑动位置,知道了滑动位置就可以通过 scrollTo 方法来完成 view 的滑动,就这样,view 的每一次重绘就会导致 view 进行小幅度的滑动,而多次的小幅度滑动就组成了弹性动画。
scrollTo () 是瞬时方法,不会自动使用动画。如果要用动画,需要配合 View.computeScroll () 方法
computeScroll () 在 View 重绘时被自动调用,使用方式:
1 |
|
VelocityTracker
如果 GestureDetector 不能满⾜足需求,或者觉得 GestureDetector 过于复杂,可以自己处理 onTouchEvent () 的事件。但需要使用 VelocityTracker 来计算手指移动速度。
Canvas 使用
- save:用来保存 Canvas 的状态。save 之后,可以调用 Canvas 的平移、放缩、旋转、错切、裁剪等操作。
- restore:用来恢复 Canvas 之前保存的状态。防止 save 后对 Canvas 执行的操作对后续的绘制有影响。
- save 和 restore 要配对使用 ( restore 可以比 save 少,但不能多),如果 restore 调用次数比 save 多,会引发 Error 。save 和 restore 之间,往往夹杂的是对 Canvas 的特殊操作。
Canvas 还提供了一系列位置转换的方法:rorate、scale、translate、skew (扭曲)等
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 |
|