参考: 《Android进阶之光》《Android开发艺术探索》
https://www.jianshu.com/p/06ff0dfeed39
View 的位置参数
View滑动
layout() 方法
View 在绘制的时候会调用onLayout() 方法设置i显示的位置,所以我们也可以通过 layout() 方法设置View 的坐标
这样,这个View 就会随着拖动进行移动
offsetLeftAndRight() 与 offsetTopAndBottom() 方法
使用的方法也很简单,效果和使用 layout() 是完全一样的。
layoutParams 方法
先使用的是 线性布局 的参数设置方法, 如果使用的是其他布局,如RelativeLayout, 那么类型也应该是 Relativelayout.LayoutParams
ViewGroup 的参数修改与上面类似。
需要注意的是,这两种方法,在点击的时候会鬼畜,(我认为是点击时获取的是LinearLayout子布局的坐标,而不是点击处的坐标导致)
而且拖动时会影响其他View,如vertical 线性布局中,上下拖动,则会导致其他View 也随之上下滑动(前文的其他方法并不会这样)
scrollTo() 与 scrollBy() 方法
scrollTo(x,y) (绝对移动) 表示移动到 x,y 坐标处
scrollBy(dx,dy) (相对移动) 表示分别在 x,y 方向移动 dx, dy 距离
事实上,scrollBy 内部 也是计算好了终点位置,然后调用scrollTo() 方法
case MotionEvent.ACTION_MOVE: int offsetX = x - lastX; int offsetY = y - lastY; ((View)getParent()).scrollBy(-offsetX,-offsetY);
// 这里获取父布局,会带着父布局中所有View一起移动,
// 如果不获取父布局,则会很轻易的把View 滑出其占有的空间,而消失在屏幕上
// 因为scrollTo 和 scrollBy 只能改变View 内容的位置,而不是view的位置
使用方法非常简单。但要注意 偏移量使用的是相反数。
因为这里的偏移是指手机屏幕的偏移,所以相对View位置的移动,这里应该输入相反数表示反方向移动屏幕
- getX 与 getScrollX
getX() 与getY()方法,获得的是View的左上角坐标,当用layout() 等方法时,获取的值会随之改变
getScrollX 方法则是获取View内容与View 左上角坐标(不动)的相对位置,当使用scroller 或scrollTo 等方法时,其获得值会改变
- 弹性滑动
- scroller 方式
典型使用方式:
流程:
确定位置参数后,调用了 startScroll() 但是内部什么都没做,只是存储了位置和时间等参数,实际滑动过程是invalidate() 和computeScroll配合完成的
先说computeScroll() 中的computeScrollOffset 方法: 其内部用 已过时间 和 预计时间比值 ,与移动距离做计算,求出应移动的小距离,然后根据时间是否
走完,如果走完,表示滑动结束,返回false,如果还未结束,返回true.
我们通过 computeScrollOffset ,返回true,则说明还未结束,于是在内部调用 scrollTo(),计算后的位置参数已存储在scroller中,直接使用即可
postInvalidate 和 invalidate 会让 view 重绘,在view 的draw 方法中又会调用computeScroll , 所以整个过程是一个循环:
view 绘制 -> draw -> computeScroll (得到位置参数,scrollTo )-> invalidate -> View重绘 -> draw …… 直到完成滑动。
- 延时方式
以handler为例,用handler的延时方法每次按比例移动View的内容,也可以达到弹性滑动的效果,代码如下:
- 使用动画
只需一句代码就可以实现滑动,需要注意的是,动画同样不会改变View实际的位置,只是挪动内容
事件分发机制
事件分发机制的基本思路就是,从顶端View 开始,不消耗则向子View传递事件,一直传递到没有子VIew, 如果还不消耗点击事件,则再依次
向上传递,由ViewGroup 消耗点击事件;
( dispatchTouchEvent 伪代码)
大致含义: 用 onInterceptTouchEvent 判断是否拦截这个事件,如果拦截,调用自己的 onTouchEvent 进行处理,
如果返回false,,表示不拦截,就传递给子view 的函数再进行分发
点击事件,由 Activity -> PhoneWindow -> DecorView -> 根ViewGroup 到ViewGroup ,开始通过DispatchTouchEvent() 方法分发事件
事件分发的三个主要方法:
dispatchTouchEvent() : 分发点击事件(传递),把事件传递给子View,如果该ViewGroup没有子View,
则调用 super.dispatchTouchEvent() ,把事件回给父ViewGroup 处理
onInterceptTouchEvent() ; 拦截点击事件。(只有ViewGroup 有,View 处于最底层,不需要拦截)
一旦拦截,就进入onTouchEvent()方法处理点击事件,并结束事件分发。
不拦截则继续传递。
onTouchEvent() : 进行事件处理。
在响应点击事件时,不同方法具有不同优先级:onTouchListener > onTouchEvent > onClickListener
当处于前方的函数响应点击时,不再走优先级低的函数。
- 滑动冲突
滑动冲突解决方案,主要分为 外部拦截法 和 内部拦截法。
外部拦截法:
重写父容器的 onInterceptTouchEvent 方法,在 case : MotionEvent.ACTION_MOVE 中 判断是否消耗点击事件。
核心部分伪代码:
注意: 在ACTION_DOWN 下,父容器一定要返回false,因为一旦返回true, 接下来的动作都由父容器处理,不再考虑传给子view
ACITON_UP 也要返回false,因为如果子view消耗了DOWN MOVE等事件,但up被父容器拦截,会导致 onClick事件等无法触发而出现问题,
而父容器一旦拦截了某个动作,之后的动作也全都由其消耗,所以即使在ACTION_UP返回false,如果在MOVE中拦截了事件,那么最后
ACTION_UP也一定可以到达父容器。
内部拦截法:
父容器不拦截事件,先传递给子元素,子元素决定是否消耗。这种方法需要配合 requestDisallowInterceptTouchEvent 方法使用。
核心部分伪代码:
这时父容器也要做出对应的修改,要拦截除 ACTION_DOWN外的其他情况,这样当子元素调用 requestDisallowInterceptTouchEvent(false)时
父容器才能成功拦截所需事件。
而 ACTION_DOWN 事件不受 FLAG_DISALLOW_INTERCEPT标记控制,一旦拦截后续事件将无法传给子元素,所以不能拦截ACTION_DOWN;
内部拦截相对于外部拦截更为复杂,外部拦截法使用更为广泛。其中有两个常见的滑动冲突场景:
1. 父容器 与 子元素滑动方向互相垂直
2. 父容器 与 子元素滑动方向相同
1.方向相互垂直时 。 我们在 父容器的 onInterceptTouchEvent 中, ACTION_MOVE时,通过坐标做差,得到纵向和横向的绝对值,
通过绝对值大小的比较,得出滑动的方向,如果方向是父容器对应的方向,就拦截。关键代码如下:
这样就可以解决了内外滑动方向垂直的冲突
2. 内外滑动方向相同时
这里示例情况是 ScrollView 中,有固定大小的头部和一个ListView ,这里解决使用的是内部拦截法
拦截的思路是:
优先ListView的滑动,当第一个Item 到达ListView 顶部,并继续向下滑动时,把事件给父布局处理;
当最后一个Item 到达 ListView 底部,并继续向上滑动时,把事件交给父布局处理。
调用 requestDisallowInterceptTouchEvent(); 并传入false,表示需要父布局处理滑动;
关键代码如下:
最终效果如图: