第三章 View事件体系(3)之滑动冲突

有了前面储存知识的储备,接下来我们就来解决这个在开发中常遇到的问题滑动冲突。

1.1常用的滑动冲突的场景

场景1 内外滑动方向不一致
场景2 内外滑动一致
场景3 上面两种情况的嵌套

  • 先说场景1,一般主流应用都是采用ViewPager和Fragment搭配,通过左右滑动来切换页面,一般页面里又有Listview进行上下滑动。本来这种情况下会出现滑动冲突,但是ViewPager内部已经处理了这个问题,所以我们无须关注这个问题。但是如果不是使用ViewPager而是使用ScrollView,那就必须手动处理滑动冲突了,否则会造成的后果就是内外两层只能有一层能够滑动,这是因为两者之间的滑动事件有冲突。当然除了这种情况,外部上下滑动,内部左右滑动也一样,但是他们都属于第一种场景冲突。
  • 再说场景2,这种情况就稍微复杂一点,内外两层都在同一个方向可以滑动的时候,系统无法知道用户到底想让那一层滑动,所以当手指滑动的时候就会出现问题,要么只有一层能滑动,要么就是内外两层都滑动会有点顿卡。在实际开发中就比如ScrollView嵌套这Listview。
  • 最后说场景3,场景3是场景1和场景2两种情况的嵌套,因为场景3的滑动冲突看起来更加复杂。比如有一个导航条,每一个导航里面有ViewPager和Fargmeng。虽然说场景3的滑动冲突看起来更复杂,但是它是几个单一的滑动冲突的叠加,因此只需要分别处理内层和中间层,中间层和外层的滑动冲突即可。
    本质上来说,这三种滑动冲突的场景的复杂度其实都相同,因为他们的区别仅仅是滑动策略的不同,接下来讲解一下处理方法,它们几个是通用的。

1.2 滑动冲突的处理规则

对于场景1,假设外层是左右滑动,内层是上下滑动,那么当我们在左右滑动的时候,让外层对事件进行拦截,当我们上下滑动的时候,让内层对事件进行拦截。这里我们就要去分别用户到底是上下滑动呢?还是只有滑动?其实很简单,我们可以根据滑动过程中两个点之间的坐标判断,我们可以去计算出水平滑动的速度与竖直滑动的速度去分别是上下还是左右滑动。我们可以去计算水平滑动的长度和竖直滑动的长度去比较是上下还是左右滑动。我们可以根据两点之间的直线与水平成的夹角去判断是上下还是左右滑动。
对于场景2,比较特别我们无法通过速度,夹角,水平竖直长度去判断,我们只能通过业务逻辑去判断,当某种状态时需要响应外部的view,某种状态是需要响应内部的view。场景3也一样需要根据业务逻辑这个突破口去判断。

1.3滑动冲突的方式

这里我们会一一讲解各各场景对于的处理办法。首先我们先来将场景1的处理办法,因为场景是是最简单也是最典型的,因为它的滑动规则比较简单,不管多么复杂的滑动冲突,它们之间的区别仅仅就是滑动规则的不同。我们抛开滑动规则不说,我们需要找到一种能够不具体依赖滑动规则的一个方法去解决场景1的滑动冲突。
上面说过,针对场景1的滑动冲突,我们可以根据滑动的距离差来判断。这个距离差就是滑动规则。如果用ViewPager去实现场景1,我们不需要手动解决,因为ViewPager已经为我们去解决该问题。因此我们不采用ViewPager,然而在实际的滑动过程中,去获取滑动角度也是相当简单的,那么到底怎么去分发这个事件呢?根据之前的分发机制我们可以得出两种方法,1.外部拦截2.内部拦截

1外部拦截法

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    boolean intercept = false;
    int x = (int) ev.getX();
    int y = (int) ev.getY();
    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            intercept = false;
            break;
        case MotionEvent.ACTION_UP:
            intercept = false;
            break;
        case MotionEvent.ACTION_MOVE:
            if (父容器点击事件的判断){
                intercept = true;
            }else{
                intercept = false;
            }
            break;
    }
    mlastx = x;
    mlasty = y;
    return intercept;
}

只需要在ACTION_MOVE里填写需要拦截时的条件即可,ACTION_DOWN必须要false,因为父容器一旦拦截那么后面的ACTION_MOVR ACTION_UP都会交给父容器去处理,ACTION_UP也必须返回false,因为其本身没有太多意义。还有就是如果父容器拦截了ACTION_UP,那么子view的onclick事件就无法触发,因为具有回调onclick的performClick方法在ACTION_UP里面。


2 内部拦截法

内部拦截法就是让父容器不去拦截事件,让子view去判断是否拦截,如果子View需要去处理事件就是事件进行拦截,如果不去出来就不是拦截事件,把事件交给父容器去处理。为此我们需要去重写子view的diapatchTouchEvent和配合使用requstDisallowInterceptTouchEvent。内部拦截法相对于外部拦截法稍显复杂。
首先我们先重写子View的diapatchTouchEvent

@Override
protected boolean dispatchHoverEvent(MotionEvent event) {

    int x = (int) event.getX();
    int y = (int) event.getY();
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            getParent().requestDisallowInterceptTouchEvent(true);
            break;
        case MotionEvent.ACTION_UP:
            break;
        case MotionEvent.ACTION_MOVE:
            if (父容器点击事件的判断) {
                getParent().requestDisallowInterceptTouchEvent(false);
            }
            break;
    }
    mlastx = x;
    mlasty = y;
    return super.dispatchHoverEvent(event);

}

