时间:2022-06-11 10:09:29 | 栏目:Android代码 | 点击:次
View是Android中所有控件的基类,不管是Button、ListView还是RelativeLayout,它们的基类都是View。View是一种界面层的控件的一种抽象,代表了一个控件。
而什么是ViewGroup,从字面上看,ViewGroup应该指的是一个控件组,即ViewGroup中可以包含许多控件。而ViewGroup继承自View,所以View本身就可以是单个控件也可以由多个控件组成的一组控件。这样就构成了View树。
View的位置由它的四个顶点确定,top(左上角纵坐标)、left(左上角横坐标)、bottom(右下角纵坐标)、right(右下角横坐标),这几个参数都是相对父级容器而言的。
在Android中,X轴和Y轴的正方向分别为向右和向下。
根据四个顶点及AndroidView的坐标系,我们可以很容易得到View的宽高和坐标的关系:
width=right-left
height=bottom-top
那么如何得到这四个顶点呢?
left=getLeft();
right=getRight();
top=getTop();
bottom=getBottom();
从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
在手指接触屏幕后所产生的一系列事件中,典型的事件有:
ACTION_DOWN——手指刚接触屏幕
ACTION_MOVE——手指在屏幕上移动
ACTION_DOWN——手指从屏幕上松开
一般我们可以将一次手指接触屏幕的行为分为两种情况:
点击屏幕后松开,事件序列为DOWN->UP
点击屏幕滑动一段时间后松开,事件序列为DOWN->MOVE->…->MOVE->UP
TouchSlop即系统能识别滑动的最小距离,这是一个与设备有关的系统常量。不难得知其意思,当手指在屏幕上滑动小于这个距离时,系统不认为你在进行滑动操作。
通过ViewConfiguration.get(getContext()).getScaledTouchSlop()方法来获取这个系统常量。
速度追踪,用于追踪手指在滑动过程中的速度,包括水平和竖直方向上的速度。
具体用法:
在View的onTouchEvent方法中追踪当前单击事件的速度。
VelocityTracker velocityTracker =VelocityTracker.obtain(); velocityTracker.addMovement(event); //接着当我们我们想知道当前的滑动速度时 //获取速度前先计算速度, 参数 时间间隔 单位ms velocityTracker.computeCurrentVelocity(1000); //获取速度 int xVelocity = (int)velocityTracker.getXVelocity(); int yVelocity=(int)velocityTracker.getYVelocity();
需要注意的是,这边的计算得到的速度与时间间隔有关,其计算公式如下:
速度=(终点位置-起点位置)/时间间隔
计算速度时得到是就是一定时间间隔内手指在水平或竖直方向上滑动的像素数,如
100像素/1000ms,这里的速度值即为100。
当然在不需要使用它时,需要调用clear方法来重置并回收内存。
velocityTracker.clear();
velocityTracker.recycle();
4.GestureDetector
手势检测,用于辅助检测用户的单击、滑动、长按、双击等行为。如果只是监听滑动相关的建议在onToucheEvent实现,如果需要监听双击,使用GestureDetector。
弹性滑动对象,用来实现View的弹性滑动,View的scrollTo/scrollBy是瞬间完成的,使用Scroller配合View的computeScroll方法配合使用达到弹性滑动的效果
其典型代码是通用的.
/** * 平滑滚动 * @param dx 横向位移 * @param dy */ private void smoothScrollBy(int dx, int dy) { //水平滑动 mScroller.startScroll(getScrollX(),0,dx,0,500); invalidate(); } @Override public void computeScroll() { if(mScroller.computeScrollOffset()){ scrollTo(mScroller.getCurrX(),mScroller.getCurrY()); postInvalidate(); } }
实现View滑动的几种方式:
View的滑动方式 | 特点 | 适用场景 |
使用ScrollTo/ScrollBy | 只能改变View的内容,不能改变View本身的位置 | 适合对view内容的滑动 |
通过动画实现View的平移效果 | 只对影像进行操作,不能改变View的位置参数 | 适用于没有交互的View和实现复杂的动画效果。 |
使用属性动画实现View的平移效果 | 改变View的位置参数,可响应触摸等事件 | 适用于有交互的View,适配到Android3.0 |
改变View的LayoutParams,使得View重新布局实现滑动 | 改变View的位置参数,可响应触摸等事件 | 适用于有交互的View,使用稍复杂 |
前面提到了弹性滑动对象Scroll,其实实现弹性滑动的方法不止这一种,它们的共同思想就是将一次大的滑动分成若干次小的滑动并要求在一定时间内完成。实现弹性滑动的具体实现方式有:
使用Scroll实现弹性滑动需要配合View的computeScroll方法实现,简单来讲就是实现多次重绘,每一次重绘有一定的时间间隔,通过这个时间间隔Scroller可以得到View的当前滑动位置,然后通过ScrollTo方法实现滑动。
具体实现方法是在自己实现的平滑滑动方法中调用invalidate方法,它会导致View重绘,又因为在View的draw方法中又会去调用computeScroll方法,而在computeScroll方法中,我们实现了scrollTo方法来实现滑动,接着调用postInvalidate来进行第二次重绘,此时又会调用View中的draw方法,,继而调用computeScroll方法,如此反复,直到整个滑动过程完成。
动画本身就是一种渐渐地过程,可以很好地实现弹性滑动。
使用延时策略完成滑动,核心思想就是通过发送一系列的延时消息从而达到一种渐进的效果。具体的实现可以采用Handler或View的postDelayed方法,也可以采用sleep休眠。对于postDelayed方法们可以通过它来延时发送一个消息,然后在消息中进行View的滚动。如果接连不断发的发送这种消息,则可以达到弹性滑动对象。
而对于sleep方法,通过在while循环中不断滑动View和sleep即可实现。
分发对象:MotionEvent,所谓的事件分发其实就是对MotionEvent事件的分发过程,即需要将这个事件传递到一个具体的View上进行处理。而完成这一过程需要三个重要方法来共同完成:dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent。
public boolean dispatchTouchEvent(MotionEvent ev)
用来进行事件的分发,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent方法的影响,表示是否消耗当前事件。
public boolean onInterceptTouchEvent(MotionEvent ev)
在dispatchTouchEvent方法内部调用,用于判断是否拦截某个时间,如果当前View拦截了某个事件,那么在同一个事件序列当中,此方法不会被再次调用。
public boolean onTouchEvent(MotionEvent event)
在dispatchTouchEvent方法中调用,用于处理点击事件,返回结果表示是否消耗事件,如果不消耗,则在同一个事件序列中,当前View无法再次收到事件。
建立在上述方法的基础上,我们简单分析一下事件分发的过程。
由上述流程图不难发现,在MotionEvent被一个View所拦截时,其内部的事件分发的过程中,onTouchListener的优先级高于onTouchEvent,而常用的onClickListener的优先级是最低的。即在onTouch->onTouchEvent->onClick。
几个重要的结论:
1.外部滑动和内部滑动方向不一致
2.外部滑动方向和内部滑动方向一致
3.上面两张情况的嵌套
对于第一种情况,一个很好的例子就是ViewPager和Fragment嵌套使用组成的页面滑动效果,而在Fragment内部又会嵌套一个ListView。大家都知道ViewPager的滑动方向是水平的,而ListView的滑动方向是竖直的,这种情形和第一种情况是相符的。当然ViewPager在内部处理了这种滑动冲突,因此采用ViewPager不用考虑这个问题。而如果我们采用的是ScrollView,则必须手动处理这种滑动冲突了。
对于第二种情况,即内外两层都是需要上下滑动或者左右滑动的。可以举一个常见的例子,即ViewPager和NavigationDrawer。这两者都是水平方向的滑动。当然在实际使用中,会发现并没有滑动冲突,还是上一个原因,ViewPager内部处理了这种滑动冲突。
第三种情况即前面两个例子的融合。
如何解决滑动冲突,这就需要用到前面讲到的事件分发机制了,其核心思想就是根据实际事件的特点(down的位置,水平滑动距离,竖直滑动距离等)来判断由哪个View来拦截事件。对于第一种情况可以简单地判断是水平滑动还是竖直滑动来判断由哪个View来拦截事件。(可以根据水平和竖直方向上的距离差或速度差来进行判断),而对于第二种情况,可根据down的位置来加以区分。
两种拦截方法的范式(伪代码形式):
外部拦截法:只需要重写父容器的onInterceptTouchEvent方法
private int mLastXIntercepet=0; private int mLastYIntercepet=0; @Override public boolean onInterceptTouchEvent(MotionEvent ev) { boolean intercepted =false; int x=(int) ev.getX(); int y=(int) ev.getY(); switch (ev.getAction()){ case MotionEvent.ACTION_DOWN: intercepted=false; break; case MotionEvent.ACTION_MOVE: if(父容器需要当前点击事件){ intercepted=true; }else{ intercepted=false; } break; case MotionEvent.ACTION_UP: intercepted=false; break; default: break; } mLastXIntercepet=x; mLastYIntercepet=y; return intercepted; }
内部拦截法:需要重写子元素的dispatchTouchEvent方法和父容器的onInterceptTouchEvent方法。
子元素的dispatchTouchEvent方法
//分别记录上次滑动的坐标 private int mLastX=0; private int mLastY=0; @Override public boolean dispatchTouchEvent(MotionEvent ev) { int x= (int) ev.getX(); int y= (int) ev.getY(); switch (ev.getAction()){ case MotionEvent.ACTION_DOWN: getParent().requestDisallowInterceptTouchEvent(true); break; case MotionEvent.ACTION_MOVE: int deltaX=x-mLastX; int deltaY=y-mLastY; if(父容器需要此类点击事件){ getParent().requestDisallowInterceptTouchEvent(false); } break; case MotionEvent.ACTION_UP: break; default: break; } mLastX=x; mLastY=y; return super.dispatchTouchEvent(ev); }
父容器的onInterceptTouchEvent:
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { int x= (int) ev.getX(); int y= (int) ev.getY(); int action=ev.getAction(); if(action==MotionEvent.ACTION_DOWN) return false; else return true; }