1、Android View架构
View是Android中所有控件的积累,不管是简单的TextView还是复杂的ListView,它们都有共同的基类就是View。View是一种界面层的空间的一种抽象,它代表了一个控件,除了View,还有ViewGroup,从名字来看是控件组,但是Android中ViewGroup也继承了View,这就意味着View本身可以是单个控件也可以是多个控件组成的一组控件。
上层控件负责测量和绘制,并传递事件。findViewById方法就是以树的深度优先遍历查找对应的控件。
ViewGroup重写findViewTraversal方法,
具体的分析可以网上搜索,这里暂不分析。
图4
Android的每一个Activity都包含一个Window对象,实现类是PhoneWindow,PhoneWindow将一个DecorView设置为整个应用窗口的根View。DecorView是我们可以看到的顶层试图,里面封装一些窗口操作的通用方法,所有View的监听事件,通过WindowManagerServie进行接收,并通过Activity对象来回到对应的ClickListener。
DecorView包含TitleView和ContentView,TitleView实现是ActionBar,ContentView是一个Framelayout,setContentView()就是将调用PhoneWindow的方法setContentView方法;
具体的framework层实现逻辑可以看代码,后面的文章我也会分析一下;
2、View基本知识
2.1、View坐标体系
图5
top:左上角纵坐标;left:左上角横坐标;
right:右下角横坐标;bottom:右下角纵坐标;
left = getLeft(); right = getRight();
top = getTop();bottom = getBottom();
width = right - left; height = bottom - top;
从Android3.0喀什,View增加了额外几个参数:x,y,translationX,translationY,其中x和y是view的左上角的坐标,而translationX和translationY是View左上角相对于父容器的偏移量。
x = left + translationX; y = top + translationY;
需要注意的是View平移过程中,top和left表示的是原始左上角的位置信息,值不会改变,改变的是x,y,translationX,translationY
2.2 MotionEvent 和TouchSlop
1、MotionEvent
ACTION_DOWN 手指刚接触屏幕
ACTION_MOVE 手指在屏幕上移动
ACTION_UP 手指从屏幕抬起
上面三种事件通过MotionEvent对象我们可以得到事件发生时的x和y坐标。
getX(), getY() 返回相对于当前View的左上角的x和y坐标;
getRawX() ,getRawY() 获取相对于手机屏幕的左上角的x和y坐标
2、TouchSlop
TouchuSlop 获取系统能识别的最小滑动距离,这是个常亮和设备有关,在不同设备获取的值是不同的。
获取方式:ViewConfiguration.get(getContext()).getScaledTouchSlop();
3 VelocityTracker、GestureDetector、Scroller
3.1、VelocityTracker
速度追踪,用于追踪滑动过程中的速度,包括水平和竖直速度。如下具体使用步骤:
图6
速度 = (终点位置-起始位置)/ 时间段
从左至右滑动,xVelocity > 0; 相反 < 0;
图7
从上至下滑动,yVelocity > 0; 相反 < 0;
图8
3.2、GestureDetector
手势检测,用于辅助检测单击、滑动、长按、双击等手势。
需要创建一个GestrueDecector 对象并实现GestureDetector.OnGestureListener接口,如果需要双击,则需实现GestureDetector.OnDoubleTapListener接口;
图9
然后在 onTouchEvent 接管目标View的onTouchEvent方法,在待监听的onTouchEvent方法中添加如下实现:
图10
listener实现
图11
GestureDetector 类中的 OnGestureListener 接口和 OnDoubleTapListener 接口相关实现方法说明:
3.3、Scroller
弹性滑动对象,用于实现View的弹性滑动。当使用View的ScrollTo或者ScrollBy方法来进行滑动的时候,其过程是瞬间完成的,没有过渡效果和动画,体验是不太好的。
使用Scroller是可以实现有过渡效果的滑动,其过程不是瞬间完成,而是在一定时间间隔完成的。Scroller本身无法让View弹性滑动,它需要和View的computeScroll方法配合才可以实现。代码如下:
图12
4、View滑动
一般实现View滑动有三种方式:
(1) ScrollTo/ScrollBy :对view内容滑动
(2)使用动画
(3)改变View的布局参数
4.1 使用View的ScrollTo/ScrollBy方法
图13
图14
从源码可以看出,scrollBy实际上也是调用了scrollTo方法,它实现了基于当前位置的相对滑动,而scrollTo则实现了基于所传递的参数的绝对滑动。利用scrollTo和scrollBy来实现View的滑动,这不是一件困难的事,但是我们要明白滑动过程中View内部的两个属性mScrollX和mScrollY的改变规则,这两个属性可以通过getScrollX和getScrollY方法分别获得。
在滑动过程中,mScrollX的值总是等于View的上边缘和VIew的内容的上边缘水平方向的距离;
mScrollY等于View的上边缘和View内容的上边缘竖直方向的距离;
scrollTo和scrollBy只能改变View内容的位置而不能改变View在布局中的位置。
从左向右滑动,mScrollX < 0,反之大于0,从上往下滑动,mScrollY < 0,反之大于0;
图15
4.2 使用动画
使用动画来移动View主要是操作viewd translationX和translationY属性,既可以采用传统的View动画,也可以采用属性动画,如果采用属性动画的话,为了兼容Android3.0以下的版本,需要采用开源动画库nineoldandroids
(1)如下所示,此动画(translate.xml)可以在100ms内将一个View从原始位置向右下角移动100像素
图16
View调用startAnimation方法执行动画
图17
还可以使用属性动画,比较简单,以下代码可以将一个View在100ms内从原始位置向右平移滑动100像素
图18
查看源码可以看出ObjectAnimator可以做这些动画操作
图19
更多使用动画方式后面还会讲到,要注意的是View动画只是对View的影响做操作,并不能真正改变View的位置参数,如果希望动画后的状态得以保留还必须将fillAfter属性设置为true,否则动画完成后View会还原至原来的位置,如果设置为true,动画完成后View会停留在动画完成后的位置。
Android3.0一下无法使用系统api的属性动画,我们需要引入nineoldandroids库来实现属性动画。
注意:使用动画移动View后,在新的位置并不能出发View的点击触摸等事件,因为View的属性没有变化,所以在View原来的位置还是可以触发点击触摸等操作,因此我们不能简单的给一个View做平移操作并且还希望在新的位置可以触发一些点击操作。
可以通过在新的位置创建一个隐藏的View来代替原来的View的点击操作来解决这个问题。
4.3 通过改变布局参数滑动View
图20
注意:设置完毕layout属性后需要调用View.requestLayout来刷新View
各种滑动方式对比:
scrollTo/scrollBy:操作简单,适合对View内容滑动,但是只能滑动View内容,不能滑动View本身;
动画:操作简单,主要使用于没有交互的View和实现复杂的动画效果,但是动画滑动只是滑动的View的影像,没有改变View属性;
改变布局参数:操作稍微复杂,适用于有交互的View;
nineoldandroids 中有个ViewHelper类,也可以实现多个滑动动画,下面代码会实现一个可以随意拖动的view
重写自定义View的onTouchEvent方法
图21
图22
可以看出只有Android3.0(API 11)以上View才有自己的setTranslationX和setTranslationY方法,3.0以下就要使用ViewHelper类setTranslationX和setTranslationY方法了。实际上ViewHelper还提供了一系列的get/set方法执行动画,比如setX.setScaleX.setAlpha等方法,这一系列方法实际上是为属性动画服务的,更详细的我们后面会讲到。
下面实现了一个点击放大的效果
图23
代码如下:
图24
更多效果自行实验,只是调用方法不一样而已。
4.5、弹性滑动
直接使用View的滑动方法或者改变View属性的滑动都比较生硬,体验上非常差,所以我们要实现弹性滑动,如何实现弹性滑动呢,方法很多,其实都是一个思想:将一次性的移动分成若干次小的滑动并在一段时间内完成,实现方式有多种,比如通过Scroller、Handler的postDelayed方法,以及Thread的sleep方法,下面我们逐一介绍。
4.5.1 使用Scroller
图25
图26
实现效果就是每次点击View都会向左移动10像素,注意是它的内容移动,View并没有移动。
图27
使用Scroller要搭配实现View的方法computeScroll方法实现。先说下它的工作原理:我们先构造一个Scroller对象并调用它的startScroll方法时,Scroller内部其实并没有做什么具体的滑动工作,只是保存了我们传递的几个参数。代码如下:
图28
startX:滑动的起始横向偏移量,注意不是横坐标;startY 滑动的起始纵向偏移量;
startX > 0 View内容滑动时在起始位置左边,反之在右边;startY > 0,view内容滑动时在起始位置上方,反之在起始位置下方,一般View内容起始位置在View的中间;
dx:要横向滑动的距离,dx > 0,View内容向左滑动,反之向右滑动;
dy:要纵向滑动的距离,dy > 0 View的内容向上滑动,反之向下滑动;
在进行滑动的时候我调用了mScroller.startScroll(getScrollX(), 0, dx, 0, 500);
这里的startX是传入的getScrollX()方法,那么getScrollX()返回的是什么值呢?我们可以分析下
图29
返回值mScrollX和mScrollY分别表示距离起始位置的X轴或Y轴方向上的偏移量,而不是View在X轴或Y轴方向上的坐标值,用于记录偏移增量的两个变量。所以,mScrollX和mScrollY的初始值为0和0。
所以每次滑动完毕后再次点击再次滑动View内容的时候传入的startX是getScrollX()的值,这样每次滑动相对位置加10个像素,getScrollX也就增加10,这样就会实现每次点击向左滑动10个像素;
图30
每次点击向左滑动10个像素,相对偏移量每次增加10个像素;
getScrollX(): 滑动后的位置相对于原始位置横向偏移量,向左滑动 getScrollX() > 0,反之小于0;
getScrollY():滑动后的位置相对于原始位置纵向偏移量,向上滑动getScrollY() > 0,反之小于0;
上图为Button的内容滑动偏移量;
回到上面说StartScroll并没有做实际性的滑动工作,那么View是怎么滑动的呢?答案就是StartScroll方法下面的invalidate方法,invalidate方法会导致View重绘,重绘的过程中会调用View的draw方法,draw方法又会调用vomputeScroll方法,而computeScroll方法又会去向Scroller获取当前的scrollX和scrollY;然后通过scrollTo方法实现滑动;接着又调用了postInvalidate方法进行第二次重绘,这一次重绘和第一次重绘过程一样的还是会导致computeScroll方法调用;如此返回直到整个滑动过程结束。
上面我们在computeScroll方法中调用了Scroller的computeScrollOffset方法,下面我们分析下Scroller的computeScrollOffset方法实现:
图31
是不是豁然开朗了,这个方法会根据时间来计算当前的scrollX和scrollY的值。计算方法也很简单,就是根据时间流逝百分比来算出scrollX和scrollY改变的百分比计算出当前的值,这个过程类似于动画中的插值器的概念。注意这个方法的返回值很重要,返回true的时候表示滑动还未结束,false则表示滑动已经结束,因此这个方法返回true时,我们要继续进行View的滑动。
打印log可以看出每次刷新都会调用computeScroll方法:
图32
图33
看log已经很明白了吧,O(∩_∩)O~
总结下,Scroll本身并不能实现View的滑动,它需要配合View的computeScroll方法才能完成弹性滑动的效果,它不断的让View重绘,注意这里滑动的是View的内容而不是View本身。
4.5.2 使用动画滑动
使用动画让一个View的内容在100ms 向左滑动100像
图34
查看源码可以看出ObjectAnimator可以做这些动画操作
图35
我们还可以利用动画的特性来实现一些动画不能实现的效果。比如我们可以用动画来实现Scroller的滑动功能,代码如下:
图36
效果图,真实效果是比较平滑的,但是做出来的gif图是一卡一卡的,表被迷惑了,可自行实验哦~
图37
上述代码中,我们我们实现了一个持续时间为1000ms的动画,我们在动画的每一帧到来是获取动画完成的比例,然后根据这个比例计算出当前View要滑动的距离,注意,这里滑动还是View内容的滑动,而不是View本身滑动。可以发现,这个方法的思想其实个Scroller的实现思想类似的,都是通过改变一个百分比配个scroll方法来完成View内容的滑动。
我们完全可以在这种动画onAnimationUpdate方法中实现我们想要的其他操作。具体的动画我们后面还会具体讲到。
4.5.3 使用延时策略
延时策略核心思想就是通过发送一系列的延时消息从而达到一种渐进式的效果,具体说就是可以使用Handler或View的postDelayed方法,也可以使用线程的sleep方法。下面采用Handler做个实例,下面的代码是在1000ms内将View的内容向左移动了100像素;
图38
上述代码思想也是和Scroller滑动思想类似,将滑动的距离平均分割成均等份,每次滑动相同的距离,逐渐滑动至目标位置。
下期分享《View的事件分发机制和View滑动冲突》
欢迎关注我的公众号
分享Android进阶,Java进阶系列文章