Android自定義-點(diǎn)擊事件檢測(cè)View

前言

  • 需求:2個(gè)View并不在同一層父布局布局中,點(diǎn)擊上層的檢測(cè)View,將點(diǎn)擊事件轉(zhuǎn)發(fā)給背后的兄弟View
  • 原理:基于自定義事件處理點(diǎn)擊的x、y坐標(biāo),判斷是否在背后的兄弟View的范圍內(nèi)

Java代碼

import android.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewTreeObserver;

import androidx.annotation.Nullable;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * 觸摸檢測(cè)View,可以將自身的點(diǎn)擊事件,轉(zhuǎn)發(fā)給指定的目標(biāo)View
 */
public class TouchDetectorView extends View {
    /**
     * 觸摸滑動(dòng)的最小距離,超過(guò)這個(gè)距離則認(rèn)為是拖動(dòng),而不是點(diǎn)擊
     */
    private int touchSlop;

    /**
     * 目標(biāo)View列表
     */
    private final List<TargetWrapper> targets = new ArrayList<>();

    /**
     * 點(diǎn)擊監(jiān)聽器
     */
    private OnTargetTouchListener touchListener;

    /**
     * Down事件的X坐標(biāo)
     */
    private float downX;

    /**
     * Up事件的Y坐標(biāo)
     */
    private float downY;

    public TouchDetectorView(Context context) {
        this(context, null);
    }

    public TouchDetectorView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public TouchDetectorView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    private void init(Context context) {
        touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    }

    public interface OnTargetTouchListener {
        void onTargetTouched(View targetView);
    }

    /**
     * 設(shè)置觸摸監(jiān)聽器
     */
    public void setOnTargetTouchListener(OnTargetTouchListener listener) {
        this.touchListener = listener;
    }

    /**
     * 動(dòng)態(tài)添加監(jiān)控目標(biāo)View
     */
    public void addTargetView(View targetView) {
        if (targetView == null) return;

        // 避免重復(fù)添加
        for (TargetWrapper wrapper : targets) {
            if (wrapper.getView() == targetView) {
                return;
            }
        }

        TargetWrapper wrapper = new TargetWrapper(targetView);
        targets.add(wrapper);
        registerTargetViewLayoutListener(targetView);
    }

    /**
     * 移除對(duì)目標(biāo)View的監(jiān)控
     */
    public void removeTargetView(View targetView) {
        Iterator<TargetWrapper> iterator = targets.iterator();
        while (iterator.hasNext()) {
            TargetWrapper wrapper = iterator.next();
            if (wrapper.getView() == targetView) {
                unregisterTargetViewLayoutListener(targetView);
                iterator.remove();
                break;
            }
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            downX = event.getRawX();
            downY = event.getRawY();
        } else if (event.getAction() == MotionEvent.ACTION_UP) {
            int deltaX = (int) (event.getRawX() - downX);
            int deltaY = (int) (event.getRawY() - downY);
            // 如果移動(dòng)距離超過(guò)touchSlop,則認(rèn)為是滑動(dòng),不是點(diǎn)擊
            boolean isClick = (Math.abs(deltaX) <= touchSlop) && (Math.abs(deltaY) <= touchSlop);
            // 如果是點(diǎn)擊,則開始判斷點(diǎn)擊范圍,是否在目標(biāo)View的矩形區(qū)域內(nèi)
            if (isClick) {
                // 倒序遍歷(后添加的View優(yōu)先檢測(cè))
                for (int i = targets.size() - 1; i >= 0; i--) {
                    TargetWrapper wrapper = targets.get(i);
                    if (wrapper.rect == null) {
                        continue;
                    }
                    if (wrapper.rect.contains((int) downX, (int) downY)) {
                        View target = wrapper.getView();
                        if (target != null && touchListener != null) {
                            touchListener.onTargetTouched(target);
                            return true;
                        }
                    }
                }
            }
        }
        return super.onTouchEvent(event);
    }