getParent().requesDisallowInterceptTouchEvent(true)只要声明一次就够了,我们要在ACTION_MOVE中声明呢?因为一次滑动操作中最新出发的事件是点击也就是ACTION_DOWN。然后根据移动的状态去判断是否要消耗事件,若不消耗则requesDisallowInterceptTouchEvent(false),让父容器去拦截该事件。我们重写子View的dispatchTouchEvent远远不够的,因为父容器的ACTION_MOVE会重置

FLAG_DISALLOW_INTERCEPT标识,之前也讲过了,所以我们还要去重写父容器的onInterceptTouchEvent对ACTION_MOVE事件不去拦截。

public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        return false;

    } else {
        return true;
    }
}

下面我们来通过一个实例分别来介绍,我们来实现一个类似ViewPager中嵌套这ListView的效果,为此我们写了一个类,类似于ViewPager的HorizontalScrollViewEx的类,这个控件的具体实现后面过再讲,这里只讲滑动冲突。我们还需要定义一个类似于水平的LinearLayout的东西,只不过它可以水平滑动,这样一来他内部有上下滑动的ListView,自身又是左右滑动的,一个典型的场景1就出来了。


现对Demo1Activity进行初始化

public class Demo1Activity extends AppCompatActivity {
    String TAG = "DEMO1";
    private HorizontalScrollViewEx mListContainer;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_demo1);
        Log.d(TAG, "onCreate");
        initView();
    }

    private void initView() {
        LayoutInflater inflater = getLayoutInflater();
        mListContainer = (HorizontalScrollViewEx) findViewById(R.id.container);
        final int screenWidth = MyUtils.getScreenMetrics(this).widthPixels;
        final int screenHeight = MyUtils.getScreenMetrics(this).heightPixels;
        for (int i = 0; i < 3; i++) {
            ViewGroup layout = (ViewGroup) inflater.inflate(
                    R.layout.content_layout, mListContainer, false);
            layout.getLayoutParams().width = screenWidth;
            TextView textView = (TextView) layout.findViewById(R.id.title);
            textView.setText("page " + (i + 1));
            layout.setBackgroundColor(Color.rgb(255 / (i + 1), 255 / (i + 1), 0));
            createList(layout);
            mListContainer.addView(layout);
        }
    }

    private void createList(ViewGroup layout) {
        ListView listView = (ListView) layout.findViewById(R.id.list);
        ArrayList<String> datas = new ArrayList<String>();
        for (int i = 0; i < 50; i++) {
            datas.add("name " + i);
        }

        ArrayAdapter<String> adapter = new ArrayAdapter<String>(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(Demo1Activity.this, "click item",
                        Toast.LENGTH_SHORT).show();

            }
        });
    }
}

很简单我们创建了三个ListView添加到HorizontalScrollViewEx容器中,首先我们先用外部拦截法来处理滑动冲突的问题。
我们来重写onInterceptTouchEvent

@Override
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;
       * if (!mScroller.isFinished()) {*
            mScroller.abortAnimation();//这句话是为了优化滑动体验
            intercepted = true;
        }
        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;
    }
    case MotionEvent.ACTION_UP: {
        intercepted = false;
        break;
    }
    default:
        break;
    }

    Log.d(TAG, "intercepted=" + intercepted);
    mLastX = x;
    mLastY = y;
    mLastXIntercept = x;
    mLastYIntercept = y;

    return intercepted;
}

这代码和之前的伪代码没什么区别只是改了一个父容器对拦截事件的判断。但有一种特殊情况,假设当用户正在左右滑动的时候,此时滑动还没结束用户又向下滑动,那要怎么办呢?*包裹的判断就是为了该目的,当华东还没结束的时候,父容器会对事件进行拦截。

内部拦截法也一样,我们要去重写Listview的diapatchTouchEvent

@Override
public boolean dispatchTouchEvent(MotionEvent event) {
    int x = (int) event.getX();
    int y = (int) event.getY();

    switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN: {
        mHorizontalScrollViewEx2.requestDisallowInterceptTouchEvent(true);
        break;
    }
    case MotionEvent.ACTION_MOVE: {
        int deltaX = x - mLastX;
        int deltaY = y - mLastY;
        Log.d(TAG, "dx:" + deltaX + " dy:" + deltaY);
        if (Math.abs(deltaX) > Math.abs(deltaY)) {
            mHorizontalScrollViewEx2.requestDisallowInterceptTouchEvent(false);
        }
        break;
    }
    case MotionEvent.ACTION_UP: {
        break;
    }
    default:
        break;
    }

    mLastX = x;
    mLastY = y;
    return super.dispatchTouchEvent(event);
}

然后还需要修改HorizontalScrollViewEx的onInterceptTouchEvent,修改后的类暂且叫HorizontalScrollViewEx2

@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
    int x = (int) event.getX();
    int y = (int) event.getY();
    int action = event.getAction();
    if (action == MotionEvent.ACTION_DOWN) {
        mLastX = x;
        mLastY = y;
        if (!mScroller.isFinished()) {
            mScroller.abortAnimation();
            return true;
        }
        return false;
    } else {
        return true;
    }
}

这样内部拦截法就完成了,相对于外部拦截代码量会相对要多,但是萝卜青菜各有所爱!
前面说过了,只要我们根据场景1的情况来得出通用的解决方案,那么对于场景2和场景3来说我们只需要修改相关滑动规则和逻辑就可以。那么接下来我们来演示如用利用场景1得到的解决方案来解决更复杂的滑动冲突。这里只详细分析场景2的滑动冲突,对于场景3中的叠加的滑动冲突,由于它可以拆解成单一的滑动冲突,所以其解决思想和场景1、2中的解决思想一致,就不再对场景3详细分析。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容