Android触摸事件传递机制实践——可拖动、大小切换的SizeSwitchView

出处
炎之铠邮箱:yanzhikai_yjk@qq.com
博客地址:http://blog.csdn.net/totond
本文原创,转载请注明本出处!
本项目GitHub地址:https://github.com/totond/SizeSwitchViewDemo
欢迎 Star or Fork!

前言

对于Android的触摸事件传递机制,网上有很多讲解,有结合源码的,有图文结合的,其中不乏一些讲解清晰明了的文章,看完之后都能有所收获。然而,理论终究是要应用在实践上的,最近工作的时候,做出了一个可拖动,可以大小切换,大形态嵌套着ViewGroup的SizeSwitchView,其中涉及了比较复杂的触摸事件处理,实践完之后我感觉对事件传递机制熟悉了很多,在这里做出记录,并分享给大家。

介绍

这个需求是做一个方向键,然后这个方向键有5个按键,整体比较大,可能会挡着其他的内容,然后就要求支持拖动和大小切换:



  由于SizeSwitchView的可扩展性不高(大形态的ViewGroup可以是多种多样的),要修改的话改动比较大,功能也不是很全面(只支持父ViewGroup为RelativeLayout),所以我把它定位为demo的方式分享出去,就不把它封装并上传到Jcenter了。

实现

1.结构分析

SizeSwitchView本质上是一个ViewGroup,里面包含两个互斥居中的控件(一个显示,另外一个就不显示),一个是小形态的控件,就只是一个ImageView,另一个是大形态的控件,是一个ViewGroup,里面包含着5个ImageView,也就是下面的BigDirectionKey:


<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent"
    >

    <yanzhikai.sizeswitchview.BigDirectionKey
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_centerInParent="true"
        android:id="@+id/big_dk"/>

    <ImageView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_centerInParent="true"
        android:id="@+id/small_dk"
        android:clickable="true"
        android:scaleType="fitCenter"
        android:src="@drawable/direction"
        android:background="@drawable/background_button"
        />

</RelativeLayout>

2.状态切换

关于大小形态的互相切换,主要注意的有两个点:宽高和位置的变化、动画的处理。

宽高和位置的变化

由于大形态和小形态的宽高不同,所以SizeSwitchView就要根据形态来变化大小,这里使用修改LayoutParams的方式来修改大小和位置:


    public void setMode(boolean isSmallMode){
        this.isSmallMode = isSmallMode;
        Log.d(TAG, "setMode: ");
        //设置大小形态的宽高和位置
        if (isSmallMode){
            LayoutParams smallParams = (LayoutParams) getLayoutParams();
            smallParams.width = mSmallWidth;
            smallParams.height = mSmallHeight;
            smallParams.leftMargin += (getWidth() - mSmallWidth)/2;
            smallParams.bottomMargin += (getHeight() - mSmallHeight)/2;
            setLayoutParams(smallParams);
        }else {
            LayoutParams bigParams = (LayoutParams) getLayoutParams();
            bigParams.width = mBigWidth;
            bigParams.height = mBigHeight;
            bigParams.leftMargin -= (mBigWidth - mSmallWidth)/2;
            bigParams.bottomMargin -= (mBigHeight - mSmallHeight)/2;
            setLayoutParams(bigParams);
//            requestLayout();
        }
        setKeysVisibility();
        isDraggable = true;
    }

通过getLayoutParams()获取LayoutParams来改动SizeSwitchView的宽高和位置,然后根据大小的宽高变化量来调整位置,使大小形态的中心点保持在同一个点上,这样就让人看起来是在中心点缩放变化一样。
  这个LayoutParams的类型取决于父ViewGroup,所以这里就限定了父ViewGroup是要使用RelativeLayout(使用到Margin属性)。

动画的处理

这个切换的动画实际上就是一个旋转缩小透明度减少的动画,加上反向旋转放大透明度增加的动画,这两个组合起来(没错,就是在模仿宇智波带土的神威)。。。

旋转缩小透明度减少动画shrink.xml:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="400">

    <rotate
        android:fromDegrees="0"
        android:toDegrees="360"
        android:pivotX="50%"
        android:pivotY="50%"
        />

    <scale
        android:fromXScale="1"
        android:fromYScale="1"
        android:pivotX="50%"
        android:pivotY="50%"
        android:toXScale="0"
        android:toYScale="0" />

    <alpha
        android:fromAlpha="1"
        android:toAlpha="0.3"
        />
</set>

反向旋转放大透明度增加动画:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="400">
    <rotate
        android:fromDegrees="0"
        android:toDegrees="-360"
        android:pivotX="50%"
        android:pivotY="50%"
        />

    <scale
        android:fromXScale="0"
        android:fromYScale="0"
        android:pivotX="50%"
        android:pivotY="50%"
        android:toXScale="1"
        android:toYScale="1" />

    <alpha
        android:fromAlpha="0.3"
        android:toAlpha="1"
        />
</set>

