Android自定义ViewView事件分发

Android事件分发机制在实战开发中的应用之二

2019-05-15  本文已影响3人  门心叼龙

学习的最终目标就是要学以致用,本文所分享的案例都是自己在公司实战开发过程中的真实案例,现在把它分享出来,希望对初学者有所帮助

版权声明:本文来自门心叼龙的博客,属于原创内容,转载请注明出处:https://blog.csdn.net/geduo_83/article/details/90145083

github源码下载地址:https://github.com/geduo83/android-touch-event
Android事件分发机制的探索与发现之View篇
Android事件分发机制的探索与发现之ViewGroup篇
Android事件分发机制的探索与发现之Activity篇
Android事件分发机制的探索与发现之总结篇
Android事件分发机制在实战开发中的应用之一
Android事件分发机制在实战开发中的应用之二

上一篇我们讲了了两个小案例,想必大家通过这两个小案例的学习,对Android事件分发的认知就更加深刻了,那么这篇文章我们通过一个稍微复杂的案例,来进一步加深对事件分发机制的理解,该案例都是自己在公司实战项目中的总结与归纳,我们先睹为快,看一下效果图:


20190514_163412.gif

通过gif动态效果图我们可以看到,整个页面分割为了两部分,上半部分为地图,下半部分为文字内容区RxJava详解和Dagger详解,需求是当我们手指在文字内容区上下滑动的时候,整个文字内容区会跟随手指上下的移动并且它会盖在地图的上面进行滚动,当我们点击缩放按钮的时候,地图会逐渐的放大直到占满整个屏幕的高度,此时文字内容区随着地图的放大会逐渐的消失,当我们再次点击按钮的时候地图会逐渐的缩小直到初始位置,此时文字内容区也会从屏幕底部逐渐移动到初始的位置,缩放按钮可以来回的切换,当我们手指在地图上进行点击,滑动地图会放大,移动等操作。

文字内容区的滚动

我们想了文字内容区能够随着手指上下滑动让我们很容易想到ScrollView控件,当然ScrollView控件已经过时了,我们就用NestedScrollView了来代替他了,用它来包裹我们的地图和文字内容区可行吗?当然不行了因为我们的地图是不需要滚动的,文字内容能活动,地图不能滚动,而且文字内容区还能在在地图上滚动,分析到这儿我们不难下出结论:地图和文字内容区肯定是被相对布局RelativeLayout包裹的,而且还只能是文字内容区盖在地图之上,否则的话用线性布局LinearLayout来包裹,根本就不可能文字内容区在地图上滚动了,那又有问题了,因为刚才分析过文字内容区是盖在地图之上的,那么地图怎么显示出来?有人可能会想到用margin来解决这个问题,如果这样那么文字内容区就只能在地图的下方位置滚动了。怎么办?灵机一动,我在地图之上盖一个和地图同等大小的透明View不就行了吗?是的,这样问题就解决了,此时文字内容区和和地图等大的View就被NestedScrollView包裹起来了,然后NestedScrollView盖在地图之上就ok了。

布局代码如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout android:layout_width="match_parent"
              android:layout_height="match_parent"
              xmlns:app="http://schemas.android.com/apk/res-auto"
              android:orientation="vertical"
              xmlns:android="http://schemas.android.com/apk/res/android">
    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="?attr/colorPrimary"
        app:popupTheme="@style/AppTheme.PopupOverlay"
        />

    <RelativeLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:id="@+id/layout_blog_root"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <com.android.touchevent.view.BlogMapView
            android:id="@+id/blog_map_view"
            android:layout_width="match_parent"
            android:layout_height="250dp"
            android:layout_alignParentTop="true"
            />
        <com.android.touchevent.view.BlogScrollView
            android:id="@+id/view_scrollview"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_alignParentTop="true"
            >
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical">
                <com.android.touchevent.view.BlogTransView
                    android:id="@+id/view_blog_trans"
                    android:layout_width="match_parent"
                    android:layout_height="250dp"
                    android:background="#00000000"/>
                <LinearLayout
                    xmlns:android="http://schemas.android.com/apk/res/android"
                    xmlns:app="http://schemas.android.com/apk/res-auto"
                    android:id="@+id/layout_blog_detail"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:background="#ffffff"
                    android:orientation="vertical">
                      <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:padding="10dp"
                        android:text="RxJava线程切换之subscribeOn和observeOn详解"
                        />
                </LinearLayout>
            </LinearLayout>
        </com.android.touchevent.view.BlogScrollView>
    </RelativeLayout>
