• 自定义控件(视图)2期笔记12:View的滑动冲突之 外部拦截法


    1. 外部拦截法:

    点击事件通过父容器拦截处理,如果父容器需要就拦截,不需要就不拦截。

    这种方法比较符合事件分发机制。外部拦截法需要重写父容器的onInterceptTouchEvent方法,在内部做相应的拦截即可。

    这种方法的伪代码,如下: 

     1 @Override
     2     public boolean onInterceptTouchEvent(MotionEvent event) {
     3         boolean intercepted = false;
     4         int x = (int) event.getX();
     5         int y = (int) event.getY();
     6 
     7         switch (event.getAction()) {
     8         case MotionEvent.ACTION_DOWN: {
     9             intercepted = false;
    10             break;
    11         }
    12         case MotionEvent.ACTION_MOVE: {
    13       
    14             if (父容器需要当前点击事件) {
    15                 intercepted = true;
    16             } else {
    17                 intercepted = false;
    18             }
    19             break;
    20         }
    21         case MotionEvent.ACTION_UP: {
    22             intercepted = false;
    23             break;
    24         }
    25         default:
    26             break;
    27         }
    28 
    29         mLastXIntercept = x;
    30         mLastYIntercept = y;
    31 
    32         return intercepted;
    33     }

    (1)在onInterceptTouchEvent方法之中,首先是ACTION_DOWN这个事件,父容器必须返回false,也就是不拦截ACTION_DOWN事件,因为一旦父容器拦截了ACTION_DOWN事件,那么后续的ACTION_MOVE 和 ACTION_UP这些事件会直接交给父容器处理,这个时候事件没有办法再传递给子元素;

    (2)其次是ACTION_MOVE事件,这个事件可以根据需要来决定是否拦截,如果父容器需要拦截的话就返回true,否则就返回false,最后是ACTION_UP事件,这里必须要返回false因为ACTION_UP事件本身没有太多意义

    (3)假如事件交给子元素处理,如果父容器在ACTION_UP时候返回了true,就会导致子元素无法接收到ACTION_UP事件,这个时候子元素中的onClick事件就无法触发,但是父容器比较特殊,一旦它开始拦截任何一个事件,那么后续的事件都会交给它来处理,而ACTION_UP作为最后一个事件也必定会传递给父容器,即便父容器的OnInterceptTouchEvent方法中ACTION_UP时候返回false.

    2. 下面通过一个Demo示例说明:

    (1)首先我们创建一个Android工程,如下:

    (2)我们来到activity_main.xml,如下:

     1 <com.himi.viewconflict.ui.RevealLayout
     2     xmlns:android="http://schemas.android.com/apk/res/android"
     3     xmlns:tools="http://schemas.android.com/tools"
     4     android:layout_width="match_parent"
     5     android:layout_height="match_parent"
     6     android:orientation="vertical"
     7     android:padding="12dp"
     8     tools:context="${relativePackage}.${activityClass}" >
     9 
    10     <Button
    11         android:id="@+id/button1"
    12         style="@style/AppTheme.Button.Green"
    13         android:onClick="onButtonClick"
    14         android:text="滑动冲突场景1-外部拦截" />
    15 
    16 </com.himi.viewconflict.ui.RevealLayout>

    这里的RevealLayout是一个自定义控件(继承自ViewGroup),任何放入内部的clickable元素,当它被点击的时候,都具有波纹效果。

    感觉这个RevealLayout很好用,存放自己的Github代码库之中。

    (3)接下来来到MainActivity,如下:

     1 package com.himi.viewconflict;
     2 
     3 import android.app.Activity;
     4 import android.content.Intent;
     5 import android.os.Bundle;
     6 import android.view.View;
     7 
     8 public class MainActivity extends Activity {
     9 
    10     @Override
    11     protected void onCreate(Bundle savedInstanceState) {
    12         super.onCreate(savedInstanceState);
    13         setContentView(R.layout.activity_main);
    14     }
    15 
    16     
    17     
    18     public void onButtonClick(View view) {
    19          Intent intent = new Intent(this, DemoActivity_1.class);
    20          startActivity(intent);
    21     }
    22 }

    (4)上面很自然地跳转到DemoActivity_1之中,如下:

    package com.himi.viewconflict;
    
    import java.util.ArrayList;
    
    import com.himi.viewconflict.ui.HorizontalScrollViewEx;
    import com.himi.viewconflict.utils.MyUtils;
    
    import android.app.Activity;
    import android.graphics.Color;
    import android.os.Bundle;
    import android.util.Log;
    import android.view.LayoutInflater;
    import android.view.View;
    import android.view.ViewGroup;
    import android.widget.AdapterView;
    import android.widget.AdapterView.OnItemClickListener;
    import android.widget.ArrayAdapter;
    import android.widget.ListView;
    import android.widget.TextView;
    import android.widget.Toast;
    
    public class DemoActivity_1 extends Activity {
        private static final String TAG = "DemoActivity_1";
    
        private HorizontalScrollViewEx mListContainer;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.demo_1);
            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;
            //初始化3页ListView内容
            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 OnItemClickListener() {
                @Override
                public void onItemClick(AdapterView<?> parent, View view,
                        int position, long id) {
                    Toast.makeText(DemoActivity_1.this, "click item "+position,
                            Toast.LENGTH_SHORT).show();
    
                }
            });
        }
    }

    上面的DemoActivity_1主布局demo_1.xml,如下:

     1 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     2     xmlns:tools="http://schemas.android.com/tools"
     3     android:layout_width="match_parent"
     4     android:layout_height="match_parent"
     5     android:background="#ffffff"
     6     android:orientation="vertical" >
     7 
     8     <com.himi.viewconflict.ui.HorizontalScrollViewEx
     9         android:id="@+id/container"
    10         android:layout_width="wrap_content"
    11         android:layout_height="match_parent" />
    12 
    13 
    14 </LinearLayout>

    上面使用到HorizontalScrollViewEx是自定义控件(继承自ViewGroup),在HorizontalScrollViewEx里面实现外部拦截法逻辑,如下:

      1 package com.himi.viewconflict.ui;
      2 
      3 import android.content.Context;
      4 import android.util.AttributeSet;
      5 import android.util.Log;
      6 import android.view.MotionEvent;
      7 import android.view.VelocityTracker;
      8 import android.view.View;
      9 import android.view.ViewGroup;
     10 import android.widget.Scroller;
     11 
     12 public class HorizontalScrollViewEx extends ViewGroup {
     13     private static final String TAG = "HorizontalScrollViewEx";
     14 
     15     private int mChildrenSize;
     16     private int mChildWidth;
     17     private int mChildIndex;
     18 
     19     // 分别记录上次滑动的坐标
     20     private int mLastX = 0;
     21     private int mLastY = 0;
     22     // 分别记录上次滑动的坐标(onInterceptTouchEvent)
     23     private int mLastXIntercept = 0;
     24     private int mLastYIntercept = 0;
     25 
     26     private Scroller mScroller;
     27     private VelocityTracker mVelocityTracker;
     28 
     29     public HorizontalScrollViewEx(Context context) {
     30         super(context);
     31         init();
     32     }
     33 
     34     public HorizontalScrollViewEx(Context context, AttributeSet attrs) {
     35         super(context, attrs);
     36         init();
     37     }
     38 
     39     public HorizontalScrollViewEx(Context context, AttributeSet attrs,
     40             int defStyle) {
     41         super(context, attrs, defStyle);
     42         init();
     43     }
     44 
     45     private void init() {
     46         mScroller = new Scroller(getContext());
     47         mVelocityTracker = VelocityTracker.obtain();
     48     }
     49 
     50     @Override
     51     public boolean onInterceptTouchEvent(MotionEvent event) {
     52         boolean intercepted = false;
     53         int x = (int) event.getX();
     54         int y = (int) event.getY();
     55 
     56         switch (event.getAction()) {
     57         case MotionEvent.ACTION_DOWN: {
     58             intercepted = false;
     59             /**
     60                         如果滑动动画还没结束,我们就按下了结束的按钮,那我们就结束该动画.
     61                        目的是为了优化滑动体验 62                                 倘若用户正在水平滑动,在滑动停止之前用户迅速转化为竖直滑动,导致
     63                                 界面在水平方向无法滑动至终点从而处于一种中间状态。为了避免这种状态,
     64                                 用户正在水平滑动时候,下一个序列的点击事件仍然交给父容器处理,这样就不会处于中间状态          
     65                 
     66             */
     67             if (!mScroller.isFinished()) {
     68                 mScroller.abortAnimation();
     69                 intercepted = true;
     70             }
     71             break;
     72         }
     73         case MotionEvent.ACTION_MOVE: {
     74             int deltaX = x - mLastXIntercept;
     75             int deltaY = y - mLastYIntercept;
     76             if (Math.abs(deltaX) > Math.abs(deltaY)) {//水平滑动距离差 > 竖直滑动距离差
     77                 intercepted = true;
     78             } else {//水平滑动距离差 < 竖直滑动距离差
     79                 intercepted = false;
     80             }
     81             break;
     82         }
     83         case MotionEvent.ACTION_UP: {
     84             intercepted = false;
     85             break;
     86         }
     87         default:
     88             break;
     89         }
     90 
     91         Log.d(TAG, "intercepted=" + intercepted);
     92         mLastX = x;
     93         mLastY = y;
     94         mLastXIntercept = x;
     95         mLastYIntercept = y;
     96 
     97         return intercepted;
     98     }
     99 
    100     @Override
    101     public boolean onTouchEvent(MotionEvent event) {
    102         //表示追踪当前点击事件的速度
    103         mVelocityTracker.addMovement(event);
    104         int x = (int) event.getX();
    105         int y = (int) event.getY();
    106         switch (event.getAction()) {
    107         case MotionEvent.ACTION_DOWN: {
    108             if (!mScroller.isFinished()) {
    109                 mScroller.abortAnimation();
    110             }
    111             break;
    112         }
    113         case MotionEvent.ACTION_MOVE: {
    114             int deltaX = x - mLastX;
    115             int deltaY = y - mLastY;
    116             scrollBy(-deltaX, 0);
    117             break;
    118         }
    119         case MotionEvent.ACTION_UP: {
    120             /**
    121              * 表示计算速度,比如:时间间隔为1000 ms ,在1秒内,
    122              * 手指在水平方向从左向右滑过100像素,那么水平速度就是100;
    123              * 计算速度+获取速度----三步曲
    124              *   1. mVelocityTracker.computeCurrentVelocity(1000);
    125              *   2. float xVelocity = mVelocityTracker.getXVelocity(); //获取水平方向的滑动速度
    126              *   3. float yVelocity = mVelocityTracker.getYVelocity();//获取垂直方向的滑动速度
    127              * 由于我们需要的是xVelocity,
    128              * 这里只是提一下,不计入代码;
    129              * 注意:这里的速度指的是一段时间内手指所滑过的像素数!像素数!像素数!重要事说3遍;
    130              */
    131             int scrollX = getScrollX();
    132             int scrollToChildIndex = scrollX / mChildWidth;
    133             mVelocityTracker.computeCurrentVelocity(1000);
    134             float xVelocity = mVelocityTracker.getXVelocity();
    135             
    136             /**
    137              *当你滑动手机相册中的照片的时候有没有发现,必须滑动到一定距离它才会切到下张图片,
    138              * 否则,它就回退回原来的照片了,原来,它是通过“速度”来进行控制的~
    139              * 还有就是"速度“可以为负值,很好理解,就像我们规定车前进的方向为正,反向为负;
    140              *
    141              */
    142             if (Math.abs(xVelocity) >= 50) {
    143                 mChildIndex = xVelocity > 0 ? mChildIndex - 1 : mChildIndex + 1;
    144             } else {
    145                 mChildIndex = (scrollX + mChildWidth / 2) / mChildWidth;
    146             }
    147             mChildIndex = Math.max(0, Math.min(mChildIndex, mChildrenSize - 1));
    148             int dx = mChildIndex * mChildWidth - scrollX;//缓慢地滑动到目标的x坐标
    149             smoothScrollBy(dx, 0);
    150             mVelocityTracker.clear();//对速度跟踪进行回收
    151             break;
    152         }
    153         default:
    154             break;
    155         }
    156 
    157         mLastX = x;
    158         mLastY = y;
    159         return true;
    160     }
    161 
    162     @Override
    163     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    164         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    165         int measuredWidth = 0;
    166         int measuredHeight = 0;
    167         final int childCount = getChildCount();
    168         measureChildren(widthMeasureSpec, heightMeasureSpec);
    169 
    170         int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec);
    171         int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
    172         int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec);
    173         int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
    174         if (childCount == 0) {
    175             //这个方法必须由onMeasure(int, int)来调用来存储测量的宽,高值
    176             setMeasuredDimension(0, 0);
    177             
    178             /**
    179             1.UNSPECIFIED
    180                 父不没有对子施加任何约束,子可以是任意大小(也就是未指定)
    181                 (UNSPECIFIED在源码中的处理和EXACTLY一样。当View的宽高值设置为0的时候或者没有设置宽高时,
    182                         模式为UNSPECIFIED)
    183             2.EXACTLY
    184                  父决定子的确切大小,子被限定在给定的边界里,忽略本身想要的大小。对应LayoutParams中的 match_parent具体的数值
    185                 (当设置width或height为match_parent时,模式为EXACTLY,因为子view会占据剩余容器的空间,
    186                         所以它大小是确定的)
    187             3.AT_MOST
    188                    子最大可以达到的指定大小,对应LayoutParams中的wrap_content
    189            */
    190         } else if (heightSpecMode == MeasureSpec.AT_MOST) {
    191             final View childView = getChildAt(0);
    192             measuredHeight = childView.getMeasuredHeight();
    193             setMeasuredDimension(widthSpaceSize, childView.getMeasuredHeight());
    194         } else if (widthSpecMode == MeasureSpec.AT_MOST) {
    195             final View childView = getChildAt(0);
    196             measuredWidth = childView.getMeasuredWidth() * childCount;
    197             setMeasuredDimension(measuredWidth, heightSpaceSize);
    198         } else {
    199             final View childView = getChildAt(0);
    200             measuredWidth = childView.getMeasuredWidth() * childCount;
    201             measuredHeight = childView.getMeasuredHeight();
    202             setMeasuredDimension(measuredWidth, measuredHeight);
    203         }
    204     }
    205 
    206     @Override
    207     protected void onLayout(boolean changed, int l, int t, int r, int b) {
    208         int childLeft = 0;
    209         final int childCount = getChildCount();
    210         mChildrenSize = childCount;
    211 
    212         for (int i = 0; i < childCount; i++) {
    213             final View childView = getChildAt(i);
    214             if (childView.getVisibility() != View.GONE) {
    215                 final int childWidth = childView.getMeasuredWidth();
    216                 mChildWidth = childWidth;
    217                 childView.layout(childLeft, 0, childLeft + childWidth,
    218                         childView.getMeasuredHeight());
    219                 childLeft += childWidth;
    220             }
    221         }
    222     }
    223 
    224     private void smoothScrollBy(int dx, int dy) {
    225         mScroller.startScroll(getScrollX(), 0, dx, 0, 500);
    226         invalidate();
    227     }
    228 
    229     /**
    230      * computeScroll:主要功能是计算拖动的位移量、更新背景231      *               设置要显示的屏幕(setCurrentScreen(mCurrentScreen);)
    232      * 通常是用mScroller记录/计算View滚动的位置,再重写View的computeScroll(),完成实际的滚动233      */
    234     @Override
    235     public void computeScroll() {
    236         if (mScroller.computeScrollOffset()) {
    237             scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
    238             postInvalidate();
    239         }
    240     }
    241 
    242     /**
    243      * onAttachedToWindow: 是在第一次onDraw前调用的。也就是我们写的View在没有绘制出来时调用的,但只会调用一次。
    244      * 比如,我们写状态栏中的时钟的View,在onAttachedToWindow这方法中做初始化工作,比如注册一些广播等等
    245      */
    246     
    247     @Override
    248     protected void onDetachedFromWindow() {
    249         mVelocityTracker.recycle();
    250         super.onDetachedFromWindow();
    251     }
    252 }

    (5)来到主布局之中,在HorizontalScrollViewEx之中包含一个子布局content_layout.xml,如下:

     1 <?xml version="1.0" encoding="utf-8"?>
     2 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     3     android:layout_width="match_parent"
     4     android:layout_height="match_parent"
     5     android:orientation="vertical" >
     6 
     7     <TextView
     8         android:id="@+id/title"
     9         android:layout_width="wrap_content"
    10         android:layout_height="wrap_content"
    11         android:layout_marginTop="5dp"
    12         android:layout_marginBottom="5dp"
    13         android:text="TextView" />
    14 <!-- 
    15   android:cacheColorHint="#00000000":去除listview的拖动背景色
    16   android:listSelector:当你不使用android:listSelector属性,默认会显示选中的item为橙黄底色17                                    有时候我们需要去掉这种效果  -->
    18     <ListView
    19         android:id="@+id/list"
    20         android:layout_width="match_parent"
    21         android:layout_height="match_parent"
    22         android:background="#fff4f7f9"
    23         android:cacheColorHint="#00000000"
    24         android:divider="#dddbdb"
    25         android:dividerHeight="1.0px"
    26         android:listSelector="@android:color/transparent" />
    27 
    28 </LinearLayout>

    这布局文件之中包含一个ListView是上下滑动,而HorizontalScrollViewEx是左右滑动的,两者之间的滑动冲突在上面使用外部拦截法解决了。

    接下来就是上面Listview 的item布局,如下:

     1 <?xml version="1.0" encoding="utf-8"?>
     2 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     3     android:layout_width="match_parent"
     4     android:layout_height="50dp"
     5     android:gravity="center_vertical"
     6     android:orientation="vertical" >
     7 
     8     <TextView
     9         android:id="@+id/name"
    10         android:layout_width="wrap_content"
    11         android:layout_height="wrap_content"
    12         android:text="TextView" />
    13 
    14 </LinearLayout>

    (6)最终项目如下:

    (7)部署程序到手机上,运行效果如下:

     

    3. 示例源码下载

  • 相关阅读:
    python列表切片
    python注释行与段落
    PCL安装与配置
    自动驾驶相关
    (转)ping命令
    (转)linux应用之test命令详细解析
    (转)shell解析命令行的过程以及eval命令
    (转)ssh-keygen 中文手册
    (转)stty 命令说明及使用讲解
    (转)CentOS下的trap命令
  • 原文地址:https://www.cnblogs.com/hebao0514/p/5700606.html
Copyright © 2020-2023  润新知