背景
之前写过一篇文章uniapp地图电子围栏(多边形)绘制和编辑,这篇文章中使用高德地图绘制电子围栏,那个效果还比较有意思,有兴趣的可以去考古。恰好最新项目中需要用原生Android实现类似一个效果。今天就将这个效果给大家分享,老规矩,先上图镇楼,分别对应编辑状态和完成状态。
思路
老Androider一看肯定知道这玩意肯定得上自定义view,然后重载onDraw方法,然后根据拦截手指触摸事件之后进行重新绘制。
那流程就很清楚了:
1、自定义一个view。
2、拦截onTouchEvent事件并实现逻辑。
3、重载onDraw方法,实现自定义图形绘制。
需求分析
1、我们要求可以绘制任意多(这里任意多个是狭义上的,后面说的任意多个点也一样,由于内存限制肯定无法无限制增加)个多边形,并且每个多边形可以包含任意多个点。
2、编辑状态下每个多边形包含绿色基准点和蓝色中间点。第一次绘制的时候记录手指触摸到抬起两个点为基准点,中间点自动生成。
3、蓝色点中间点被触摸移动后,手指抬起时变成基准点然后重新计算生成中间点。
4、基准点被触摸移动后更新该点位置,手指抬起时然后重新计算中间点位置,注意这里不新生成中间点。
5、只有两个基准点时,绘制直线,其他情况绘制path并闭合。
6、只有两个基准点时,触摸基准点,中间点会自动转换成基准点。
实现
根据以上分析我们可以先定义辅助类和属性,首先肯定要有一个Point类,用来记录X,Y坐标,还有是否是中间点。
class Point {
private float x;
private float y;
private boolean isMid = false;
public Point(float x, float y){
this.x = x;
this.y = y;
}
public Point(float x, float y, boolean isMid){
this.x = x;
this.y = y;
this.isMid = isMid;
}
public float getX(){
return this.x;
}
public float getY(){
return this.y;
}
public boolean getIsMid() {
return this.isMid;
}
public void setX(float x){
this.x = x;
}
public void setY(float y){
this.y = y;
}
public void setIsMid(boolean isMid) {
this.isMid = isMid;
}
@Override
public String toString() {
return "Point{" +
"x=" + x +
", y=" + y +
", isMid=" + isMid +
'}';
}
}
接下来自定义view中需要定义一些属性。
//绘制多个多边形时用来存储所有的point
private final ArrayList<ArrayList<Point>> allPoints = new ArrayList<>();
//当前触摸产生的点
private final ArrayList<Point> cPoints = new ArrayList<>();
//所有的path
private final ArrayList<Path> paths = new ArrayList<>();
//用来记录触摸的是哪个多边形的第几个元素
private int[] touchPoint = new int[]{-1, -1};
//是否完成
private boolean isComplete = false;
//各种画笔
private final Paint dotPaint = new Paint();
private final Paint dotInnerPaint = new Paint();
private final Paint areaPaint = new Paint();
private final Paint midPaint = new Paint();
private final Paint midInnerPaint = new Paint();
//最大多变形个数
private int maxArea = 3;
做下画笔初始化工作,然后在构造中调用。
//画笔初始化
private void initPaint(){
areaPaint.setColor(Color.argb(20, 255, 0, 0)); //设置绿色画笔颜色
areaPaint.setStyle(Paint.Style.FILL); //设置填充样式为描边
areaPaint.setStrokeWidth(10); //设置画笔宽度,好大的一个画笔
dotPaint.setColor(Color.argb(100, 196, 196, 196));
dotPaint.setStyle(Paint.Style.FILL); //设置填充样式为描边
dotPaint.setStrokeWidth(10); //设置画笔宽度,好大的一个画笔
dotInnerPaint.setColor(Color.argb(255, 7, 193, 96));
dotInnerPaint.setStyle(Paint.Style.FILL); //设置填充样式为描边
dotInnerPaint.setStrokeWidth(10); //设置画笔宽度,好大的一个画笔
midPaint.setColor(Color.argb(100, 196, 196, 196));
midPaint.setStyle(Paint.Style.FILL); //设置填充样式为描边
midPaint.setStrokeWidth(10); //设置画笔宽度,好大的一个画笔
midInnerPaint.setColor(Color.argb(255, 28, 149, 250));
midInnerPaint.setStyle(Paint.Style.FILL); //设置填充样式为描边
midInnerPaint.setStrokeWidth(10); //设置画笔宽度,好大的一个画笔
}
提供编辑,完成和清除,设置多边形最大个数的方法。
//设置最大区域个数
public void setMaxArea(int count){
maxArea = count;
}
//清除所有图形
public void reset(){
allPoints.clear();
isComplete = false;
invalidate();
}
//完成绘制
public void complete(){
isComplete = true;
invalidate();
}
//进入编辑模式
public void edit(){
isComplete = false;
invalidate();
}
onTouchEvent拦截会稍微复杂点,首先手指触摸的时候要先判断没有触摸在某个多边形的Point且没有落在多边形内才能新增多边形,并把这个多边形的所有Point放到cPoints 然后加入到allPoints里面。如果落在了某个多边形的某个Point上则执行相关更新点的位置和属性(是否中间点)的逻辑,这里代码较多我就不单独贴了,后续会统一把所有代码贴出来。
重载onDraw无非就是遍历allPoints,然后根据isComplete 状态来绘制path或者直线和圆,代码我也会在下面一并贴出,相对来说逻辑简单点,关键地方也会做好注释。
下面就是整个代码的实现了,我这里做了好几版,这是最终版V2。新建自定义BasisView2类如下:
/**
* @ProjectName : path-demo
* @Author : Jay.Chou
* @Time : 2024/11/21 16:02
* @Description : 绘制任意多边形
*/
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.graphics.Region;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Toast;
import java.util.ArrayList;
public class BasisView2 extends View {
private static final String TAG = BasisView2.class.getName();
//绘制多个多边形时用来存储所有的point
private final ArrayList<ArrayList<Point>> allPoints = new ArrayList<>();
//当前触摸产生的点
private final ArrayList<Point> cPoints = new ArrayList<>();
private final ArrayList<Path> paths = new ArrayList<>();
//用来记录触摸的是哪个多边形的第几个元素
private int[] touchPoint = new int[]{-1, -1};
private boolean isComplete = false;
private final Paint dotPaint = new Paint();
private final Paint dotInnerPaint = new Paint();
private final Paint areaPaint = new Paint();
private final Paint midPaint = new Paint();
private final Paint midInnerPaint = new Paint();
//最大多变形个数
private int maxArea = 3;
public BasisView2(Context context) {
this(context, null);
initPaint();
}
public BasisView2(Context context, AttributeSet attrs) {
this(context, attrs, 0);
initPaint();
}
public BasisView2(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initPaint();
}
//设置最大区域个数
public void setMaxArea(int count){
maxArea = count;
}
//清除所有图形
public void reset(){
allPoints.clear();
isComplete = false;
invalidate();
}
//完成绘制
public void complete(){
isComplete = true;
invalidate();
}
//进入编辑模式
public void edit(){
isComplete = false;
invalidate();
}
//找到触摸点所在的位置
private int[] touchPoint(float x, float y){
for(int j = 0; j < allPoints.size(); j ++){
for (int i = 0; i < allPoints.get(j).size(); i ++){
Point point = allPoints.get(j).get(i);
float absx = Math.abs(point.getX() - x);
float absy = Math.abs(point.getY() - y);
//这里扩大圆圈的范围,更容易触摸,圆圈大小为30*30
if(absx*absx + absy*absy <= 80*80){
return new int[]{j,i};
}
}
}
return new int[]{-1,-1};
}
//计算所有的点,包括拖动点和中心点
private void composeAllPoints(){
ArrayList<ArrayList<Point>> allPointsTemp = new ArrayList<>();
for(int i = 0; i < allPoints.size(); i ++){
ArrayList<Point> pointsTemp = allPoints.get(i);
ArrayList<Point> pointsTempNew = new ArrayList<>();
int size = pointsTemp.size();
if(size <= 1){
return;
}
if(size == 2){
pointsTempNew.add(pointsTemp.get(0));
float x = (pointsTemp.get(0).getX() + pointsTemp.get(1).getX()) / 2;
float y = (pointsTemp.get(0).getY() + pointsTemp.get(1).getY()) / 2;
Point midPoint = new Point(x,y,true);
pointsTempNew.add(midPoint);
pointsTempNew.add(pointsTemp.get(1));
}else {
for (int j = 0; j < pointsTemp.size(); j ++){
Point point = pointsTemp.get(j);
pointsTempNew.add(point);
Point pointNext;
if(j == pointsTemp.size() - 1){
Point pointPre = pointsTemp.get(j-1);
//最后一个元素且上一个不是中间点才重新计算中间点
if(!point.getIsMid() && !pointPre.getIsMid()){
pointNext = pointsTemp.get(0);
float x = (point.getX() + pointNext.getX()) / 2;
float y = (point.getY() + pointNext.getY()) / 2;
Point midPoint = new Point(x,y,true);
pointsTempNew.add(midPoint);
}
}else {
pointNext = pointsTemp.get(j+1);
if(!point.getIsMid() && !pointNext.getIsMid()){
float x = (point.getX() + pointNext.getX()) / 2;
float y = (point.getY() + pointNext.getY()) / 2;
Point midPoint = new Point(x,y,true);
pointsTempNew.add(midPoint);
}
}
}
}
allPointsTemp.add(pointsTempNew);
}
allPoints.clear();
allPoints.addAll(allPointsTemp);
}
//判断path是否包含某个点,用来做碰撞冲突检测
private int containsPoint(Point point){
//最后一个当前绘制的path不检测
for(int i = 0; i < paths.size(); i ++){
Path path = paths.get(i);
RectF r = new RectF();
path.computeBounds(r, true);
Region re = new Region();
re.setPath(path, new Region((int)r.left,(int)r.top,(int)r.right,(int)r.bottom));
boolean contains = re.contains((int)point.getX(),(int)point.getY());
if(contains){
return i;
}
}
return -1;
}
private void showToast(String msg){
Toast.makeText(getContext(),msg,Toast.LENGTH_SHORT).show();
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {
if(isComplete){
return true;
}
int action = event.getAction();
float x = event.getX();
float y = event.getY();
switch (action) {
case MotionEvent.ACTION_DOWN:
touchPoint = touchPoint(x ,y);
//触摸没有落到任何一个多边形
if(touchPoint[0] == -1){
//没有落在别的区域内才绘制
int pos = containsPoint(new Point(x,y));
if(pos == -1){
if(allPoints.size() >= maxArea){
showToast("绘制区域个数已达最大值");
return true;
}else {
cPoints.add(new Point(x,y));
allPoints.add(cPoints);
invalidate();
}
}else {
Log.e(TAG,"落在第"+pos+"个区域");
}
}
break;
case MotionEvent.ACTION_MOVE:
//触摸落到某个点
if(touchPoint[0] >= 0 && touchPoint[1] >= 0){
//获取触摸的点所在多边形的所有点
ArrayList<Point> pTemp = allPoints.get(touchPoint[0]);
//如果是中间点直接拖动
if(pTemp.get(touchPoint[1]).getIsMid()){
pTemp.set(touchPoint[1], new Point(x,y,pTemp.get(touchPoint[1]).getIsMid()));
}else {
pTemp.set(touchPoint[1], new Point(x,y,pTemp.get(touchPoint[1]).getIsMid()));
//移动拖动点时实时计算并更新中间点,并处理边界情况
if(touchPoint[1] == 0){
//只有三个点的时候对第一个元素单独处理
if(pTemp.size() == 3){
pTemp.set(touchPoint[1], new Point(x,y,pTemp.get(touchPoint[1]).getIsMid()));
pTemp.get(1).setIsMid(false);
}else {
float mxp = (pTemp.get(touchPoint[1]).getX() + pTemp.get(pTemp.size() - 2).getX())/2;
float myp = (pTemp.get(touchPoint[1]).getY() + pTemp.get(pTemp.size() - 2).getY())/2;
pTemp.set(pTemp.size() - 1, new Point(mxp,myp,pTemp.get(pTemp.size() - 1).getIsMid()));
float mxn = (pTemp.get(touchPoint[1]).getX() + pTemp.get(touchPoint[1] + 2).getX())/2;
float myn = (pTemp.get(touchPoint[1]).getY() + pTemp.get(touchPoint[1] + 2).getY())/2;
pTemp.set(touchPoint[1] + 1, new Point(mxn,myn,pTemp.get(touchPoint[1] + 1).getIsMid()));
}
}else if(touchPoint[1] == 1){
float mxp = (pTemp.get(touchPoint[1]).getX() + pTemp.get(pTemp.size() - 1).getX())/2;
float myp = (pTemp.get(touchPoint[1]).getY() + pTemp.get(pTemp.size() - 1).getY())/2;
pTemp.set(touchPoint[1] - 1, new Point(mxp,myp,pTemp.get(touchPoint[1] - 1).getIsMid()));
float mxn = (pTemp.get(touchPoint[1]).getX() + pTemp.get(touchPoint[1] + 2).getX())/2;
float myn = (pTemp.get(touchPoint[1]).getY() + pTemp.get(touchPoint[1] + 2).getY())/2;
pTemp.set(touchPoint[1] + 1, new Point(mxn,myn,pTemp.get(touchPoint[1] + 1).getIsMid()));
}else if(touchPoint[1] == pTemp.size() - 2){
float mxp = (pTemp.get(touchPoint[1]).getX() + pTemp.get(touchPoint[1] - 2).getX())/2;
float myp = (pTemp.get(touchPoint[1]).getY() + pTemp.get(touchPoint[1] - 2).getY())/2;
pTemp.set(touchPoint[1] - 1, new Point(mxp,myp,pTemp.get(touchPoint[1] - 1).getIsMid()));
float mxn = (pTemp.get(touchPoint[1]).getX() + pTemp.get(0).getX())/2;
float myn = (pTemp.get(touchPoint[1]).getY() + pTemp.get(0).getY())/2;
pTemp.set(touchPoint[1] + 1, new Point(mxn,myn,pTemp.get(touchPoint[1] + 1).getIsMid()));
}else if(touchPoint[1] == pTemp.size() - 1){
//只有三个点的时候对最后一个元素单独处理
if(pTemp.size() == 3){
pTemp.set(touchPoint[1], new Point(x,y,pTemp.get(touchPoint[1]).getIsMid()));
pTemp.get(1).setIsMid(false);
}else{
float mxp = (pTemp.get(touchPoint[1]).getX() + pTemp.get(touchPoint[1] - 2).getX())/2;
float myp = (pTemp.get(touchPoint[1]).getY() + pTemp.get(touchPoint[1] - 2).getY())/2;
pTemp.set(touchPoint[1] - 1, new Point(mxp,myp,pTemp.get(touchPoint[1] - 1).getIsMid()));
float mxn = (pTemp.get(touchPoint[1]).getX() + pTemp.get(1).getX())/2;
float myn = (pTemp.get(touchPoint[1]).getY() + pTemp.get(1).getY())/2;
pTemp.set(0, new Point(mxn,myn,pTemp.get(0).getIsMid()));
}
}else {
float mxp = (pTemp.get(touchPoint[1] - 2).getX() + pTemp.get(touchPoint[1]).getX())/2;
float myp = (pTemp.get(touchPoint[1] - 2).getY() + pTemp.get(touchPoint[1]).getY())/2;
pTemp.set(touchPoint[1] - 1, new Point(mxp,myp,pTemp.get(touchPoint[1] - 1).getIsMid()));
float mxn = (pTemp.get(touchPoint[1]).getX() + pTemp.get(touchPoint[1] + 2).getX())/2;
float myn = (pTemp.get(touchPoint[1]).getY() + pTemp.get(touchPoint[1] + 2).getY())/2;
pTemp.set(touchPoint[1] + 1, new Point(mxn,myn,pTemp.get(touchPoint[1] + 1).getIsMid()));
}
}
}else {
if(cPoints.size() <= 2 && cPoints.size() > 0){
if(cPoints.size() == 1){
cPoints.add(new Point(x,y));
}else {
cPoints.set(cPoints.size() - 1, new Point(x,y,cPoints.get(cPoints.size() - 1).getIsMid()));
}
}
}
invalidate();
Log.d(TAG, "Action was MOVE");
break;
case MotionEvent.ACTION_UP:
Log.d(TAG, "Action was UP");
if(touchPoint[0] >= 0 && touchPoint[1] >= 0){
allPoints.get(touchPoint[0]).get(touchPoint[1]).setIsMid(false);
}else {
ArrayList<Point> pt = (ArrayList<Point>)allPoints.get(allPoints.size() - 1).clone();
cPoints.clear();
allPoints.set(allPoints.size() - 1, pt);
}
touchPoint = new int[]{-1,-1};
composeAllPoints();
invalidate();
break;
case MotionEvent.ACTION_CANCEL:
if(touchPoint[0] >= 0 && touchPoint[1] >= 0){
allPoints.get(touchPoint[0]).get(touchPoint[1]).setIsMid(false);
}else {
ArrayList<Point> pt = (ArrayList<Point>)allPoints.get(allPoints.size() - 1).clone();
cPoints.clear();
allPoints.set(allPoints.size() - 1, pt);
}
touchPoint = new int[]{-1,-1};
composeAllPoints();
invalidate();
Log.d(TAG, "Action was CANCEL");
break;
default:
return false;
}
return true;
}
//画笔初始化
private void initPaint(){
areaPaint.setColor(Color.argb(20, 255, 0, 0)); //设置绿色画笔颜色
areaPaint.setStyle(Paint.Style.FILL); //设置填充样式为描边
areaPaint.setStrokeWidth(10); //设置画笔宽度,好大的一个画笔
dotPaint.setColor(Color.argb(100, 196, 196, 196));
dotPaint.setStyle(Paint.Style.FILL); //设置填充样式为描边
dotPaint.setStrokeWidth(10); //设置画笔宽度,好大的一个画笔
dotInnerPaint.setColor(Color.argb(255, 7, 193, 96));
dotInnerPaint.setStyle(Paint.Style.FILL); //设置填充样式为描边
dotInnerPaint.setStrokeWidth(10); //设置画笔宽度,好大的一个画笔
midPaint.setColor(Color.argb(100, 196, 196, 196));
midPaint.setStyle(Paint.Style.FILL); //设置填充样式为描边
midPaint.setStrokeWidth(10); //设置画笔宽度,好大的一个画笔
midInnerPaint.setColor(Color.argb(255, 28, 149, 250));
midInnerPaint.setStyle(Paint.Style.FILL); //设置填充样式为描边
midInnerPaint.setStrokeWidth(10); //设置画笔宽度,好大的一个画笔
}
//绘制拖拽点和中间点圆圈
private void drawCicle(Canvas canvas, ArrayList<Point> points, Path path){
for(int i = 0; i < points.size(); i ++){
Point point = points.get(i);
if(!isComplete){
if(point.getIsMid()){
//没有触摸或者触摸拖动的是中间点时才绘制中间点
if(touchPoint[0] < 0 || allPoints.get(touchPoint[0]).get(touchPoint[1]).getIsMid()){
canvas.drawCircle(point.getX(),point.getY(),30,midPaint);
canvas.drawCircle(point.getX(),point.getY(),15,midInnerPaint);
}
}else {
canvas.drawCircle(point.getX(),point.getY(),30,dotPaint);
canvas.drawCircle(point.getX(),point.getY(),15,dotInnerPaint);
}
}
if(path != null){
if(i == 0){
path.moveTo(point.getX(),point.getY());
}else {
path.lineTo(point.getX(),point.getY());
}
}
}
}
//通过两个point画连线
private void drawLine(Point p1, Point p2,Canvas canvas, Paint p){
canvas.drawLine(p1.getX(),p1.getY(),p2.getX(),p2.getY(),p);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
paths.clear();
for(int j = 0; j < allPoints.size(); j ++){
ArrayList<Point> points = allPoints.get(j);
if (points.size() <= 0) return;
if(points.size() <= 2){
//如果只有两个点,则只画线
if(points.size() == 2){
drawLine(points.get(0),points.get(1),canvas,areaPaint);
}
for(int i = 0; i < points.size(); i ++){
Point point = points.get(i);
if(!isComplete){
if(point.getIsMid()){
canvas.drawCircle(point.getX(),point.getY(),30,midPaint);
canvas.drawCircle(point.getX(),point.getY(),15,midInnerPaint);
}else {
canvas.drawCircle(point.getX(),point.getY(),30,dotPaint);
canvas.drawCircle(point.getX(),point.getY(),15,dotInnerPaint);
}
}
}
}else {
float midX = points.get(1).getX();
float midY = points.get(1).getY();
float cMidX = (points.get(0).getX() + points.get(2).getX())/2;
float cMidY = (points.get(0).getY() + points.get(2).getY())/2;
if(points.size() == 3){
//初始状态下的点,一条线上还未闭合则画线
if(midX == cMidX && midY == cMidY){
drawLine(points.get(0),points.get(2),canvas,areaPaint);
drawCicle(canvas,points,null);
}else {
Path path = new Path();
drawCicle(canvas,points,path);
path.close();
paths.add(path);
canvas.drawPath(path, areaPaint);
}
}else {
Path path = new Path();
drawCicle(canvas,points,path);
path.close();
paths.add(path);
canvas.drawPath(path, areaPaint);
}
}
}
}
}
使用姿势,在activiy的xml中引用该自定义view(注意这里包名换成你自己的):
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.xxx.path_demo.BasisView2
android:id="@+id/basisView"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:ignore="MissingConstraints" />
<Button
android:id="@+id/clear"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="清除"/>
<Button
android:id="@+id/complete"
android:layout_toRightOf="@+id/clear"
android:layout_marginLeft="30dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="完成" />
<Button
android:id="@+id/edit"
android:layout_toRightOf="@+id/complete"
android:layout_marginLeft="30dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="编辑" />
</RelativeLayout>
Activity中代码:
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
public class MainActivity extends AppCompatActivity {
public BasisView2 baseView;
public Button clear;
public Button complete;
public Button edit;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
}
public void initView(){
baseView = (BasisView2)findViewById(R.id.basisView);
baseView.setMaxArea(2);
clear = (Button) findViewById(R.id.clear);
clear.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
baseView.reset();
}
});
complete = (Button) findViewById(R.id.complete);
complete.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
baseView.complete();
}
});
edit = (Button) findViewById(R.id.edit);
edit.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
baseView.edit();
}
});
}
}
所有代码都在上面了,可以直接拷贝运行。
尾巴
有问题可以给我留言,希望能帮助到有需要的人。如果你喜欢我的文章欢迎给我点赞,谢谢!