前言: 1.view不属于四大组件,但是它的作用堪比四大组件,甚至比Receiver和Provider还要重要 2.Activity提供可视化的功能,Android系统提供了很多基础控件,比例button,textview.checkbox, 但是很多时候控件不能满足我们的需求,这个时候就需要自定义控件,而控件的自定义就需要对于android整个view的事件体系有深入的理解。 3.如何解决滑动冲突问题?需要对view的事件分发机制有一定的了解
一.view的位置参数 view的位置主要有他的四个顶点决定,分别对应view的四个属性:mLeft,mTop,mRight,mBottom这几个只代表View的原始位置,这个值是不变的, 如何获取view的四个属性呢? mleft = getLeft(); mRight = getRight(); mTop = getTop(); mBottom = getBottom(); 从3.0开始,view增加了额外的几个属性:x,y,translateX,translateY; x,y: 是view的左上角的坐标; translateX,translateY:是view左上角相对之前左上角的偏移量 如果view发生了偏移,那么x,y,translateX,translateY就会变化,x,y,translateX,translateY之间的对应关系如下: x = mLeft + translateX; y = mTop + translateY; 注意:这些坐标是相对于view的父容器来的,因此它是一种相对坐标,由此我们可以得出view的宽高和坐标关系: int with = right - left; int height = bottom - top;
二.MotionEvent和TouchSlop对象 1.获取点击事件发生的坐标,系统提供了两组方法:getX/getRawX,getY/getRawY ; getX/getY:返回的是相对于当前view左上角的x,y坐标; getRawX/getRawY:返回时相对于屏幕左上角的x,y坐标 举个例子:假如现在有一个LinearLayout,它相对屏幕的绝对坐标是222,333,linearlayout中有一个button, 这个button相对LinearLayout是39,34,所以如果getX,getY就是39,34, 如果是getRawX,getRawY就是 222+39,333+34 2、TouchSlop是系统所能识别出的被认为是滑动的最小距离,这是一个常量,这个值在framework/base/core/res/res/values/config.xml中,这个值和设备有关,不同的设备这个值也不尽相同 TouchSlop获取方式: ViewConfiguration.get(getContex()).getScaledTouchSlop();
三.VelocityTracker:速度追踪,用于追踪手指在滑动过程中的速度,包括水平和垂直方向的速度,在view的ontouchEvent()方法中追踪当前滑动的速度, 代码如下: VelocityTracker vVelocityTracker = VelocityTracker.obtain(); vVelocityTracker.addMovement(event);//此方法在onTouchEvent(Event event)中调用 // 如果想知道当前滑动的速度,可以采用以下方式: vVelocityTracker.computeCurrentVelocity(1000);//表示 1000ms int xVelocity = vVelocityTracker.getXVelocity(); int yVelocity = vVelocityTracker.getYVelocity(); 注意两点: 1.获取速度之前必须先计算速度,vVelocityTracker.computeCurrentVelocity(1000)就是计算速度, 2.速度单位是像素,比如将时间设置1000ms,就是1s,手指在水平方向划过200,那么水平速度就是200, 速度可以是负数:手指从右向左滑就是负数,速度 = (终点 - 起点)/ 时间段 3.最后不需要的时候调用clear方法重置并回收: VelocityTracker.clear(); vVelocityTracker.recycle();
四.GestureDetector:用户辅助检测用户的单击,滑动,长按,双击等行为,使用GestureDector也并不复杂,参考如下: GestureDetector gestureDector = new GestureDetector(this); 解决长按屏幕后无法拖动的现象gestureDector.setIsLongPressEnabled(false); 接着接管View的ontouchEvent()方法,在view的onTouchevent()方法中添加如下代码: int consume = gestureDector.onTouchEvent(event); return consume; 做完上面两步就可以有选择的实现OnGestureListener和OnDoubleTapListener中的方法 OnlGestureListener: onDown():一个action_down触发 onShowPress():一个action_down触发,他和onDown()的区别就是,没有松开,没有拖动 onSingleTapup();这是一个单击行为 onScroll();手指按下屏幕并拖动:有一个Action_down和多个Action)_move这是拖动行为 onLongPress:用户长时间按着屏幕不放,长按 onFling:用户按下屏幕,快速滑动后松开,一个down,多个move,一个up OnDoubleTapListener: nDoubleTap:双击,两次连续的单击组成,他不可能和onSingleTapComfirmed()并存 onSingleTapComfirmed:严格的单击行为,注意它和onSingleTapUp的区别,如果触发了onSingleTapComfirmed后面不能跟着单击行为,这只能是单击,而不可能是双击 onDoubleTapEvent:发生了双击行为,down,move.up都会触发
五.Scroller弹性滑动对象: Scroller是一个专门用于处理滚动效果的工具类,可能在大多数情况下,我们直接使用Scroller的场景并不多, 但是很多大家所熟知的控件在内部都是使用Scroller来实现的,如ViewPager、ListView等。 而如果能够把Scroller的用法熟练掌握的话,我们自己也可以轻松实现出类似于ViewPager这样的功能。 用于实现view的弹性滑动, 我们知道,当使用view的scrollTo/scrollBy方法来滑动时,其过程是瞬间的,这种用户体验很不好, 这个时候需要使用scroller来实现过度效果的滑动,Scroller本身无法让view弹性滑动,他需要和view的computeScroll()方法配合使用才能完成这个功能, 如何实现Scroller呢? 它的典型代码是固定的:Scroller Scroller = new Scroller(this); 缓慢滚动到指定位置 private void smoothScrollTo(int destX,int destY){ int scrollX = getScrolllX(); int delta= destX - scrollX;//100ms滑动到destX、效果就是慢慢滑动 scroller.startScroll(scrollX,0,delta,0,100); invalidate();//调用onDraw()---->调用computeScroll() } @override public void computeScroll(){ if(mScroller.computeScrollOffset()){ scrollTo(mScroller.getCurrX(),mScroller.getCurrY()); postInvalidate();//--->调用onDraw()--->computeScroll(),如此反复 } } 解析:scroller的startScroll(int startx,int starty,int dx,int dy,int duration)其实很简单, startx,starty表示滑动的起点,dx,dy表示的是要滑动的距离,duration是滑动的时间 Scroller的startScroll是无法滑动的,真正滑动的方法是invalidate();此方法会回调ondraw(),onDraw()方法又会回调computeScroll(), 在computeScroll()中通过mScroller.computeScrollOffset()判断当前时刻应该所在的位置,如果当前位置不是startScroll设置的终点位置, 就会调用scrollTo滑动到这个时刻应该位于的位置
六、View的滑动 其实任何一个控件都是可以滚动的,因为在View类当中有scrollTo()和scrollBy()这两个方法。 通过三种方法可是实现view的滑动: 1.view本身提供的scrollTo/scrollBy方法 scrollBy实际内部也是调用了scrollTo(),他是基于当前位置的相对移动,而scrollTo则是基于所传参数的绝对移动 view的内部有两个属性mScrollX,mScrollY,这个两个属性通过getScrollX,getScrollY获取, 在滑动过程中mScrollX总是等于view的左边缘和view内容的左边缘的距离,mscrollY等于view上边缘与view内容上边缘的距离 scrollTo和ScrollBy只改变view内容的位置,并不改变view的位置 view的左边缘在view内容左边缘的右边时,mScrollX为正,反之为负; view的上边缘在view内容上边缘的下边时,mScrollY为正,反之为负; 换句话说,上滑为正,左滑为正,否则反之 2.通过动画给view添加平移效果来实现滑动 其实平移就是一种滑动,他可以采用传统的view动画,也可以使用属性动画, 如果属性动画为了兼容3.0以下的版本,需要采用开源动画库nineoldandroids 3、改变布局参数 LinearLayout.LayoutParams marginLayoutParams = (LinearLayout.LayoutParams) scrollByBtn.getLayoutParams(); marginLayoutParams.leftMargin += 100; scrollByBtn.setLayoutParams(marginLayoutParams); 各种滑动方式的对比: scrollTo/scrollBy是专门用于view的滑动,但是只能针对内容,不能滑动view本身 动画:操作简单,主要用于没交互的view和实现复杂的动画效果 改变布局参数:操作稍微复杂,适用于有交互的view
qi、View的弹性滑动 1.使用scroller 2.通过动画(使用valueAnimator,通过监听某一个值的变化,来计算view应该在什么位置,并使用scrollto滑动,这种方式和scrooller非常相似 3.使用延时:核心思想就是发送一系列延迟消息从而达到一个渐进的效果,具体可以使用Handler和view的postDelayed方法, 使用线程的sleep也可以实现,在while循环中不断的滑动view合理sleep
八、View的事件分发机制 1.点击事件的传递规则 点击事件的分发其实就是MotionEvent事件分发的过程,当一个MotionEvent产生以后,系统需要把这个事件传递给具体的view,这个传递的过程就是分发过程, 点击事件的分发过程由三个重要的方法共同完成:dispathTouchEvent(),onInterceptTouchEvent(),onTouchEvent(); dispathTouchEvent: 如果事件能够传递到当前view,那么此方法一定被调用,返回值受View的onInterceptTouchEvent和下级view的dispathTouchEvent的影响 onInterceptTouchEvent: 在dispathTouchEvent方法的内部调用,用来判断是否拦截某个事件,如果当前view拦截了某个事件,那么同一事件序列当中此方法不会被再次调用, 返回值表示是否拦截当前事件 onTouchEvent: 在dispathTouchEvent方法中调用,用来处理点击事件,返回值表示是否消耗此事件,如果不消耗,同一个事件序列中,当前view不会再次接受事件 以下代码来表示三者之间的区别: @Override protected boolean dispatchHoverEvent(MotionEvent event) { boolean consume = false; if(onInterceptTouchEvent(event)){ consume = onTouchEvent(event); }else { consume = child.dispatchTouchEvent(event); } return consume; } 流程梳理: 1.对于一个viewGroup,当点击事件产生以后,他的dispatchTouchEvent就会调用,如果这个ViewGroup的onInterceptTouchEvent返回true就表示要拦截当前事件, 接着事件就会交给viewgroup处理,即它的onTouchEvent就会被调用; 如果这个Viewgroup的onInterceptTouchEvent返回false就表示不拦截当前事件, 这时当前事件就会传递给它的子元素, 接着子元素的dispatchTouchEvent方法就会被调用,如此反复,直到事件被最终处理 2.当一个view需要处理事件时,如果它设置了OnTouchListener,那么OnTouchEvent的onTouch()方法就会回调, 如果ontouch返回true,onTouchEvent不会回调 如果ontouch返回false,onTouchEvent就会回调, 由此可见,view设置的onTouchListener的优先级比onTouchEvent高, 在onTouchEvent方法中,如果设置了onClickListener,那么它的onClick()方法就会被调用可以看出我们平时用的OnClickListener, 其优先级最低,处于事件的末尾3.当一个点击事件产生后,事件传递顺序:actiivty---》windows---》view,view收到事件以后就会按照分发机制进行分发, 考虑一种情况:如果view的onTouchEvent返回false,那么它的父容器的onTouchEvent()就会调用, 一次类推,如果所有的元素都不处理这个事件,那么这个事件将会最终传给activity,即Actiivty的ontouchEvent调用 结论: 1.同一个事件是指从手指触屏到手指离开,这个过程所产生的一系列事件 2.正常情况下,一个事件只能被一个view拦截消耗(但也可以通过特殊手段,强行传递给其他的view处理), 因为一旦一个元素拦截了某个事件,那么同一系列的所有事件都会直接交由他处理 因此就不会再调用onInterceptTouchEvent来询问了, 3,viewGroup默认不拦截任何事件 4.view没有onInterceptTouchEvent方法,一旦点击事件传递给他,它的ontouchEvent就会调用 5.view的ontouchEvent默认是消耗事件的,除非他是不可点击(clickable,longClickable同时为false) 6、view的enable属性不影响onTouchEvent的默认返回值,哪怕一个view是disable状态,只要它的clickable和longclickable返回true, 那么ontouchEvent就返回true 7.onclick发生的前提是当前view是可点击的,并且它收到download和up事件 8.事件总是由外到内,从父元素传递给子元素,通过requestDisallowInterceptTouchEvent方法可以在子元素干预父元素的事件分发过程,但是action_down事件除外
九.事件分发的源码解析; Activity.dispatchTouchEvent-->getWindow().superdispatchTouchEvent--->mDecor.superDispatchtouchEvent() getWindow.getDecorView().findViewByID(android.R.id.content).getChildAt(0)这种方式可以获取通过setContentView()设置的内容 viewGroup在如下两种情况会判断是否要拦截当前事件: 1.事件类型是Action_down 或者 mFirstTouchTarget != null,当事件有viewGroup的子元素成功处理,mFirstTouchTarget会被赋值并指向子元素, 反过来一旦viewGroup拦截此事件,mFirstTouchTarget != null不成立,导致viewGroup的onInterceptTouchEvent不会再调用 当然有一种情况:FLAG_DISALLOW_INTERCEPT标志位,这个标志位是通过requestDisallowInterceptTouchEvent方法设置的, FLAG_DISALLOW_INTERCEPT一旦设立,viewGroup无法拦截除ACTION_DOWN以外的事件,为什么说是ACTION_DOWN以外的事件呢? 这是因为ACTION_DOWN会重置此标志 View的CLICKABLE和LONGCLICKABLEL有一个为true,那么他就会消耗这个事件,即onTouchEvent返回true,不管它是不是DISABLE状态, 当ACTION_UP事件发生时,会触发performClick方法 如果设置了onClickListener,那么performClick方法内部会调用它的onclick方法 view的LONGCLICKABLE默认是false,view的CLICKABLE是否为false,跟具体的view有关,button的CLICKABLE默认是true,textview默认是false setonClickListener自动将view的clickable设为true,setlongClicklistener()默认将longClickable设为true;
十.滑动冲突解决方案:
这里给出了两种解决滑动冲突的方式:外部拦截法和内部拦截法
外部拦截法:点击事件经过父容器的拦截处理,如果父容器需要此事件就拦截,不需要就不拦截,外部拦截法需要重写父容器 的onInterceptTouchEvent方法,在内部做相应的拦截即可,代码如下:
父类中: int mLastXIntercept; int mLastYIntercept; @Override public boolean onInterceptTouchEvent(MotionEvent ev) { boolean intercept = false; int x = (int) ev.getX(); int y = (int) ev.getY(); switch (ev.getAction()){ case MotionEvent.ACTION_DOWN: intercept = false;//必须返回false,如果返回true,move,up就没法传给子view break; case MotionEvent.ACTION_MOVE: if(父容器需要当前点击事件){ intercept = true; }else { intercept = false; } break; case MotionEvent.ACTION_UP: intercept = false;//必须返回false break; default: break; } mLastXIntercept = x; mLastYIntercept = y; return intercept; }
内部拦截法:需要配合requestDisallowInterceptTouchEvent方法才能正常工作,
子类中: int mLastXIntercept; int mLastYIntercept; @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 deltX = x - mLastXIntercept; int deltaY = y - mLastYIntercept; if(父容器需要当前点击事件){ getParent().requestDisallowInterceptTouchEvent(false);//请求要拦截 } break; case MotionEvent.ACTION_UP: break; default: break; } mLastXIntercept = x; mLastYIntercept = y; return super.dispatchTouchEvent(ev); } 父类: @Override public boolean onInterceptTouchEvent(MotionEvent ev) { int action = ev.getAction(); if(action == MotionEvent.ACTION_DOWN){ return false; }else { return true; } }