觸摸事件分發機制

觸摸事件分發這是我之前寫的一篇事件分發的博客,這篇文章是在看了《Android開發藝術探索》後寫的,書中已經給出了【外部攔截法】和【內部攔截法】的模板代碼,我們可以直接拿來使用即可,書中也給出了看源碼後的重要結論,博客裏我寫了個demo,以打印log的方式驗證了一遍,幫助理解了一遍事件分發的流程。更加詳細內容可參考觸摸事件分發

本篇博客貼出【外部攔截法】和【內部攔截法】代碼,供大家上手使用,然後給出事件分發機制核心流程的僞代碼。(其實看源碼,不就是揣測源碼意圖,加以驗證的過程嗎,誰能把所有源碼都搞懂呢,關鍵也沒這個必要)

知識點

先上模板代碼

(來自《Android開發藝術探索》)


在這裏插入圖片描述
在這裏插入圖片描述
大神給出的模板代碼,很好理解,理解不了,記住就行了,用的時候,把模板複製進去,只是修改需要按自己需求變的那塊邏輯就好了。

基本知識點

  1. dispatchTouchEvent 事件分發,這個方法是入口,如果事件能傳遞到給View,則一定會被調用的
    在這裏插入圖片描述
  2. onInterceptTouchEvent 事件攔截,ViewGroup獨有的,如果事件能傳遞到該View,也不一定每次事件都會被調用到,

如果當前View攔截了事件,在同一個事件序列中,沒必要再詢問當前View的onInterceptTouchEvent,如果是當前View的子View攔截了事件,那麼onInterceptTouchEvent會一直被調用,他在時刻等待子View想要交出事件處理權的那一剎那(requestDisallowInterceptTouchEvent())。

注意當前ViewGroup一旦攔截,一次事件序列中就再也不會調用onInterceptTouchEvent了,所以子View再也不會得到事件處理的機會了
爲了解決這個問題,就引出了《嵌套滑動》這個新的事物,見下篇博客

在這裏插入圖片描述
3. onTouchEvent 事件消費,如果返回true,表示消費事件,並導致該View的dispatchTouchEvent返回true
4. 三者關係是通過dispatchTouchEvent方法組織起來的

ViewGroup的dispatchTouchEvent方法
在這裏插入圖片描述
View的dispatchTouchEvent方法

    public boolean dispatchTouchEvent(MotionEvent ev) {
		//如果控件可用&&設置了mOnTouchListener,
		//&&onTouch方法返回true,該方法直接返回true,
		//不去調用onTouchEvent方法
        if (mOnTouchListener != null && enable 
        && mOnTouchListener.onTouch(ev)) {
            return true;
        } else {
            return onTouchEvent(ev);
        }
    }

應用

做一個下拉刷新的自定義View

只是最基本的實現,沒有做封裝,沒有做下拉刷新、刷新中等接口回調,最核心原理就是前面說到的,view的滑動+平滑移動+事件分發,到現在這三件套在一起就可以做很多自定義View的效果了

其實onMeasure和onLayout只要你不是直接繼承自View或ViewGroup,一般都是不需要重寫的,我們很懶,也不想處理,所以繼承一個現成的View即可,迫不得已才重寫onMeasure和onLayout

說一個小問題:上文中的SwitchView開關,我們是在LinearLayout裏添加了一個ImageView,如果,你添加的是ImageButton,會發現拖動滑塊後,不動了,這是本節觸摸事件分發的知識點,因爲ImageButton天生可點擊,而LinearLayout默認是不攔截事件的,所以手指觸摸ImageButton時候,會走ImageButton的onTouchEvent方法,LinearLayout裏的onTouchEvent就不生效了,所以不會拖動滑塊,所以:

  1. 你可以使用ImageView,因爲LinearLayout裏沒有子View能消費事件,會走LinearLayout的onTouchEvent方法,觸摸事件分發博客裏我打印了log就驗證了這個結論
  2. 或者使用ImageButton後,把他的clickable=false,
  3. 或者在重寫onInterceptTouchEvent方法,讓LinearLayout永遠攔截事件

上面這點,不注意會經常遇到類似的問題,爲啥拖不動呀,看看你拖的是不是Button,ImageButton這種天生可以消費事件的view