</LinearLayout>

地图事件的响应

分析了这么久我们只解决了第一个问题,文字内容区可以在地图上滚动,但是此时我手指在地图上操作的时候,只是文字内容区在滚动,地图并没有任何响应,为什么?因为表面上看你此时操作的是地图,其实上你操作的是NestedScrollView包裹的透明View,此时你的手势事件,都被NestedScrollView拦截处理了,那么我们怎么办?我们只要自定义一个NestedScrollView在手势滑动的时候,我们在它的事件处理方法里我们判断只要此时是手指是在透明View的操作,我们就把该事件传递给透明View,然后透明View把这个事件传递给地图就ok了。

public class BlogScrollView extends NestedScrollView {
    public BlogTransView.OnTouchEventListener mTouchEventListener;

    public BlogScrollView(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        //如果触摸的是透明BlogTransView,那么该事件继续往下分发给BlogScrollView的子View:BlogTransView
        if (mTouchEventListener != null && mTouchEventListener.isTouchTransView(ev)) {
            //返回false表示继续往下层View树进行分发
            return false;
        }
        //否则就调用ScrollView的拦截事件,进行滚动处理
        return super.onInterceptTouchEvent(ev);
    }
    //设置透明View的触摸事件监听器
    public void setTransViewTouchListener(BlogTransView.OnTouchEventListener listener) {
        mTouchEventListener = listener;
    }
}
public class BlogTransView extends RelativeLayout {
    public static final String TAG = BlogTransView.class.getSimpleName();
    private CheckBox mBtnMapZoom;
    private CompoundButton.OnCheckedChangeListener mBtnZoomChangeListener;
    private DispatchEventListener mDispatchEventListener;
    //是否被触摸的监听器
    public interface OnTouchEventListener {
        boolean isTouchTransView(MotionEvent event);
    }
    //透明View的事件分发监听器
    public interface DispatchEventListener {
        boolean dispathTouchEvent(MotionEvent event);
    }
    public BlogTransView(Context context, AttributeSet attrs) {
        super(context, attrs);
        LayoutInflater.from(context).inflate(R.layout.view_blog_trans, this, true);
        mBtnMapZoom = findViewById(R.id.btn_map_zoom);
        mBtnMapZoom.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
                if (mBtnZoomChangeListener != null) {
                    mBtnZoomChangeListener.onCheckedChanged(compoundButton, b);
                }
            }
        });
    }

    //如果触摸了ScrollView上半部分的透明的部分,则事件会分发至此
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        if (mDispatchEventListener != null) {
            Rect rect = new Rect();
            mBtnMapZoom.getHitRect(rect);
            Log.v(TAG, "x:" + event.getX() + ";y:" + event.getY());

            //如果用户点击了放大,缩小按钮则该事件会继续往下分发给他的子View:CheckBox
            if (rect.contains((int) event.getX(), (int) event.getY())) {
                Log.v(TAG, "mBtnMapZoom click yes");
                return super.dispatchTouchEvent(event);//继续往下分发给CheckBox
            } else {
                Log.v(TAG, "mBtnMapZoom click no");
                return mDispatchEventListener.dispathTouchEvent(event);
            }
        } else {
            return super.dispatchTouchEvent(event);
        }
    }

      public void setBtnZoomChangeListener(CompoundButton.OnCheckedChangeListener listener) {
        mBtnZoomChangeListener = listener;
    }

    public void setBtnMapZoomChecked(boolean b) {
        if (mBtnMapZoom != null) {
            mBtnMapZoom.setChecked(b);
        }
    }
    public boolean isTouchTransView(MotionEvent event){
        Rect rect = new Rect();
        getLocalVisibleRect(rect);
        //根据坐标来判断,是否是点击了透明View,如果是则返回true,否则返回false
        if (rect.contains((int) event.getX(), (int) event.getY())) {
            return true;
        } else {
            return false;
        }
    }
    public void setDispatchEventListener(DispatchEventListener dispatchEventListener) {
        mDispatchEventListener = dispatchEventListener;
    }
}

缩放按钮事件响应

