Android开发艺术探索——第三章View事件体系读书笔记
2016-04-23 18:18
621 查看
android开发艺术探索——第三章View的事件体系
3.1
View的基础知识点
(一) View 和 ViewGroup
Veiw 是Android中所有控件的基类。View是一种界面层的控件的抽象。 ViewGroup也是继承之View,翻译为控件组。ViewGroup内部包含了许多控件。所以View本身可以是单个控件也可以是由多个控件组成的一个控件。 例: LinearLayout不但是一个View还是一个ViewGroup,而ViewGroup内部可以有子View的,这个子View同样还可以是ViewGroup。
(二)View的位置参数
View的位置由四个顶点来决定,分别对应于四个属性:top,left,right,bottom。这四个参数都是相对于Veiw的父容器来说的,是相对坐标。
(三)MotionEvent 和 TouchSlop
1.MotionEvent· ACTION_DOWN 手指刚接触屏幕 · ACTION_MOVE 手指在屏幕上移动 · ACTION_UP 手指从屏幕上松开的一瞬间 通过MotionEvent对象可以得到点击事件发生的x 和 y坐标。系统提供了两组方法: getX/getY 和 getRawX/getRawY 区别: getX/getY返回的是相对于当前View的左上角的x和y坐标; getRawX/getRawY返回的是相对于屏幕的左上角的x和y的坐标
2.TouchSolp
系统所能识别出的被认为是滑动的最小距离。也就是说,当手指在屏幕上滑动时,如果两次滑动之间的距离小于这个常量, 系统会认为这不是一次滑动。需要注意的是,不同的 设备这个常量值可能是不相同的。可通过如下的方式获取这个常量值: ViewConfiguration.get(getContext()).getScaledTouchSlop()
(四)VelocityTracker、GestureDetector和Scroller
1.VelocityTracker速度跟踪,用于追踪手指在滑动过程中的速度,包括水平和垂直方向的速度。 首先,在View的onTouchEvent方法中追踪当前的点击事件的速度: > VelocityTracker velocityTracker = VelocityTracker.obtain(); velocityTracler.addMovement(event); 接着, > velocityTracker.computeCurrentVelocity(1000); int xVelocity= (int)velocityTracker.getXVelocity(); int yVelocity= (int)velocityTracker.getYVelocity(); 这里需要注意两点: 第一: 获取速度之前必须先计算速度,即必须先调用computeCurrentVelocity(1000)之后是getXVelocity(),getYVelocity() 第二: 这里的速度是在指定时间内划过的像素数 例:将时间间隔设为1s,在1s内,手指在水平方向从左向右滑动100像素,水平速度就是100; 速度也可以为负数。当手指从右向左滑过100像素,水平方向的速度就是负值。 公式表示: 速度 = (终点位置-起始位置)/ 时间段 最后,当不需要的时候,需要调用clear方法重置并回收内存。 > velocityTracker.clear(); velocityTracker.recycle();
2.GestureDetector
手势检测,用于辅助检测用户单击、滑动、长按和双击等行为。(如果只是监听滑动相关,建议在onTouchEvent; 如果是双击,需要使用GestureDetector) 首先,需要创建一个GestureDetector对象实现onGestureListener接口, 这里根据需要还可以实现OnDoubleTapListener 实现双击监听 > GestureDetector mGestureDetector = new GestureDetector(this); //解决长按屏幕后无法拖动的现象 mGestureDetector.setIsLongpressEnabled(false); 接着,接管目标的View的OnTouchEvent方法,在待监听的View的OnTouchEvent中添加: > boolean consume = mGestureDetector.onTouchEvent(event); return consume; 然后,可以有选择地实现OnGestureListener 和 OnDoubleTapListener中的方法
3.Scroller
Socroller本身无法让View弹性滑动,需要和View的computeScroll配合使用 > Scroller scroller = new Scroller(this); //缓慢滑到指定位置 private void smoothScrollTo(int destX, int destY){ int scrollX = getScrollX(); int delta= destX - getScrollX(); //1000ms内滑向destX,效果就是慢慢滑动 mScroller.startScroll(scrollX,0 , delta,0 , 1000); invalidata(); } @Override public void computeScroll(){ if(mScroller.computeScrollOffset()){ scrollTo(mScroller.getCurrX(),mScroller.getCurrY()); postiInvalidate(); } }
3.2
View的滑动
一般View的滑动通过三种方式实现: 第一种通过View本身的提供的scrollTo/scrollBy方法实现 第二种通过动画给View施加平移效果实现滑动 第三种通过改变View的LayoutParams使得View重新布局从而实现滑动
(一)使用scrollTo/scrollBy
public void scrollTo(int x,int y){ if( mScrollX != x && mScrollY != null){ int oldX = mScrollX; int oldY = mScrollY; mScrollX = x; mScrollY = y; invalidateParentCaches(); onScrollChanged(mScrollX,,mScrollY,oldX, oldY); if( !awakenScrollBars()){ postInvalidateOnAnimation(); } } } /*************************************/ public void scrollBy(int x , int y){ scrollTo(mScrollX + x, mScrollY + y); }
scrollBy()调用scrollTo()方法。scrollTBy()基于当前位置的相对滑动,scrollTo()实现了基于 所传递参数的绝对滑动。
* 注意:*
使用scrollTo()和scrollBy()来实现View的滑动,只能将View上的内容进行移动,
并不能将View本身进行移动。
无论怎么滑动都不可能将当前的View滑动到附近的View所在区域。
(二)使用动画
使用动画主要是操作View的transklationX和transklationY属性 动画代码,此动画在100ms内将一个View从原始位置向右下角移动100个像素。 <set xmlns:android="http://schemas.android.com/apk/res/android" android:fillAfter="true" android:zAdjustment="normal"> <translate adnroid:duration = "100" android:fromXDelta = "0" android:fromYDelta = "0" android:interpolator = "@android:anim/linear_interpolator" android:toXDelta = "100" android:toYDelta = "100" /> </set> 属性动画代码 ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start(); View的动画是对View的影像做操作,并不能真正改变View的位置参数,包括宽/高, 如果希望保留动画后的状态,将 fillAfter设置为true;
(三)改变参数布局
MarginLayoutParams params = (MarginLayoutParams)mButtion1.getLayoutParams(); params.width += 100; params.leftMargin += 100;0 mButtion1.requestLayout(); //或者mButton1.setLayoutParams(params)
(四)对比
-scrollTo/scrollBy : 操作简单,适合对View内容的滑动-动画 : 操作简单,适合用于没有交互的View和实现复杂的动画效果
-改变布局参数 : 操作稍微复杂了点,适用于有交互的View
3.3弹性滑动
(一)使用Scroller
Scroller源码: Scroller scroller = new Scroller(mContext); //缓慢滚动到指定位置 private void smoothScrollTo(int destX,int destY){ int scrollX = getScrollX(); int deltaX = destX - srollX; //1000ms内滑向destX,效果就是慢慢滑动 mScroller.startScroll(scrollX,0,deltaX,1,1000); invalidate(); } @Override public void computeScroll(){ if(mScroller.computeScrollOffset()){ scrollTo(mScroller.getCurrX(),mScroller.getCurrY()); postInvalidate(); } }
3.4
View的事件分发
(一)点击事件的传递规则
* public boolean dispatchTouchEvent(MotionEvent ev) *如果事件能够传递给当前View,此方法一定会被调用,返回结果受当前View的oTouchEvent 和
下级View的dispatchTouchEvent 方法的影响,表示是否消耗当前事件
* public boolean onInterceptTouchEvent(MotionEvent event) *
在上述内部调用,用来判断是否拦截某个事件,如果当前View拦截某个事件,
那么同一事件序列当中,此方法不会再调用,返回结果表示是否拦截当前事件
* public boolean onTouchEvent(MotionEvent event) *
在dispatchTouchEvent 方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,
如果不消耗,则在同一个事件序列中,当前View无法再次接收到事件
public boolean dispatchTouchEvent(MotionEvent ev){ boolean consume = false ; if(onInterceptTouchEvent (ev)){ consume = onTouchEvent(ev); }else{ consume = child.dispatchTouchEvent(ev); } return consume; }
传递规则:
对于一个根ViewGroup来说,点击事件产生后,首先会传递给它,这时它的dispatchTouchEvent就会被调用,如果这个
ViewGroup的onInterceptTouchEvent方法返回true就表示它要拦截当前事件,接着事件就会交给这个ViewGroup处理,即
它的onTouchEvent方法就会被调用;如果这个ViewGroup的onInterceptTouchEvent方法返回false就表示不拦截当前事件,
这时当前事件就会继续传递给它的子元素,接着子元素的dispatchTouchEvent方法就会调用,如此反复,直到事件被处理。
当一个View需要处理事件时,如果它设置了onTouchListener,onTouchListener中的onTouch方法会被回调。这件事如何处理还要 看onTouch的返回值,如果返回false。则当前View的onTouch方法会被调用;如果返回true,那么onTouchEvent方法将不会被调用。 由此可见,给View设置onTouchListener,其优先级比onTouchEvent要高。在onTouch方法中,如果设置的有onClickListener,那么 onClick方法会被调用。 点击事件的传递顺序: activity --> window --> View
事件分布的源码分析
(一)Activity对点击事件的分布 过程
点击事件用MotionEvent来表示,当一个点击操作发生时,事件最先传递给当前的Activicity,由Activity的disaptchTouchEvent来进行事件的派发,具体的工作是由Activcity内部的Window来完成的。Window会将事件传递给decor view, decor view一般就是当前
界面的底层容器,通过Activity.getWindow.getDecorView()获得
源码:Activity#dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev){ if(ev.getAction() == MotionEvent.ACTION_DOWN){ onUserInteraction(); } if(getWindow().superDispatchTouchEvent (ev)){ return true; } return onTouchEvent(ev); }
事件开始交给Acitivity所属的Winow进行分布,如果返回true,整个事件循环就结束,返回false
意味着事件没人处理,所有View的onTouchEvent都返回了false,Activity的onTouchEvent会调用。Window
将事件传递给ViewGroup的。
Window是个抽象类,Window的dispatchTouchEvent是个抽象方法,实现类是PhoneWindow。
源码:PhoneWindow#superDispatchTouchEvent
public boolean superDispatchTouchEvent(MotionEvent ev){ return mDecor.superDispatchTouchEvent(ev); }
PhoneWindow将事件直接传递给了DecorView。
通过((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0)
获得Activity所设置的View,这个mDecor就是getWindow().getDecorView()返回的View,通过setContentView
设置的View就是mDecor的子View。目前事件传递到了这里。
(二)顶级View 对点击事件的分发过程
View事件分发回顾: 点击事件达到顶级View(一般是一个ViewGroup)以后,会调用ViewGroup的dispatchTouchEvent方法; 此时, · 如果顶级ViewGroup拦截事件onInterceptTouchEvent返回 true,则事件由ViewGroup本身处理。 这时如果ViewGroup的mOnTouchListener被设置,则OnTouch被调用,否则OnTouchEvent会被调 用。也就是说如果能提供的话,onTouch会屏蔽掉onToucheEvent。在onTouchEvent中,如果设 置了mOnClickListener,则onClick会被调用。 · 如果顶级ViewGroup不拦截事件,则事件会传递到它所在的点击事件链上的子View,这个时候子 View的dispatchTouchEvent会被调用。
(三)View对点击事件的处理过程
* 分发过程 和 处理过程第一遍没看懂*View的setClickable 和 setOnLongClickListener会自动将View的 CLICKABLE 和 LONG_CLICKABLE属性
设置为true。
3.5
View的滑动冲突
(一)常见的滑动冲突场景
常见的滑动冲突场景,简单分为三种:场景1——外部滑动方向和内部滑动方向不一致
场景2——外部滑动方向和内部滑动方向一致
场景3——上面两种情况的嵌套
(二)滑动冲突的解决方式
* 针对场景1的解决方式 *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.ACTION_MOVE: intercepted = true; break; case MotionEvent.Move: if(父容器需要当前点击事件){ intercepted = true; }else{ intercepted = false; } break; case MotionEvent.ACTION_UP: intercepted = false; break; } mLastXIntercept = x; mLastYIntercept = y; return intercepted; }
在onInterceptTouchEvent方法中,首先是在ACTION_DOWN,父容器必须返回false,即不拦截
ACTION_DOWN,ACTION_MOVE和ACTION_UP事件都会直接交由父容器处理,这个时候事件没法再传递
子元素了;其次是ACTION_MOVE事件,这个事件可以根据需求决定是否需要拦截,如果父容器需要
拦截就返回true,否则返回false;最后是ACTION_UP事件,必须要返回false,因为ACTION_UP时事
件本身没有啥意义。
2.内部拦截
内部拦截法指父容器不拦截任何事件,所有的事件都传给子元素,如果子元素需要此事件就
直接消耗掉,若不需要,就交由父容器进行处理,这种方法和Andorid中的事件分发机制不一致,
需要配合requestDisallowInterceptTouchEvent方法才能正常工作m,使用起来外部拦截稍显复杂。
伪代码如下:
public boolean dispatchTouchEvent(MotionEvent event){ int x = (int) event.getX(); int y = (int) event.getY(); switch(event.getAction()){ case MotionEvent.ACTION_DOWN: parent.requestDisallowInterceptTouchEvent(true); break; case MotionEvent.ACTION_MOVE: int deltaX = x - mLastX; int deltaY = y - mLastY; if(父容器需要此类点击事件){ parent.requestDisallowInterceptTopuchEvent(false); } break; case MotionEvent.ACTION_UP: break; break; } mLastX = x ; mLastY = y ; return super.dispatchTouchEvent(event); }
当面对不同的滑动策略时只需要修改里面的条件即可,其他不需要做改动也不能动。除了
子元素需要做处理以外,父元素也要默认拦截了 ACTION_DOWN 以外的其他事件,这样当子元素调用parent.requestDisalowInterceptTouchEvent(false)方法时,父元素才能拦截所需的
事件。
为什么父容器不能拦截ACTION_DOWN 事件呢?那是因为ACTION_DOWN 事件并不受FLAG_DISALLOW_INTERCEPT这个标记的控制,所以一旦父容器拦截ACTION_DOWN事件,那么所有的事件都无法传递到子元素中去,这样内部拦截就起不了作用,所以父元素做下面的改动。
public boolean onInterceptTouchEvent(MotionEvent event){ int action = event.getAction(); if(action == MotionEvent.ACTION_DOWN){ return false; }else{ return true; } }
相关文章推荐
- 安卓等边三角形
- Android 根据包名,获取应用程序的签名
- Android中的Bitmap
- Y3_01_关于专业技能
- 自定义FlowLayout,支持多种布局优化--android-flowlayout
- Android邮件发送详解
- Android百度地图开发(二):显示地图界面
- Android系统自带样式 @android:style
- Android学习(40) -- 自定义控件(4)广告轮播(ViewPager)
- Android SensorManager分析
- 搭建Android底层开发环境
- Android自定义最简单的dialog
- Android驱动开发前的准备
- Android自定义控件之继承ViewGroup创建新容器(四)
- Android百度地图开发(一):地图下载与配置
- android 常用color代码
- Android自定义View实战-100行带你写出SwitchButton
- Android Services 创建一个Bound服务
- 直接拿来用!最火的Android开源项目(完结篇)
- Android-Intent详解