View滑动冲突解决方案

滑动冲突产生的原因:只要在界面中存在内外两层可以同时滑动,就会产生滑动冲突。
解决方案:根据实际情况,判断到底需要谁去响应滑动事件。

1.外部为左右滑动,内部为上下滑动

滑动冲突1.png

首先看已经解决过滑动冲突的情况,是可以很顺利的实现左右滑动和上下滑动的。

外部水平滑动,内部垂直滑动.gif

而没有处理滑动冲突的情况下,是不能够响应外部的左右滑动的。

存在滑动冲突.gif

这两种情况的根本区别在于左右滑动布局的onInterceptTouchEvent方法,添加了判断逻辑是否需要进行拦截滑动事件。

            case MotionEvent.ACTION_MOVE:
                int deltaX = x - mLastXIntercept;
                int deltaY = y - mLastYIntercept;
                //判断是否为水平滑动,水平滑动则拦截处理
                if (Math.abs(deltaX) > Math.abs(deltaY)) {
                    intercepted = true;
                }
                else {
                    intercepted = false;
                }
                break;

主要的判断逻辑是,水平方向滑动的距离大于竖直方向,则认为是水平滑动,需要拦截事件,否则认为是竖直滑动,不需要拦截事件,即交由listview去处理。虽然任玉刚老师的git上有随书的代码,但是很多童鞋也是比较忙,没有时间去下载,我这里就附上用到的代码,其中自己添加了一些注释,也是便于我自己的学习。

自定义左右滑动的类,这里叫做MyViewPage.java


package com.tom.viewdemo.ui;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Scroller;

