这篇博客主要讲解一下几个问题android
文章首发地址CSDN:http://blog.csdn.net/gdutxiaoxu/article/details/52939127git
这篇博客大打算详细讲解View的事件分发机制,由于网上已经出现了一系列的好 文章,我本身的水平也有限,目前确定写得不咋的。github
先啰嗦一下,View 的事件分发机制主要涉及到一下三个 方法微信
- 仔细看的话,图分为3层,从上往下依次是Activity、ViewGroup、View
- 事件从左上角那个白色箭头开始,由Activity的dispatchTouchEvent作分发
- 箭头的上面字表明方法返回值,(return true、return false、return super.xxxxx(),super 的意思是调用父类实现。
- dispatchTouchEvent和 onTouchEvent的框里有个【true---->消费】的字,表示的意思是若是方法返回true,那么表明事件就此消费,不会继续往别的地方传了,事件终止。
- 目前全部的图的事件是针对ACTION_DOWN的,对于ACTION_MOVE和ACTION_UP咱们最后作分析。
- 以前图中的Activity 的dispatchTouchEvent 有误(图已修复),只有return super.dispatchTouchEvent(ev) 才是往下走,返回true 或者 false 事件就被消费了(终止传递)。
当TouchEvent发生时,首先Activity将TouchEvent传递给最顶层的View,TouchEvent最早到达最顶层 view 的 dispatchTouchEvent ,而后由 dispatchTouchEvent 方法进行分发,app
若是dispatchTouchEvent返回true 消费事件,事件终结。ide
若是dispatchTouchEvent返回 false ,则回传给父View的onTouchEvent事件处理;布局
onTouchEvent事件返回true,事件终结,返回false,交给父View的OnTouchEvent方法处理this
若是dispatchTouchEvent返回super的话,默认会调用本身的onInterceptTouchEvent方法google
默认的状况下interceptTouchEvent回调用super方法,super方法默认返回false,因此会交给子View的onDispatchTouchEvent方法处理spa
若是 interceptTouchEvent 返回 true ,也就是拦截掉了,则交给它的 onTouchEvent 来处理,
若是 interceptTouchEvent 返回 false ,那么就传递给子 view ,由子 view 的 dispatchTouchEvent 再来开始这个事件的分发。
关于更多详细分析,请查看原博客图解 Android 事件分发机制,真心推荐,写得很好。
第一种状况,滑动方向不一样
第二种状况,滑动方向相同
第三种状况,上述两种状况的嵌套
看了上面三种状况,咱们知道他们的共同特色是父View 和子View都想争着响应咱们的触摸事件,但遗憾的是咱们的触摸事件 同一时刻只能被某一个View或者ViewGroup拦截消费,因此就产生了滑动冲突?那既然同一时刻只能由某一个View或者ViewGroup消费拦截,那咱们就只须要 决定在某个时刻由这个View或者ViewGroup拦截事件,另外的 某个时刻 有另一个View或者ViewGroup拦截事件不就OK了吗?综上,正如 在 《Android开发艺术》 一书提出的,总共 有两种接觉方案
如下解决思路来自于 《Android开发艺术》 书籍
下面的两种方法针对第一种状况(滑动方向不一样),父View是上下滑动,子View是左右滑动的状况。
从父View着手,重写onInterceptTouchEvent方法,在父View须要拦截的时候拦截,不要的时候返回false,为代码大概 以下
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { final float x = ev.getX(); final float y = ev.getY(); final int action = ev.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: mDownPosX = x; mDownPosY = y; break; case MotionEvent.ACTION_MOVE: final float deltaX = Math.abs(x - mDownPosX); final float deltaY = Math.abs(y - mDownPosY); // 这里是够拦截的判断依据是左右滑动,读者可根据本身的逻辑进行是否拦截 if (deltaX > deltaY) { return false; } } return super.onInterceptTouchEvent(ev); }
从子View左右,父View先不要拦截任何事件,全部的 事件传递给 子View,若是子View须要此事件就消费掉,不须要此事件的话就交给 父View处理。
实现思路 以下,重写子 View的dispatchTouchEvent方法,在Action_down 动做中经过方法 requestDisallowInterceptTouchEvent(true) 先请求 父 View不要拦截事件,这样保证 子View可以 接受到Action_move事件,再在Action_move动做中根据 本身的逻辑是否要拦截事件,不要的 话交给 父View处理
@Override public boolean dispatchTouchEvent(MotionEvent ev) { int x = (int) ev.getRawX(); int y = (int) ev.getRawY(); int dealtX = 0; int dealtY = 0; switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: dealtX = 0; dealtY = 0; // 保证子View可以接收到Action_move事件 getParent().requestDisallowInterceptTouchEvent(true); break; case MotionEvent.ACTION_MOVE: dealtX += Math.abs(x - lastX); dealtY += Math.abs(y - lastY); Log.i(TAG, "dealtX:=" + dealtX); Log.i(TAG, "dealtY:=" + dealtY); // 这里是够拦截的判断依据是左右滑动,读者可根据本身的逻辑进行是否拦截 if (dealtX >= dealtY) { getParent().requestDisallowInterceptTouchEvent(true); } else { getParent().requestDisallowInterceptTouchEvent(false); } lastX = x; lastY = y; break; case MotionEvent.ACTION_CANCEL: break; case MotionEvent.ACTION_UP: break; } return super.dispatchTouchEvent(ev); }
如上面所述,从 父ViewScrollView着手,重写 OnInterceptTouchEvent方法,在上下滑动的时候拦截事件,在左右滑动的时候不拦截事件,返回 false,这样确保子View 的dispatchTouchEvent方法会被调用,代码 以下
/** * @ explain:这个ScrlloView不拦截水平滑动事件, * 是用来解决 ScrollView里面嵌套ViewPager使用的 * @ author:xujun on 2016/10/25 15:28 * @ email:gdutxiaoxu@163.com */ public class VerticalScrollView extends ScrollView { public VerticalScrollView(Context context) { super(context); } public VerticalScrollView(Context context, AttributeSet attrs) { super(context, attrs); } public VerticalScrollView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @TargetApi(21) public VerticalScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } private float mDownPosX = 0; private float mDownPosY = 0; @Override public boolean onInterceptTouchEvent(MotionEvent ev) { final float x = ev.getX(); final float y = ev.getY(); final int action = ev.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: mDownPosX = x; mDownPosY = y; break; case MotionEvent.ACTION_MOVE: final float deltaX = Math.abs(x - mDownPosX); final float deltaY = Math.abs(y - mDownPosY); // 这里是够拦截的判断依据是左右滑动,读者可根据本身的逻辑进行是否拦截 if (deltaX > deltaY) { return false; } } return super.onInterceptTouchEvent(ev); } }
如上面上述,经过requestDisallowInterceptTouchEvent(true)方法来影响父View是否拦截事件,咱们经过重写ViewPager的 dispatchTouchEvent()方法,在左右滑动的时候请求父View ScrollView不要拦截事件,其余的时候拦截事件
/** * @ explain:这个 ViewPager是用来解决ScrollView里面嵌套ViewPager的 内部解决法的 * @ author:xujun on 2016/10/25 16:38 * @ email:gdutxiaoxu@163.com */ public class MyViewPager extends ViewPager { private static final String TAG = "xujun"; int lastX = -1; int lastY = -1; public MyViewPager(Context context) { super(context); } public MyViewPager(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean dispatchTouchEvent(MotionEvent ev) { int x = (int) ev.getRawX(); int y = (int) ev.getRawY(); int dealtX = 0; int dealtY = 0; switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: dealtX = 0; dealtY = 0; // 保证子View可以接收到Action_move事件 getParent().requestDisallowInterceptTouchEvent(true); break; case MotionEvent.ACTION_MOVE: dealtX += Math.abs(x - lastX); dealtY += Math.abs(y - lastY); Log.i(TAG, "dealtX:=" + dealtX); Log.i(TAG, "dealtY:=" + dealtY); // 这里是够拦截的判断依据是左右滑动,读者可根据本身的逻辑进行是否拦截 if (dealtX >= dealtY) { getParent().requestDisallowInterceptTouchEvent(true); } else { getParent().requestDisallowInterceptTouchEvent(false); } lastX = x; lastY = y; break; case MotionEvent.ACTION_CANCEL: break; case MotionEvent.ACTION_UP: break; } return super.dispatchTouchEvent(ev); } }
当咱们ScrollView的最上层的Layout里面多多个孩子的时候,当下面一个孩子是RecyclerView或者ListView的时候,每每会活动滑动到ListView或者RecyclerView 的第一个item,致使进入界面的时候会致使RecyclerView 上面的 View被滑动到界面意外,看不见,这时候的用户体验是比较差的
即结构以下面的时候
因而我查找了相关的资料,在Activity中完美解决,主要要一下两种方法
第一种方法,重写Activity的onWindowFocusChanged()方法,在里面调用mNoHorizontalScrollView.scrollTo(0,0);方法,滑动到顶部,由于onWindowFocusChanged是在全部View绘制完毕的时候才会回调的,不熟悉的话建议先回去看一下Activity的生命周期的相关介绍
private void scroll() { mNoHorizontalScrollView.scrollTo(0,0); } @Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); if(hasFocus && first){ first=false; scroll(); } }
第二种解决方法,调用RecyclerView上面的View的一下方法,让其获取焦点
view.setFocusable(true); view.setFocusableInTouchMode(true); view.requestFocus();
这段代码在初始化的时候就让该界面的顶部的某一个控件得到焦点,滚动条天然就显示到顶部了。
一样是调用第二种方法,调用RecyclerView上面的View的一下方法,让其获取焦点
view.setFocusable(true); view.setFocusableInTouchMode(true); view.requestFocus();
这段代码在初始化的时候就让该界面的顶部的某一个控件得到焦点,滚动条天然就显示到顶部了。可是给方法存在缺点,就是当咱们上面的view若是滑动到一半的时候,切换到下一个Fragment,在切换回来的时候,RecyclerView的第一个item会自动滑动到顶部。目前我尚未找到相对比较好的解决这个问题的方法,你们知道相关解决方法的话也欢迎联系我,能够加我 微信或者在留言区评论,谢谢
借鉴于解决Activity的方法,目前我尚未找到一个方法是在Fragemnt界面彻底绘制完毕之后回调的方法,若是你们知道怎样处理的 话,欢迎你们提出来
从子View ViewPager着手,重写 子View的 dispatchTouchEvent方法,在子 View须要拦截的时候进行拦截,不然交给父View处理,代码以下
public class ChildViewPager extends ViewPager { private static final String TAG = "xujun"; public ChildViewPager(Context context) { super(context); } public ChildViewPager(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean dispatchTouchEvent(MotionEvent ev) { int curPosition; switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: getParent().requestDisallowInterceptTouchEvent(true); break; case MotionEvent.ACTION_MOVE: curPosition = this.getCurrentItem(); int count = this.getAdapter().getCount(); Log.i(TAG, "curPosition:=" +curPosition); // 当当前页面在最后一页和第0页的时候,由父亲拦截触摸事件 if (curPosition == count - 1|| curPosition==0) { getParent().requestDisallowInterceptTouchEvent(false); } else {//其余状况,由孩子拦截触摸事件 getParent().requestDisallowInterceptTouchEvent(true); } } return super.dispatchTouchEvent(ev); } }
这个若是要采用内部解决法来解决的话想,相对很麻烦,我提一下本身的我的思路,咱们能够先测量子View在哪一个区域,而后咱们在根据咱们按下的点是否在区域之内,若是是的话,在根据子View时候须要拦截进行处理
对于这种效果,上面是轮播图的,下面是RecyclerView或者ListView的,通常有一下几种实现方式
其布局文件以下,Activity代码见项目中的SixActivity
<?xml version="1.0" encoding="utf-8"?> <android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/background_light" android:fitsSystemWindows="true" > <android.support.design.widget.AppBarLayout android:layout_width="match_parent" android:layout_height="300dp" android:fitsSystemWindows="true" android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" > <android.support.design.widget.CollapsingToolbarLayout android:layout_width="match_parent" android:layout_height="match_parent" app:layout_scrollFlags="scroll|snap"> <android.support.v4.view.ViewPager android:id="@+id/viewPager" android:layout_width="match_parent" android:layout_height="match_parent" > </android.support.v4.view.ViewPager> <TextView android:id="@+id/tv_page" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="bottom" android:gravity="right" android:text="1/10" android:textColor="#000"/> </android.support.design.widget.CollapsingToolbarLayout> </android.support.design.widget.AppBarLayout> <android.support.v7.widget.RecyclerView android:id="@+id/recyclerView" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior"> </android.support.v7.widget.RecyclerView> </android.support.design.widget.CoordinatorLayout>
关于CoordinatorLayout的更多用法,能够参考个人这一篇博客使用CoordinatorLayout打造各类炫酷的效果
参考文章:图解 Android 事件分发机制
文章首发地址CSDN:http://blog.csdn.net/gdutxiaoxu/article/details/52939127
源码下载地址:https://github.com/gdutxiaoxu/TouchDemo.git
做者:xujun9411 连接:https://www.jianshu.com/p/fd528c2311da 來源:简书 著做权归做者全部。商业转载请联系做者得到受权,非商业转载请注明出处。