zoukankan      html  css  js  c++  java
  • 自定义控件(视图)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. 示例源码下载

  • 相关阅读:
    I.MX6 Surfaceflinger 机制
    理解 Android Fragment
    RPi 2B DDNS 动态域名
    RPi 2B IPC webcam server
    理解 Android MVP 开发模式
    I.MX6 system.img unpack repack
    can't set android permissions
    VMware Ubuntu 共享文件夹
    解决oracle数据库连接不上的问题
    perfect-scrollbar示例
  • 原文地址:https://www.cnblogs.com/hebao0514/p/5700606.html
Copyright © 2011-2022 走看看