/**
 * 

Title: MyViewPage

*

Description:

* * @author tom * @date 2018/12/29 08:38 **/ public class MyViewPage extends ViewGroup { public static final String TAG = "MyViewPage"; //子view的数量 private int mChildrenCount; //子View的宽度 private int mChildWidth; private int mChildIndex; // 分别记录上次滑动的坐标 private int mLastX = 0; private int mLastY = 0; // 分别记录上次滑动的坐标(onInterceptTouchEvent) private int mLastXIntercept = 0; private int mLastYIntercept = 0; private Scroller mScroller; //追踪屏幕触摸事件的速度 private VelocityTracker mVelocityTracker; public MyViewPage(Context context) { super(context); init(); } public MyViewPage(Context context, AttributeSet attrs) { super(context, attrs); init(); } public MyViewPage(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } /** *

初始化相关参数

*/ private void init() { mScroller = new Scroller(getContext()); mVelocityTracker = VelocityTracker.obtain(); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { boolean intercepted = false; int x = (int) ev.getX(); int y = (int) ev.getY(); switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: intercepted = false; if (!mScroller.isFinished()) { mScroller.abortAnimation(); intercepted = true; } break; case MotionEvent.ACTION_UP: intercepted = false; break; case MotionEvent.ACTION_MOVE: int deltaX = x - mLastXIntercept; int deltaY = y - mLastYIntercept; //判断是否为水平滑动,水平滑动则拦截处理 if (Math.abs(deltaX) > Math.abs(deltaY)) { intercepted = true; } else { intercepted = false; } break; default: break; } Log.d(TAG, "intercepted = " + intercepted); mLastX = x; mLastY = y; mLastXIntercept = x; mLastYIntercept = y; return intercepted; } @Override public boolean onTouchEvent(MotionEvent event) { mVelocityTracker.addMovement(event); int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: if (!mScroller.isFinished()) { //停止动画 mScroller.abortAnimation(); } break; case MotionEvent.ACTION_MOVE: int deltaX = x - mLastX; scrollBy(-deltaX, 0); break; case MotionEvent.ACTION_UP: int scrollX = getScrollX(); mVelocityTracker.computeCurrentVelocity(1000); float xVelocity = mVelocityTracker.getXVelocity(); if (Math.abs(xVelocity) >= 50) { mChildIndex = xVelocity > 0 ? mChildIndex - 1 : mChildIndex + 1; } else { mChildIndex = (scrollX + mChildWidth / 2) / mChildWidth; } mChildIndex = Math.max(0, Math.min(mChildIndex, mChildrenCount - 1)); int dx = mChildIndex * mChildWidth - scrollX; smoothScrollBy(dx, 0); mVelocityTracker.clear(); break; default: break; } mLastX = x; mLastY = y; return true; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int measuredWidth = 0; int measuredHeight = 0; final int childCount = getChildCount(); measureChildren(widthMeasureSpec, heightMeasureSpec); int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec); int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec); int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); //没有子View if (childCount == 0) { setMeasuredDimension(0, 0); } else if (heightSpecMode == MeasureSpec.AT_MOST) { final View childView = getChildAt(0); measuredHeight = childView.getMeasuredHeight(); setMeasuredDimension(widthSpaceSize, measuredHeight); } else if (widthSpecMode == MeasureSpec.AT_MOST) { final View childView = getChildAt(0); measuredWidth = childView.getMeasuredWidth() * childCount; setMeasuredDimension(measuredWidth, heightSpaceSize); } else { final View childView = getChildAt(0); measuredHeight = childView.getMeasuredHeight(); measuredWidth = childView.getMeasuredWidth() * childCount; setMeasuredDimension(measuredWidth, measuredHeight); } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int childLeft = 0; //获取子 View 的数量 final int childCount = getChildCount(); mChildrenCount = childCount; for (int i = 0; i < childCount; i++) { final View childView = getChildAt(i); //判断可见性不为GONE if (childView.getVisibility() != View.GONE) { final int childWidth = childView.getMeasuredWidth(); mChildWidth = childWidth; childView.layout(childLeft, 0, childLeft + childWidth, childView.getMeasuredHeight()); childLeft += childWidth; } } } private void smoothScrollBy(int dx, int dy) { mScroller.startScroll(getScrollX(), 0, dx, 0, 500); invalidate(); } @Override public void computeScroll() { if (mScroller.computeScrollOffset()) { scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); postInvalidate(); } } @Override protected void onDetachedFromWindow() { mVelocityTracker.recycle(); super.onDetachedFromWindow(); } }

这部分代码我也没有仔细的去研读,只是把和滑动冲突有关的看了下,后面学习到《View的工作原理》这一章,再好好过一遍代码。

布局文件activity_main.xml




    


布局文件content_layout.xml 这个文件就定义了一个title和一个listview,title用来显示是第几页,listview用来竖直方向的滑动。




    

    




布局文件content_list_item.xml 用于listview每一行的展示。也可以使用系统自带的布局文件,只是一个demo,用什么都ok。





    



主文件MainActivity.java 这里使用了ButterKnife插件,基本用法可以在仿QQ未读消息的动画效果中找到,也可以自行搜索。


package com.tom.viewdemo;

import android.content.Context;
import android.graphics.Color;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;

import com.tom.viewdemo.ui.MyViewPage;

import java.util.ArrayList;

import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.Unbinder;

public class MainActivity extends AppCompatActivity {

    Unbinder mBinder;

    @BindView(R.id.container)
    MyViewPage mContainer;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mBinder = ButterKnife.bind(this);
        initViews();
    }

    private void initViews() {

        LayoutInflater inflater = getLayoutInflater();
        //获取屏幕的宽度,因为这两个值为固定值(同一设备),所以使用final关键字
        final int screenWidth = getScreenMetrics(this).widthPixels;

        //添加3页
        for (int i = 0; i < 3; i++) {
            ViewGroup layout;
            layout = (ViewGroup) inflater.inflate(
                    R.layout.content_layout, mContainer, false);
            //设置宽度为屏幕宽度
            layout.getLayoutParams().width = screenWidth;
            TextView textView = layout.findViewById(R.id.title);
            textView.setText("第 " + (i + 1) + " 页");
            layout.setBackgroundColor(Color.rgb(255 / (i + 1), 255 / (i + 1), 0));
            createList(layout);
            mContainer.addView(layout);
        }



    }

    /**
     * 

创建 listView

* @param layout 父布局 */ private void createList(ViewGroup layout) { ListView listView = layout.findViewById(R.id.list); ArrayList datas = new ArrayList<>(); //填充测试数据 for (int i = 0; i < 50; i++) { datas.add("name " + i); } ArrayAdapter adapter = new ArrayAdapter<>(this, R.layout.content_list_item, R.id.name, datas); listView.setAdapter(adapter); listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView parent, View view, int position, long id) { Toast.makeText(MainActivity.this, "click item", Toast.LENGTH_SHORT).show(); } }); } /** *

获取设备屏幕信息

* @param context * @return */ private DisplayMetrics getScreenMetrics(Context context) { WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); DisplayMetrics dm = new DisplayMetrics(); wm.getDefaultDisplay().getMetrics(dm); return dm; } @Override protected void onDestroy() { super.onDestroy(); mBinder.unbind(); } }

2.滑动方向一致,以竖直方向为例

滑动冲突2.png

这种情况的产生其实还是比较常见的,比如外部是一个竖直方向的scrollView,里面嵌套了一个listView等,处理的原则也是,根据实际需要,判断父容器是否需要拦截事件即可。

在布局文件中使用如下布局activity_demo.xml





    


        

        


        
    



这时的运行结果如图所示:

scrollView显示问题.png

可以看到,并没有按照布局文件的预期结果,显示“头部”,并且listView是无法滑动的。要让布局文件显示正确,只需要在Activity中添加如下代码。

mScrollView.smoothScrollTo(0,0);

接下来处理滑动冲突,需求是点击的如果是listView,则让listView滑动。需要重写ScrollViewonInterceptTouchEvent方法。这里给出重写后的MyScrollView.java文件。

package com.tom.viewdemo.ui;

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ListView;
import android.widget.ScrollView;

/**
 * 

Title: MyScrollView

*

Description:

* * @author tom * @date 2019/1/8 10:56 **/ public class MyScrollView extends ScrollView { private ListView mListView; public ListView getListView() { return mListView; } public void setListView(ListView listView) { mListView = listView; } public MyScrollView(Context context) { super(context); } public MyScrollView(Context context, AttributeSet attrs) { super(context, attrs); } public MyScrollView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { //如果点击的范围在listView内,则由listView来处理 if (mListView != null && checkArea(mListView,ev)) { return false; } return super.onInterceptTouchEvent(ev); } /** *

判断点击位置是否在view的范围内

* @param view * @param ev * @return */ private boolean checkArea(View view, MotionEvent ev) { //获取点击位置的绝对坐标,相对屏幕左上角的位置 float x = ev.getRawX(); float y = ev.getRawY(); int[] locate = new int[2]; view.getLocationOnScreen(locate); int left = locate[0]; int right = left + view.getWidth(); int top = locate[1]; int bottom = top + view.getHeight(); if (x > left && x < right && y > top && y < bottom) { return true; } return false; } }

把布局文件的ScrollView替换为MyScrollView,同时在ScrollDemoActivity.java文件中添加mScrollView.setListView(mList);

ScrollDemoActivity.java文件如下:


package com.tom.viewdemo;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.widget.ArrayAdapter;
import android.widget.ListView;

import com.tom.viewdemo.ui.MyScrollView;

import java.util.ArrayList;
import java.util.List;

import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.Unbinder;

public class ScrollDemoActivity extends AppCompatActivity {

    Unbinder mBinder;
    @BindView(R.id.list)
    ListView mList;
    @BindView(R.id.scrollView)
    MyScrollView mScrollView;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_demo);
        mBinder = ButterKnife.bind(this);
        mScrollView.smoothScrollTo(0, 0);
        mScrollView.setListView(mList);
        initListData();
    }

    private void initListData() {

        List dataList = new ArrayList<>();
        for (int i = 0; i < 30; i++) {
            dataList.add("tom" + i);
        }

        ArrayAdapter arrayAdapter = new ArrayAdapter(ScrollDemoActivity.this, android.R.layout.simple_list_item_1,android.R.id.text1, dataList);
        mList.setAdapter(arrayAdapter);

    }


    @Override
    protected void onDestroy() {
        super.onDestroy();
        mBinder.unbind();
    }
}

此时滑动冲突问题已经解决,效果图如下。

解决滑动冲突-不完全版.gif

虽然已经解决了滑动冲突的问题,ListView可以滑动,ScrollView也可以滑动,但是当ListView滑动到底部或者顶部时,我是希望ScrollView可以接着滑动,因此进行如下修改。

滑动冲突解决优化改良版

重写listView里的dispatchTouchEvent方法,在移动时判断ListView是否可以继续滑动(根据滑动方向判断),如果不能滑动,则由ScrollView滑动。

MyListView.java

package com.tom.viewdemo.ui;

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.ListView;

/**
 * 

Title: MyListView

*

Description:

* * @author tom * @date 2019/1/8 16:14 **/ public class MyListView extends ListView { private float mLastY; public MyListView(Context context) { super(context); } public MyListView(Context context, AttributeSet attrs) { super(context, attrs); } public MyListView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override public boolean dispatchTouchEvent(MotionEvent ev) { int action = ev.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: //拦截down事件,由 listView 来处理 getParent().getParent().requestDisallowInterceptTouchEvent(true); break; case MotionEvent.ACTION_MOVE: //向上滑动 if (mLastY > ev.getY()) { //滑动到listView的底部了,手指无法继续向上滑动,即listView无法向下滑动,则交由父布局处理 if (!canScrollList(1)) { getParent().getParent().requestDisallowInterceptTouchEvent(false); return false; } } //向下滑动 else if (mLastY < ev.getY()) { //滑动到listView的顶部了,手指无法继续向下滑动,即list View无法向上滑动,则交由父布局处理 if (!canScrollList(-1)) { getParent().getParent().requestDisallowInterceptTouchEvent(false); return false; } } break; case MotionEvent.ACTION_UP: getParent().getParent().requestDisallowInterceptTouchEvent(false); break; } mLastY = ev.getY(); return super.dispatchTouchEvent(ev); } }

ListView里的canScrollList()方法用来判断是否能够继续滑动,可以查看源码里的注释。


    /**
     * Check if the items in the list can be scrolled in a certain direction.
     *
     * @param direction Negative to check scrolling up, positive to check
     *            scrolling down.
     * @return true if the list can be scrolled in the specified direction,
     *         false otherwise.
     * @see #scrollListBy(int)
     */

如果传入的参数为负,则检查listView是否可以向上滑动;为正,检查listView是否可以向下滑动。如果分不清楚向上还是向下,可以根据listviewscrollBar的运动方向来判断,或者理解为和手指的运动方向相反

MyScrollView.java


package com.tom.viewdemo.ui;

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ListView;
import android.widget.ScrollView;

/**
 * 

Title: MyScrollView

*

Description:

* * @author tom * @date 2019/1/8 10:56 **/ public class MyScrollView extends ScrollView { private ListView mListView; public ListView getListView() { return mListView; } public void setListView(ListView listView) { mListView = listView; } public MyScrollView(Context context) { super(context); } public MyScrollView(Context context, AttributeSet attrs) { super(context, attrs); } public MyScrollView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { //如果点击的范围在listView内,则由listView来处理 // if (mListView != null && checkArea(mListView,ev)) { // return false; // } //滑动冲突改良版 if (ev.getAction() == MotionEvent.ACTION_DOWN) { onTouchEvent(ev); return false; } return super.onInterceptTouchEvent(ev); } /** *

判断点击位置是否在view的范围内

* @param view * @param ev * @return */ private boolean checkArea(View view, MotionEvent ev) { //获取点击位置的绝对坐标,相对屏幕左上角的位置 float x = ev.getRawX(); float y = ev.getRawY(); int[] locate = new int[2]; view.getLocationOnScreen(locate); int left = locate[0]; int right = left + view.getWidth(); int top = locate[1]; int bottom = top + view.getHeight(); if (x > left && x < right && y > top && y < bottom) { return true; } return false; } }

MotionEvent.ACTION_DOWN必须要让给ListView,这样ListView才能判断自己是否还能滑动,因为ScrollView的滑动需要在MotionEvent.ACTION_DOWN时做一些准备,因此需要主动调用一次onTouchEvent()方法。

ScrollDemoActivity.java

package com.tom.viewdemo;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.widget.ArrayAdapter;

import com.tom.viewdemo.ui.MyListView;
import com.tom.viewdemo.ui.MyScrollView;

import java.util.ArrayList;
import java.util.List;

import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.Unbinder;

public class ScrollDemoActivity extends AppCompatActivity {

    Unbinder mBinder;
    @BindView(R.id.list)
    MyListView mList;
    @BindView(R.id.scrollView)
    MyScrollView mScrollView;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_demo);
        mBinder = ButterKnife.bind(this);
        mScrollView.smoothScrollTo(0, 0);
        mScrollView.setListView(mList);
        initListData();
    }

    private void initListData() {

        List dataList = new ArrayList<>();
        for (int i = 0; i < 30; i++) {
            dataList.add("tom" + i);
        }

        ArrayAdapter arrayAdapter = new ArrayAdapter(ScrollDemoActivity.this, android.R.layout.simple_list_item_1,android.R.id.text1, dataList);
        mList.setAdapter(arrayAdapter);

    }


    @Override
    protected void onDestroy() {
        super.onDestroy();
        mBinder.unbind();
    }
}

布局文件activity_demo.xml






    


        

        


        
    



最后,附上改良版的效果图。

解决滑动冲突-改良版.gif

两种常见的滑动冲突已经解决,主要还是重写父布局的onInterceptTouchEvent()方法,根据具体的业务,来判断需要由谁去处理事件。

你可能感兴趣的:(View滑动冲突解决方案)