现在我们来解决第三个问题: 点击黑色缩放按钮地图放大缩小的问题,有人说很简单,把按钮加载地图上,因为NestedScrollView的事件传递给了透明View,而它再次把事件传递给了地图View,地图再把事件传递给缩放按钮,我确实也是这么做的,但是发现了一个问题,当点击按钮放大的时候,按钮能接受事件,当地图整屏显示之后,我们在点击按钮缩小地图的时候,很奇怪,通过代码测试发现无法判断此时的点击事件是在按钮之上,换了几个方法都行不通,不知道谁有什么好的解决办法,行不通了?怎么办,那就只能把缩放按钮放在透明View里了,但是又有问题了,当你手指在文字区域往上滑动的时候,缩放按钮回跟着往上移动,这是和我们的需求不符的。

20190514_160652_bug.gif

此时我再次脑洞大开我把透明View里的缩放按钮做成透明的,让他来响应触摸事件,然后在地图View的相同位置在放一个同样大小的缩放按钮,让它只做一个显示的作用,此时的逻辑就是这样的:当用户点击缩放按钮的时候NestedScorllView会把事件传递给透明View,如果此时触摸点在地图上就把该事件传递给地图,如果触摸点是在缩放按钮上就把事件继续往下分发给自己子View缩放按钮,然后缩放按钮添加的监听器响应事件,此时再来改变地图View中缩放按钮的状态,运行测试,大功告成,实在是,快哉!快哉!

//如果触摸了ScrollView上半部分的透明的部分,则事件会分发至此
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        if (mDispatchEventListener != null) {
            Rect rect = new Rect();
            mBtnMapZoom.getHitRect(rect);
            Log.v(TAG, "x:" + event.getX() + ";y:" + event.getY());

            //如果用户点击了放大,缩小按钮则该事件会继续往下分发给他的子View:CheckBox
            if (rect.contains((int) event.getX(), (int) event.getY())) {
                Log.v(TAG, "mBtnMapZoom click yes");
                return super.dispatchTouchEvent(event);//继续往下分发给CheckBox
            } else {
                Log.v(TAG, "mBtnMapZoom click no");
                return mDispatchEventListener.dispathTouchEvent(event);
            }
        } else {
            return super.dispatchTouchEvent(event);
        }
    }
public class BlogMapView extends RelativeLayout {
    public static final String TAG = BlogMapView.class.getSimpleName();
    private AMap mMap;
    private TextureMapView mTextureMapView;
    private CheckBox mBtnMapZoom;
    private CompoundButton.OnCheckedChangeListener mChangeListener;

    public BlogMapView(Context context, AttributeSet attrs) {
        super(context, attrs);
        LayoutInflater.from(context).inflate(R.layout.view_blog_map, this, true);
        mTextureMapView = findViewById(R.id.map);
        mMap = mTextureMapView.getMap();
        mMap.getUiSettings().setZoomControlsEnabled(false);

        mBtnMapZoom = findViewById(R.id.btn_map_zoom);
        mBtnMapZoom.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
                if (mChangeListener != null) {
                    mChangeListener.onCheckedChanged(compoundButton, b);
                }
            }
        });
    }

    public boolean dispatchMapTouchEvent(MotionEvent event) {
        return mTextureMapView.dispatchTouchEvent(event);
    }

    public void setMapLayoutParams(int height) {
        setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, height));
        mTextureMapView.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, height));
    }

    public void setBtnMapZoomChecked(boolean b) {
        mBtnMapZoom.setChecked(b);
    }

    public void onCreate(Bundle bundle) {
        mTextureMapView.onCreate(bundle);
    }

    public AMap getMap() {
        return mMap;
    }

    public void setChangeListener(CompoundButton.OnCheckedChangeListener changeListener) {
        mChangeListener = changeListener;
    }
}

总结

截止目前整个案例我就讲完了,怎么样?是不是还是有些难度的,我觉得这个案例实在是太经典了,因为里面的一些事件传递问题和透明占位View的技巧使用,它涵盖了整个View事件分发的方方面面,如果对事件分发机制没有一定的理解和认识,还真搞不定它,只要把它搞懂了,我相信就能解决事件分发中的大部分问题了,最后我把整个测试项目放到github上了,希望对初学者有所帮助,测试过程中有什么问题可以在文章下方留言。

github源码下载地址:https://github.com/geduo83/android-touch-event

上一篇下一篇

猜你喜欢

热点阅读