下面是动画的设置:

    //初始化动画
    private void initAnim(){
        smallShrinkAnimation = AnimationUtils.loadAnimation(mContext,R.anim.shrink);
        bigLargenAnimation = AnimationUtils.loadAnimation(mContext,R.anim.largen);
        smallShrinkAnimation.setAnimationListener(new Animation.AnimationListener() {
            @Override
            public void onAnimationStart(Animation animation) {
                setKeysClickable(false);
                isDraggable = false;
            }

            @Override
            public void onAnimationEnd(Animation animation) {
                setMode(false);
                startAnimation(bigLargenAnimation);
            }

            @Override
            public void onAnimationRepeat(Animation animation) {

            }
        });
        bigLargenAnimation.setAnimationListener(new Animation.AnimationListener() {
            @Override
            public void onAnimationStart(Animation animation) {

            }

            @Override
            public void onAnimationEnd(Animation animation) {
                setKeysClickable(true);
                isDraggable = true;
                setSmallKeyClick();
                checkBoundary();
            }

            @Override
            public void onAnimationRepeat(Animation animation) {

            }
        });

        bigShrinkAnimation = AnimationUtils.loadAnimation(mContext,R.anim.shrink);
        smallLargenAnimation = AnimationUtils.loadAnimation(mContext,R.anim.largen);
        bigShrinkAnimation.setAnimationListener(new Animation.AnimationListener() {
            @Override
            public void onAnimationStart(Animation animation) {
                setKeysClickable(false);
                isDraggable = false;
            }

            @Override
            public void onAnimationEnd(Animation animation) {
                setMode(true);
                startAnimation(smallLargenAnimation);
                setSmallKeyClick();
            }

            @Override
            public void onAnimationRepeat(Animation animation) {

            }
        });
        smallLargenAnimation.setAnimationListener(new Animation.AnimationListener() {
            @Override
            public void onAnimationStart(Animation animation) {

            }

            @Override
            public void onAnimationEnd(Animation animation) {
                setKeysClickable(true);
                isDraggable = true;
                checkBoundary();
            }

            @Override
            public void onAnimationRepeat(Animation animation) {

            }
        });

    }

上面主要是4个动画(变大变小各两个)监听器的实现,思路就是:动画开始的时候不能点击,不能拖动;等到缩小动画完毕之后瞬间切换大小状态,再进行放大动画;动画都结束后就是恢复可以点击和可拖动状态,还有进行一次边界检测,看变换后的View是否越出了父View的边界,越出了的话就移动越出的位移,这个checkBoundary()方法的实现在后面讲。

3.触摸事件处理

由于SizeSwitchView需要支持拖动,需要实现拦截触摸事件,但是它也是一个父ViewGroup,还需要把点击事件(DOWN和UP事件)传递到子ViewGroup里面的View。了解过Android触摸事件传递机制的都知道,如果父View拦截了DOWN事件之后,后面的事件就不会传递到它的子View了。
  所以我在SizeSwitchView的拦截思路是这样的:


  其实也不复杂,就是让父ViewGroup只有在拖动达到一定距离的时候才拦截MOVE事件,DOWN事件就传递给子View处理,但是这样有一个问题:在父ViewGroupOnTouchEvent()方法是没有DOWN事件的,不能在这里获取DOWN事件的坐标,MotionEvent.getX()MotionEvent.getY()方法获取的只是点击事件相对于当前View的零点的位置,而不是在屏幕上的XY坐标,所以光靠MOVE事件的位置数据是无法准确计算SizeSwitchView的移动的(每次都要平移到View的零点才能正常拖动),如下面的效果:

  所以就直接在onInterceptTouchEvent()里面获取DOWN事件的坐标,用全局变量保存着,用MOVE事件的坐标减去它,才能正确计算出它的位移,从而进行准确移动:

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                //记录DOWN事件的点击位置,因为不拦截DOWN事件,移动的时候需要这个起点坐标来计算距离。
                    lastX = ev.getX();
                    lastY = ev.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                //拖动距离超过最小拖动量才会被拖动
                if (Math.abs(ev.getX() - lastX) > clickOffset && Math.abs(ev.getY() - lastY )> clickOffset){
                    if (canDrag && isDraggable && ev.getAction() == MotionEvent.ACTION_MOVE){
                        return true;
                    }
                }
                break;
        }
        return false;
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_UP){
            //抬手就进行一次边界检测
            checkBoundary();
        }else if (event.getAction() == MotionEvent.ACTION_MOVE){
            //进行移动操作
            if (canDrag && isDraggable) {
                int offX = (int) (event.getX() - lastX);
                int offY = (int) (event.getY() - lastY);
                LayoutParams params =
                        (LayoutParams) getLayoutParams();
                params.leftMargin = params.leftMargin + offX;
                params.rightMargin = params.rightMargin - offX;
                params.topMargin += offY;
                params.bottomMargin -= offY;
                setLayoutParams(params);

                return true;
            }
            return false;
        }
        return super.onTouchEvent(event);
    }

