zoukankan      html  css  js  c++  java
  • 一个Demo带你彻底掌握View的滑动冲突

    自定义View的难点就是滑动冲突,本文作者通过一个demo,覆盖大多数的滑动冲突问题,看完可以回味一下,编程是为了理解这个思想,真正的应用,还需要大家举一反三。


    最近在重新学习Android自定义View这一块的内容,遇到了平时开发中经常碰到的一个棘手问题:View的滑动冲突。相信不少小伙伴都有相同的感觉,看似简单真正做起来却又不知道从何下手。今天就从一个简单的Demo带你彻底掌握解决View滑动冲突的办法。

    老规矩,先上图:

    示例图中是一个常见的下拉回弹,手指向下滑动的时候,整个布局会一起滑动。下拉到一定距离的时候松手,布局会自动回弹到开始的位置;手指向上滑动的时候,布局的子View会滑动到最底部,然后手指再向下滑动,布局的子View会滑动到最顶部,最后手指继续向下滑动,整个布局会一起滑动,下拉到一定距离后松手自动回弹到开始位置。

     

    最终实现的效果如上所示,一起看看怎样一步步实现最终的效果:

     

    1布局的下拉回弹实现   

    下拉回弹的实现本质其实就是View的滑动,目前Android中实现View的滑动可以分为三种方式:通过改变View的布局参数使得View重新布局从而实现滑动;通过scrollTo/scrollBy方法来实现View的滑动;通过动画给View施加平移效果来实现滑动。这里我们采用第一种方式来实现,考虑到整个布局是竖直排列,我们可以直接自定义一个LinearLayout来作为父布局。然后调用layout(int l, int t, int r, int b)方法重新布局,达到滑动的效果。

    MyParentView.class
     1 package costom.widget;
     2 
     3 import android.content.Context;
     4 import android.util.AttributeSet;
     5 import android.view.MotionEvent;
     6 import android.widget.LinearLayout;
     7 
     8 /**
     9  * Created by ${火龙裸先生} on 2017/1/25.
    10  * 邮箱:791335000@qq.com
    11  */
    12 public class MyParentView extends LinearLayout {
    13 
    14     private int mMove;
    15     private int yDown, yMove;
    16     private int i = 0;
    17     private boolean isIntercept;
    18 
    19     public MyParentView(Context context) {
    20         super(context);
    21     }
    22 
    23     public MyParentView(Context context, AttributeSet attrs) {
    24         super(context, attrs);
    25     }
    26 
    27     public MyParentView(Context context, AttributeSet attrs, int defStyleAttr) {
    28         super(context, attrs, defStyleAttr);
    29     }
    30 
    31     @Override
    32     public boolean onTouchEvent(MotionEvent event) {
    33         int y = (int) event.getY();
    34         switch (event.getAction()) {
    35             case MotionEvent.ACTION_DOWN:
    36                 yDown = y;//获取刚开始触碰View的y坐标
    37                 break;
    38             case MotionEvent.ACTION_UP:
    39                 //将滑动的总距离作为layout(int l, int t, int r, int b)方法的参数,重新
    40                 // 进行布局,达到自动回弹的效果.
    41                 layout(getLeft(), getTop() - i, getRight(), getBottom() - i);
    42                 break;
    43             case MotionEvent.ACTION_MOVE:
    44                 yMove = y;
    45                 //如果是向下移动,计算出每次滑动的距离与滑动的总距离,将每次滑动的
    46                 // 距离作为layout(int l, int t, int r, int b)方法的参数,重新进行布局,达到布局滑动的效果.
    47                 if ((yMove - yDown) > 0) {
    48                     mMove = yMove - yDown;
    49                     i += mMove;
    50                     layout(getLeft(), getTop() + mMove, getRight(), getBottom() + mMove);
    51                 }
    52                 break;
    53         }
    54         return true;
    55     }
    56 
    57     //重写父容器的onInterceptTouchEvent()方法也就是通过外部拦截法,
    58     // 如果父容器需要处理该滑动事件,就进行拦截,从而解决滑动冲突的问题.
    59     @Override
    60     public boolean onInterceptHoverEvent(MotionEvent event) {
    61         int y = (int) event.getY();
    62         switch (event.getAction()) {
    63             case MotionEvent.ACTION_DOWN:
    64                 yDown = y;
    65                 break;
    66             case MotionEvent.ACTION_MOVE:
    67                 yMove = y;
    68                 if (yMove - yDown < 0) {
    69                     isIntercept = false;
    70                 } else if (yMove - yDown > 0) {
    71                     isIntercept = true;
    72                 }
    73                 break;
    74             case MotionEvent.ACTION_UP:
    75                 break;
    76         }
    77         return isIntercept;
    78     }
    79 }
    
    

    MotionEvent.ACTION_DOWN: 获取刚开始触碰的y坐标 

    MotionEvent.ACTION_MOVE: 如果是向下滑动,计算出每次滑动的距离与滑动的总距离,将每次滑动的距离作为layout(int l, int t, int r, int b)方法的参数,重新进行布局,达到布局滑动的效果。 
    MotionEvent.ACTION_UP: 将滑动的总距离作为layout(int l, int t, int r, int b)方法的参数,重新进行布局,达到布局自动回弹的效果。

     

    此时的布局文件是这样的:

    中间重复的RelativeLayout就不贴出来了。至此,一个简单的下拉回弹就已经实现了,关于快速滑动以及惯性滑动感兴趣的可以加进去,这里不是本篇博客的重点就不做讨论了。

     

    2子View的滚动实现   

    手指向下滑动的时候,布局的下拉回弹已经实现,现在我希望手指向上滑动的时候,布局的子View能够滚动。平时接触最多的能滚动的View就是ScrollView,所以我的第一反应就是在自定义的LinearLayout内,添加一个ScrollView,让子View能够滚动。说干就干:

    兴高采烈的加上去,最后运行的结果是:布局完全变成了一个ScrollView,之前的下拉回弹效果已经完全消失!!!这显然不是我期待的结果。

     

    仔细分析一下这种现象,其实这就是常见的View滑动冲突场景之一:外部滑动方向与内部滑动方向一致。父布局MyParentView需要响应竖直方向上的向下滑动,实现下拉回弹,子布局ScrollView也需要响应竖直方向上的上下滑动,实现子View的滚动。当内外两层都在同一个方向上可以滑动的时候,就会出现逻辑问题。因为当手指滑动的时候,系统无法知道用户想让哪一层滑动。所以这种场景下的滑动冲突需要我们手动去解决。

     

    解决办法: 

    外部拦截法:外部拦截法是指点击事件先经过父容器的拦截处理,如果父容器需要处理此事件就进行拦截,如果不需要此事件就不拦截,这样就可以解决滑动冲突的问题。外部拦截法需要重写父容器的onInterceptTouchEvent()方法,在内部做相应的拦截即可。

     

    具体实现:

    实现分析: 

     


    在自定义的父布局中重写onInterceptTouchEvent()方法,MotionEvent.ACTION_MOVE的时候,进行判断。如果手指是向上滑动,onInterceptTouchEvent()返回false,表示父布局不拦截当前事件,当前事件交给子View处理,那么我们的子View就能滚动;如果手指是向下滑动,onInterceptTouchEvent()返回true,表示父布局拦截当前事件,当前事件交给父布局处理,那么我们父布局就能实现下拉回弹。

    
    3连续滑动的实现   

    刚开始我以为这样就万事大吉了,可后来我又发现一个很严重的问题:手指向上滑动的时候,子View开始滚动,然后手指再向下滑动,整个父布局开始向下滑动,松手后便自动回弹。也就是说,刚才滚动的子View已经回不到开始的位置。仔细分析一下其实这结果是意料之中的,因为只要我手指是向下滑动,onInterceptTouchEvent()便返回true,父布局会拦截当前事件。这里其实又是上面提到的View滑动冲突:理想的结果是当子View滚动后,如果子View没有滚动到开始的位置,父布局就不要拦截滑动事件;如果子View已经滚动到开始的位置,父布局就开始拦截滑动事件。

     

    解决办法: 

     

    内部拦截法:内部拦截法是指点击事件先经过子View处理,如果子View需要此事件就直接消耗掉,否则就交给父容器进行处理,这样就可以解决滑动冲突的问题。内部拦截法需要配合requestDisallowInterceptTouchEvent()方法,来确定子View是否允许父布局拦截事件。

     

    具体实现:

     
     1 package costom.widget;
     2 
     3 import android.content.Context;
     4 import android.util.AttributeSet;
     5 import android.view.MotionEvent;
     6 import android.widget.ScrollView;
     7 
     8 /**
     9  * Created by ${火龙裸先生} on 2017/1/25.
    10  * 邮箱:791335000@qq.com
    11  */
    12 public class MyScrollView extends ScrollView {
    13     public MyScrollView(Context context) {
    14         this(context, null);
    15     }
    16 
    17     public MyScrollView(Context context, AttributeSet attrs) {
    18         this(context, attrs, 0);
    19     }
    20 
    21     public MyScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
    22         super(context, attrs, defStyleAttr);
    23     }
    24 
    25     @Override
    26     public boolean onTouchEvent(MotionEvent ev) {
    27         switch (ev.getAction()) {
    28             case MotionEvent.ACTION_DOWN:
    29                 break;
    30             case MotionEvent.ACTION_MOVE:
    31                 //在MotionEvent.ACTION_MOVE的时候,得到滑动的距离。如果滑动的距离为0,表示子View已经
    32                 // 滚动到开始位置,此时调用 getParent().requestDisallowInterceptTouchEvent(false)方法,
    33                 // 允许父View进行事件拦截;如果滑动的距离不为0,表示子View没有滚动到开始位置,此时调用
    34                 int scrollY = getScrollY();
    35                 if (scrollY == 0) {
    36                     //允许父View进行事件拦截
    37                     getParent().requestDisallowInterceptTouchEvent(false);
    38                 } else {
    39                     //禁止父View进行事件拦截
    40                     getParent().requestDisallowInterceptTouchEvent(true);
    41                 }
    42                 break;
    43             case MotionEvent.ACTION_UP:
    44                 break;
    45         }
    46         return super.onTouchEvent(ev);
    47     }
    48 }
     

    实现分析: 

     

    自定义一个ScrollView,重写onTouchEvent()方法,在MotionEvent.ACTION_MOVE的时候,得到滑动的距离。如果滑动的距离为0,表示子View已经滚动到开始位置,此时调用 getParent().requestDisallowInterceptTouchEvent(false)方法,允许父View进行事件拦截;如果滑动的距离不为0,表示子View没有滚动到开始位置,此时调用 getParent().requestDisallowInterceptTouchEvent(true)方法,禁止父View进行事件拦截。这样只要子View没有滚动到开始的位置,父布局都不会拦截事件,一旦子View滚动到开始的位置,父布局就开始拦截事件,形成连续的滑动。

    
    

    好了,针对其他场景更复杂的滑动冲突,解决滑动冲突的原理与方式无非就是这两种方法。

    
    
    
  • 相关阅读:
    django-搭建BBS关键点总结
    关于django中input标签中file类型以及开路由
    Bzoj1115 石子游戏Kam
    HDU1907 John
    HDU2509 Be the Winner
    洛谷P1082 同余方程
    POJ1065 Area
    Vijos1889 天真的因数分解
    Bzoj2440 完全平方数
    Bzoj2705 Longge的问题
  • 原文地址:https://www.cnblogs.com/huolongluo/p/6349219.html
Copyright © 2011-2022 走看看