    /**
     * 注冊(cè),目標(biāo)View的布局監(jiān)聽器,以便在布局變化時(shí)更新矩形區(qū)域
     */
    private void registerTargetViewLayoutListener(View targetView) {
        LayoutTracker layoutTracker = new LayoutTracker(targetView);
        ViewTreeObserver observer = targetView.getViewTreeObserver();
        observer.addOnGlobalLayoutListener(layoutTracker);
        // 保存監(jiān)聽器引用
        targetView.setTag(layoutTracker);
    }

    /**
     * 取消注冊(cè),目標(biāo)View的布局監(jiān)聽器
     */
    private void unregisterTargetViewLayoutListener(View targetView) {
        Object tag = targetView.getTag();
        if (tag instanceof LayoutTracker) {
            LayoutTracker layoutTracker = (LayoutTracker) tag;
            ViewTreeObserver observer = targetView.getViewTreeObserver();
            observer.removeOnGlobalLayoutListener(layoutTracker);
        }
    }

    /**
     * 保存目標(biāo)View的引用和它的矩形區(qū)域
     */
    private static class TargetWrapper {
        /**
         * View弱引用,避免內(nèi)存泄漏
         */
        private final WeakReference<View> viewRef;

        /**
         * View的矩形區(qū)域
         */
        private Rect rect;

        TargetWrapper(View view) {
            this.viewRef = new WeakReference<>(view);
            updateRect();
        }

        View getView() {
            return viewRef.get();
        }

        /**
         * 更新View的矩形區(qū)域
         */
        void updateRect() {
            View view = viewRef.get();
            if (view == null || view.getVisibility() != View.VISIBLE) {
                rect = null;
                return;
            }

            int[] location = new int[2];
            view.getLocationOnScreen(location);
            rect = new Rect(
                    location[0],
                    location[1],
                    location[0] + view.getWidth(),
                    location[1] + view.getHeight()
            );
        }
    }

    /**
     * 監(jiān)聽目標(biāo)View的布局變化,更新View的矩形區(qū)域
     */
    private class LayoutTracker implements ViewTreeObserver.OnGlobalLayoutListener {
        private final WeakReference<View> trackedView;

        LayoutTracker(View view) {
            this.trackedView = new WeakReference<>(view);
        }

        @Override
        public void onGlobalLayout() {
            View view = trackedView.get();
            if (view != null) {
                updateViewRect(view);
            }
        }

        private void updateViewRect(View targetView) {
            for (TargetWrapper wrapper : targets) {
                if (wrapper.getView() == targetView) {
                    wrapper.updateRect();
                    break;
                }
            }
        }
    }
}

XML布局

<com.xxx.xxx.widget.TouchDetectorView
    android:id="@+id/touch_detector_view"
    android:layout_width="match_parent"
    android:layout_height="354"
    android:clickable="true" />

簡(jiǎn)單使用

  • 添加需要檢測(cè)的View,以及監(jiān)聽回調(diào),轉(zhuǎn)發(fā)點(diǎn)擊事件給檢測(cè)View
// 因?yàn)槭堑剐虮闅v,所以底層的View,先添加,上層的View后添加
binding.touchDetectorView.addTargetView(binding.magicMvVideo);
binding.touchDetectorView.addTargetView(binding.startCreateMvLayout);
binding.touchDetectorView.addTargetView(binding.oneKeyCreateMvLayout);
binding.touchDetectorView.addTargetView(binding.albumMvBtnLayout);
binding.touchDetectorView.addTargetView(binding.templateMvBtnLayout);
binding.touchDetectorView.setOnTargetTouchListener(new TouchDetectorView.OnTargetTouchListener() {
    @Override
    public void onTargetTouched(View targetView) {
    // 轉(zhuǎn)發(fā)點(diǎn)擊事件
    targetView.performClick();
    }
});
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容