1. 程式人生 > >Android觸控事件傳遞機制實踐——可拖動、大小切換的SizeSwitchView

Android觸控事件傳遞機制實踐——可拖動、大小切換的SizeSwitchView

前言

  對於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都是屬於我做的一個專案,目前專案處於總結階段,在實現功能中學到很多東西,做完過段時間將這些東西整理一遍,又改進了一遍,自己理解得又更深了。

相關推薦

Android觸控事件傳遞機制實踐——大小切換SizeSwitchView

前言   對於Android的觸控事件傳遞機制,網上有很多講解,有結合原始碼的,有圖文結合的,其中不乏一些講解清晰明瞭的文章,看完之後都能有所收穫。然而,理論終究是要應用在實踐上的,最近工作的時候,做出了一個可拖動,可以大小切換,大形態巢狀著ViewG

Android 觸控事件傳遞機制

android系統中的每個View的子類都具有下面三個和TouchEvent處理密切相關的方法:1)public boolean dispatchTouchEvent(MotionEvent ev)  這個方法用來分發TouchEvent2)public boolean onInterceptTouchEve

android觸控事件傳遞機制

看到一篇文章,將事件傳遞機制講得很透徹 【場景】 在cy的Home頁,每個點選塊都是自定義view來做的,組要用自定義自合view,如果需要將其中點選image和text分別有不同的反應,則應該去設定處理事件? 【詳情】 Android系統中的每個View的子類都具有下面三

初識Android觸控事件傳遞機制

前言 今天總結的一個知識點是Andorid中View事件傳遞機制,也是核心知識點,相信很多開發者在面對這個問題時候會覺得困惑,另外,View的另外一個難題滑動衝突,比如在ScrollView中巢狀ListView,都是上下滑動,這該如何解決呢,它解決的依據就是View事件的傳遞機制,所以開發者需要對View的

Android觸控事件傳遞機制簡要分析

Android開發中經常會遇到多個View、ViewGroup巢狀的情況, 此時就可能遇到滑動衝突的問題。 為了這種問題,就必須對View的事件傳遞機制有一定的瞭解。 本篇部落格就以一些簡單的例子, 來看看Activity、View、ViewGroup三

android 觸控事件傳遞機制與筆記

一、筆記連結1. android 觸控事件傳遞機制2. android OnTouchListener,onTouchEvent,onClickListener執行順序 二、簡記1. android 觸控事件傳遞機制1.1Touch事件分發中只有兩個主角:ViewGroup和

Android觸控事件傳遞機制學習筆記

1、Android 觸控事件傳遞機制 http://blog.csdn.net/awangyunke/article/details/22047987 2、Android-onInterceptTouchEvent()和onTouchEvent()總結 h

Android觸控事件傳遞機制,這一篇就夠了

整個觸控事件牽涉到的是,Activity,View,ViewGroup三者的傳遞機制。 這個觸控事件就是從外層往內層一層層的傳遞。 整個傳遞機制,分為3個步驟:分發,攔截,和消費。 1. 觸控事件的型別 事件型別是MotionEvent類:看下最新的sdk29的原始碼,一堆的Action,我們常用的其實就3個

Android ViewGroup 觸控事件傳遞機制

引言 上一篇部落格我們學習了Android View 觸控事件傳遞機制,不瞭解的同學可以檢視Android View 觸控事件傳遞機制。今天繼續學習Android觸控事件傳遞機制,這篇部落格將和大家一起探討ViewGroup的觸控事件傳遞機制。 示例

android view觸控事件傳遞機制測試

沒有其它人為干預時: 詳細測試可以參考Github中的程式,地址: https://github.com/yifan42421/PhoneToPhoneScreen/tree/master/testmotionevent

Android Touch事件傳遞機制全面解析(從WMS到View樹)

了解 分支 per seve from 這一 params 雞湯 dcl 轉眼間近一年沒更新博客了,工作一忙起來。非常難有時間來寫博客了,因為如今也在從事Andro

Android View事件傳遞機制

view事件傳遞機制,在很多面試中會問道,我曾經也被問道,卻沒有回答上來。 今天我在這裡寫了一個demo去理解這個view的事件傳遞機制。 首先這個view包括兩種,viewGroup和普通view。viewGroup就是裡面還可以包含子控制元件的那種,如Linear

android 觸控事件傳遞(一)

android 觸控事件傳遞 1、主要相關程式碼路徑 基於展訊7.0原始碼 native frameworks/base/services/core/jni/com_android_server_input_InputManagerService.cpp frameworks

Android TouchEvent事件傳遞機制

public class MyActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState)

安卓觸控事件傳遞機制

概述 安卓的觸控事件傳遞大體上是檢視收到事件後進行決定是否要攔截,不攔截可以繼續向內傳遞,攔截了不消費也可以回傳給上層檢視。 事件型別主要有 ACTION_DOWN(按下) ACTION_MOVE(移動) ACTION_UP(擡起) ACTION_C

Android 觸控事件傳遞流程解析

android中的Touch事件都是從ACTION_DOWN開始的: 單手指操作:ACTION_DOWN---ACTION_MOVE----ACTION_UP 多手指操作:ACTION_DOWN---ACTION_POINTER_DOWN---ACTION_MOV

Android onTouch事件傳遞機制

Android onTouch事件介紹: Android的觸控事件:onClick, onScroll, onFling等等,都是由許多個Touch組成的。其中Touch的第一個狀態肯定是ACTION_DOWN, 表示按下了螢幕。之後,touch將會有後續事件,可能是: ACT

觸控[0] 觸控事件傳遞機制

【參考連結】 開發藝術探索 1. 涉及到的類和方法主要有 ViewGroup的dispatchTouchEvent()、onInterceptTouchEvent()、onTouchEvent() View的dispatchTouchEvent()、onInterceptTouchEvent()

android觸發事件傳遞機制

一 事件傳遞的三個階段1 分發(Dispatch):事件的分發對應著dispatchTouchEvent方法,在Android系統中,所有的觸控事件的分發都是由改方法分發。 public boole

Android Touch事件傳遞機制解析

// 表示事件是否攔截, 返回false表示不攔截 @Override public boolean onInterceptTouchEvent(MotionEvent arg0) { return false; } /** * 重寫onTouchEven