株洲新程IT 教育 李赞红老师 第三章 使用Graphics2D实现动态效果

本文摘抄至 :株洲新城 IT教育 李赞红老师。非常感谢老师。想过摘抄一边的方式去让自己记住一些知识

第三章 使用Graphics2D 实现动态效果


3.1、概述

  本章讲述如何如何通过绘制实现动态效果。动态包括两个方面。一是,让画面动起来。如实现游戏中的爆照效果,地球仪的自转和公转,小鸟翅膀的摆动,手表时针、分针、秒针的运转等。这一类动画可以通过周期性重画实现。二是,实现和用户的互动,用户通过手指在手机屏幕上移动。在绘制区绘制曲线、矩形、圆、文字等图案。如绘图软件就是这种类型。   在绘图过程中,双缓存技术是一项很重要的技术;一方面能,大大提高绘图效率;另一方面,可以实现绘图过程与结果分离,拥有身临其境的用户体验。理解和掌握双缓存技术的作用和意义是真正掌握 Graphics2D 的关键性因素之一。    上一章节(Graphics2D API ) 我们在 ImageView上绘制,这只是权宜之计。本章我们定义有个继承自 View的子类,重写 onDraw(),在该方法中绘制。当View显示时会回调 onDraw()方法,这也是 View的扩展方法。用于绘制组件的外观。基本结构如下:
public class MyView extends View{
    @Override
    protected void onDraw(Canvas canvas) {
        ......
    }
}

3.2、invalidate() 方法

  View类定义了一组 invalidate()方法,该方法有好几个版本:
  • public void invalidate()
  • public void invalidate(int l, int t, int r, int b)
  • public void invalidate(Rect dirty)
  invalidate()用于重回组件,不带参数表示重绘正个视图区域。带参数表述重绘指定的区域。如果要追溯该方法的源码。大概是将重绘请求一级级往上提交到 ViewRoot,调用 ViewRoot的 scheduleTraversals()方法重新发起重绘请求。schedulTraversals()方法会发送一个异步消息,调用 preformTraversals()方法执行重绘,而 performTraversals()方法最终调用 onDraw()方法( 详见第一章:View的绘图流程)。所以,简单的来说,调用 View的 invalidate()方法就相当于调用了 onDraw()方法,而 onDraw()方法中就是我们编写的绘图代码。   如果要刷新组件或者让画面动起来,我们只需要调用 invalidate()方法即可。通过改变数据来影响绘制结果,这样实现组件刷新或实现动画的基本思路。   invalidate()方法只能在 UI线程中调用,如果是在子线线程中刷新组件,View类还定义了另一组名为 postInvalidate 的方法:
  • public void postInvalidate()
  • public void postInvalidate(int left, int top, int right, int bottom)
  现在我们编写一个案例,让小球在 View的Canvas中水平的往返移动。当小球触碰到左边边界时往右移动,小球触碰到右边边界时往左移动,循环往复。
public class BallMoveView extends View {

    /**小球的水平位置*/
    private int x ;
    /**小球的垂直位置,固定为 100*/
    private static final int Y= 100 ;
    /**小球的半径*/
    private static final int RADIUS = 30     ;
    private Paint paint ;
    private Paint paint2 ;
    /**移动的方向*/
    private boolean direction ;

    public BallMoveView(Context context, AttributeSet attrs) {
        super(context, attrs);


        paint = new Paint() ;
        paint.setAntiAlias(true) ;
        paint.setStyle(Style.FILL) ;
        paint.setColor(Color.RED);

        paint2 = new Paint() ;
        paint2.setAntiAlias(true) ;
        paint2.setStyle(Style.STROKE) ;
        paint2.setStrokeWidth(8);
        paint2.setColor(Color.BLUE);

        x = RADIUS ;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        //回去组件的宽度
        int width = this.getMeasuredWidth() ;

        //根据 x,y的坐标画小球
        canvas.drawCircle(x, Y, RADIUS, paint) ;
        canvas.drawPoints(new float[]{30,100,width-RADIUS,100}, paint2) ;
        //改变 x坐标,再 invalidate(),使小球动起来

        if(x <= RADIUS){
            direction = true ;
        }
        if(x >= width - RADIUS){
            direction = false ;
        }

        x = direction ? x +5 : x-5 ;
    }
}
  我们定义了一个 BallMoveView类,继承自 View,并重写了 ondraw()方法,该方法适用于在组件上绘图的方法,同时,我们的定义了有两个参数的构造方法。如果在布局文件中定义了该组件,则会调用此构造方法来创建对象。   在 onDraw()方法中,画布 Canvas对象自动传入。显示在 View上的内容最终都必须绘制在 canvas对象上。我们I调用 canvas对象的 drawCricle()方法回一个圆,根据 Paint对象的定义,这是一个红色的实心圆。和另外用 drawPoints()方法绘制出来,小球的起始和终止的临界值的点。未来路判断小球的移动的方法,需要判断小球的当前位移,如果小球的位置 x 值小于等于小球的半径。说明小球已达到左边边界,讲标识变量 direction赋值为 true,小球即掉头向右移动;如果 x 的值大于等于组件宽度减去小球的半径,direction赋值为 false,小球将再次向左移动。getMeasuedWidht()方法用于获取组件的宽度,与之对应的是 getMeasuredHeigth()方法,用于获取组件的高度。   自定义组件在布局文件中的标签是组件的全限定名称,并且 View类的所有属性会继承下来,即使 BallMovaView组件没有定义任何属性,android:layout_width 和 android:layout_height 等由View定义的属性依然会有。   那么,小球是如何水平移动的呢 ? 在 onDraw()方法中,我们发现该方法的最后几行代码会根据 direction来修改成员变量 x 的值,如果 direction 为 true,x 累加 5;如果 direction 为 false,x 累减 5 。正如上面所说,我们在 Activity中恰恰是通过定时器周期性调用 invalidate()方法不断重绘组件。也就是不断调用 onDraw()方法。因为小球的位置由 x 来决定,onDraw()梅调用一次,x 的值就会变化一次,小球的位置自然也会跟着一起变化,最后形成了小球的移动效果。
        BallMoveView ball = (BallMoveView) findViewById(R.id.ballview) ;

        new Timer().schedule(new TimerTask() {

            @Override
            public void run() {

                ball.postInvalidate() ;
            }
            //延时 2秒后,没有 50毫秒计时一次
        }, 2000,50);
  上面代码中,通过 Timer类定义一个计时器,延时 2000毫秒开始计时,每隔 50毫秒计时一次。定时任务 TimerTask其实就是一个子线程。所以,不能使用只能运行在 UI线程的 invalidate()方法,只能调用 postInvalidate()方法来重绘组件。   如图所示,小球的移动效果:

3.3、坐标转换

  默认情况下,画布坐标的原点就是绘图区域的左上角,向左为负,向右为正;向上为负,向下为正。但是,通过 Canvas提供的方法可以对坐标进行转换。转换的方式主要有 4种:平移、旋转、缩放 和 拉斜 :

  public void translate(float dx, float dy)
  坐标平移,在当前原点的基础上水平移动 dx 个距离,垂直移动 dy 个距离。正负发符决定方向。坐标原点改变之后,所有的坐标都是以新额原点为参照进行定位。

  下面两段代码是等效的:

  • canvas.drawPoint(10, 10, paint);
  • canvas.translate(10, 10); canvas.drawPoint(0, 0, paint)

  public void rotate(float degrees)
  将画布的坐标以当前原点为中心旋转指定的角度,如果就哦啊度为正,则为顺时针旋转,否则为逆时针旋转

  • public final void rotate(float degrees, float px, float py)
  • 以点(px,py)为中心对画布坐标进行旋转 degrees度,为 正,表示顺时针;为负,表示逆时针

  • public void scale(float sx, float sy)
  • 缩放画布的坐标,sx,sy分别是 x 方向和 y 方向的缩放比例。小于 1 表示缩放,等于 1 表示不变,大于 1 表示放大。画布缩放后,绘制在画布上的图形也会等比例缩放。

  • public final void scale(float sx, float sy, float px, float py)
  • 以(px,py)为中心对画布进行缩放

  • public void skew(float sx, float sy)
  • 将画布分别在 x 方向和 y 方向斜拉一定的角度,sx 为 x 方向倾斜角度 tan值,sy 为 y 方向 倾斜的 tan 值,比如我们打算在 x 轴方向上倾斜 45度,则 tan 45 = 1 ,写成:canvas.skew(1,0)

  坐标转换后,后面的图形绘制功能将跟随新坐标,转换前已经绘制的图形不会有任何的变化。另外,为了能恢复到坐标之前的状态,Canvas
定义了两个方法用于保存现场和恢复现场:

  • public int save()
  • 保存现场

  • public void restore()
  • 恢复现场到 save()执行之前额状态

  下面的案例使用演示坐标的平移、旋转、缩放三种变换的使用。通过平移,我们沿 45度方向绘制出一系列额正方形;通过缩放,我们绘制一个万花筒;通过旋转,绘制一个手表四周的刻度

public class CoordinateView extends View {

    public CoordinateView(Context context, AttributeSet attrs) {
        super(context, attrs);

    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        Paint paint = new  Paint() ;
        paint.setAntiAlias(true) ;
        paint.setColor(Color.RED) ;
        paint.setStrokeWidth(2) ;
        paint.setStyle(Style.STROKE) ;

        //保存现场
        canvas.save() ;

        for (int i = 0; i <10; i++) {
            canvas.drawRect(0, 0,150,50,paint) ;
            canvas.translate(20, 20) ;
        }

        //恢复现场
        canvas.restore() ;

        //平移坐标,让接下来的图形绘制在上一次图像的下面
        canvas.translate(0, 300) ;
        canvas.save() ;
        for (int i = 0; i < 10; i++) {
            canvas.drawRect(0, 0,400,300,paint) ;
            canvas.scale(0.9f, 0.9f,200,200) ;
        }
        canvas.restore() ;

        //平移坐标,让接下来的图形绘制在上一次图像的下面
        canvas.translate(0, 350) ;
        canvas.save() ;
        canvas.drawCircle(200, 200, 150, paint);
        for(int i = 0; i < 12; i ++){
        canvas.drawLine(310, 200, 350, 200, paint);
        canvas.rotate(30, 200, 200);
        }
        canvas.restore();

    }

}
  如果不使用坐标的转换,我们需要通过找出图形的变化规律,不断计算各个点的坐标。以达到绘制一系列的目的。但是,有了坐标的转换,很轻松的将这一工作完成。运行如下:

  绘制第一种效果是,我们只需需啊哟考虑第一个正方形的位置和大小,即通过 canvas.drawRect(0, 0,150,50,paint) 语句绘制一个正方形,平移(20,20)坐标后,使用相同的挨骂绘制一个正方形。此时,新的正方形向右下角移动一段距离,按照相同的思路循环 10次,直到画完最后一个正方形为止;绘制第二种效果时,先调用 canvas.drawRect(0, 0,400,300,paint) 绘制一个大小为 400 * 300 的正方形。此时,我们注意到,正方形的中心是(200,150)。通过 canvas.scale(0.9f, 0.9f,200,200) 方法以(200,200)为中心,将当前新绘制的正方形缩小为撒花姑娘一个正方形的 0.9 倍,以同样的方式直到绘制结束为止;第三个绘制,我们先执行 canvas.drawCircle(200, 200, 150, paint);语句画一个圆,圆心坐标为(200,200),如果执行 canvas.drawLine(310, 200, 350, 200, paint);语句会绘制一条从 (310, 200)到 (350, 200) 的长为 40的直线。然后,以(200,200)为中心,每次旋转 30度,依次绘出 12 条直线。

  我们没有计算新图形坐标,而是利用同一个绘图语句,通过改变画布坐标,轻松实现了复杂的绘图。需要注意的是一个绘图周期内最好 save 保存现场调用 restore恢复现场,这样才不会影响下一次绘图。

  另外,Anroid中定义了一个名为 Matrix的类,该类定义了一个 3 *3 的矩阵,只要知道通过 Matrix同样可以实现坐标的变换,相关方法如下:

  • 位移
  • public void setTranslate(float dx, float dy)

  • 旋转
  • public void setRotate(float degrees, float px, float py)
  • public void setRotate(float degrees)

  • 缩放
  • public void setScale(float sx, float sy)
  • public void setScale(float sx, float sy, float px, float py)

  • 拉斜
  • public void setSkew(float kx, float ky)
  • public void setSkew(float kx, float ky, float px, float py)

  Matrix的应用范围很广,Canvas‘Shader 等都支持通过 Matrix 实现位移、旋转、缩放 等效果。Matrix额基本使用形如:

Matrix matrix = new Matrix();
matrix.setTranslate(10, 10);
canvas.setMatrix(matrix);

3.4、剪切区(Clip)

  clip 是指剪切区,理解“剪切区”这个概念不需要费什么周折。我们想象一下,春乱花开的季节,你在海岸边的别墅里,面朝海边的墙上开了一个窗户。看潮起潮落、鸟来鸟往,伴随着落日余晖和万丈光霞,还不惬意。这里的“剪切区”就是在 Canvas上开一个口子,开了这个口子之后,接下来绘制的内容只有通过口子才能看见,口子外的图形就看不到了

  Canvas 提供了剪切区的功能,剪切区可以是一个 Rect 或者是一个 Path。两个剪切区还能进行图形运算,得到更加复杂的剪切区。相关方法如下:

  • public boolean clipRect(Rect rect)
  • public boolean clipRect(RectF rect)
  • public boolean clipRect(float left, float top, float right, float bottom)
  • public boolean clipRect(int left, int top, int right, int bottom)
  • 以上 4个方法定义一个矩形的剪切区

  • public boolean clipPath(Path path)
  • 以上方法一个 Path剪切区,用于定义更加复杂的区域

  现在有一张很照片,照片里有两只干净蠢萌的小狗。在 Canvas上开一个矩形的剪切区并将照片绘制在画布上,然后,我们看到什么 ?

public class ClipView extends View {
    public ClipView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Bitmap bmpDog = BitmapFactory.decodeResource(getResources(), R.mipmap.dog);
        //绘制完整照片
        canvas.drawBitmap(bmpDog, 0, 0, null);
        //平移坐标
        canvas.translate(0, 500);
        //定义剪切区
        canvas.clipRect(new Rect(100, 100, 500, 500));
        //再次绘制
        canvas.drawBitmap(bmpDog, 0, 0, null);

    }
}

  从结果中看出,平移坐标后,在 Rect(100,100,500,500) 区域定义了一个剪切区,接下来的图片只有剪切区才会显示

  剪切区还能进行图形运算,前面学习 Path是我们接触过 Op,事实上剪切区的 Op运算也没有什么太大的不同,一共有 6种:

public static enum Op {
    DIFFERENCE,
    INTERSECT,
    REPLACE,
    REVERSE_DIFFERENCE,
    UNION,
    XOR
}
  Op.DIFFERENC : 计算 A 和 B 的差集范围,即 A-B,只有在此范围内的绘制内容才会被显示

  Op.REVERSE_DIFFERENCE : 计算 B 和 A的差集范围,即 B-A,只有在范围内的绘制内容才会被显示

  Op.INTERSECT: 计算 B 和 A的交集范围,只有在范围内的绘制内容才会被显示

  Op.REPLACE: 不论 A和 B 的集合状况,B 的范围将全部进行显示,如果和 A 有交集,则覆盖 A 的交集范围

  Op.UNION : 计算 A 和 B的并集范围,两者所包括的范围的绘制内容欧惠被显示

  Op.XO: 计算 A 和 B 的补集范围,也就是先获取 A 和 B 的并集,再减去 A 和 B 的交集。只有在范围内的绘制内容才会被显示

  与剪切区 Op运算相关的方法如下:

  • public boolean clipRect(RectF rect, Op op)
  • public boolean clipRect(Rect rect, Op op)
  • public boolean clipRect(float left, float top, float right, float bottom, Op op)
  • public boolean clipPath(Path path, Op op)

  根据上面的代码做修改,先创建一个矩形剪切区,再创建一个 Path剪切区(Path内添加一个圆),添加第二个剪切区时做 Op.UNION运算(计算 A 和 B的并集范围,两者所包括的范围的绘制内容欧惠被显示),运行结果显示剪切区是有一个矩形和圆构成的

        //绘制完整图片
        canvas.drawBitmap(bitmap, 0, 0,null) ;
        //平移坐标
        canvas.translate(0, 500);
        //定义剪切区
        canvas.clipRect(new Rect(100,100,500,500));

        //定义一个新的剪切区,与上面一个 做 Op运算
        Path path = new  Path() ;
        path.addCircle(500, 350, 200, Direction.CCW) ;
        canvas.clipPath(path) ;

        //再次绘制
        canvas.drawBitmap(bitmap, 0, 0,null) ;

  利用剪切区还可以实现帧动画的播放,制作游戏时,不断拍打翅膀的小鸟、轰然一声的爆炸效果等都可以通过剪切区很好的播放出来。我们以爆破效果为例,爆破的所有帧都是事先绘制在一张图片上,如下 :

  上面一张大图包含了 7帧,定义一个 1/7 大小的剪切区,每隔一段时间按照顺序连续播放其中一帧,原理类似于以前的胶片电影,这样就构成了一个动感十足的动画

  播放过程中,剪切区(clip)是固定不动的,实际上移动的恰恰是图片,图片每次向左移动一帧。假设图片总长度为 70.显示第一帧时,图片的 left为 0,然后向左移动一帧,left为 -10.向左移动 两帧,left 为 -20…向左移动 6帧,left 为 -60,此时,真整个动画播放完成。如果要循环播放,将 left的值重新置 0即可,具体实现看下面代码

public class ClipView extends View {

    private int i = 0 ;
    private Bitmap bitmap ;


    public ClipView(Context context, AttributeSet attrs) {
        super(context, attrs);

        bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.k) ;

    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);


        //获取位置的宽度和高
        int width = bitmap.getWidth() ;
        int height = bitmap.getHeight() ;

        //剪切区
        int frameWidth = width / 7;
        Rect rect = new  Rect(0,0,frameWidth,height)  ;

        canvas.save() ;
        canvas.translate(100, 100) ;
        canvas.clipRect(rect) ;
        canvas.drawBitmap(bitmap, -i * frameWidth,0, null) ;
        canvas.restore() ;

        i++ ;
        if(i == 7){
            i = 0 ;
        }       
    }
}

  为了让动画每隔一段时间播放一帧,需要定义一个 Timer计时器,调用 postInvalidate()方法,发送消息让组件重绘

        clipView = (ClipView) findViewById(R.id.clipview) ;

        new Timer().schedule(new TimerTask() {

            @Override
            public void run() {

                clipView.postInvalidate() ;
            }
        }, 2000,100);
运行效果如下:

3.5、案例:指针走动的手表

  下面通过代码模拟一个时钟,我们知道时针总共有 3根针,分别为 时针、分针、秒针。表盘周围是一圈有规律长短分配的刻度。秒针每隔一秒移动一次,分针每隔一分钟移动一次,时针比较特殊,并不是每隔一小时移动一次,而是随着分针慢慢移动,为了不复杂下面设置成,每隔一小时,时针移动一次

  分步骤来解决这个问题,大致来讲分为 三步 :

  • 绘制表盘周围刻度
    刻度的变化规律是 1 长 4 短,通过旋转画布坐标来绘制刻度,一共 60根刻度,以 6 °度为单位对画布进行旋转,从 3 点钟所在的位置也就是 0°度开始绘制

  • 绘制指针
    指针有秒针‘分针和时针,我们抛弃三者联动的思路,采用实时获取设备的系统时间。读取出当前时间的时、分、秒数值。将数值转化成角度,再将角度转化成两个点的 坐标。
    已知圆的中心点坐标(xo,yo),圆的半径 r,那么,圆上任何角度 α 对应的点的坐标公式为 :
    x = xo + r * cosα
    y = yo + r * sinα
    要注意的是,cosα对应的 Math.cos()方法 和 Math.sin() 方法的参数是 弧度,所以要利用 Math.toRadians()方法将°度转成弧度

  • 定义 Timer定时器
    定义一个 Timer定时器,每隔一秒刷新一次绘图区,实现指针运动的效果
public class WatchView extends View {

    private Paint paint ;
    private Calendar calendar ;

    public WatchView(Context context, AttributeSet attrs) {
        super(context, attrs);


        paint = new  Paint() ;
        paint.setAntiAlias(true) ;
        paint.setColor(Color.GRAY) ;
        paint.setStyle(Style.STROKE) ;
        paint.setStrokeWidth(1) ;

        calendar = Calendar.getInstance() ;

    }

    public void run(){
        new Timer().schedule(new TimerTask() {

            @Override
            public void run() {
                    postInvalidate() ;
            }
        }, 0,1000) ;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        //获取组件尺寸
        int width = this.getMeasuredWidth() -getPaddingLeft() - getPaddingRight();
        int height = this.getMeasuredHeight()  - getPaddingTop() - getPaddingBottom();


        int len  = Math.min(width, height) ;

        //绘制表盘
        drawPlate(canvas, len);

        //绘制指针

        drawPoints(canvas, len);
    }

    private void drawPlate(Canvas canvas, int len) {

        canvas.save() ;
         int r = (int) (len *0.5f) ;
         int startX = r + getPaddingLeft() ;
         int startY = r + getPaddingTop();
         canvas.rotate(-90,startX,startY) ;

        canvas.drawCircle(startX, startY, r, paint);


        //化刻度(60个)
        for (int i = 0; i < 60; i++) {
            if(i%5 == 0){ 
                paint.setColor(Color.RED) ;
                paint.setStrokeWidth(4) ;
                //取值 r 的 9/10
                canvas.drawLine(startX + 9 * r / 10 , startY, len, startY, paint);
            }else{
                paint.setColor(Color.GRAY);
                paint.setStrokeWidth(2);
                //取值 r 的 1/15
                canvas.drawLine(startX + 14 * r / 15 , startY, len , startY, paint);
            }
                //以圆中心将画笔旋转 6度
            canvas.rotate((float)360/60,r+getPaddingLeft(), r+getPaddingTop());
        }
        canvas.restore() ;
    }


private void drawPoints(Canvas canvas, int len) {

        //获取系统的时间
    calendar.setTimeInMillis(System.currentTimeMillis()) ;
    //获取时分秒
     int hours = calendar.get(Calendar.HOUR) % 12;
     int miuntes = calendar.get(Calendar.MINUTE) ;
     int seconds = calendar.get(calendar.SECOND) ;

     //画是时针(顺时针)
     int degree = 360 / 12 *hours ;
     //转成 弧度值
     double radians = Math.toRadians(degree) ;


     //根据当前时间计算时针两个点的坐标
     int r = (int) (len *0.5f) ;
     int startX = r + getPaddingLeft() ;
     int startY = r + getPaddingTop();

     int endX = (int) (startX + r*0.5*Math.cos(radians)) ;
     int endY = (int) (startY + r * 0.5 * Math.sin(radians));
     canvas.save();
     paint.setStrokeWidth(6);

     //12即也是开始点,因为 Android的坐标系绘制相差 90°。所以需要将画面旋转 90度
     canvas.rotate(-90,startX,startY) ;
     canvas.drawLine(startX, startY, endX, endY, paint) ;
     canvas.restore() ;


     //画分针
     degree = 360 / 60 * miuntes;
     radians = Math.toRadians(degree);
     endX = (int) (startX + r * 0.6 * Math.cos(radians));
     endY = (int) (startY + r * 0.6 * Math.sin(radians));
     canvas.save();
     paint.setStrokeWidth(4);
     //旋转同上
     canvas.rotate(-90,startX,startY) ; 
     canvas.drawLine(startX, startY, endX, endY, paint);
     canvas.restore();

     //画秒针
     degree = 360 /60 * seconds ;
     radians = Math.toRadians(degree) ;
     endX = (int) (startX + r * 0.8 * Math.cos(radians));
     endY = (int) (startY + r * 0.8 * Math.sin(radians));
     canvas.save();
     paint.setStrokeWidth(2);

     //旋转同上
     canvas.rotate(-90,startX,startY) ;
     canvas.drawLine(startX, startY, endX, endY, paint);
     //形象的给秒针价格尾巴
     radians = Math.toRadians(degree + 180) ;//因为是延伸出去的,所以多 180度。
     endX = (int) (startX + r * 0.15 * Math.cos(radians));
     endY = (int) (startY + r * 0.15 * Math.sin(radians));
     canvas.drawLine(startX, startY, endX, endY, paint);
     canvas.restore();

    }

}
运行如下:
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值