项目背景
如何实现点击视频或者图片指定位置,弹出对话框或者气泡等特效?
具体实现
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();
}
}
}
项目总结
由于,热区可能会有重叠区域,因此为每个区域制定不同的优先级,以及具体的响应事件,采用责任链设计模式,节点越靠前的优先级越高,越先拦截事件