然后就是边界检测了,具体思路很简单,就是计算SizeSwitchView当前位置是不是超出它的父ViewGroup的范围,如果超过的话就要移回来:

  //检测View是否跑出边界,如果是则移回来
    private void checkBoundary(){
        Log.d("checkBoundary", "checkBoundary: ");
        ViewGroup parent = (ViewGroup) getParent();
        boolean isOut = false;
        int moveX = 0;
        int moveY = 0;
        if (getLeft() < 0){
            moveX = getLeft();
            isOut = true;
        }
        if (getTop() < 0){
            moveY = getTop();
            isOut = true;
        }
        if (getRight() > parent.getWidth()){
            moveX = (getRight() - parent.getWidth());
            isOut = true;
        }
        if (getBottom() > parent.getHeight()){
            moveY = (getBottom() - parent.getHeight());
            isOut = true;
        }
        //有出界才进行LayoutParams的设置,节省性能
        if (isOut) {
            LayoutParams params =
                    (LayoutParams) getLayoutParams();
            params.setMargins(params.leftMargin - moveX,
                    params.topMargin - moveY,
                    params.rightMargin + moveX,
                    params.bottomMargin + moveY);
            setLayoutParams(params);
        }
    }

这样的话拖动的功能就处理好了,点击事件也传进去子ViewGroup了,但是怎样把子ViewGroup的点击实现实现接口传出来呢?这就和我上一篇的YMenuView的设计差不多了:由于在大形态的BigDirectionKey有5个子View,所以就自己实现一个带index索引的接口OnKeyClickListener去实现点击,外层调用的话只需要实现这个OnKeyClickListener然后传入进来就行了。

    private OnKeyClickListener mOnKeyClickListener;

    //初始化
    private void initKeys(){
        okKey = new ImageView(mContext);
        upKey = new ImageView(mContext);
        downKey = new ImageView(mContext);
        leftKey = new ImageView(mContext);
        rightKey = new ImageView(mContext);

        okKey.setImageResource(R.drawable.background_ok);
        upKey.setImageResource(R.drawable.background_up);
        downKey.setImageResource(R.drawable.background_down);
        leftKey.setImageResource(R.drawable.background_left);
        rightKey.setImageResource(R.drawable.background_right);

        okKey.setClickable(true);
        upKey.setClickable(true);
        downKey.setClickable(true);
        leftKey.setClickable(true);
        rightKey.setClickable(true);

        okKey.setScaleType(ImageView.ScaleType.FIT_XY);
        upKey.setScaleType(ImageView.ScaleType.FIT_XY);
        downKey.setScaleType(ImageView.ScaleType.FIT_XY);
        leftKey.setScaleType(ImageView.ScaleType.FIT_XY);
        rightKey.setScaleType(ImageView.ScaleType.FIT_XY);

        okKey.setId(generateViewId());
        upKey.setId(generateViewId());
        downKey.setId(generateViewId());
        leftKey.setId(generateViewId());
        rightKey.setId(generateViewId());

        addView(okKey);
        addView(upKey);
        addView(downKey);
        addView(leftKey);
        addView(rightKey);

        setBackgroundResource(R.drawable.button_shape);

        //设置点击监听器
        for (int i = 0; i < getChildCount(); i++){
            getChildAt(i).setOnClickListener(new MyOnClickListener(i));
        }
    }

    //重写一个带索引的OnClickListener,索引用于标识5个子View
    private class MyOnClickListener implements OnClickListener {
        private int index;

        public MyOnClickListener(int index) {
            this.index = index;
        }

        @Override
        public void onClick(View v) {
            if (mOnKeyClickListener != null) {
                mOnKeyClickListener.onKeyClick(index);
            }
        }
    }

    //暴露给外部的点击接口
    public interface OnKeyClickListener{
        public void onKeyClick(int index);
    }

这样子,在Activity里面只需要实现BigDirectionKey.OnKeyClickListener接口然后重写里面的方法就可以处理点击事件了了:

    @Override
    public void onKeyClick(int index) {
        switch (index){
            case 0:
                mSizeSwitchView.toSmallMode();
                break;
            case 1:
                makeToast("1");
                break;
            case 2:
                makeToast("2");
                break;
            case 3:
                makeToast("3");
                break;
            case 4:
                makeToast("4");
                break;
        }
    }

总结

这样就介绍完了SizeSwitchView大体实现思路了,总体来说就是点击、切换、拖动。其实难点并不多,但是实现的时间还是不短的,就是实现的时候遇到搞不定的功能会各种各种的尝试,最后才得到解决方法,而且思路还是很乱,经过总结原理之后发现很多可以改善的地方,小改的地方我都优化了,可以大改的地方,如换一种移动的方式(使用layout()方法的方式,可以大大减少Measure的次数,让拖动更平滑),这个改动比较大,留到后面。

后话

这个SizeSwitchView和上一篇的YMenuView都是属于我做的一个项目,目前项目处于总结阶段,在实现功能中学到很多东西,做完过段时间将这些东西整理一遍,又改进了一遍,自己理解得又更深了。

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

推荐阅读更多精彩内容