在画图白板应用中,实现图形的位移、放大和缩小是基本且重要的功能。本文将介绍如何使用 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 * nn 是点的数量每个点两个连续的浮点数表示,分别是 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个点:可以实现平移缩放旋转任意形变(透视变换)

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">

    <LinearLayout
        android:id="@+id/ll_test16"
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    </LinearLayout>

    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">

        <Button
            android:id="@+id/bt_test16_model"
            android:text="画画"
            android:layout_margin="1dp"
            android:layout_width="65dp"
            android:layout_height="wrap_content"/>

        <Button
            android:layout_margin="1dp"
            android:id="@+id/bt_test16_clear"
            android:text="清空"
            android:layout_width="65dp"
            android:layout_height="wrap_content"/>

        <Button
            android:layout_margin="1dp"
            android:id="@+id/bt_test16_cancel"
            android:text="撤销"
            android:layout_width="65dp"
            android:layout_height="wrap_content"/>
        <Button
            android:layout_margin="1dp"
            android:id="@+id/bt_test16_revoke"
            android:text="反撤销"
            android:layout_width="80dp"
            android:layout_height="wrap_content"/>

    </LinearLayout>

    <TextView
        android: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")
    @Override
    protected 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显示百分比
        //一般使用一个要经常变化的数据,一般使用什么东西????先试试handle
        handler = 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到3

    static 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;


    }

    @Override
    public 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);
                        }

                        //查看出问题的id

                        mStartXs[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")
    @Override
    protected 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;
    }
}

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