功能需求: 要求将机器人实时扫描到的激光雷达地图绘制到安卓屏幕上,并且要具备缩放、旋转、平移的功能。
整体思路
- 将原始数据转成图片
- 将图片绘制到屏幕上
- 实现对整体内容的缩放、旋转、平移
1、生成图片
为了实现激光雷达地图的绘制总共使用了两套方案,第一个由服务端将机器人扫描到的点云发送过来,然后客户端通过以机器人当前位置(x1,y1)为起点,点云(x2,y2)为终点连成线的方式来绘制出扫描到的环境。这种方式简单粗暴但是缺点很多,一个是性能问题,当扫描的环境过大点云数量将达到十几万这就意味着到了后面每次要绘制十几万条线,每绘制一帧要花费三四百毫秒的时间这是令人难以接受的事情;另一个问题就是显示效果,这样生成地图不但不美观,而且没办法放大看细节。第二套方案就是接下来要描述的内容。
OpevCV你需要了解以下内容: 导入OpenCv的库: 了解OpenCv的基本使用: 或者看一下这本书:https://weread.qq.com/web/reader/0463227071578dce046679a
第二套方案整体思路
服务端每次都发送一块781x781大小的三通道Mat数据过来,并且会带上索引,如第一块Mat(781x781)索引就是(0,0),这一块对应到现实中的扫描面积大概就是40x40m大小的区域, 如果机器人跑出当前(40X40)的区域则会再发送一块索引不一样的Mat,组合规律如下:每一块小矩形都相当于一个Mat,最终一定要是要生成一个大的矩形,如果碰到有三块Mat不是组成一个矩形那么像图中阴影部分自动补齐一块。
最终通过一个完整的Mat来生成一张图片该部分代码如下:
/**
* 检查矩阵的索引,计算总共需要多少块像素承载数据,并改变相应位置的像素,并生成BItmap
*/
private void checkMatNumber(){
//获得所有块Mat数据
Map<GridItem,Mat> matMap=openCVUtility.getMatMap();
//获得Mat的宽高(每块都是781x781)
eachPixelW=notifyLidarCurSubMap.getWidth();
int minX=0,minY=0,maxX=0,maxY=0;
int numberX,numberY;
for (GridItem gridItem:matMap.keySet()){
int cx=gridItem.getX();
int cy=gridItem.getY();
if (minX>=cx){ minX=cx;}
if (minY>=cy){ minY=cy;}
if (maxX<=cx){ maxX=cx;}
if (maxY<=cy){ maxY=cy;}
}
numberX=maxX-minX+1;
numberY=maxY-minY+1;
int w=eachPixelW*numberY;
int h=eachPixelW*numberX;
srcMat=openCVUtility.getSrcMat();
if (matMap.size()==1){
srcMat=openCVUtility.createSrcMat(w,h);
Mat mat=matMap.get(new GridItem(0,0));
mat.copyTo(srcMat.submat(0,h,0,w));
originY=eachPixelW/2;
originX=eachPixelW/2;
}else {
if (w>srcMat.width()|h>srcMat.height()){
srcMat.release();
srcMat=openCVUtility.createSrcMat(w,h);
for (GridItem gridItem:matMap.keySet()){
Mat mat=matMap.get(gridItem);
int rowStart=(maxX-gridItem.getX())*eachPixelW;
int rowEnd=rowStart+eachPixelW;
int colStart=(maxY-gridItem.getY())*eachPixelW;
int colEnd=colStart+eachPixelW;
mat.copyTo(srcMat.submat(rowStart,rowEnd,colStart,colEnd));
if (gridItem.getX()==0&&gridItem.getY()==0){
originY=rowStart+eachPixelW/2;
originX=colStart+eachPixelW/2;
}
}
}else {
Mat mat=matMap.get(notifyLidarCurSubMap.getGridItem());
int rowStart=(maxX-notifyLidarCurSubMap.getGridItem().getX())*notifyLidarCurSubMap.getHeight();
int rowEnd=rowStart+notifyLidarCurSubMap.getHeight();
int colStart=(maxY-notifyLidarCurSubMap.getGridItem().getY())*notifyLidarCurSubMap.getWidth();
int colEnd=colStart+notifyLidarCurSubMap.getWidth();
mat.copyTo(srcMat.submat(rowStart,rowEnd,colStart,colEnd));
}
}
srcBitmap=openCVUtility.matToBitmap(srcMat);
srcMat.release();
surfaceTouchEventHandler.setDefaultBitmap(srcBitmap);
// Logger.e("生成的图片的大小:"+srcBitmap.getWidth()+";"+srcBitmap.getHeight());
perMeter=eachPixelW/(lidarRange*2);
}
OpenCv工具类:
/**
* desc:openCv操作类
*/
public class OpenCVUtility {
private static OpenCVUtility openCVUtility;
private Mat srcMat; //原始灰度图
private Map<GridItem,Mat> matMap=new HashMap<>();
public static OpenCVUtility getInstance(){
if (openCVUtility==null){
synchronized (OpenCVUtility.class){
if (openCVUtility==null){
openCVUtility=new OpenCVUtility();
}
}
}
return openCVUtility;
}
private OpenCVUtility() {
}
public Mat createMat(Size size,Scalar scalar){
Mat mat=new Mat(size,CvType.CV_8UC3,scalar);
return mat;
}
/**
* 创建指定大小的Mat
* @param w
* @param h
* @return
*/
public Mat createSrcMat(int w,int h){
//创建三通道的灰色(127)Mat
srcMat=new Mat(new Size(w,h),CvType.CV_8UC3,new Scalar(18,17,16));
return srcMat;
}
/**
* 将Mat转成图片Bitmap
* @param source
* @return
*/
public Bitmap matToBitmap(Mat source){
Bitmap bitmap=Bitmap.createBitmap(source.width(),source.height(),Bitmap.Config.ARGB_8888);
Utils.matToBitmap(source,bitmap);
return bitmap;
}
public Mat getSrcMat() {
if (srcMat==null){
srcMat=createSrcMat(700,700);
}
return srcMat;
}
/**
* 保存不同索引的Mat数据
* @param gridItem
* @param mat
*/
public void putValue(GridItem gridItem,Mat mat){
matMap.put(gridItem, mat);
}
public Map<GridItem, Mat> getMatMap() {
return matMap;
}
public void onDestroy(){
if (srcMat!=null){
matMap.clear();
srcMat.release();
srcMat=null;
openCVUtility=null;
}
}
}
对象作为Map的Key:
/**
* desc:Mat块的索引,用于作为Map集合的Key
*/
public class GridItem {
private int x;
private int y;
public GridItem(int x,int y){
this.x=x;
this.y=y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
@Override
public boolean equals(@Nullable Object obj) {
if (this==obj){ return true; }
if (!(obj instanceof GridItem)){ return false; }
GridItem gridItem= (GridItem) obj;
return x==gridItem.x&&y==gridItem.y;
}
@Override
public int hashCode() {
return x^y*137;
}
}
2、将相关信息通过SurfaceView绘制到屏幕上
- 绘制Bitmap图片
- 绘制机器人行走路径
- 绘制机器人当前位置以及当前雷达扫描的
如图:
代码如下:
/**
* desc:生成栅格地图的View
* time:2020/06/19
*/
public class GenerateMapView extends SurfaceView implements SurfaceHolder.Callback {
public boolean isRunning=false;
private DrawMapThread drawThread; //绘制线程
private SurfaceHolder holder;
private Bitmap srcBitmap;
private Matrix matrix,matrix1,matrix2;
private Paint paint,lastFrame,pathPaint;
private NotifyLidarCurSubMap notifyLidarCurSubMap;
private OpenCVUtility openCVUtility;
private SurfaceTouchEventHandler surfaceTouchEventHandler;
private float lidarRange=19.5f;
private float perMeter;
private float originX,originY; //相对于Mat的原点坐标
private int eachPixelW; //每块像素的宽高
private Bitmap directionBitmap,targetBitmap;
private float angle;
private List<BaseCmd.notifyLidarPts.Position> positionList=new ArrayList<>(); //雷达当前扫到的点云
private NotifyLidarPtsEntity notifyLidarPtsEntity;
private List<RobotCoordinates> robotPath=new ArrayList<>();
public GenerateMapView(Context context) {
super(context);
init();
}
public GenerateMapView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init(){
holder=getHolder();
//holder.setFormat(PixelFormat.TRANSPARENT);//设置背景透明
holder.addCallback(this);
matrix=new Matrix();
matrix1=new Matrix();
matrix2=new Matrix();
paint=new Paint();
paint.setAntiAlias(true);
paint.setDither(true);
openCVUtility=OpenCVUtility.getInstance();
notifyLidarCurSubMap=NotifyLidarCurSubMap.getInstance();
notifyLidarPtsEntity=NotifyLidarPtsEntity.getInstance();
directionBitmap= BitmapFactory.decodeResource(getResources(), R.mipmap.direction);
targetBitmap=BitmapFactory.decodeResource(getResources(), R.mipmap.target_point);
lastFrame=new Paint();
lastFrame.setStrokeWidth(1);
lastFrame.setStyle(Paint.Style.FILL);
lastFrame.setColor(Color.parseColor("#9900CED1"));
pathPaint=new Paint();
pathPaint.setColor(Color.BLACK);
pathPaint.setStyle(Paint.Style.STROKE);
pathPaint.setStrokeWidth(2);
EventBusManager.register(this);
}
/**
* 开始绘制
*/
public void startThread(){
drawThread=new DrawMapThread();
drawThread.start();
}
/**
* 停止绘制
*/
public void onStop(){
if (drawThread!=null&&isRunning){
drawThread.stopThread();
}
}
/**
* 绘制图像的线程
*/
public class DrawMapThread extends Thread{
public DrawMapThread(){
isRunning=true;
}
public void stopThread(){
if (isRunning){
isRunning=false;
boolean workIsNotFinish=true;
while (workIsNotFinish){
try {
drawThread.join(); //保证run方法执行完毕
}catch (InterruptedException e){
e.printStackTrace();
}
workIsNotFinish=false;
}
Logger.e("终止线程");
}
}
@Override
public void run() {
super.run();
while (isRunning){
long startTime=System.currentTimeMillis();
Canvas canvas=null;
try {
canvas=holder.lockCanvas();
drawMap(canvas);
drawPath(canvas);
drawRobot(canvas);
}catch (Exception e){
e.printStackTrace();
}finally {
if (canvas!=null){
holder.unlockCanvasAndPost(canvas);
}
}
long endTime=System.currentTimeMillis();
Logger.i("------地图绘制耗时:"+(endTime-startTime));
long time=endTime-startTime;
if (time<100){
try {
Thread.sleep(100-time);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
/**
* 绘制地图
* @param canvas
*/
private void drawMap(Canvas canvas){
if (srcBitmap!=null){
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
canvas.drawPaint(paint);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER));
canvas.drawColor(Color.parseColor("#101112"));
matrix=surfaceTouchEventHandler.getMatrix();
canvas.drawBitmap(srcBitmap,matrix,paint);
}
}
/**
* 绘制机器人当前位置
* @param canvas
*
*/
private void drawRobot(Canvas canvas){
positionList=notifyLidarPtsEntity.getPositionList();
int size = positionList.size();
if (positionList != null && size > 0) {
XyEntity xyEntity1 = coordinatesToMat(notifyLidarPtsEntity.getPosX(), notifyLidarPtsEntity.getPosY());
xyEntity1=surfaceTouchEventHandler.coordinatesToCanvas(xyEntity1.getX(),xyEntity1.getY());
for (int i = 0; i < size; i++) {
XyEntity xyEntity = coordinatesToMat(positionList.get(i).getPtX(), positionList.get(i).getPtY());
xyEntity=surfaceTouchEventHandler.coordinatesToCanvas(xyEntity.getX(),xyEntity.getY());
canvas.drawLine(xyEntity1.getX(), xyEntity1.getY(), xyEntity.getX(), xyEntity.getY(), lastFrame);
}
angle=radianToangle(notifyLidarPtsEntity.getPosdirection());
float cx = xyEntity1.getX()-directionBitmap.getWidth()/2;
float cy =xyEntity1.getY()-directionBitmap.getHeight()*13/20;
matrix1.reset();
matrix1.postTranslate(cx,cy);
matrix1.postRotate(-angle+(float) surfaceTouchEventHandler.getAngle(),xyEntity1.getX(),xyEntity1.getY());
canvas.drawBitmap(directionBitmap,matrix1,paint);
}
}
/**
*显示机器人行走路径
* @param canvas
*/
private void drawPath(Canvas canvas){
int size=robotPath.size();
if (size>1){
Path path=new Path();
for (int i=0;i<size;i++){
XyEntity xyEntity = coordinatesToMat(robotPath.get(i).getX(), robotPath.get(i).getY());
xyEntity=surfaceTouchEventHandler.coordinatesToCanvas(xyEntity.getX(),xyEntity.getY());
if (i==0){
path.moveTo(xyEntity.getX(),xyEntity.getY());
}else {
path.lineTo(xyEntity.getX(),xyEntity.getY());
}
}
canvas.drawPath(path,pathPaint);
}
XyEntity xyEntity=coordinatesToMat(0,0);
xyEntity=surfaceTouchEventHandler.coordinatesToCanvas(xyEntity.getX(),xyEntity.getY());
float cx=xyEntity.getX()-targetBitmap.getWidth()/2;
float cy=xyEntity.getY()-targetBitmap.getHeight()/2;
matrix2.reset();
matrix2.postTranslate(cx,cy);
matrix2.postRotate((float) surfaceTouchEventHandler.getAngle(),xyEntity.getX(),xyEntity.getY());
canvas.drawBitmap(targetBitmap,matrix2,paint);
}
private Mat srcMat;
/**
* 检查矩阵的索引,计算总共需要多少块像素承载数据,并改变相应位置的像素
*/
private void checkMatNumber(){
//获得所有块Mat数据
Map<GridItem,Mat> matMap=openCVUtility.getMatMap();
//获得Mat的宽高(每块都是781x781)
eachPixelW=notifyLidarCurSubMap.getWidth();
int minX=0,minY=0,maxX=0,maxY=0;
int numberX,numberY;
for (GridItem gridItem:matMap.keySet()){
int cx=gridItem.getX();
int cy=gridItem.getY();
if (minX>=cx){ minX=cx;}
if (minY>=cy){ minY=cy;}
if (maxX<=cx){ maxX=cx;}
if (maxY<=cy){ maxY=cy;}
}
numberX=maxX-minX+1;
numberY=maxY-minY+1;
int w=eachPixelW*numberY;
int h=eachPixelW*numberX;
srcMat=openCVUtility.getSrcMat();
if (matMap.size()==1){
srcMat=openCVUtility.createSrcMat(w,h);
Mat mat=matMap.get(new GridItem(0,0));
mat.copyTo(srcMat.submat(0,h,0,w));
originY=eachPixelW/2;
originX=eachPixelW/2;
}else {
if (w>srcMat.width()|h>srcMat.height()){
srcMat.release();
srcMat=openCVUtility.createSrcMat(w,h);
for (GridItem gridItem:matMap.keySet()){
Mat mat=matMap.get(gridItem);
int rowStart=(maxX-gridItem.getX())*eachPixelW;
int rowEnd=rowStart+eachPixelW;
int colStart=(maxY-gridItem.getY())*eachPixelW;
int colEnd=colStart+eachPixelW;
mat.copyTo(srcMat.submat(rowStart,rowEnd,colStart,colEnd));
if (gridItem.getX()==0&&gridItem.getY()==0){
originY=rowStart+eachPixelW/2;
originX=colStart+eachPixelW/2;
}
}
}else {
Mat mat=matMap.get(notifyLidarCurSubMap.getGridItem());
int rowStart=(maxX-notifyLidarCurSubMap.getGridItem().getX())*notifyLidarCurSubMap.getHeight();
int rowEnd=rowStart+notifyLidarCurSubMap.getHeight();
int colStart=(maxY-notifyLidarCurSubMap.getGridItem().getY())*notifyLidarCurSubMap.getWidth();
int colEnd=colStart+notifyLidarCurSubMap.getWidth();
mat.copyTo(srcMat.submat(rowStart,rowEnd,colStart,colEnd));
}
}
srcBitmap=openCVUtility.matToBitmap(srcMat);
srcMat.release();
surfaceTouchEventHandler.setDefaultBitmap(srcBitmap);
// Logger.e("生成的图片的大小:"+srcBitmap.getWidth()+";"+srcBitmap.getHeight());
perMeter=eachPixelW/(lidarRange*2);
}
/**
* 计算相对于图片的坐标
* @param x
* @param y
*/
private XyEntity coordinatesToMat(float x, float y){
//Logger.d("-----:"+x+";"+y);
float cx=(-y)*perMeter+originX;
float cy=(-x)*perMeter+originY;
//Logger.e("坐标:"+cx+";"+cy+";"+perMeter+";"+originX+";"+originY);
return new XyEntity(cx,cy);
}
/**
* 弧度转角度
*/
private float radianToangle(float angle){
return (float)(180/Math.PI*angle);
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
Logger.e("-------surfaceChanged:"+width+";"+height);
surfaceTouchEventHandler=SurfaceTouchEventHandler.getInstance(width,height);
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
if (surfaceTouchEventHandler!=null){
surfaceTouchEventHandler.onDestroy();
}
EventBusManager.unregister(this);
openCVUtility.onDestroy();
}
public boolean onTouchEvent(MotionEvent event) {
if (surfaceTouchEventHandler!=null){
//Logger.e("---触摸");
surfaceTouchEventHandler.touchEvent(event);
}
return true;
}
@Subscribe(threadMode = ThreadMode.BACKGROUND,sticky = true)
public void receiveMessage(MessageEvent messageEvent){
switch (messageEvent.getType()){
case receiveLidarMap:
checkMatNumber();
break;
case receivePointCloud:
if (isRunning){
float cx=notifyLidarPtsEntity.getPosX();
float cy=notifyLidarPtsEntity.getPosY();
RobotCoordinates robotCoordinates=new RobotCoordinates();
robotCoordinates.setX(cx);
robotCoordinates.setY(cy);
robotPath.add(robotCoordinates);
robotCoordinates.save();
}
break;
case getRobotCoordinates:
robotPath=LitePal.findAll(RobotCoordinates.class);
Logger.e("-----本地保存的数据:"+robotPath.size());
break;
}
}
}
3、关于缩放、平移、旋转
对图形内容的操作这里主要用到Matrix的变换矩阵
/**
* 处理SurfaceView的点击事件(平移、缩放、旋转)
*/
public class SurfaceTouchEventHandler {
private static SurfaceTouchEventHandler surfaceTouchEventHandler;
// Matrix getValues 矩阵参数
private float[] values=new float[9];
private static final int MSCALE_X=0;
private static final int MSKEW_X=1;
private static final int MTRANS_X=2;
private static final int MSKEW_Y=3;
private static final int MSCALE_Y=4;
private static final int MTRANS_Y=5;
private static final int MPERSP_0=6;
private static final int MPERSP_1=7;
private static final int MPERSP_2=8;
// 当前操作的状态
public int currentStatus;
public static final int DEFAULT_BITMAP=0; // 默认状态下
public static final int SCALE_BITMAP=1; // 缩放状态下
public static final int TRANSLATE_BITMAP=2; // 平移
public static final int ROTATION_BITMAP=3; // 旋转
public static final int NONE_BITMAP=4; // 不作任何操作
public static final int SCALE_BITMAP_OUT=5; //放大
public static final int SCALE_BITMAP_IN=6; // 缩小
private float x_down = 0;
private float y_down = 0;
private PointF mid = new PointF();
private float oldDist = 1f; // 平移的距离
private float oldRotation = 0; // 手指第一次放上去的角度
private float rotation; // 正在旋转中变化的角度
private Matrix matrix = new Matrix();
private Matrix matrix1 = new Matrix();
private Matrix originalMatrix=new Matrix(); //地图最初显示时的矩阵
//用于保存matrix
private Matrix savedMatrix = new Matrix();
// 检测是否出界
boolean matrixCheck = false;
// 控件的大小
private int widthScreen;
private int heightScreen;
private boolean canRotate=true; // 默认可旋转
private Bitmap sourceBitmap;
/**
* 获取类唯一对象
* @param widthScreen 显示控件的宽高
* @param heightScreen
* @return
*/
public static SurfaceTouchEventHandler getInstance(int widthScreen,int heightScreen){
if (surfaceTouchEventHandler==null){
synchronized (SurfaceTouchEventHandler.class){
if (surfaceTouchEventHandler==null){
surfaceTouchEventHandler=new SurfaceTouchEventHandler(widthScreen,heightScreen);
}
}
}
return surfaceTouchEventHandler;
}
private SurfaceTouchEventHandler(int widthScreen,int heightScreen) {
Logger.e("初始化TouchEvenHandler");
this.widthScreen=widthScreen;
this.heightScreen=heightScreen;
}
public void setDefaultBitmap(Bitmap srcBitmap){
if (srcBitmap!=null){
int bitmapWidth=srcBitmap.getWidth();
int bitmapHeight=srcBitmap.getHeight();
if (sourceBitmap==null||(sourceBitmap.getWidth()!=bitmapWidth||sourceBitmap.getHeight()!=bitmapHeight)){
this.sourceBitmap=srcBitmap;
matrix.reset();
savedMatrix.set(matrix);
matrix1.set(savedMatrix);
float translateX=(widthScreen-bitmapWidth)/2f;
float translateY=(heightScreen-bitmapHeight)/2f;
matrix1.postTranslate(translateX,translateY);
matrix.set(matrix1);
matrix.getValues(values);
originalMatrix.set(matrix);
}
}
}
/**
* 获取最终用于绘制的矩阵变量
* @return
*/
public Matrix getMatrix(){
return matrix;
}
/**
* 选择是否可以旋转
* @param canRotate
*/
public void setCanRotate(boolean canRotate) {
this.canRotate = canRotate;
Logger.e("是否可以旋转"+canRotate);
}
/**
* 将矩阵还原最初
*/
public void initMatrix(){
matrix.set(originalMatrix);
}
/**
* 获取图片缩放比例
* @return
*/
public double getZoomX(){
return values[MSCALE_X]/getCosA();
}
public double getZoomY(){
return values[MSCALE_Y]/getCosA();
}
/**
* 获取图片左上角在画布中的坐标
* @return X
*/
public float getTranslateX(){
return values[MTRANS_X];
}
/**
* 获取图片左上角在画布中的坐标
* @return Y
*/
public float getTranslateY(){
return values[MTRANS_Y];
}
/**
* 返回图片旋转的弧度
* @return
*/
public double getRadians(){
double radians= Math.atan2(values[Matrix.MSKEW_Y], values[Matrix.MSCALE_Y]);
return radians;
}
/**
* 返回图片旋转的角度
* @return
*/
public double getAngle(){
double radians= Math.atan2(values[Matrix.MSKEW_Y], values[Matrix.MSCALE_Y]);
return Math.toDegrees(radians);
}
/**
* 返回图片弧度的余弦值
* @return
*/
public double getCosA(){
return Math.cos(getRadians());
}
/**
* 返回图片旋转弧度的正弦值
* @return
*/
public double getSinA(){
return Math.sin(getRadians());
}
/**
* 将values数组变成字符串输出
* @returnhai
*/
public String getValuesToString(){
return Arrays.toString(values);
}
/**
* 已知原始相对于图片的坐标,计算图片平移缩放或绕某点旋转之后相对于整个画布的坐标
* @return 画布坐标
*/
public XyEntity coordinatesToCanvas(float x, float y){
matrix.getValues(values);
double radians= Math.atan2(values[Matrix.MSKEW_Y], values[Matrix.MSCALE_Y]);
double cosA= Math.cos(radians);
double sinA= Math.sin(radians);
double cx=x*getZoomX();
double cy=y*getZoomY();
double x1=cx*cosA-cy*sinA;
double y1=cx*sinA+cy*cosA;
double x2=getTranslateX()+x1;
double y2=getTranslateY()+y1;
return new XyEntity((float) x2,(float) y2);
}
/**
* 已知相对于画布的坐标计算出该点相对于图片左上角的坐标,按照上面的方式反着推
* @return
*/
public XyEntity coordinatesToImage(float x, float y){
double cosA=getCosA();
double sinA=getSinA();
float x1=x-getTranslateX();
float y1=y-getTranslateY();
double cx=x1/cosA+sinA/(cosA*cosA+sinA*sinA)*(y1-sinA*x1/cosA);
double cy=(y1-x1*sinA/cosA)*cosA/(cosA*cosA+sinA*sinA);
double x2=cx/getZoomX();
double y2=cy/getZoomY();
return new XyEntity((float) x2,(float) y2);
}
/**
* 处理点击触摸事件
* @param event
*/
public void touchEvent(@NotNull MotionEvent event){
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
currentStatus = TRANSLATE_BITMAP;
x_down = event.getX();
y_down = event.getY();
savedMatrix.set(matrix);
break;
case MotionEvent.ACTION_POINTER_DOWN:
currentStatus = SCALE_BITMAP;
oldDist = spacing(event);
oldRotation = rotation(event);
savedMatrix.set(matrix);
midPoint(mid, event);
break;
case MotionEvent.ACTION_MOVE:
if (currentStatus == SCALE_BITMAP) {
matrix1.set(savedMatrix);
rotation = rotation(event) - oldRotation;
float newDist = spacing(event);
if (newDist>oldDist){
currentStatus=SCALE_BITMAP_OUT;
}else {
currentStatus=SCALE_BITMAP_IN;
}
float scale = newDist / oldDist;
matrix1.postScale(scale, scale, mid.x, mid.y);// 缩放
if (canRotate){
matrix1.postRotate(rotation, mid.x, mid.y);// 旋转
}
matrixCheck = matrixCheck();
if (matrixCheck == false) {
currentStatus=SCALE_BITMAP;
matrix1.getValues(values);
matrix.set(matrix1);
}
} else if (currentStatus == TRANSLATE_BITMAP) {
matrix1.set(savedMatrix);
matrix1.postTranslate(event.getX() - x_down, event.getY()
- y_down);// 平移
matrixCheck = matrixCheck();
if (matrixCheck == false) {
currentStatus=TRANSLATE_BITMAP;
matrix1.getValues(values);
matrix.set(matrix1);
}
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
currentStatus = NONE_BITMAP;
break;
}
}
/**
* 边界检测
* @return
*/
private boolean matrixCheck() {
float[] f = new float[9];
matrix1.getValues(f);
// 图片4个顶点的坐标
float x1 = f[0] * 0 + f[1] * 0 + f[2];
float y1 = f[3] * 0 + f[4] * 0 + f[5];
float x2 = f[0] * widthScreen+ f[1] * 0 + f[2];
float y2 = f[3] * widthScreen + f[4] * 0 + f[5];
float x3 = f[0] * 0 + f[1] * heightScreen + f[2];
float y3 = f[3] * 0 + f[4] * heightScreen+ f[5];
float x4 = f[0] * widthScreen + f[1] * heightScreen + f[2];
float y4 = f[3] * widthScreen + f[4] * heightScreen + f[5];
// 图片现宽度
double width = Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
// 缩放比率判断
if (width < widthScreen/3|| width > widthScreen * 6) {
return true;
}
// 出界判断
if ((x1 < widthScreen / 3 && x2 < widthScreen / 3
&& x3 < widthScreen / 3 && x4 < widthScreen / 3)
|| (x1 > widthScreen * 2 / 3 && x2 > widthScreen * 2 / 3
&& x3 > widthScreen * 2 / 3 && x4 > widthScreen * 2 / 3)
|| (y1 < heightScreen / 3 && y2 < heightScreen / 3
&& y3 < heightScreen / 3 && y4 < heightScreen / 3)
|| (y1 > heightScreen * 2 / 3 && y2 > heightScreen * 2 / 3
&& y3 > heightScreen * 2 / 3 && y4 > heightScreen * 2 / 3)) {
return true;
}
return false;
}
// 触碰两点间距离
private float spacing(MotionEvent event) {
float x = Math.abs(event.getX(0) - event.getX(1));
float y = Math.abs(event.getY(0) - event.getY(1));
return (float) Math.sqrt(x * x + y * y);
}
// 取手势中心点
private void midPoint(PointF point, MotionEvent event) {
float x = event.getX(0) + event.getX(1);
float y = event.getY(0) + event.getY(1);
point.set(x / 2, y / 2);
}
// 取旋转角度
private float rotation(MotionEvent event) {
double delta_x = (event.getX(0) - event.getX(1));
double delta_y = (event.getY(0) - event.getY(1));
double radians = Math.atan2(delta_y, delta_x);
// Logger.e("取弧度:"+radians);
return (float) Math.toDegrees(radians);
}
/**
* 重置销毁当前类
*/
public void onDestroy(){
surfaceTouchEventHandler=null;
sourceBitmap=null;
matrix.reset();
}
}
在这个工具类主要是对Matrix矩阵缩放、平移、旋转的一个集合类,里面提供了计算坐标coordinatesToCanvas(X,Y)的方法,保证在绘制图片和相对图片位置的点或者线后在旋转平移等操作下不会发生位置错乱。如果有需求要实现View内容缩放旋转功能的可以直接复制该类使用。 也可以到我的github看看这个相关内容Demo https://github.com/meijiadev/MatrixDemo
这里说一下为什么不使用缩放旋转整个控件的,一是SurfaceView不支持缩放旋转平移整个控件; 二、即使是正常的View在缩放旋转整个View也会出现显示不完整和显示效果不好的问题,虽然这种方式简单粗暴但是总归不美。