Android 项目:画图白板APP开发(八)——Matrix位移放大缩小(附demo)
在画图白板应用中,实现图形的位移、放大和缩小是基本且重要的功能。本文将介绍如何使用 Android 的 Matrix 类来实现这些变换,并提供一个完整的示例代码。
一、Matrix
Matrix 是 Android 中用于处理 2D 图形变换的核心类,它封装了一个 3×3 的矩阵,用于实现坐标变换。理解 Matrix 对于开发图形处理应用至关重要。
(1)主要功能
缩放(Scale):可以改变图片的大小,比如放大或缩小
旋转(Rotate):可以将图片绕某个点旋转一定的角度
平移(Translate):可以移动图片的位置
倾斜(Skew):可以让图片变形,比如将图片斜着拉长
(2)主要方法
1. matrix.set(Matrix matrix)
将一个矩阵的值复制到当前矩阵中
Matrix matrix1 = new Matrix();
Matrix matrix2 = new Matrix();
matrix1.setScale(2f, 2f);
matrix2.set(matrix1); // 把 matrix1 的值赋给 matrix2
2. matrix.setScale(float sx, float sy)
缩放图片
Matrix matrix = new Matrix();
matrix.setScale(2f, 2f); // 把图片放大两倍
- 参数解析:
- sx:横向缩放比例;值大于 1 表示放大,值小于 1 表示缩小
- sy:纵向缩放比例;值大于 1 表示放大,值小于 1 表示缩小
3. matrix.setRotate(float degrees)
旋转图片
Matrix matrix = new Matrix();
matrix.setRotate(45); // 旋转45度
- 参数解析:
- degrees:旋转的角度;正数表示顺时针旋转,负数表示逆时针旋转
4. matrix.setTranslate(float dx, float dy)
移动图片
Matrix matrix = new Matrix();
matrix.setTranslate(100, 200); // 将图片向右移动100像素,向下移动200像素
- 参数解析:
- dx:横向移动距离
- dy:纵向移动距离
5. matrix.setSkew(float kx, float ky)
倾斜图片;它可以让图片产生类似于拉长、拉伸的效果
Matrix matrix = new Matrix();
matrix.setSkew(0.5f, 0.2f); // 使图片横向倾斜0.5,纵向倾斜0.2
- 参数解析:
- kx:横向倾斜系数
- ky:纵向倾斜系数
6. matrix.postTranslate(float dx, float dy)
基于现有的变换再做移动操作
Matrix matrix = new Matrix();
matrix.setScale(2f, 2f);
matrix.postTranslate(100f, 100f); // 先缩放再平移
后面的操作都是一样的,前面加post就是追加的方法
matrix.postScale(float sx, float sy)
:基于当前变换追加缩放操作matrix.postRotate(float degrees)
:基于现有的矩阵进行旋转操作matrix.postSkew(float kx, float ky)
:在已有的矩阵操作上追加倾斜操作matrix.postConcat(Matrix other)
:请先执行我当前已有的所有变换,然后追加执行other
矩阵所代表的变换。
7. matrix.invert(Matrix inverse)
用于求当前矩阵的逆矩阵,如果当前矩阵是可逆的,会将逆矩阵存储到 inverse 参数中,返回 true,否则返回 false
Matrix matrix = new Matrix();
matrix.setScale(2f, 2f);
Matrix inverseMatrix = new Matrix();
boolean isInvertible = matrix.invert(inverseMatrix); // 计算逆矩阵
- 参数解析:
- inverse:用于得出的 当前matrix 对象 是否为逆矩阵
8. matrix.mapRect(RectF rect)
对矩形进行变换。矩形会根据当前矩阵进行缩放、平移、旋转等变换
Matrix matrix = new Matrix();
matrix.setScale(2f, 2f);
RectF rect = new RectF(0, 0, 100, 100);
matrix.mapRect(rect); // 变换后的矩形坐标会改变
- 参数解析:
- rect:接收一个重定矩形的 RectF 对象
9. matrix.reset()
将当前的矩阵重置为单位矩阵。单位矩阵即初始状态,表示没有任何变换
Matrix matrix = new Matrix();
matrix.setScale(2f, 2f);
matrix.reset(); // 重置为单位矩阵
10. matrix.preTranslate(float dx, float dy)
在已有矩阵操作之前进行平移
Matrix matrix = new Matrix();
matrix.setScale(2f, 2f);
matrix.preTranslate(50f, 50f); // 先平移再缩放
- 参数解析:
- dx:横向移动距离
- dy:纵向移动距离
类似的还有;
matrix.preScale(float sx, float sy):
在已有矩阵操作之前进行缩放matrix.preRotate(float degrees):
在已有矩阵操作之前进行旋转matrix.preSkew(float kx, float ky):
在已有矩阵操作之前进行倾斜matrix.preConcat(Matrix other)
:先执行other
矩阵,然后执行已有的矩阵所
11. matrix.setConcat(Matrix a, Matrix b)
将两个矩阵相乘,并将结果赋给当前矩阵。相当于 Matrix 的乘法运算
Matrix matrix1 = new Matrix();
matrix1.setScale(2f, 2f);Matrix matrix2 = new Matrix();
matrix2.setRotate(45);Matrix result = new Matrix();
result.setConcat(matrix1, matrix2); // 将 matrix1 和 matrix2 组合
- 参数解析:
- a:要组合的第一个 Matrix 对象
- b:要组合的第二个 Matrix 对象
12. matrix.setPolyToPoly(float[] src, int srcIndex, float[] dst, int dstIndex, int pointCount)
将源点数组 src 变换为目标点数组 dst。可以指定多少个点参与变换,允许更复杂的几何操作。主要将源多边形的多个点通过变换映射到目标多边形的相应点。通过指定多个点,Matrix 会计算出从源点到目标点的变换矩阵。
Matrix matrix = new Matrix();
float[] src = {0f, 0f, 100f, 0f}; // 源多边形的两个点:两个点为 (0, 0) 和 (100, 0)
float[] dst = {0f, 0f, 150f, 50f}; // 目标多边形的两个点:目标点为 (0, 0) 和 (150, 50
matrix.setPolyToPoly(src, 0, dst, 0, 2); // 使用两个点进行变换。srcIndex 和 dstIndex 都为 0,表示从第一个点开始使用。pointCount 为 2,表示我们使用两个点来计算变换
- 参数解析:
- src:源点数组,包含源多边形的所有点的坐标。这个数组按顺序存储点的坐标信息,数组的长度应该是
2 * n
,n 是点的数量。每个点由两个连续的浮点数表示,分别是 x 和 y 坐标。例如:src = {x1, y1, x2, y2, x3, y3, ...}
- srcIndex:源点数组的起始索引,从这个位置开始读取源点数据。例如:如果
srcIndex = 0
,则从 src 数组的第一个点开始使用;如果srcIndex = 2
,则从数组中的第三个浮点数(即第二个点的 x 坐标)开始使用 - dst:目标点数组,包含目标多边形的所有点的坐标。这个数组与 src 类似,也按顺序存储目标多边形的坐标。Matrix 会根据这些点计算变换矩阵,将源多边形的点映射到这些目标点上
- dstIndex:目标点数组的起始索引,从这个位置开始读取目标点数据。例如:如果
dstIndex = 0
,则从 dst 数组的第一个点开始使用;如果dstIndex = 2
,则从数组中的第三个浮点数(即第二个点的 x 坐标)开始使用 - pointCount:参与变换的点的数量。pointCount 决定了 src 和 dst 中的点有多少个会被用于计算变换矩阵。最多支持 4 个点的变换,具体的点数决定了变换的复杂程度:
- 1个点:只能实现平移
- 2个点:可以实现平移和缩放(单轴或双轴缩放)
- 3个点:可以实现平移、缩放和旋转
- 4个点:可以实现平移、缩放、旋转和任意形变(透视变换)
- src:源点数组,包含源多边形的所有点的坐标。这个数组按顺序存储点的坐标信息,数组的长度应该是
13. matrix.getValues(float[] values)
将当前 Matrix 对象中的 9 个值(即 3x3 矩阵的所有元素)复制到指定的浮点数数组中。3X3的结构如下:
[ scaleX, skewX, transX ]
[ skewY, scaleY, transY ]
[ 0, 0, 1 ]
这是最关键的部分。传入的数组 values
的 9 个元素(索引 0 到 8)分别对应矩阵的以下位置:
数组索引 (index) | 矩阵中的含义 | 简写 | 对应的变换 |
---|---|---|---|
values[0] | MSCALE_X | a | X 轴缩放 |
values[1] | MSKEW_X | b | X 轴错切 |
values[2] | MTRANS_X | c | X 轴平移 |
values[3] | MSKEW_Y | d | Y 轴错切 |
values[4] | MSCALE_Y | e | Y 轴缩放 |
values[5] | MTRANS_Y | f | Y 轴平移 |
values[6] | MPERSP_0 | g | 透视变换 (通常为0) |
values[7] | MPERSP_1 | h | 透视变换 (通常为0) |
values[8] | MPERSP_2 | i | 透视变换 (通常为1) |
更直观的矩阵视图:
[ values[0] values[1] values[2] ]
[ values[3] values[4] values[5] ]
[ values[6] values[7] values[8] ]
二、demo
接下来,我们通过一个demo程序讲解下,上面方法的用法。展示图如下
平移功能:
放大缩小功能:限制了范围 30%-300%
至于撤销,反撤销与本章无关,并且这个demo里的撤销、反撤销功能也不完善。接下来直接直接上代码。
(1)activity_test16.xml
布局代码
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:background="@drawable/beijing"android:layout_height="match_parent"><LinearLayoutandroid:id="@+id/ll_test16"android:orientation="vertical"android:layout_width="match_parent"android:layout_height="match_parent"></LinearLayout><LinearLayoutandroid:orientation="horizontal"android:layout_width="wrap_content"android:layout_height="wrap_content"><Buttonandroid:id="@+id/bt_test16_model"android:text="画画"android:layout_margin="1dp"android:layout_width="65dp"android:layout_height="wrap_content"/><Buttonandroid:layout_margin="1dp"android:id="@+id/bt_test16_clear"android:text="清空"android:layout_width="65dp"android:layout_height="wrap_content"/><Buttonandroid:layout_margin="1dp"android:id="@+id/bt_test16_cancel"android:text="撤销"android:layout_width="65dp"android:layout_height="wrap_content"/><Buttonandroid:layout_margin="1dp"android:id="@+id/bt_test16_revoke"android:text="反撤销"android:layout_width="80dp"android:layout_height="wrap_content"/></LinearLayout><TextViewandroid:id="@+id/tv_16"android:layout_marginTop="30dp"android:layout_gravity="end"android:text="100%"android:textStyle="bold"android:textColor="@color/black"android:layout_width="40dp"android:layout_height="wrap_content"/></FrameLayout>
(2)Test16Activity.java
public class Test16Activity extends Activity {private Button bt_model;private Button bt_cancel,bt_revoke,bt_clear;private DrawView_Canvas drawView;private TextView tv_16;private Toast mToast;private Handler handler;@SuppressLint("ShowToast")@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_test16);LinearLayout line = findViewById(R.id.ll_test16);bt_model = findViewById(R.id.bt_test16_model);bt_clear = findViewById(R.id.bt_test16_clear);bt_cancel = findViewById(R.id.bt_test16_cancel);bt_revoke = findViewById(R.id.bt_test16_revoke);tv_16 = findViewById(R.id.tv_16);//使用testView显示百分比//一般使用一个要经常变化的数据,一般使用什么东西????先试试handlehandler = new Handler(msg -> {if(msg.what == 0x101){//将一个float的值传入tv_16.setText(FTOString((Float) msg.obj));if(mToast == null) {System.out.println("AAAAAAAAAAAAAA 1:"+msg.obj);mToast = Toast.makeText(Test16Activity.this,"当前比例:"+FTOString((Float) msg.obj) , Toast.LENGTH_SHORT);}else {System.out.println("AAAAAAAAAAAAAA 2:"+msg.obj);mToast.setText("当前比例:"+FTOString((Float) msg.obj));mToast.setDuration(Toast.LENGTH_SHORT);}mToast.show();}return false;});DisplayMetrics displayMetrics = new DisplayMetrics();//获取创建的宽度和高度getWindowManager().getDefaultDisplay().getRealMetrics(displayMetrics);//创建一个DrawView 宽高一致drawView = new DrawView_Canvas(Test16Activity.this,displayMetrics.widthPixels,displayMetrics.heightPixels,handler);line.addView(drawView);bt_clear.setOnClickListener(v-> drawView.clear());bt_cancel.setOnClickListener(v-> drawView.revoked());bt_revoke.setOnClickListener(v-> drawView.unRevoked());bt_model.setOnClickListener(v->{if(bt_model.getText()=="拖拽"){bt_model.setText("画画");drawView.mModel=1;}else {bt_model.setText("拖拽");drawView.mModel=2;}});//--------------handle最新使用方法
// handler = new Handler(new Handler.Callback() {
// @Override
// public boolean handleMessage(@NonNull Message msg) {
// return false;
// }
// });}private String FTOString(float f) {String s = "";int i;f=f*100f;i = (int) f;if(i<30){i=30;}else if(i>300){i=300;}s=i+"%";return s;}
}
通过 handler 传递放大缩小的比例消息,使用Toast显示的同时修改文字百分比
(3)DrawView_Canvas.java
public class DrawView_Canvas extends View {private float[] mStartXs = new float[20];private float[] mStartYs = new float[20];private PaintDates_Canvas[] Paints = new PaintDates_Canvas[20];private ArrayList<PaintDates_Canvas> mPaintedList = new ArrayList<>();private ArrayList<PaintDates_Canvas> mRevokedList = new ArrayList<>();private final int REVOKE = 1;private final int UN_REVOKE = 2;//private Path path = new Path();private Path[] paths = new Path[20];//其他的路径Paint paint = new Paint(Paint.DITHER_FLAG);private Bitmap cacheBitmap;private Canvas cacheCanvas;//为了保障大小来个总的private Matrix matrixMain = new Matrix();//在来一个中介private Matrix matrix=new Matrix();private Matrix savedMatrix=new Matrix();float[] mainDate = new float[9]; //说白了只获取一个数据float[] runDate = new float[9];float nowBL = 1.0f;//数据的范围在0.3到3static final int NONE = 0;static final int DRAG = 1;static final int ZOOM = 2;int mode = NONE;//精度为f的点PointF start = new PointF();PointF mid = new PointF();PointF midStart = new PointF();float oldDist = 1f;//求两指操作时间距离private float spacing(MotionEvent event) {float x = event.getX(0) - event.getX(1);float y = 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);}public int mModel = 1;//默认为先画画static final int HUA_HUA = 1;static final int CAO_ZUO = 2;private Handler handler;public DrawView_Canvas(Context context , int width, int height,Handler handler){super(context);if(cacheCanvas == null){cacheBitmap = Bitmap.createBitmap(width,height,Bitmap.Config.ARGB_8888);cacheCanvas = new Canvas(cacheBitmap);}paint.setColor(Color.RED);//设置画笔的风格paint.setStyle(Paint.Style.STROKE);paint.setStrokeJoin(Paint.Join.ROUND);paint.setStrokeCap(Paint.Cap.ROUND);paint.setStrokeWidth(12);//反锯齿paint.setAntiAlias(true);paint.setDither(true);matrix.set(getMatrix());matrixMain.set(getMatrix());//将每个path都实例化for (int i = 0; i < 20; i++) {paths[i] = new Path();}this.handler = handler;}@Overridepublic boolean onTouchEvent(MotionEvent event) {switch (event.getActionMasked()){case MotionEvent.ACTION_DOWN://保存原始状态if(mModel ==HUA_HUA){// 从前一个点绘制到当前点之后,把当前点定义成下次绘制的前一个点paths[0].moveTo(event.getX(0),event.getY(0));mStartXs[0] = event.getX(0);mStartYs[0] = event.getY(0);//这里没画上去的原因:因为目前保存线的那个类没有画点的功能//cacheCanvas.drawPoint(x,y,paint);//保存画笔//mPaintedList.add(new PaintDates_Canvas(new Paint(paint),new Path(path)));Paints[0] = new PaintDates_Canvas(new Paint(paint),new Path(paths[0]));}else {savedMatrix.set(matrix);start.set(event.getX(),event.getY());mode=DRAG;}break;case MotionEvent.ACTION_POINTER_DOWN:if(mModel == HUA_HUA){//获取手指的个数//System.out.println("AAAAAAAAAAAAAAAAA 有几个点"+event.getPointerCount());for (int i = 1; i < event.getPointerCount(); i++) {int pointerId = event.getPointerId(i);if(Paints[pointerId] == null){paths[pointerId].moveTo(event.getX(i),event.getY(i));mStartXs[pointerId] = event.getX(i) ;//获取手指落下的x坐标mStartYs[pointerId] = event.getY(i);//获取手指落下的y坐标Paints[pointerId] = new PaintDates_Canvas(new Paint(paint),new Path(paths[pointerId]));}}}else {oldDist = spacing(event);if(oldDist>10f){savedMatrix.set(matrix);//设置mid点midPoint(midStart,event); //当个起点midPoint(mid,event);//模式设置为缩放mode=ZOOM;//缩放也可以拖拽}}break;case MotionEvent.ACTION_POINTER_UP:if(mModel == CAO_ZUO){mode=NONE;}else {int pointerId = event.getPointerId(event.getActionIndex());if(Paints[pointerId]!=null){ ////???????mPaintedList.add(new PaintDates_Canvas(Paints[pointerId].mPaint,Paints[pointerId].mPath));}paths[pointerId].reset();Paints[pointerId] = null;}break;case MotionEvent.ACTION_MOVE:if(mModel == HUA_HUA){// 从前一个点绘制到当前点之后,把当前点定义成下次绘制的前一个点float cx;float cy;System.out.println("AAAAAAAAAAA HS:" + +event.getHistorySize());for (int i = 0; i < event.getPointerCount(); i++) {int pointerId = event.getPointerId(i);cx = (event.getX(i)+mStartXs[pointerId])/2f;cy = (event.getY(i)+mStartYs[pointerId])/2f;if(Paints[pointerId] != null){ ////????是因为相应的太多劈叉了?Paints[pointerId].mPath.quadTo(mStartXs[pointerId],mStartYs[pointerId],cx,cy);}//查看出问题的idmStartXs[pointerId] = event.getX(i);mStartYs[pointerId] = event.getY(i);}}else {if(mode==DRAG){matrix.set(savedMatrix);matrix.postTranslate(event.getX()-start.x, event.getY()-start.y);}else if(mode==ZOOM){float newDist=spacing(event);if(newDist>10){//防止抖动if(Math.abs(newDist-oldDist)>1f){matrix.set(savedMatrix);//两者的比midPoint(mid,event);float scale=newDist/oldDist;//比例+基点//两者的顺序不一样最后导致的结果也不同(注意)先位移在缩放matrix.postTranslate(mid.x-midStart.x, mid.y-midStart.y);matrixMain.getValues(mainDate);//在此需要判断一下是否在缩放范围if(nowBL<0.3f){if(scale>1){matrix.postScale(scale, scale, mid.x, mid.y);}else {matrix.postScale((0.3f/mainDate[0]), (0.3f/mainDate[0]), mid.x, mid.y);}}else if(nowBL>3f){if(scale<=1){matrix.postScale(scale, scale, mid.x, mid.y);}else {matrix.postScale((3f/mainDate[0]), (3f/mainDate[0]), mid.x, mid.y);}}else {matrix.postScale(scale, scale, mid.x, mid.y);//首先排除掉两个临界的错误值(变换后的值)matrix.getValues(runDate);nowBL = mainDate[0]*runDate[0];if(nowBL<0.3f){matrix.reset();matrix.postTranslate(mid.x-midStart.x, mid.y-midStart.y);matrix.postScale((0.3f/mainDate[0]), (0.3f/mainDate[0]), mid.x, mid.y);}else if(nowBL>3f){matrix.reset();matrix.postTranslate(mid.x-midStart.x, mid.y-midStart.y);matrix.postScale((3f/mainDate[0]), (3f/mainDate[0]), mid.x, mid.y);//这块部分有个bug://放大的过程中,到达300%时出现了短暂的偏移。(问题很小就这样吧)}}matrix.getValues(runDate);nowBL = mainDate[0]*runDate[0];Message m = this.handler.obtainMessage();m.what = 0x101;m.obj = nowBL;this.handler.sendMessage(m);//到达临界值是可能传入不在规定范围内的值(所以要限定)}}}}break;case MotionEvent.ACTION_UP:if(mModel==HUA_HUA){//保存路径//path.reset();
// mPaintedList.add(new PaintDates_Canvas(Paints[0].mPaint,Paints[0].mPath));
// Paints[0] = null;
// paths[0].reset();int pointerId = event.getPointerId(event.getActionIndex());if(Paints[pointerId]!=null){ ////????mPaintedList.add(new PaintDates_Canvas(Paints[pointerId].mPaint,Paints[pointerId].mPath));}paths[pointerId].reset();Paints[pointerId] = null;}else {//每个都循环一次保存一次状态for (int i = 0; i <mPaintedList.size() ; i++) {mPaintedList.get(i).mMatrixS.add(new MatrixDate(new Matrix(matrix),0,0,0)) ;}//每次使用的时候都保存一下matrixMain.postConcat(matrix);//保存存完之后应该清空matrix.reset();}break;}invalidate();return true;}@SuppressLint("DrawAllocation")@Overrideprotected void onDraw(Canvas canvas) {cacheCanvas.drawColor(0,PorterDuff.Mode.CLEAR); //清空画布//清空的原因:因为在移动的时候会画很多重叠。//临时画一下for(PaintDates_Canvas paint1:Paints){if(mModel==HUA_HUA){if(paint1!=null){paint1.draw(canvas);}}}for(PaintDates_Canvas paint1:mPaintedList){if(mModel==HUA_HUA){Matrix matrix_ls = null;cacheCanvas.save();for (int i = 0; i < paint1.mMatrixS.size(); i++) {if(i==0){matrix_ls =new Matrix(paint1.mMatrixS.get(i).mMatrix);}else {matrix_ls.postConcat(paint1.mMatrixS.get(i).mMatrix);}}if(matrix_ls != null){cacheCanvas.setMatrix(matrix_ls);}paint1.draw(cacheCanvas);cacheCanvas.restore();}else {Matrix matrix_ls = null;cacheCanvas.save();for (int i = 0; i < paint1.mMatrixS.size(); i++) {if(i==0){matrix_ls =new Matrix(paint1.mMatrixS.get(i).mMatrix);}else {matrix_ls.postConcat(paint1.mMatrixS.get(i).mMatrix);}}if(matrix_ls != null){matrix_ls.postConcat(matrix); //实现动态的加载//matrix_ls.preConcat(matrix);cacheCanvas.setMatrix(matrix_ls);}else {cacheCanvas.setMatrix(matrix);}paint1.draw(cacheCanvas);cacheCanvas.restore();}}canvas.drawBitmap(cacheBitmap,0f,0f,null);}public void clear(){mRevokedList.clear();mPaintedList.clear();cacheCanvas.drawColor(0,PorterDuff.Mode.CLEAR);invalidate();}//撤销public void revoked(){reDraw(mPaintedList,REVOKE);}//反撤销public void unRevoked(){reDraw(mRevokedList,UN_REVOKE);}//重新绘图private void reDraw(ArrayList<PaintDates_Canvas> List, int type) {if(List.size() > 0){//获取并删除PaintDates_Canvas paint = List.get(List.size()-1);List.remove(List.size()-1);if(type == REVOKE){mRevokedList.add(paint);System.out.println("AAAAAAAAAA"+" mPaintedList:"+List.size()+" mRevokedList"+mRevokedList.size());}else {mPaintedList.add(paint);System.out.println("BBBBBBBBBB"+" mPaintedList"+ mPaintedList.size()+" mRevokedList:"+List.size());}//清空缓存画板cacheCanvas.drawColor(0,PorterDuff.Mode.CLEAR);invalidate();}}}
matrixMain
:记录所有历史变换的累积结果(即当前画布的总变换状态)。
matrix
:用于处理当前正在进行的变换(例如用户正在拖拽或缩放时的微小变换)。
savedMatrix
:在每次触摸事件开始时保存当前matrix
的状态,以便基于该状态进行增量变换(如postTranslate
或postScale
)。
处理触摸事件(onTouchEvent
)
在
ACTION_DOWN
或ACTION_POINTER_DOWN
时,保存当前matrix
状态到savedMatrix
。在
ACTION_MOVE
中,根据当前模式(DRAG
或ZOOM
)更新matrix
:拖拽(DRAG):使用
postTranslate
实现平移。注意是先平移后缩放缩放(ZOOM):使用
postScale
实现缩放,并限制缩放比例在0.3f
到3.0f
之间。
case MotionEvent.ACTION_MOVE:if (mode == DRAG) {matrix.set(savedMatrix);matrix.postTranslate(event.getX() - start.x, event.getY() - start.y);} else if (mode == ZOOM) {// 计算缩放比例并应用float scale = newDist / oldDist;matrix.postScale(scale, scale, mid.x, mid.y);// 限制缩放范围// ...}break;
在 onDraw
中应用变换
在绘制每个
PaintDates_Canvas
时,会遍历其保存的mMatrixS
列表,依次应用所有历史变换(通过postConcat
)。如果是操作模式(
CAO_ZUO
),还会叠加当前正在进行的变换(matrix
),实现实时预览效果。
if (matrix_ls != null) {matrix_ls.postConcat(matrix); // 叠加当前变换cacheCanvas.setMatrix(matrix_ls);
} else {cacheCanvas.setMatrix(matrix);
}
(4)PaintDates_Canvas.java
public class PaintDates_Canvas {Paint mPaint;Path mPath ;//保存每一笔画的偏移ArrayList<MatrixDate> mMatrixS = new ArrayList<>();public PaintDates_Canvas(Paint mPaint, Path mPath) {this.mPaint = mPaint;this.mPath = mPath;}public void draw(Canvas canvas){canvas.drawPath(mPath,mPaint);}
}
(5)MatrixDate.java
public class MatrixDate {Matrix mMatrix;//xy的偏移量float mOffsetX;float mOffsetY;//偏移倍数float mMultiple;public MatrixDate(Matrix mMatrix, float mOffsetX, float mOffsetY, float mMultiple) {this.mMatrix = mMatrix;this.mOffsetX = mOffsetX;this.mOffsetY = mOffsetY;this.mMultiple = mMultiple;}
}