在這裏插入圖片描述

直接上代碼

package com.view.custom.dosometest.view;

import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Color;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.ScrollView;

/**
 * 描述當前版本功能
 *
 * @Project: DoSomeTest
 * @author: cjx
 * @date: 2019-12-01 10:06  星期日
 */
public class RefreshView extends LinearLayout {


    private ScrollView mScrollView;
    private View mHeader;
    private int mHeaderHeight;
    private MarginLayoutParams mLp;

    public RefreshView(Context context) {
        super(context);
        init(context);
    }

    public RefreshView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public RefreshView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }


    private void init(Context context) {
        setBackgroundColor(Color.GRAY);

        post(new Runnable() {
            @Override
            public void run() {
                initView();// 因爲涉及到獲取控件寬高的問題,所以寫到post裏
            }
        });

    }

    private void initView() {

        if (getChildCount() > 2) {

            // 給刷新頭設置負高度的margin,讓他隱藏
            mHeader = getChildAt(0);
            mHeaderHeight = mHeader.getMeasuredHeight();
            mLp = (MarginLayoutParams) mHeader.getLayoutParams();
            mLp.topMargin = -mHeaderHeight;
            mHeader.setLayoutParams(mLp);

            // 得到第二個view,scrollView
            View child1 = getChildAt(1);
            if (child1 instanceof ScrollView) {
                mScrollView = (ScrollView) child1;
            }

        }
    }


    float mLastY;

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {

        boolean intercept = false;
        int x = (int) ev.getX();
        int y = (int) ev.getY();

        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                intercept = false;
                break;

            case MotionEvent.ACTION_MOVE:
                int deltaY = (int) (y - mLastY);
                if (needIntercept(deltaY)) {//外部攔截的模板代碼,只要重寫needIntercept方法邏輯就行
                     //注意當前ViewGroup一旦攔截,一次事件序列中就再也不會調用onInterceptTouchEvent了,
                    // 所以子View再也不會得到事件處理的機會了
                    // 爲了解決這個問題,就引出了《嵌套滑動》這個新的事物,見下文
                    intercept = true;
                } else {
                    intercept = false;
                }

                break;

            case MotionEvent.ACTION_UP:

                intercept = false;

                break;
            default:
                break;
        }

        mLastY = y;
        return intercept;
    }

    private boolean needIntercept(int deltaInteceptY) {
        // mScrollView已經下拉到最頂部&&你還在下來,那麼父容器攔截
        if (!mScrollView.canScrollVertically(-1) && deltaInteceptY > 0) {
            Log.e("ccc", "不能再往下拉了&&你還在往下拉,父佈局攔截,開始拉出刷新頭");
            return true;
        }
        return false;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        float y = event.getY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:

                break;

            case MotionEvent.ACTION_MOVE:
                float deltaY = y - mLastY;

                // 防止刷新頭被無限制下拉,限定個高度

                if (mLp.topMargin + deltaY > mHeaderHeight) {
                    deltaY = mHeaderHeight - mLp.topMargin;
                }
                // 動態改變刷新頭的topMargin
                mLp.topMargin += (int) deltaY;
                Log.e("ccc", "y:" + y + "mLastY:" + mLastY + "deltaY:" + deltaY + "mLp.topMargin:" + mLp.topMargin);
                mHeader.setLayoutParams(mLp);
                break;


            case MotionEvent.ACTION_UP:
                //鬆手後,看位置,如果過半,刷新頭全部顯示,沒過半,刷新頭全部隱藏
                if (mLp.topMargin > -mHeaderHeight / 2) {
                    smoothChangeTopMargin(mLp.topMargin, 0);
                } else {
                    smoothChangeTopMargin(mLp.topMargin, -mHeaderHeight);
                }

                break;
        }

        mLastY = y;

        return true;
    }

    /**
     * 使用屬性動畫平滑地過度topMargin
     *
     * @param start
     * @param end
     */
    private void smoothChangeTopMargin(int start, int end) {
        ValueAnimator valueAnimator = ValueAnimator.ofInt(start, end);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mLp.topMargin = (int) animation.getAnimatedValue();
                mHeader.setLayoutParams(mLp);

            }
        });
        valueAnimator.setDuration(300);
        valueAnimator.start();

    }
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章