1.滑动冲突的场景
好,上一篇文章我们已经基本讲述了View的分发机制了。全文解析ViewGourp事件分发以及滑动冲突。(二)
那我们就开始讲述一下滑动冲突的问题。
常见的滑动冲突的场景:
(1)滑动方向不相同,ViewPage和ListView
(2)滑动方向相同,ViewPage和Scrollview
(3)符合嵌套(禁止套娃)
当然做过一段Android开发的同志已经开始发表疑问了,我ViewPage+ListView并没有发生这样的事情啊,那是因为Google的大佬早就想到这个问题,ViewPage已经处理了这个冲突了
,如果我们自定义一个ViewGroup再用ListView,那么就要我们自己处理滑动冲突了。
2.滑动冲突解决方案
针对第一种场景,很容易就让人联想到,其实只要知道是向上下滑/向左右滑
,就知道交给谁处理了。而这种判断,我们只需要根据水平坐标差
和垂直坐标差
就可以进行判断了。
针对第二种场景,那我们就没办法根据滑动的方向进行判断了,那么咱办呢?那也只能根据业务逻辑
或View的位置
去进行判断。
上述两种场景的冲突本质区别上,只是判断逻辑不同
而已,但其实处理套路是一样的。
3.滑动冲突解决套路
套路一 外部拦截法
顾名思义,一开始是从父容器慢慢分发进去到子View的,那就是一旦判断条件是父容器的滑动,那么父容器就会将事件进行拦截。那我们将逻辑处理放到父View的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_DOWN: {
intercepted = false;
break;
}
case MotionEvent.ACTION_MOVE: {
if (满足父容器的拦截要求) {
intercepted = true;
} else {
intercepted = false;
}
break;
}
case MotionEvent.ACTION_UP: {
intercepted = false;
break;
}
default:
break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
需要注意的点:
ACTION_DOWN返回flase
.否则子容器将接受不到ACTION_MOVE和ACION_UP的事件。
ACTION_UP,返回false
.否则子容器也接收不到ACTION_UP事件,OnClick也不会被触发。
套路二 内部拦截法
父View不拦截任何事件
,所有事件都传递给子View,子View根据需要决定是自己消费事件还是给父View处理。这需要子View使用requestDisallowInterceptTouchEvent方法才能正常工作。requestDisallowInterceptTouchEvent()方法的作用就是请求父类容器是否拦截。True–>不拦截,flase–>拦截。
(伪代码)
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;
}
//ACTION_DOWN,不拦截,到子View-->ACTION_MOVE.
//告诉父View拦截,父VIEW开始拦截除了Down之外的事件,此时就不会执行子View的TouchEVent了
//这样就是执行父View的TouchEvent
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (父容器需要此类点击事件) {
parent.requestDisallowInterceptTouchEvent(false);
}
break;
}
case MotionEvent.ACTION_UP: {
break;
}
default:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
父View重写onInterceptTouchEvent方法:
public boolean onInterceptTouchEvent(MotionEvent event) {
int action = event.getAction();
if (action == MotionEvent.ACTION_DOWN) {
return false;
} else {
return true;
}
}
注意的点:这里的逻辑可能会稍微有点绕,意思就是父VIew一开始是不会拦截任何事情到子View,这是因为子View用了requestDisallowInterceptTouchEvent(true)
到了子View的ACTION_MOVE后,判断到了需要截止的事情了,那么子View告诉父容器,你要拦截事情了。那么父View就会开始拦截,不会再执行子View的onTouchEvent,而是执行父的onTouchEvent
4.喜闻乐见的试验环节
之前说过,ViewPage帮我们处理过了滑动冲突,而他是作为ViewGroup,我们把他已经帮忙处理的操作弄掉,为了造成滑动冲突,那我们自定义一个ViewPage,并重写onInterceptTouchEvent方法,默认返回flase.
public class BadViewPager extends ViewPager {
public BadViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
//除了Down,全都拦截了
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final int action = ev.getAction() & MotionEvent.ACTION_MASK;
if (action == MotionEvent.ACTION_DOWN){
super.onInterceptTouchEvent(ev);
return false;
}
return true;
}
}
好,接下来新建一个ScrollConflictActivity来测试冲突。
public class ScrollConflictActivity extends AppCompatActivity {
private BadViewPager mViewPager;
private List<View> mViews;
private Context mContext = this;
List<View> gListView = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_scroll_conflict);
initViews();
initData(false);
}
protected void initViews() {
mViewPager = findViewById(R.id.Badviewpager);
mViews = new ArrayList<>();
}
protected void initData(final boolean isListView) {
if(isListView){
for (int i =0;i<3;i++) {
BadEnternalConflictFixListView listView = new BadEnternalConflictFixListView(mContext);
final ArrayList<String> datas = new ArrayList<>();
for(int j=0;j<20;j++){
datas.add("data"+j);
}
//初始化adapter
ArrayAdapter<String> adapter = new ArrayAdapter<>
(mContext, android.R.layout.simple_list_item_1, datas);
listView.setAdapter(adapter);
View a = listView;
gListView.add(a);
}
}else {
TextView textView = new TextView(mContext);
textView.setGravity(Gravity.CENTER);
textView.setText("1");
textView.setClickable(true);
gListView.add(textView);
TextView textView2 = new TextView(mContext);
textView2.setGravity(Gravity.CENTER);
textView2.setText("2");
gListView.add(textView2);
TextView textView3 = new TextView(mContext);
textView3.setGravity(Gravity.CENTER);
textView3.setText("3");
gListView.add(textView3);
}
//将ListView赋值给当前View
mViewPager.setAdapter(new PageAdapterViewPage(gListView));
}
//ViewPage适配器
public class PageAdapterViewPage extends PagerAdapter {
private List<View> mViewList;
private int ModifyViewPosition = 0;
public PageAdapterViewPage(List<View> mviewlist) {
this.mViewList = mviewlist;
}
@Override
public int getCount() {
return mViewList.size();
}
@Override
public boolean isViewFromObject(@NonNull View view, @NonNull Object o) {
return view==o;
}
@NonNull
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
//创建item时,设置tag,有item的position放入tag
container.addView(mViewList.get(position));
return mViewList.get(position);
}
@Override
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
container.removeView(mViewList.get(position));
}
}
}
好了,我们先是用往我们的BadViewPage加了三个TextView.并把第一个TextView.setClickable(true)
。这样当我们进入的时候,就发现,ViewPage无法被拖动了。
为什么我们这里要将TextVIew设置为可以点击的呢?
因为TextView本身的不可单击的,那么它的的onTouchEvent是不会消费事件的,即返回False
,所以如果不设置的话,事件将会返回到VIewGroup,那就可以拖动了。
此时为True,即消费了事件,所以ViewGroup本身存在的onTouchEvent方法就使用不了,就无法拖动了。造成的原因就是父View不能正确相应事件。
接下来我们修改一下代码,initData传入为True,那么ViewPage加入的就是ListView.
显然ListView是可以滑动的,BadViewPager是不能滑动的。我们分别通过外部拦截和内部拦截方法来对BadViewPager进行修复。
1.外部拦截法
public class BadExternalConflictViewPage extends ViewPager {
private int mLastXIntercept;
private int mLastYIntercept;
private String TAG="BadExternalConflictViewPage";
public BadExternalConflictViewPage(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
int x = (int)ev.getX();
int y = (int)ev.getY();
final int action = ev.getAction() & MotionEvent.ACTION_MASK;
switch (action){
case MotionEvent.ACTION_DOWN:
//调用ViewPager的onInterceptTouchEvent方法初始化mActivePointerId
//这个Id默认值是-1,在onTouchEvent中会靠这个判断这个事件是否被子View消耗了.
//如果消耗了,那么也到不了这里的Move
super.onInterceptTouchEvent(ev);
break;
case MotionEvent.ACTION_MOVE:
//横坐标增量
int deltaX = x - mLastXIntercept;
//纵坐标位移增量
int deltaY = y - mLastYIntercept;
if (Math.abs(deltaX)>Math.abs(deltaY)){
intercepted = true;//相当于把事件拦截到这里,不到子View了,自己消耗了。
}else{
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
intercepted = false;
break;
default:
break;
}
mLastXIntercept = x;
mLastYIntercept = y;
Log.d(TAG, "intercepted = : "+intercepted);
return intercepted;
}
}
我们在Actiivty将BadPageView替换到我们的BadExternalConflictViewPage
。
根据外部拦截的理论,我们只需要重写ViewGroup的onInterceptTouchEvent,并且ACTION_DOWN和ACTION_UP都返回False.
在ACITON_MOVE的时候,逻辑判断了手势是左右滑动,所以,此时返回True,即在这里,ViewPage就将这个事件拦截了,就会触发ViewPage的onInterceptTouchEvent。
这里我们在ACTION_DOWN
当中还调用了super.onInterceptTouchEvent(ev);即ViewPager的onInterceptTouchEvent方法
。主要是为了初始化ViewPager的成员变量mActivePointerId。mActivePointerId默认值为-1,mActivePointerId不进行初始化,ViewPager会认为这个事件已经被子View给消费了
2.内部拦截法
内部拦截法需要重写ListView的dispatchTouchEvent方法,所以我们自定义一个ListView:
public class BadEnternalConflictFixListView extends ListView {
private static final String TAG = "FixListView";
private int mLastX;
private int mLastY;
public BadEnternalConflictFixListView(Context context) {
super(context);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int x = (int) ev.getX();
int y = (int) ev.getY();
final int action = ev.getAction() & MotionEvent.ACTION_MASK;
switch (action) {
case MotionEvent.ACTION_DOWN:
//这个方法是请求父类不要拦截
getParent().requestDisallowInterceptTouchEvent(true);
Log.d(TAG, "ACTION_DOWN: "+1);
break;
//ACTION_DOWN,不拦截,到子View-->ACTION_MOVE.
//告诉父View拦截,父VIEW开始拦截除了Down之外的事件,此时就不会执行子View的TouchEVent了
//这样就是执行父View的TouchEvent
case MotionEvent.ACTION_MOVE:
Log.d(TAG, "ACTION_MOVE: "+2);
//水平移动的增量
int deltaX = x - mLastX;
//竖直移动的增量
int deltaY = y - mLastY;
//当水平增量大于竖直增量时,表示水平滑动,此时需要父View去处理事件
if (Math.abs(deltaX) > Math.abs(deltaY)){
//请求父类拦截?。。
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(ev);
}
}
再看BadViewPager,需要重写拦截方法
public class BadViewPager extends ViewPager {
public BadViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
//除了Down,全都拦截了
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final int action = ev.getAction() & MotionEvent.ACTION_MASK;
if (action == MotionEvent.ACTION_DOWN){
super.onInterceptTouchEvent(ev);
return false;
}
return true;
}
}
其实可以看得到,我们只不过判断的逻辑一个放在VIewGroup里面,一个是放在View里面。一个是在ViewGroup判断我是否直接把这个事件“吃”掉,不留了。一个是VIew判断,告诉VIewGroup你要不要把这个事件“吃”掉。其实方法效果和逻辑是一样的。
好了,只要理解这两种方法,各种复杂的嵌套,只要细心分析逻辑操作,只要对其中的方法进行重写,我们就可以套用这种套路去解决。 当然,我们现在看得觉得外部拦截法比较简单,而且也比较符合我们的线性逻辑。所以,首先用外部拦截发是一个很好的方法来处理滑动冲突~
OVER~
大佬的文章:
事件分发:
https://bthvi-leiqi.blog.csdn.net/article/details/105333893
滑动冲突
https://www.jianshu.com/p/982a83271327