视频或者图片热区点击出气泡特效,点击气泡可进行页面跳转

371 阅读3分钟

项目背景

如何实现点击视频或者图片指定位置,弹出对话框或者气泡等特效?

具体实现

1、 和server同学商定功能开发所需要的接口以及字段信息
    具体包括热区坐标信息、响应事件类型、热区优先级
2、 实现原理
    在播放器View或者图片View上盖一层透明的蒙层View并在指定区域赋予touch事件的响应动作
3、 实现细节
    自定义View
package hotarea;

import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;

import java.util.List;


public class HotAreaMaskView extends View {
    private boolean mDownConfirmed;
    private List<HotAreaInfo> infos;
    private List<HotAreaModel> mHotAreaModels;
    private HotAreaHandler mHandler;
    private final View.OnTouchListener mTouchListener = new OnTouchListener() {
        @Override
        public boolean onTouch(View view, MotionEvent event) {
            if (event.getAction() == MotionEvent.ACTION_DOWN) {
                mDownConfirmed = true;
                return mDownConfirmed;
            } else if (event.getAction() == MotionEvent.ACTION_POINTER_UP) {
                return mDownConfirmed && onActionUp(event);//入口
            }
            return false;
        }
    };

    private boolean onActionUp(MotionEvent event) {
        return mHandler.handleRequest(this, event.getX(), event.getY());
    }

    @RequiresApi(api = Build.VERSION_CODES.N)
    public HotAreaMaskView(Context context) {
        super(context);
        init();
    }

    @RequiresApi(api = Build.VERSION_CODES.N)
    private void init() {
        mHotAreaModels =  HotAreaUtils.createModels(infos,this);
        mHandler = HotAreaUtils.createChain(mHotAreaModels);
        setOnTouchListener(mTouchListener);
    }

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

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


}
   创建责任链抽象处理者:
package hotarea;

import android.view.View;

public abstract class HotAreaHandler {
    private HotAreaHandler mNext;
    private HotAreaModel mHotAreaModel;

    public HotAreaHandler(HotAreaModel mHotAreaModel) {
        this.mHotAreaModel = mHotAreaModel;
    }

    public HotAreaHandler getNext() {
        return mNext;
    }

    public void setNext(HotAreaHandler next) {
        this.mNext = next;
    }

    public HotAreaModel getHotAreaModel() {
        return mHotAreaModel;
    }

    public void setHotAreaModel(HotAreaModel hotAreaModel) {
        this.mHotAreaModel = hotAreaModel;
    }

    public abstract boolean handleRequest(View anchorView, float x, float y);
}
创建责任链具体执行者
package hotarea;

import android.view.View;

public class HotAreaHandlerImpl extends HotAreaHandler {

    public HotAreaHandlerImpl(HotAreaModel mHotAreaModel) {
        super(mHotAreaModel);
    }

    @Override
    public boolean handleRequest(View anchorView, float x, float y) {
        HotAreaModel model = getHotAreaModel();
        if (model.getRegion().contains((int) x, (int) y)) {//消费事件
            HotAreaCallBack callBack = model.getCallBack();
            callBack.onClickEvent(anchorView, (int) x, (int) y);
            return true;
        } else {//传递事件
            return getNext().handleRequest(anchorView, x, y);
        }
    }
}
相关数据类
package hotarea;

import com.google.gson.annotations.SerializedName;

import java.io.Serializable;

public class HotAreaInfo implements Serializable {
    private static final long serialVersionUID = -317093208414453893L;
    @SerializedName("topLeft")
    public Location mTopLeft;
    @SerializedName("topRight")
    public Location mTopRight;
    @SerializedName("bottomLeft")
    public Location mBottomLeft;
    @SerializedName("bottomRight")
    public Location mBottomRight;
    @SerializedName("priority")
    public int mPriority;
    @SerializedName("taskId")
    public int mTaskId;

    public static class Location implements Serializable {
        private static final long serialVersionUID = -217993708417653897L;
        @SerializedName("x")
        public float mX;
        @SerializedName("y")
        public float mY;
    }
}
package hotarea;


import android.graphics.Region;

public class HotAreaModel {
    private Region mRegion;//热区坐标
    private int mPriority;//热区优先级
    private HotAreaCallBack mCallBack;//响应事件

    public HotAreaModel() {

    }

    public HotAreaModel(Region mRegion, int mPriority, HotAreaCallBack mCallBack) {
        this.mRegion = mRegion;
        this.mPriority = mPriority;
        this.mCallBack = mCallBack;
    }

    public Region getRegion() {
        return mRegion;
    }

    public void setRegion(Region mRegion) {
        this.mRegion = mRegion;
    }

