一、View基础知识
1、View的位置参数
- 在Android中,左上角为坐标原点,x、y的正方向分别是右和下
- View的位置主要由四个顶点(相对父容器)决定
- top:左上角纵坐标
- top = getTop();
- left:左上角横坐标
- left = getLeft();
- right:右下角横坐标
- right = getRight();
- bottom:右下角纵坐标
- bottom = getBottom();
- width = riht - left
- height = top - bottom
- top:左上角纵坐标
- Android3.0之后,新增(相对父容器)
- x、y是View左上角的坐标
- translationX、translationY是View左上角相对于父容器的偏移量
- View在平移过程中,top和left表示的是初始左上角位置信息,并不会改变,发生改变的是x、y、translationX、translationY。
- x = left + translationX
- y = top + translationY
2、MotionEvent和TouchSlop
- MotionEvent
- 手指接触屏幕后典型事件类型
- ACTION_DOWN:手指刚接触屏幕
- ACTION_MOVE:手指在屏幕上移动
- ACTION_UP:手指从屏幕上松开的一瞬间
- 通过==MotionEvent对象==可以获得点击事件发生的x、y坐标
- getX/getY返回的是相对于当前View左上角的x和y坐标
- getRawX/getRawY返回的是相对于手机屏幕左上角的x和y坐标
- 手指接触屏幕后典型事件类型
- TouchSlop
- TouchSlop是系统所能识别出的被认为是滑动的最小距离,是一个常量。不同设备上的这个值可能不同。
- 通过 ViewConfiguration.get(Context()).getScaledTouchSlop() 获取
3、VelocityTracker、GestureDetector和Scroller
VelocityTracker 速度追踪
//创建VelocityTracker对象并加入追踪对象 VelocityTracker velocityTracker = VelocityTracker.obtain(); velocityTracker.addMovement(event); //设置追踪时间间隔(此处为1000ms) velocityTracker.computeCurrentVelocity(1000); //获取横向速度 int xVelocity = (int) velocityTracker.getXVelocity(); //获取纵向速度 int yVelocity = (int) velocityTracker.getYVelocity(); //不使用时重置并回收内存 velocityTracker.clear(); velocityTracker.recycle();
- 速度指的是设置的时间间隔内划过的像素点数。速度可为负,手指逆着坐标系的正方向滑动时速度即为负。
- 速度 = (终点位置 - 起点位置) / 时间段
- 速度指的是设置的时间间隔内划过的像素点数。速度可为负,手指逆着坐标系的正方向滑动时速度即为负。
GestureDetector 手势检测
- 辅助检测用户的单击、滑动、长按、双击等行为
使用
首先创建一个GestureDetector对象并实现OnGestureListener接口,或根据需要实现OnDoubleTapListener接口从而能够监听双击行为
GestureDetector mGestureDetector = new GestureDetector(this); //解决长按屏幕后无法拖动的现象 mGestureDetector.setIsLongpressEnabled(false);
接着在待监管目标View的onTouchEvent方法中添加如下方法
boolean consume = mGestureDetector.onTouchEvent(event); return consume;
然后可以有选择的实现接口中的方法
- OnGestureDetectorListener
方法 说明 onDown 手指轻触的一瞬间,由一个ACITON_DOWN触发 onShowPress 手指轻触,尚未松开或拖动,由一个ACTION_DOWN触发 onSingleTapUp 手指轻触屏幕后松开(单击行为),伴随一个ACTION_UP触发 onScroll 手指按下屏幕并拖动(拖动行为),由一个ACITON_DOWN和多个ACTION_MOVE触发 onLongPress 长按 onFiling 用户按下触摸屏、快速滑动后松开(快速滑动行为),由一个ACTION - OnDoubleTapListener
方法 说明 onDoubleTap 双击,由两次连续单击行为组成 onSingleTapConfirmed 严格的单击行为 onDoubleTapEvent 发生了双击行为,ACTION_DOWN/MOVE/UP都会触发此回调
Scroller 弹性滑动对象
- 实现有过渡效果的滑动
- 与 View的computeScroll() 方法配合使用实现
二、View的滑动
1、使用View本身提供的scrollTo/scrollBy(操作简单,适合对View内容的滑动)
- View边缘是指View的位置,由四个顶点组成。View的内容边缘是指View中的内容边缘。
- mScrollX:
- 单位为像素
- 在滑动过程中,该参数总是等于View左边缘和View内容左边缘在水平方向上的距离
- 通过getScrollX()方法得到
- 当View左边缘在View内容左边缘的右边时,该参数为正(从右向左划),反之为负
- mScrollY:
- 单位为像素
- 在滑动过程中,该参数总是等于View上边缘和View内容上边缘在垂直方向上的距离
- 通过getScrollY()方法得到
- 当View上边缘在View内容上边缘的右边时,该参数为正(从下向上划),反之为负
- mScrollX:
- 两个方法都只能改变View内容的位置而不是改变View在布局中的位置
- scrollTo():实现了基于所传递参数的绝对滑动
- scrollBy():调用了scrollTo方法,实现了基于位置的相对滑动
2、通过动画给View施加平移效果(操作简单,主要适用于没有交互的View和实现复杂的动画效果)
- View动画是对View 的影像做操作,并不能真正的改变View的位置参数(包括宽高)
- 使用fillAfter标签设置为true使完成后的动画状态的以保留,如果为false的话会在动画完成的瞬间恢复初始状态
- 使用属性动画则不存在这样的问题(Android3.0以下的系统通过nineoldandroids实现)
3、通过改变View的LayoutParamas使得View重新布局从而实现滑动(操作稍微复杂,适用于有交互的View)
改变布局参数
//eg: MarginLayoutParamas paramas = (MarginLayoutParams) mButton1.getLayoutParamas(); paramas.width += 100; paramas.leftMargin += 100; mButton1.requestLayout(); //或者mButton1.setLayoutParams(params);
三、弹性滑动
共同思想:将一次大的滑动分成若干次小的滑动,并在一个时间段内完成
1、使用Scroller
- (1)
Scroller scroller = new Scroller(mContext);
- (2)调用 scroller.startSroll() 方法
- 该方法只是保存了5个传入的参数
- startX、startY:滑动的起点
- dx、dy:要滑动的距离
- duration:滑动时间
- 该方法在源码的 smoothScrollTo() 方法中
- 该方法只是保存了5个传入的参数
private void smoothScrollTo(int destX, int destY) {
···
scroller.startScroll(···);
invalidate();
}
- (3)调用 invalidate() 方法,导致View重绘
- (4)View重绘中的draw方法调用 computScroll()方法,computScroll()方法是实现弹性滑动的核心方法。该方法在View中是一个空实现,需要自己实现
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
//判断条件中的方法会根据时间的流逝计算出当前的scrollX和scrollY的值
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
- (5)computeScroll又会向Scroller获取当前的scrollX和scrollY,然后通过scrollTo()方法滑动,接着又去调用 postInvalidate() 方法来进行第二次重绘,然后再次调用computeScroll方法
2、通过动画
- 思想与Scroller类似(差值器)
- 除了能完成弹性滑动之外,还可以实现其他的动画效果,在onAnimationUpdate方法中实现
3、使用延时策略
- 核心思想:通过发送一系列的延时消息从而达到一种渐进式的效果
- 使用Handler或View的postDelayed方法
- 使用线程的sleep方法
四、View的事件分发机制
1、点击事件传递规则
三大方法:
- (1)dispatchTouchEvent()
- 返回值表示是否消耗当前事件
- (2)onInterceptTOuchEvent()
- 返回值表示是否拦截当前事件
- 如果当前的View拦截了某个事件,那么在同一个事件序列当中,此方法不会再次被调用
- 此方法仅存在与ViewGroup当中,View中无此方法
- (3)onTouchEvent()
- 返回值表示是否消耗当前事件
- 如果不消耗,则在同一个事件序列中,当前View无法再次接收到事件
//点击事件分发过程三大方法关系伪代码分析 public boolean dispatchTouchEvent(MotionEvent ev) { boolean consume = false; //事件未被消费 if (onInterceptTouchEvent(ev)) { consume = onTouchEvent(ev); } else { consume = child.dispatchTouchEvent(ev); } return consume; }
- (1)dispatchTouchEvent()
- View处理事件时,如果设置了OnTouchListener,则回调onTouch()方法
- 如果返回false,则当前View的onTouchEvent方法被调用
- 如果返回true,则onTouchEvent不会被调用
- 优先级排序为OnTouchListener > OnTouchEvent > OnClickListener
- 点击事件传递顺序为:Activity -> Window -> View
- 如果所有元素都不处理某一点击事件,最终会调用 Activity的onTouchEvent() 方法
- 总结
- 同一个事件序列是指以down事件开始,中间含有数量不等的move事件,最终以up事件结束
- 正常情况下,一个事件序列只能被一个View拦截且消耗。
- 某个View一旦决定拦截,那么这一个事件序列都只能由它来处理(如果事件序列能够传递给他的话),并且它的onInterceptTouchEvent不会再被调用
- 某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent方法返回了false),那么同一事件序列中的其他时间都不会在交给它处理,并将事件重新交与父元素处理,即调用父元素的onTouchEvent()
- 如果View 不消耗除ACTION_DOWN以外的其它事件 ,那么这个点击事件会消失。此时父类元素的onTouchEvent并不会调用,并且当前的View可以持续的收到后续事件,最终这些消失的点击事件会交与Activity处理
- ViewGroup默认不拦截任何事件,即onInterceptTouchEcent方法默认返回false
- View没有onInterceptTouchEvent方法,一旦有事件传递给他,那么他的onTouchEvent方法就会被调用
- View的onTouchEvent方法默认消耗事件(返回true)。
- 除非它是不可点击的(clickable和longClickable同时为false)
- View的longClickable默认都为false
- clickable分情况
- View的enable属性不影响onTouchEvent的默认返回值。
- 即使View是disable状态的,只要clickable或longclickable中有一个为true,那么它的onTouchEvent就返回true
- 除非它是不可点击的(clickable和longClickable同时为false)
- onClick会发生的前提是当前的View是可点击的,并且它收到了down和up的事件
- 事件传递的过程是由外向内的
- 但通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的事件分发过程
- ACTION_DOWN事件除外
- 但通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的事件分发过程
2、源码解析
- Activity调用 dispatchTouchEvent 进行事件分发。
- 具体的工作是由Activity内部的Window完成的,Window会将事件传递给decor view
- Window类可以控制顶级View的外观和行为策略
- 顶级View(根View):即在Activity中通过setContentView设置的View。顶级View一般来说都是ViewGroup
- 具体的工作是由Activity内部的Window完成的,Window会将事件传递给decor view
顶级View(ViewGroup)对点击事件的分发过程
//check for interception final boolean intercepted; if(actionMasked == MOtionEvent.ACTION_DOWN || mFirstTouchTarget != null) { final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT != 0); //FLAG_DISALLOW_INTERCEPT一旦设置后,标记位不为零,disallowIntercept赋值为true //进入else,在else中intercept返回true //ViewGroup将无法拦截除了ACTION_DOWN之外的其他点击事件 if(!disallowIntercept) { //如果disallowIntercept为false才会进入此代码块 //即子View不请求该标记位为不能拦截事件 intercept = onInterceptTouchEvent(ev); ev.setAction(action); } else { intercept = false; } } else { //如果ViewGroup拦截了DOWN事件,mFirstTouchTarget = 0 //非ACTION_DOWN的后续系列事件默认交给ViewGroup处理,即不会调用onInterceptTouchEvent intercept = true }
- mFirstTouchTarget:一种单链表结构,赋值在 addTouchTarget 中完成。当事件由ViewGroup的子元素成功处理时,该参数会被赋值并指向子元素,即mFirstTouchTarget != null。ViewGroup拦截时值为空
- FLAG_DISALLOW_INTERCEPT:通过 requestDisallowInterceptTouchEvent 方法来设置。一旦设置后,ViewGroup将无法拦截除了ACTION_DOWN之外的其他点击事件
- 因为ViewGroup在分发事件时,如果是ACTION_DOWN就会在 resetTouchState 方法中重置FLAG_DISALLOW_INTERCEPT标记位,导致子View中的设置无效
- View对点击事件的处理
- 只要View的CLICKABLE和LONG_CLICKABLE中有一个为true,那么它就会消耗这个事件,即onTouchEvent返回true,不管是不是DISABLE状态
- 当ACTION_UP事件发生时,触发 performClick() 方法,如果View设置了OnClickListener,则performClick方法内部会调用它的 onClick() 方法
- setOnClickListener或setOnLongClickListener会将CLICKABLE和LONG_CLICKABLE设置为true
五、View的滑动冲突
1、常见的滑动冲突场景
- 场景1——外部滑动方向和内部滑动方向不一致
- 外部拦截法
- 内部拦截法
- 场景2——外部滑动方向和内部滑动方向一致
- 具体情况具体分析
- 场景3——场景一、二两种情况的嵌套
- 具体情况具体分析
2、滑动冲突的处理规则
- 依据滑动路径和水平方向所形成的夹角
- dx和dy
- 依据水平方向和竖直方向上的距离差
- dx和dy
- 依据水平和竖直方向的速度差
- VelocityTracker
3、滑动冲突的处理方法
(1)外部拦截法(推荐)
重写父容器的onInterceptTouchEvent方法,父容器根据自己的需要进行事件拦截。public boolean onInterceptTouchEvent(MotionEvent event) { boolean intercepted = false; int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACION_DOWN: { intercepted = false; break; } case MotionEvent.ACTION_MOVE: { if (父容器需要拦截当前点击事件) { intercept = true; } else { intercept = false; } break; } case MotionEvent.ACTION_UP: { intercept = false; break; } default: break; } //赋值记录位置 mLastXIntercept = x; mLastYIntercept = y; return intercept; }
- 对ACTION_DOWN事件,父容器必须返回false。否则后续事件都会交给父容器处理
- 对ACTION_MOVE事件,可以根据需要来决定是否拦截。
- 对ACTION_UP事件,父容器必须返回false。避免View的onClick不能触发
(2)内部拦截法
父容器不拦截任何事件,所有事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交给父容器处理。重写子容器的dispatchTouchEvent方法- 需要配合requetDisallowInterceptTouchEvent方法才能正常工作
- 父元素要默认拦截除了ACTION_DOWN之外的其他事件。因为DOWN事件会重置requestDisallowIntereptTouchEvent设置的标记位
public boolean dispatchTouchEvent(MotionEvent event) { int x = (int) event.getX(); int y = (int) event.getY(); switch(event.getAction()){ case MotionEvent.ACTION_DOWN: { parent.requestDisallowIntereptTouchEvent(true); break; } case MotionEvent.ACITON_MOVE: { int deltaX = x - LastX; int deltaY = y - LastY; if (父容器需要此类点击事件) { parent.requestDisallowIntereptTouchEvent(false); } break; } case MotionEvent.ACTION_UP: { break; } default: break; } //赋值记录位置 mLastX = x; mLastY = y; //保持原始的事件分发逻辑 return super.dispatchTouchEvent() }