目录
本文章作为学习总结
尊重原创老师 参考书集《《Android开发艺术探索》(任玉刚)(3)》及部分前辈网络总结,如涉及版权问题,请联系。
一、View的工作原理:
● View的事件体系:
x,y为View左上角坐标,translationX,translationY为View左上角相对于父容器的偏移量。
有关画布偏移量translate:y = top + translationY
● MotionEvent和TouchSlop:
1. MotionEvent:
ACTION_DOWN:手指刚接触屏幕
ACTION——MOVE:手指在屏幕上滑动
ACTION——UP:手指从屏幕上松开的一瞬间
getX/getY返回的是相对于当前View左上角的x和y坐标,而getRawX/getRawY返回的是相对于手机屏幕左上角的x和y坐标。
2.TouchSlop:
是系统所能识别出的被认为是滑动的最小距离。通过如下方式即可获取这个常量:ViewConfiguration.get(getScaledTouchSlop)。
● VelocityTracker、GestureDetector和Scroller
VelocityTracker:速度追踪
● View的滑动
◎ View滑动的三种方式:
1.view本身提供的scrollTo/scrollBy方法;2.通过动画给view施加平移效果来实现滑动3.通过改变view的LayoutParams使得view重新布局从而实现滑动。
源码:Scroller scroller = new Scroller(mContext);
//缓慢滑动到指定位置
private void smoothScrollerTo (int destX,int destY) {
int scrollX = getScrollerX( );
int delta = destX - scrollX;
//1000ms内滑向destX,效果就是慢慢滑动
mScroller.startScroller (scrollX , 0 , delta , 0 , 1000);
invalidate( );
}
@Override
public void computeScroll( ) {
if(mScroller.computeScrolloffset( ) ) {
scrollTo(mScroller.getCurrX( ), mScroller.getCurrY( ) );
postInvalidate( );
}
}
//示例text1--SlideControl2--实现了滑动位移变色小方块自定义控件
if (isMove) {
//滑过中控线
if (circle_x >= centerX) {
//关闭(执行开启)
//源码:1000ms内滑向destX,效果就是慢慢滑动---scrollX起点横坐标,delta终点横坐标,1000ms时间
//mScroller.startScroll(scrollX,0,delta,0,1000)
mScroller.startScroll((int) circle_x, 0, (int) (circleEndX - circle_x), 0);
isChecked = true;
} else {
//开启(执行关闭)
mScroller.startScroll((int) circle_x, 0, (int) (circleStartX - circle_x), 0);
isChecked = false;
}
} else {
//实现弹性滑动--固定代码
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
circle_x = mScroller.getCurrX();
invalidate();
}
}
二、View的绘制流程:
p174--4.1
● View常见的回调方法:
比如构造方法,onAttach、onVisibilityChanged、onDetach等。
● View的滑动,解决滑动冲突。
● 自定义View的几种固定类型:
直接继承自View和ViewGroup,继承现有的系统控件。
1.ViewRoot和DecorView:
ViewRoot对应于ViewRootImpl类,它是连接WindowManager和DecorView的纽带,View的三大流程都是通过ViewRoot来完成的。
整个View树的绘图流程是在ViewRoot.java类的performTraversals()函数展开的,该函数做的执行过程可简单概况为
根据之前设置的状态,判断是否需要重新计算视图大小(measure)、是否重新需要安置视图的位置(layout)、以及是否需要重绘
(draw),其框架过程如下:
View 绘制中主要流程分为measure,layout, draw 三个阶段。
measure :根据父 view 传递的 MeasureSpec 进行计算大小。
layout :根据 measure 子 View 所得到的布局大小和布局参数,将子View放在合适的位置上。
draw :把 View 对象绘制到屏幕上。
2.MeasureSpec:
封装了从父View 传递给到子View的布局需求。每个MeasureSpec代表宽度或高度的要求。每个MeasureSpec都包含了size(大小)和mode(模式)。
MeasureSpec 一个32位二进制的整数型int,前面2位代表的是mode,后面30位代表的是size。mode 主要分为3类,分别是
EXACTLY:父容器已经测量出子View的大小。对应是 View 的LayoutParams的match_parent 或者精确数值。
AT_MOST:父容器已经限制子view的大小,View 最终大小不可超过这个值。对应是 View 的LayoutParams的wrap_content。
UNSPECIFIED:父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量的状态。(这种不怎么常用,下面分析也会直接忽略这种情况)。
示例:
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
//获取子View的LayoutParam
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//通过父View的MeasureSpec和子View的margin,父View的padding计算,算出子View的MeasureSpec
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
//通过计算出来的MeasureSpec,让子View自己测量。
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
//计算子View的大小
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// 父View是EXACTLY的
case MeasureSpec.EXACTLY:
//子View的width或height是个精确值,则size为精确值,mode为 EXACTLY
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
//子View的width或height是MATCH_PARENT,则size为父视图大小,mode为 EXACTLY
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
//子View的width或height是WRAP_CONTENT,则size为父视图大小,mode为 AT_MOST
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 2、父View是AT_MOST的
case MeasureSpec.AT_MOST:
//子View的width或height是个精确值,则size为精确值,mode为 EXACTLY
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
//子View的width或height是MATCH_PARENT,则size为父视图大小,mode为 AT_MOST
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
//子View的width或height是WRAP_CONTENT,则size为父视图大小,mode为 AT_MOST
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 3、父View是UNSPECIFIED的
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
3.DecorView和View的测量区别:
对于顶级View(即DecorView)和普通View来说,MeasureSpec的转换过程略有不同。
DecorView:其MeasureSpec由窗口的尺寸和其自身的LayoutParams来共同决定;
普通View:其MeasureSpec由父容器的MeasureSpec和其自身的LayoutParams来共同决定;
●View 的 MeasureSpec 并不是父 View 独自决定,它是根据父 view 的MeasureSpec加上子 View 的自己的 LayoutParams,通过相应的规则转化。
需要注意一点就是,此时的MeasureSpec并不是View真正的大小,只有setMeasuredDimension之后才能真正确定View的大小。一种良好的习惯是,在onLayout方法中去获取VIew的测量宽高或者最终宽高。
●View 测量流程是父 View 先测量子 View,等子 View 测量完了,再来测量自己。在ViewGroup 测量子 View 的入口就是 measureChildWithMargins
●在调用子元素的measure方法之前会先通过getChildMeasureSpec方法来得到子元素的MeasureSpec;
●子元素的MeasureSpec的创建与 父容器的MeasureSpec和子元素的LayoutParams有关,此外还和View的margin及padding有关。
4.measure :
- performMeasure中会调用measure方法,在measure方法中又会调用onMeasure方法,在onMeasure方法中则会对所有的子元素进行measure过程。
- measure 主要功能就是测量设置 View 的大小。该方法是 final 类型,子类不能覆盖,在方法里面会调用 onMeasure(),我们可以复写 onMeasure() 方法去测量设置 View 的大小。
- measure过程决定了View的宽高。Measure完成以后,可以通过getMeasureWidth和getMeasureHeight方法来获取到View测量后的宽和高(几乎所有情况下都等于View最终的宽高,特殊情况除外)。
- Layout过程决定了View的四个顶点的坐标和实际的View的宽高。完成以后,可以通过(setFrame方来设定)getTop、getBottom、getLeft、getRight来拿到View的四个顶点位置,并可以通过getWidth和getHeight方法来拿到View的最终宽高。
- Draw过程决定了View的显示。
- ViewGroup:除了完成自己的测量过程,还会遍历去调用所有子元素的measure方法,各个子元素在递归去执行这个流程。
5.四种方法获取View的宽高:
1.Activity/View#onWindowFocusChanged(回调多次)
onWindowFocusChanged:含义是View已经初始化完毕了,宽高已经准备好了。
2.view.post(runnable)
通过post可以将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable的时候,View也已经初始化好了。
3.ViewTreeObserver(回调多次)
使用ViewTreeObserver的众多回调可以完成这个功能,比如OnGlobalLayoutListener这个接口,
当View树的状态发生改变或者View树内部的View的可见性发生改变时,onGlobalLayout方法将被回调。因此这是获View宽高的一个好时机。
4.view.measure(int widthMeasureSpec,intheightMeasureSpec)
通过手动对View进行measure 来得到View的宽高(复杂,分情况处理,根据View的LayoutParams来分)。
6.layout:
●Layout的作用是ViewGroup用来确定子元素的位置,当ViewGroup的位置被确定后,他在onLayout中会遍历所有的子元素并调用其layout,在layout方法中onlayout方法有会被调用。
●layout方法确定View本身的位置,onLayout方法确定所有子元素的位置。、
●Layout过程决定了View的四个顶点的坐标和实际的View的宽高。完成以后,可以通过(setFrame方来设定)getTop、getBottom、getLeft、getRight来拿到View的四个顶点位置,并可以通过getWidth和getHeight方法来拿到View的最终宽高。
7.Draw:
●View的绘制过程:
- 绘制背景background.draw(canvas)
- 绘制自己(onDraw)
- 绘制children(dispatchDraw)
- 绘制装饰(onDrawScrollBars)
●View的绘制过程的传递是通过dispatchDraw来实现的,便利所有子元素的draw方法。
●View有一个特殊的方法:setWillNotDraw:当我们的自定义控件继承于ViewGroup并且本身不具备绘制功能时,开启这个标志位,便于系统进行后续优化。
当明确知道一个ViewGroup需要通过onDraw来绘制内容时,需要显示地关闭WILL——NOT——DRAW这个标记位。
View默认没有启用标记位false
ViewGroup默认启用标记位true
● 自定义View的分类:
1.继承View重写onDraw方法:需要自己支持wrap_content和padding。
2.继承ViewGrop派生特殊的Layout:需要合适的处理ViewGroup的测量、布局,并同时处理自元素的测量和布局过程。
3.继承特定的View(比如TextView):用于扩展一有的View功能,不需要自己支持wrap_content和padding。
4.继承特定的ViewGroup(比如LinearLayout):与方法2的区别在于方法2更接近View的底层。
● 自定义View须知:
- 让View支持wrap_content:直接继承view或者viewgroup控件,要在onMeasure中对wrap_content做特殊处理。
- 如果有必要,让你的View支持padding:因为直接继承view的控件,如果不在draw中处理padding,padding属性是无法 起作用的。另外,直接继承自ViewGroup的控件需要在onMeasure和onLayout中考虑padding和子元素margin对其造成的 影响,不然将导致padding和子元素的margin失效。
3. 尽量不要再View中使用Handler,没必要
4. View中如果有线程或者动画,需要及时停止,参考View#onDetachedFromWindow:当其不可见是,需要停止线程和动画,防 止内存泄露。
5. View带有滑动嵌套情形时,需要处理好滑动冲突: