Android绘图之PathMeasure(16)

Android 绘图学习

android绘图之Paint(1)
android绘图之Canvas基础(2)
Android绘图之Path(3)
Android绘图之drawText绘制文本相关(4)
Android绘图之Canvas概念理解(5)
Android绘图之Canvas变换(6)
Android绘图之Canvas状态保存和恢复(7)
Android绘图之PathEffect (8)
Android绘图之LinearGradient线性渐变(9)
Android绘图之SweepGradient(10)
Android绘图之RadialGradient 放射渐变(11)
Android绘制之BitmapShader(12)
Android绘图之ComposeShader,PorterDuff.mode及Xfermode(13)
Android绘图之drawText,getTextBounds,measureText,FontMetrics,基线(14)
Android绘图之贝塞尔曲线简介(15)
Android绘图之PathMeasure(16)
Android 动态修改渐变 GradientDrawable

1 PathMeasure概述

首先思考一个问题,任意绘制一条曲线,或者一个圆弧,或者一个不规则图形,又或者利用Path 同时绘制多条曲线,如何获取某个点的坐标,曲线的方向,对于简单的图形根据已知内容很容易得到坐标,对于类似贝塞尔曲线或者不规则图形想要或者坐标,角度信息很困难,今天就来讲解Path中用于获取上述信息的一个工具类。

PathMeasure是一个用来测量Path的工具类,可以从Path中获取固定位置的坐标,横切信息。

构造函数:

/**
 * Create an empty PathMeasure object. To uses this to measure the length
 * of a path, and/or to find the position and tangent along it, call
 * setPath.
  */
public PathMeasure() ;
/**
 * Create a PathMeasure object associated with the specified path object
 * (already created and specified). The measure object can now return the
 * path's length, and the position and tangent of any position along the
 * path.
 *
 * Note that once a path is associated with the measure object, it is
 * undefined if the path is subsequently modified and the the measure object
 * is used. If the path is modified, you must call setPath with the path.
 *
 * @param path The path that will be measured by this object
 * @param forceClosed If true, then the path will be considered as "closed"
 *        even if its contour was not explicitly closed.
 */
public PathMeasure(Path path, boolean forceClosed) ;

主要有两个构造函数:
PathMeasure()
无参构造函数,构造一个空的PathMeasure,想要使用必须和一个Path进行关联,可以利用setPath函数和Path设置关联。如果关联的Path被修改了,也需要重新调用setPath进行关联。
PathMeasure(Path path, boolean forceClosed)
参数说明:
path:关联的Path,如果想要重新关联新的path,可以调用函数setPath
forceClosed:设置测量Path时是否强制闭合path(有的Path不是闭合的,如果为true,那么测量时就会测量闭合的path,所以使用时要注意,forceClose为true时测量的长度可能更大。如果关联的Path被修改了,也需要重新调用setPath进行关联。

所以可以得出:
设置关联的Path之后,Path就不会再修改,就算原Path修改了,引用的Path也不会修改,想要应用原Path的修改就需要重新setPath。

forceClosed 代码示例

public class ViewDemo25 extends View {
    private Path mPath;
    private Paint mPaint1;
    private Paint mPaint2;
    public ViewDemo25(Context context) {
        this(context,null,0);
    }

    public ViewDemo25(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs,0);
    }

    public ViewDemo25(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        mPaint1 = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint1.setStrokeWidth(7);
        mPaint1.setStyle(Paint.Style.STROKE);
        mPaint1.setColor(Color.RED);

        mPaint2 = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint2.setStrokeWidth(7);
        mPaint2.setColor(Color.RED);
        mPath = new Path();
        mPath.moveTo(100,100);
        mPath.lineTo(600,300);
        mPath.lineTo(300,500);
        PathMeasure pathMeasure1 = new PathMeasure(mPath, true);
        PathMeasure pathMeasure2 = new PathMeasure(mPath, false);
        System.out.println("===========pathMeasure1========"+pathMeasure1.getLength());
        System.out.println("===========pathMeasure2========"+pathMeasure2.getLength());

    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawPath(mPath,mPaint1);
    }
}

结果值:
===========pathMeasure1========1346.2852
===========pathMeasure2========899.0716
Path是个非闭合曲线,forceClose为true获取到的Path长度明显大于forceClose为flase时的值。

2 PathMeasure 方法说明

  • getLength: 用于获取 Path 的总长度,
  • getMatrix(float distance,Matrix matrix,int flags):获取指定长度的位置(distance)坐标及该点Matrix
  • getPosTan(float distance,float[] pos,float[] tan):获取指定位置的坐标和tan值,tan值代表曲线方向,也就是切线值,
  • getSegment(float startD,float stopD,Path dst,boolean startWithMoveTo):获取两点之间的path片段,
  • setPath 设置 PathMeasure 与 Path 关联。
  • isClosed :用于判断 Path 是否闭合,但需要注意一点,它不单单指Path本身的闭合,还指如果 关联 Path 的时候设置 forceClosed 为 true 的话,方法也返回true。
  • nextContour():获取下一条曲线的Path,因为Path可能包含多个相互之间不连接的Path。

3 setpath(Path path ,boolean forceClosed)

设置PathMeasure关联的Path,如果已设置的Path改变了,需要重新调用setPath,已设置的Path不会更新。

forceClosed:设置测量Path时是否强制闭合path(有的Path不是闭合的,如果为true,那么测量时就会测量闭合的path,所以使用时要注意,forceClose为true时测量的长度可能更大。
无论传入的forceClosed为true或者false,都不会影响原Path的绘制。

代码示例:

private void init() {
    mPaint1 = new Paint(Paint.ANTI_ALIAS_FLAG);
    mPaint1.setStrokeWidth(7);
    mPaint1.setStyle(Paint.Style.STROKE);
    mPaint1.setColor(Color.RED);

    mPaint2 = new Paint(Paint.ANTI_ALIAS_FLAG);
    mPaint2.setStrokeWidth(7);
    mPaint2.setColor(Color.RED);
    mPath = new Path();
    mPath.moveTo(100,100);
    mPath.lineTo(600,300);
    mPath.lineTo(300,500);
    PathMeasure pathMeasure1 = new PathMeasure();
    pathMeasure1.setPath(mPath, true);
    PathMeasure pathMeasure2 = new PathMeasure();
    pathMeasure2.setPath(mPath, false);
    System.out.println("===========pathMeasure1===1111====="+pathMeasure1.getLength());
    System.out.println("===========pathMeasure2====2222===="+pathMeasure2.getLength());

    mPath.lineTo(200,400);
    System.out.println("===========pathMeasure1===33333====="+pathMeasure1.getLength());
    System.out.println("===========pathMeasure2====4444===="+pathMeasure2.getLength());

    pathMeasure1.setPath(mPath, true);
    pathMeasure2.setPath(mPath, false);

    System.out.println("===========pathMeasure1===55555====="+pathMeasure1.getLength());
    System.out.println("===========pathMeasure2====66666===="+pathMeasure2.getLength());

}

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawPath(mPath,mPaint1);
}

结果:
===========pathMeasure1===1111=====1346.2852
===========pathMeasure2====2222====899.0716
修改path后没有调用setPath的结果:
===========pathMeasure1===33333=====1346.2852
===========pathMeasure2====4444====899.0716
修改path后调用setPath的结果:
===========pathMeasure1===55555=====1356.7207
===========pathMeasure2====66666====1040.4929

上面的例子首先测试了forceClosed 为true和false时的不同,接着测试了在生成PathMeasure实例后,修改关联的Path,再次获取PathMeasure信息,信息没有改变,之后重新调用setPath后,信息才改变。

所以如果测量过程中PathMeasure关联的Path发生改变,需要重新调用setPath方法进行关联。

4 nextContour

/**
 * Move to the next contour in the path. Return true if one exists, or
 * false if we're done with the path.
 */
public boolean nextContour();

由于后续需要,先讲解nextContour,看到这个函数取下一条曲线,也就是说明了一个Path可以有多条曲线。默认情况 getLength , getSegment 等PathMeasure方法,都只会在其中第一条线段上运行, nextContour 会跳转到下一条曲线到方法,如果跳转成功,则返回 true, 如果跳转失败,则返回 false。

代码示例:

mPath = new Path();
mPath.moveTo(100,100);
mPath.lineTo(600,300);
mPath.lineTo(300,500);

mPath.addRect(400,400,900,900, Path.Direction.CW);
PathMeasure pathMeasure = new PathMeasure();
pathMeasure.setPath(mPath,false);
System.out.println("======getLength1111===="+pathMeasure.getLength());
if (pathMeasure.nextContour()){
    System.out.println("======getLength2222===="+pathMeasure.getLength());
}

输出结果:
======getLength1111====899.0716
======getLength2222====2000.0
nextContour()返回true,移动到下一条曲线成功,使PathMeasure的方法作用于第二条曲线上。

5 getLength

/**
 * Return the total length of the current contour, or 0 if no path is
 * associated with this measure object.
 */
public float getLength() 

返回当前轮廓的总长度,根据nextContour()的介绍,知道返回的长度默认只是对第一条曲线的长度的计算。如果想要计算Path中所有曲线的总长度,需要循环调用nextContour,然后计算每条线段的长度,然后相加得到。

6 isClosed

用于判断 Path 是否闭合,但需要注意一点,它不单单指Path本身的闭合,还指如果 关联 Path 的时候设置 forceClosed 为 true 的话,方法也返回true。

mPath = new Path();
mPath.moveTo(100,100);
mPath.lineTo(600,300);
mPath.lineTo(300,500);

mPath.addRect(400,400,900,900, Path.Direction.CW);
PathMeasure pathMeasure = new PathMeasure();
pathMeasure.setPath(mPath,false);
System.out.println("======isclosed111===="+pathMeasure.isClosed());
if (pathMeasure.nextContour()){
    System.out.println("======isclosed222===="+pathMeasure.isClosed());
}

setPath是forceClosed传入false,结果为:
======isclosed111====false
======isclosed222====true

如果forceClosed传入true:
======isclosed111====true
======isclosed222====true

所以isClosed判断曲线是否闭合受forceClosed的影响。

7 getSegment

getSegment获取Path中的一段Path。

/**
 * Given a start and stop distance, return in dst the intervening
 * segment(s). If the segment is zero-length, return false, else return
 * true. startD and stopD are pinned to legal values (0..getLength()).
 * If startD >= stopD then return false (and leave dst untouched).
 * Begin the segment with a moveTo if startWithMoveTo is true.
 *
 * <p>On {@link android.os.Build.VERSION_CODES#KITKAT} and earlier
 * releases, the resulting path may not display on a hardware-accelerated
 * Canvas. A simple workaround is to add a single operation to this path,
 * such as <code>dst.rLineTo(0, 0)</code>.</p>
 */
public boolean getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo) ;

参数说明:
startD,stopD:开始截取和结束截取的位置,两个参数都是距离Path起点的长度。
如果startD 大于stopD直接返回false,如果截取的长度等于0直接返回false,startD和stopD都大于0小于Path的总长度。
dst:如果截取成功,截取到的Path将被加入到dst中,不会影响dst中原有的曲线。
startWithMoveTo:起始点是否使用moveTo,如果startWithMoveTo为true,截取的Path放入dst时将会使用moveto,这样可以保证截取的Path起始点不变化。

返回值:返回true表示截取片段成功,返回false表示不成功,不操作dst。

注意:如果系统版本低于等于4.4,默认开启硬件加速的情况下,更改 dst 的内容后可能绘制会出现问题,请关闭硬件加速或者给 dst 添加一个单个操作,例如: dst.rLineTo(0, 0)

首先测试截取成功,是替换dst,还是把截取到的Path添加到dst中。

原始图形:

private void init() {
    mPaint1 = new Paint(Paint.ANTI_ALIAS_FLAG);
    mPaint1.setStrokeWidth(7);
    mPaint1.setStyle(Paint.Style.STROKE);
    mPaint1.setColor(Color.BLUE);

    mPaint2 = new Paint(Paint.ANTI_ALIAS_FLAG);
    mPaint2.setStrokeWidth(7);
    mPaint2.setStyle(Paint.Style.STROKE);
    mPaint2.setColor(Color.RED);

    dst = new Path();
    dst.moveTo(300,500);
    dst.lineTo(700,700);

    mPath = new Path();
    mPath.addRect(200,200,900,900, Path.Direction.CW);
    PathMeasure pathMeasure = new PathMeasure();
    pathMeasure.setPath(mPath,true);
}

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawPath(mPath,mPaint1);
    canvas.drawPath(dst,mPaint2);
}

想要截取右下角的Path,示意图如下:


上面的图形是个正方形,开始绘制的点(200,200),每个边长700,所以截取的segment开始点距离Path的开始点为700+350=1050,结束点为1400+350=1750。

private void init() {
    mPaint1 = new Paint(Paint.ANTI_ALIAS_FLAG);
    mPaint1.setStrokeWidth(7);
    mPaint1.setStyle(Paint.Style.STROKE);
    mPaint1.setColor(Color.BLUE);

    mPaint2 = new Paint(Paint.ANTI_ALIAS_FLAG);
    mPaint2.setStrokeWidth(7);
    mPaint2.setStyle(Paint.Style.STROKE);
    mPaint2.setColor(Color.RED);

    dst = new Path();
    dst.moveTo(300,500);
    dst.lineTo(700,700);

    mPath = new Path();
    mPath.addRect(200,200,900,900, Path.Direction.CW);
    PathMeasure pathMeasure = new PathMeasure();
    pathMeasure.setPath(mPath,true);

    pathMeasure.getSegment(1050, 1750, dst, true);

}

截取成功,然后添加截取到的Path到dst,dst中原来的Path完全没有受影响。

如果设置startWithMoveTo为false:

可以看到如果startWithMoveTo 为 false,获取到的segment片段,加入到dst时,会将片段的起始点移动到 dst 的最后一个点,以保证 dst 的连续性,简单点说会忽略截取到的Path的第一个点替换成dst内部已有的最后一个点。

总结:
如果 startWithMoveTo 为 true, 获取到的片段Path会因为调用了moveTo保持原状,如果 startWithMoveTo 为 false,则会将截取出来的 Path 片段的起始点移动到 dst 的最后一个点,以保证 dst 的连续性。

8 getPosTan

/**
 * Pins distance to 0 <= distance <= getLength(), and then computes the
 * corresponding position and tangent. Returns false if there is no path,
 * or a zero-length path was specified, in which case position and tangent
 * are unchanged.
 *
 * @param distance The distance along the current contour to sample
 * @param pos If not null, returns the sampled position (x==[0], y==[1])
 * @param tan If not null, returns the sampled tangent (x==[0], y==[1])
 * @return false if there was no path associated with this measure object
*/
public boolean getPosTan(float distance, float pos[], float tan[])

获取Path路径上距离起点位置distance距离的坐标信息和tan值。

参数说明:
distance:距离Path起点的位置长度,
pos:获取该点的坐标值
tan:改点的正切值,tan0是邻边边长,tan1是对边边长

用ps钢笔工具自己画个箭头。


代码示例:

private void init() {
    mPaint1 = new Paint(Paint.ANTI_ALIAS_FLAG);
    mPaint1.setStrokeWidth(7);
    mPaint1.setStyle(Paint.Style.STROKE);
    mPaint1.setColor(Color.BLUE);

    mPaint2 = new Paint(Paint.ANTI_ALIAS_FLAG);
    mPaint2.setStrokeWidth(7);
    mPaint2.setStyle(Paint.Style.STROKE);
    mPaint2.setColor(Color.RED);

    dst = new Path();
    dst.moveTo(300,500);
    dst.lineTo(700,700);

    mPath = new Path();
    mPath.addCircle(500,500,300, Path.Direction.CW);
    PathMeasure pathMeasure = new PathMeasure();
    pathMeasure.setPath(mPath,false);
    float zhouchang = (float) (2*Math.PI*300);
    System.out.println("=======circle周长========"+ zhouchang);
    float[] pos = new float[2];
    float[] tan = new float[2] ;
    pathMeasure.getPosTan(zhouchang*3/4,pos,tan);
    System.out.println("=========pos==========="+pos[0]+"  "+pos[1]);
    System.out.println("=========tan==========="+tan[0]+"  "+tan[1]);
}

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawPath(mPath,mPaint1);
    canvas.drawPoint(500,500,mPaint2);
}

输出结果:有稍微的误差是由于精度问题导致
=======circle周长========1884.9556
=========pos===========500.56152 200.00053
=========tan===========0.9999983 0.0018716537

tan的值有什么用呢,我们可以利用切线值得到当前线段的方向,也就是倾斜的角度。

float degrees = (float) (Math.atan2(tan[1], tan[0]) * 180.0 / Math.PI);
Math中 atan2 可以根据正切值得到角度值(也可以自己计算)弧度,然后将弧度转换为具体的度数。

如果不利用获取到的tan,直接根据坐标点绘制图片

private void init() {
    mPaint1 = new Paint(Paint.ANTI_ALIAS_FLAG);
    mPaint1.setStrokeWidth(7);
    mPaint1.setStyle(Paint.Style.STROKE);
    mPaint1.setColor(Color.BLUE);

    mPaint2 = new Paint(Paint.ANTI_ALIAS_FLAG);
    mPaint2.setStrokeWidth(7);
    mPaint2.setStyle(Paint.Style.STROKE);
    mPaint2.setColor(Color.RED);

    dst = new Path();
    dst.moveTo(300,500);
    dst.lineTo(700,700);
    jiantou = BitmapFactory.decodeResource(getResources(),R.drawable.jiantou);

    mPath = new Path();
    mPath.addCircle(500,500,300, Path.Direction.CW);
    final PathMeasure pathMeasure = new PathMeasure();
    pathMeasure.setPath(mPath,false);
    final float zhouchang = (float) (2*Math.PI*300);
    System.out.println("=======circle周长========"+ zhouchang);
    float[] pos = new float[2];
    float[] tan = new float[2] ;
    pathMeasure.getPosTan(zhouchang*3/4,pos,tan);
    System.out.println("=========pos==========="+pos[0]+"  "+pos[1]);
    System.out.println("=========tan==========="+tan[0]+"  "+tan[1]);

    ValueAnimator valueAnimator = ValueAnimator.ofFloat(0,zhouchang);
    valueAnimator.setDuration(10000);
    valueAnimator.setInterpolator(new LinearInterpolator());
    valueAnimator.setRepeatCount(-1);
    valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {

            float correntLen = (float) animation.getAnimatedValue();
            float[] pos = new float[2];
            float[] tan = new float[2] ;
            pathMeasure.getPosTan(correntLen,pos,tan);
            posX = pos[0];
            posY = pos[1];
            invalidate();
        }
    });

    valueAnimator.start();
}

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawPath(mPath,mPaint1);
    canvas.drawPoint(500,500,mPaint2);
      canvas.drawBitmap(jiantou,posX,posY,mPaint2);
}

图片从左上角开始绘制,左上角会沿着坐标旋转,看着很不舒服。

对图片进行操作:
利用获取的tan值对图片进行旋转和平移操作,注意这里的旋转操作时建立在图片的初始箭头方向为水平向右的,如果水平向左,则计算图片旋转方向时时不相同的(和绘制方向无关)。

  private void init() {
        mPaint1 = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint1.setStrokeWidth(7);
        mPaint1.setStyle(Paint.Style.STROKE);
        mPaint1.setColor(Color.BLUE);

        mPaint2 = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint2.setStrokeWidth(7);
        mPaint2.setStyle(Paint.Style.STROKE);
        mPaint2.setColor(Color.RED);

        dst = new Path();
        dst.moveTo(300,500);
        dst.lineTo(700,700);
        mMatrix = new Matrix();

        jiantou = BitmapFactory.decodeResource(getResources(),R.drawable.jiantou);

        mPath = new Path();
        mPath.addCircle(500,500,300, Path.Direction.CCW);
        final PathMeasure pathMeasure = new PathMeasure();
        pathMeasure.setPath(mPath,false);
        final float zhouchang = (float) (2*Math.PI*300);
        System.out.println("=======circle周长========"+ zhouchang);
        float[] pos = new float[2];
        float[] tan = new float[2] ;
        pathMeasure.getPosTan(zhouchang*3/4,pos,tan);
        System.out.println("=========pos==========="+pos[0]+"  "+pos[1]);
        System.out.println("=========tan==========="+tan[0]+"  "+tan[1]);

        ValueAnimator valueAnimator = ValueAnimator.ofFloat(0,zhouchang);
        valueAnimator.setDuration(10000);
        valueAnimator.setInterpolator(new LinearInterpolator());
        valueAnimator.setRepeatCount(-1);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {

                float correntLen = (float) animation.getAnimatedValue();
                float[] pos = new float[2];
                float[] tan = new float[2] ;
                pathMeasure.getPosTan(correntLen,pos,tan);
                posX = pos[0];
                posY = pos[1];
                mMatrix.reset();
                //得到当前坐标的方向趋势对应的角度
                float degrees = (float) (Math.atan2(tan[1], tan[0]) * 180.0 / Math.PI);

                //下面两个比较重要,以图片中心为旋转点对图片进行旋转,把图片中心平移到Path上
                mMatrix.postRotate(degrees, jiantou.getWidth() / 2, jiantou.getHeight() / 2);
                mMatrix.postTranslate(pos[0] - jiantou.getWidth() / 2, pos[1] - jiantou.getHeight() / 2);
                invalidate();
            }
        });

        valueAnimator.start();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawPath(mPath,mPaint1);
        canvas.drawPoint(500,500,mPaint2);
        canvas.drawBitmap(jiantou,mMatrix,mPaint2);
          }
}

9 getMatrix

/**
 * Pins distance to 0 <= distance <= getLength(), and then computes the
 * corresponding matrix. Returns false if there is no path, or a zero-length
 * path was specified, in which case matrix is unchanged.
 *
 * @param distance The distance along the associated path
 * @param matrix Allocated by the caller, this is set to the transformation
 *        associated with the position and tangent at the specified distance
 * @param flags Specified what aspects should be returned in the matrix.
 */
public boolean getMatrix(float distance, Matrix matrix, int flags) 

上面的例子利用getPosTan获取正切值,然后计算位置和旋转角度过程很麻烦,所以google提供了getmatrix直接获取变换后的Matrix矩阵供我们使用。

参数说明:
distance:距离Path起始点的距离
matrix:得到的matrix矩阵
flags:规定哪些信息添加到Matrix中,flags有两个值POSITION_MATRIX_FLAG,TANGENT_MATRIX_FLAG。POSITION_MATRIX_FLAG表示把位置信息添加到matrix中,TANGENT_MATRIX_FLAG表示把正切信息添加到matrix中。
可以分别添加POSITION_MATRIX_FLAG和TANGENT_MATRIX_FLAG,如果想同时添加这两者需要POSITION_MATRIX_FLAG|TANGENT_MATRIX_FLAG。
利用getMatrix实现箭头功能。

valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {

        float correntLen = (float) animation.getAnimatedValue();
        float[] pos = new float[2];
        float[] tan = new float[2] ;
        pathMeasure.getPosTan(correntLen,pos,tan);
        mMatrix.reset();
        pathMeasure.getMatrix(correntLen,mMatrix,PathMeasure.TANGENT_MATRIX_FLAG|PathMeasure.POSITION_MATRIX_FLAG);

        //矩阵对旋转角度默认为图片的左上角,使用 preTranslate 调整原点为图片中心
        mMatrix.preTranslate(-jiantou.getWidth() / 2, -jiantou.getHeight() / 2);

     invalidate();
    }
});

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容