这篇的主要内容是对两章View的内容进行总结。不得不说,自定义View是很多开发者的痛点,一方面我们很羡慕大神们做出骚气又酷炫的界面,另一方面我们又苦于网上的资料过于凌乱和纷杂导致很多时候越学越晕。尤其工科,想要深入掌握某种知识,体系化是非常重要的,就像我们必须先学四则运算,才能进阶到更复杂的开方乘方运算,再进阶到微积分,甚至积分变换,复变函数等等。所以这个过程只要有一环断开,后面的学习就将变得十分困难。主席这两章的讲解,就可以看成a bridge joins the bottom and top,很多东西会给你一种拨云见日,恍然大悟的感觉。给人一杯水自己先要有十杯水,从中也可以看出主席对于View的深刻理解和写书时候的努力投入。
好了,不帮他吹牛逼了(我猜各位现在的表情←_←),下面还是会把重难点列出来,同时因为这两章比较复杂,主席没有提到的一些基础知识,我也会在tips中说明,以上~
View的定义:如果各位熟悉设计模式的话,我觉得可以把View理解成组合模式里的叶子结点和有枝节点的关系,本质都是Composite,而这里本质ViewGroup和View都是View,组合模式最大的好处就是遍历的时候不用关注是怎样的结点,因为抽象都是一样的。这个模式真的非常棒,推荐不熟悉的童鞋去好好补习下~
View的位置参数:
1)View的宽高和坐标关系:width = right - left,height = top - bottom。
2)View在平移过程中,top和left表示的是原始左上角的位置信息,其值不会改变,发生改变的是x、y、translationX、translationY这四个参数,x是View左上角的坐标,translation是view移动后相对于父容器(这里其实就是刚才说的左上角)的偏移量,所以有x = left + translationX。y的原理相同,不再赘述。
MotionEvent典型事件:ACTION_DOWN, ACTION_MOVE, ACTION_UP。
TouchSlop:系统所能识别的被认为是滑动的最小距离,我们可以用这个常量来判断用户的滑动是否达到阈值,提升用户体验。获取方法:ViewConfiguration.get(getContext()).getScaledTouchSlop()。
VelocityTracker加速度追踪:使用很简单,看书就好。经过测试一般建议类似ViewPager这样的空间,将时间间隔设置为1000(也就是1秒)时,加速度阈值设为1000-2000左右体验较好,各位可自行测试。
View的滑动:
1) ScrollBy和ScrollTo,简单归纳下:
ScrollBy的0点在一般情况下均为可见的top那条线,有一种特殊情况就是(书中未提及)当某个ViewGroup在内部的layout的时候设置margin为负值的View时,0点会在可见top上方的高度(或宽度)为margin的地方,这个鬼东西实在是太绕口,具体的大家可以参考我写的这篇blog:http://blog.csdn.net/amurocrash/article/details/48524895,参考headerView的设置。竖向滑动时,上滑ScrollY不断增加(所以应该传正值),下滑时ScrollY不断减少(所以应该传负值);同理,横向滑动时,左滑ScrollX不断增加,右滑不断减少。
ScrollTo可以理解为把View滑动到ScrollX或ScrollY为指定值的位置(看源码也是这意思~)
2)动画:注意View动画的View移动只是位置移动,其本身还是在原来位置,会导致一些bug。
3)通过LayoutParams
Scoller:本人不才,这个鬼东西我也是看了两三天,自己写了好多测试代码才真正理解,虽然代码很好写。具体的使用方法大家看书就好,说得很清楚,我就讲几种使用弹性滑动的场景吧,这个场景有助于大家去理解scroller代码的使用,这里以原书附带的源码HorizontalScrollViewEx为例:
1)TouchEvent的ACTION_UP事件中,用户滑动速度很快,但是滑动距离又不足以“翻页”的时候,通过scroller来帮助用户scrollBy掉滑动一页还需要的dx或dy。
2)当用户滑动到最上端或最下端时,我们仍然允许用户继续滑动,但是一旦松手,就把页面弹回到最上端和最下端的位置,用IOS的用户都知道IOS几乎所有页面都有这个弹性效果,用户体验非常好,其实我们用scroller也能轻松实现。
3)第三种场景和第一种类似,大家依然可以参考我上面发的那篇仿网易的blog,现在不是翻页,当用户滑动的加速度很大的时候,我们认为用户需要滑动的距离肯定是不只他手从放下到松开的那段距离的,所以这种情况下我们需要通过scroller帮助用户去多滑一段,这个距离具体设置一般需要交互给出,我测试的时候设置的是滑加速度的十分之一,感觉还是太少,各位可以自行设置。
View事件的分发机制:不得不说,主席这段写得实在精彩,我看了这么多博客,书籍,真心是写得最棒的,说得最清楚的,同时如果你看不懂,直接按照他那个代码复制粘贴,基本上小的问题都能解决了。
1)三大方法关系的伪代码(屌炸天系列~)
public boolean dispatchTouchEvent(MotionEvent ev)
{
boolean consume = false;
if(onInterceptTouchEvent(ev))
{
consume = onTouchEvent(ev);
}
else
{
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
关系很清楚了,如果当前View拦截事件,就交给自己的onTouchEvent去处理,否则就丢给子View继续走相同的流程。
2)onTouchListener优先级高于onTouchEvent。
3)事件传递顺序:Activity -> Window -> View,如果View都不处理,最终将由Activity的onTouchEvent处理。
4)一些结论:拦截的一定是事件序列;不消耗ACTION_DOWN,则事件序列都会由其父元素处理;只消耗ACTION_DOWN事件,该事件会消失,消失的事件最终会交给Activity来处理;requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的事件分发过程,除了ACTION_DOWN;
事件分发源码解析:这里具体的分析就请看书了,列一些有价值的tips
1)Window的实现类为PhoneWindow。
2)获取Activity的contentView的方法((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0);
View的滑动冲突处理,这里没什么好tips的了,因为主席已经提供了非常棒的外部拦截法和内部拦截法,普通需求基本直接复用代码就能搞定。但是如果想要深刻理解,只有自己多写多测多读代码,才能很好的掌握,对于自定义View来说,掌握这个专题将对你的功力有大幅的提升。
ViewRoot和DecorView
1)ViewRoot对应ViewRootImpl类,它是连接WindowManager和DecorView的纽带,View的三大流程均通过ViewRoot来完成。
2)ActivityThread中,Activity创建完成后,会将DecorView添加到Window中,同时创建ViewRootImpl对象,并建立两者的关联。
3)View的绘制流程从ViewRoot的performTraversals方法开始,经过measure、layout和draw三大流程。
MeasureSpec
1)复习下位运算……
a) <<左移,>>右移,所以源码中EXACTLY = 1 << MODE_SHIFT就相当于010000……..0000(30个0),其他可类推;
b) &运算 0011 & 1100 = 0000,按位与;|或运算 0011 | 1100 = 1111,按位或;
c)~取反运算,~0000 = 1111,所以(size & ~MODE_MASK) | (mode & MODE_MASK) 就好理解了;
2)三类specMode:UNSPECIFIED,基本无视;EXACTLY,精确大小或match_parent;AT_MOST,warp_content;
3)子View和父容器的MeasureSpec关系归纳:
a. 子View为精确宽高,无论父容器的MeasureSpec,子View的MeasureSpec都为精确值且遵循LayoutParams中的值。
b. 子View为match_parent时,如果父容器是精确模式,则子View也为精确模式且为父容器的剩余空间大小;如果父容器是wrap_content,则子View也是wrap_content且不会超过父容器的剩余空间。
c. 子View为wrap_content时,无论父View是精确还是wrap_content,子View的模式总是wrap_content,且不会超过父容器的剩余空间。
View的工作流程:onMeasure, onLayout, onDraw
1)getSuggestedMinimumWidth的逻辑:View如果没有背景,那么返回android:minWidth这个属性指定的值,这个值可以为0;如果设置了背景,则返回背景的最小宽度和minWidth中的较大值。
2)一个比较好的习惯是在onLayout方法中去获取View的测量宽高或最终宽高。
3)如何在Activity初始化时获取View的宽高:
a. Activity或者View的onWindowFocusChanged方法(注意该方法会在Activity Pause和resume时被多次调用)。
b. view.post(new Runnable( {@Overiddepublic void run(){})});在run方法中获取。
c. ViewTreeObserver中的onGlobalLayoutListener中。
d. view.measure手动获取:
match_parent:无法测量;
精确值:int wMeasureSpec = MeasureSpec.makeMeasureSpec(exactlyValue, MeasureSpec.EXACTLY);
wrap_content:int wMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30) - 1, MeasureSpec.AT_MOST);
PS一下,还不懂(1<<30) - 1的,再多一句嘴,其实就是1111….11111(30个)
4)layout中可能会使view的大小大于测量的大小
5)draw的过程:绘制背景(background.draw(canvas)),绘制自己(onDraw()),绘制chidren(dispatchDraw),绘制装饰(onDrawScrollBars)
6)setWillNotDraw方法用于在一个View不需要绘制时的优化(设置为true时)。
自定义View:前面打字打得我累死了,终于到这里了,Orz。还是那句话如果想要深刻理解,只有自己多写多测多读代码,才能很好的掌握。想要知道轮子怎么转的,最好的方法就是自己写一个,然后再对比成熟的轮子去找差距和不足。
tips还是把几个重要的须知列一下:
1)直接继承View或ViewGroup的需要自己处理wrap_content。
2)View要在onDraw方法中要处理padding,而ViewGroup要在onMeasure和onLayout中处理padding和margin。
3)View中的post方法可以取代handler。
4)在View的onDetachedFromWindow中停止动画,线程或回收其他资源。
5)滑动冲突处理。