    public int getPriority() {
        return mPriority;
    }

    public void setPriority(int mPriority) {
        this.mPriority = mPriority;
    }

    public HotAreaCallBack getCallBack() {
        return mCallBack;
    }

    public void setCallBack(HotAreaCallBack mCallBack) {
        this.mCallBack = mCallBack;
    }

}
回调函数接口
package hotarea;

import android.view.View;

public interface HotAreaCallBack {
    /**
     * @param anchorView 锚点View
     * @param anchorX 展示气泡、弹窗等位置x坐标
     * @param anchorY 展示气泡、弹窗等位置y坐标
     */
    void onClickEvent(View anchorView, int anchorX, int anchorY);
}
具体实现
package hotarea;

import android.view.View;
import android.widget.Toast;

public class BubbleCallBack implements HotAreaCallBack {
    @Override
    public void onClickEvent(View anchorView, int anchorX, int anchorY) {
        Toast.makeText(anchorView.getContext(), "你点击了我", Toast.LENGTH_SHORT).show();
    }
}
构建链式结构的工具类
package hotarea;

import android.graphics.Path;
import android.graphics.RectF;
import android.graphics.Region;
import android.os.Build;
import android.view.View;

import androidx.annotation.RequiresApi;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.PriorityQueue;

public class HotAreaUtils {

    @RequiresApi(api = Build.VERSION_CODES.N)
    public static HotAreaHandler createChain(List<HotAreaModel> models) {
        List<HotAreaHandler> handlers = new ArrayList<>();
        for (HotAreaModel model : models) {
            handlers.add(new HotAreaHandlerImpl(model));
        }
        //构建大顶堆
        PriorityQueue<HotAreaHandler> maxHeap = new PriorityQueue<>(new Comparator<HotAreaHandler>() {
            @Override
            public int compare(HotAreaHandler h1, HotAreaHandler h2) {
                return h2.getHotAreaModel().getPriority() - h1.getHotAreaModel().getPriority();
            }
        });
        HotAreaHandler head = null;
        if (maxHeap != null) {//串成链
            maxHeap.addAll(handlers);
            head = maxHeap.poll();
            HotAreaHandler cur = head;
            while (!maxHeap.isEmpty()) {
                cur.setNext(maxHeap.poll());
                cur = cur.getNext();
            }
        }
        return head;
    }

    public static List<HotAreaModel> createModels(List<HotAreaInfo> infos, View mAnchor) {
        List<HotAreaModel> models = new ArrayList<>();
        for (HotAreaInfo info : infos) {
            HotAreaModel model = new HotAreaModel();
            model.setPriority(info.mPriority);
            model.setCallBack(createCallBack(info.mTaskId,mAnchor));
            mAnchor.post(() -> {
                model.setRegion(comPuteRegion(info.mTopLeft, info.mTopRight, info.mBottomLeft, info.mBottomRight,mAnchor));
            });
            models.add(model);
        }
        return models;
    }
    
    //获取点击区域
    private static Region comPuteRegion(HotAreaInfo.Location mTopLeft, HotAreaInfo.Location mTopRight,
                                 HotAreaInfo.Location mBottomLeft, HotAreaInfo.Location mBottomRight
            , View anchor) {
        Path path = new Path();
        RectF rectF = new RectF();
        int anchorWidth = anchor.getWidth();
        int anchorHeight = anchor.getHeight();
        path.reset();
        path.moveTo(mTopLeft.mX * anchorWidth + anchor.getLeft(),
                mTopLeft.mY * anchorHeight + anchor.getTop());
        path.lineTo(mTopRight.mX * anchorWidth + anchor.getLeft(),
                mTopRight.mY * anchorHeight + anchor.getTop());
        path.lineTo(mBottomRight.mX * anchorWidth + anchor.getLeft(),
                mBottomRight.mY * anchorHeight + anchor.getTop());
        path.lineTo(mBottomLeft.mX * anchorWidth + anchor.getLeft(),
                mBottomLeft.mY * anchorHeight + anchor.getTop());
        path.computeBounds(rectF, true);
        Region clipRegion = new Region();
        clipRegion.set((int) rectF.left, (int) rectF.top, (int) rectF.right, (int) rectF.bottom);
        Region result = new Region();
        result.setPath(path, clipRegion);
        return result;
    }
    
    private static HotAreaCallBack createCallBack(int mTaskId) {
        switch (mTaskId){
            case 1:
                return new BubbleCallBack();
            default:
                return new DialogCallBack();
        }
    }
}

项目总结

由于,热区可能会有重叠区域,因此为每个区域制定不同的优先级,以及具体的响应事件,采用责任链设计模式,节点越靠前的优先级越高,越先拦截事件