Android绘图使用 Graphics2D 实现动态效果

Posted mChenys

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android绘图使用 Graphics2D 实现动态效果相关的知识,希望对你有一定的参考价值。

目录

一、View的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()方法重新发起重绘请求,scheduleTraversals()方法会发送一个异步消息,调用 performTraversals()方法执行重绘,而 performTraversals()方法最终调用 onDraw()方法,简单来说,调用 View 的 invalidate()方法就相当于调用了 onDraw()方法,而 onDraw()方法中就是我们编写的绘图代码。

如果要刷新组件或者让画面动起来,我们只需调用 invalidate()方法即可。通过改变数据来影响绘制结果,这是实现组件刷新或实现动画的基本思路。
invalidate()方法只能在 UI 线程中调用,如果是在子线程中刷新组件,View 类还定义了另一组名为 postInvalidate 的方法:

public void postInvalidate()
public void postInvalidate(int left, int top, int right, int bottom)

1.1 案例-小球循环滚动

现在我们编写一个案例,让小球在 View 的 Canvas 中水平往返移动。当小球触碰到左边边界时往右移动,小球触碰到右边边界时往左移动,循环往复。

首先创建自定义View

public class MyView extends View 
    public MyView(Context context) 
        this(context,null);
    

    public MyView(Context context, AttributeSet attrs) 
        this(context, attrs,0);
    

    public MyView(Context context, AttributeSet attrs, int defStyleAttr) 
        super(context, attrs, defStyleAttr);
        init();
    

    private void init() 
        
    

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

注意:后面的案例统一使用该View,只是init和onDraw方法不同而已,其他都是模板代码.
然后在Activity中使用

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:gravity="center"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.mchenys.nagivation.MyView
        android:id="@+id/myView"
        android:layout_width="match_parent"
        android:layout_height="300dp" />

</LinearLayout>

Activity代码固定如下:


public class MainActivity extends AppCompatActivity 

    @Override
    protected void onCreate(Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        MyView myView = findViewById(R.id.myView);
        new Timer().schedule(new TimerTask() 
            @Override
            public void run() 
                // 子线程刷新调用postInvalidate
                myView.postInvalidate();
            
        , 200, 50); // 延迟200ms执行,每50ms刷新一次
    

下面是重点代码

// 小球的圆心坐标
int centerX;
int centerY = 100; // y坐标固定
// 半径
int radius = 50;
// 小球颜色
int color = Color.RED;
// 移动的方向,true向右边,false向左边
boolean direction;
// 画笔
Paint mPaint = new Paint();

private void init() 
    mPaint.setAntiAlias(true);
    mPaint.setColor(color);
    centerX = radius;


@Override
protected void onDraw(Canvas canvas) 
    canvas.drawColor(Color.BLACK);
    // 绘制小球
    canvas.drawCircle(centerX, centerY, radius, mPaint);
    // 获取控件最大宽度
    int maxWidth = getMeasuredWidth();
    if (centerX < radius) 
        // 如果圆心x的位置小于半径,那么需要向右移动
        direction = true;
     else if (centerX > maxWidth -  radius) 
        // 如果圆心x的位置大于控件最大宽度-圆的半径,那么需要向左边移动
        direction = false;
    
    // 修改圆心x的坐标,每次变化10px
    centerX = direction ? centerX + 10 : centerX - 10;

效果图:

如果想要垂直方向也变化,那也很简单,只需要修改centerY为随机值即可,如下所示:


// 小球的圆心坐标
int centerX;
int centerY; // y坐标固定
// 半径
int radius = 50;
// 小球颜色
int color = Color.RED;
// 水平移动的方向,true向右边,false向左边
boolean directionX;
// 垂直移动方向,true向下,false向上
boolean directionY;

// 画笔
Paint mPaint = new Paint();

private void init() 
    mPaint.setAntiAlias(true);
    mPaint.setColor(color);
    centerX = radius;
    centerY = radius;


@Override
protected void onDraw(Canvas canvas) 
    canvas.drawColor(Color.BLACK);
    // 绘制小球
    canvas.drawCircle(centerX, centerY, radius, mPaint);
    // 获取控件最大宽度和高度
    int maxWidth = getMeasuredWidth();
    int maxHeight = getMeasuredHeight();
    if (centerX < radius) 
        // 如果圆心x的位置小于半径,那么需要向右移动
        directionX = true;
     else if (centerX > maxWidth - radius) 
        // 如果圆心x的位置大于控件最大宽度-圆的半径,那么需要向左边移动
        directionX = false;
    
    // 修改圆心x的坐标,每次变化10px
    centerX = directionX ? centerX + 10 : centerX - 10;

    if (centerY < radius) 
        directionY = true;
     else if (centerY > maxHeight - radius) 
        directionY = false;
    

    // 修改圆心y的坐标,每次变化5px
    centerY = directionY ? centerY + 5 : centerY - 5;

效果如下:

二、 坐标转换

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

public void translate(float dx, float dy)

2.1 平移

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

下面两段代码是等效的:

//代码段 1
canvas.drawPoint(10,10,paint);
//代码段 2
canvas.translate(10,10); // 先平移  
canvas.drawPoint(0,0,paint); // 在新的坐标原点下绘制

2.2 旋转

public void rotate(float degrees)

将画布的坐标以当前原点为中心旋转指定的角度,如果角度为正,则为顺时针旋转,否则为逆时针旋转。

旋转还能指定坐标作为旋转的中心

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

2.3 缩放

public void scale(float sx, float sy)

缩放画布的坐标,sx、sy 分别是 x 方向和 y 方向的缩放比例,小于 1 表示缩小,等于1 表示不变,大于 1 表示放大。画布缩放后,绘制在画布上的图形也会等比例缩放。缩放的单位是倍数,比如 sx 为 0.5 时,就是在 x 方向缩小 0.5 倍。
同样缩放也可以指定坐标作为缩放的中心点

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

2.4 拉斜

public void skew(float sx, float sy)

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

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

// 保存现场
public int save()
// 恢复现场到 save()执行之前的状态
public void restore()

2.5 案例-平移旋转缩放


// 画笔
Paint mPaint = new Paint();

private void init() 
    mPaint.setAntiAlias(true);
    mPaint.setStyle(Paint.Style.STROKE);


@Override
protected void onDraw(Canvas canvas) 
    canvas.drawColor(Color.BLACK);
    // 水平方向平移画布,然画布内容离控件的左上角有点间距
    canvas.translate(300, 100);

    mPaint.setColor(Color.RED);
    // 保存现场
    canvas.save();
    for (int i = 0; i < 10; i++) 
        // 绘制正方型
        canvas.drawRect(0, 0, 100, 100, mPaint);
        // 每绘制一个,则平移下画布原点
        canvas.translate(10, 10);
    
    // 恢复现场
    canvas.restore();

    // 平移坐标,让接下来的图形绘制在上一次图形的下面
    canvas.translate(0, 220);
    mPaint.setColor(Color.WHITE);
    // 保存现场
    canvas.save();
    for (int i = 0; i < 10; i++) 
        canvas.drawRect(0, 0, 100, 100, mPaint);
        // 每绘制一个,则缩放一次,缩放中心是图形的中心点
        canvas.scale(0.9f, 0.9f, 50, 50);
    
    // 恢复现场,恢复到上一次save的位置,也就是垂直平移了220的位置
    canvas.restore();


    //平移坐标,让接下来的图形绘制在上一次图形的下面
    canvas.translate(0, 150); // 垂直方向加上前面平移的220和此次150,那就是平移了370了
    mPaint.setColor(Color.RED);
    canvas.save();

    // 绘制时钟
    canvas.drawCircle(50, 50, 50, mPaint);
    for (int i = 0; i < 12; i++) 
        // 绘制直线线条(水平直线,y坐标在圆心上)
        canvas.drawLine(0, 50, 10, 50, mPaint);
        // 每次绘制完旋转30度,旋转中心是圆心,这样直线就变成刻度线了
        canvas.rotate(30, 50, 50);
    
    canvas.restore();

效果图:

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

2.6 canvas中使用Matrix

Android 中定义了一个名为 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);

三、 剪切区(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个方法定义一个矩形的剪切区,除此之外path也可以用来裁剪区域。

public boolean clipPath(Path path)

以上方法定义一个 Path 剪切区,可以用于定义更加复杂的区域。

3.1 案例-裁剪相片

@Override
protected void onDraw(Canvas canvas) 
    canvas.drawColor(Color.BLACK);
    Bitmap sourceBmp = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher);
    // 绘制原图
    canvas.drawBitmap(sourceBmp, 0, 0, null);
    // 平移坐标,让新的绘制内容在原图下面
    canvas.translate(0, sourceBmp.getHeight());
    // 定义剪切区为原图的左上角(原图的1/4区域)
    canvas.clipRect(new Rect(0, 0, (int) (sourceBmp.getWidth()/2f), (int) (sourceBmp.getHeight()/2f)));
    //再次绘制图片
    canvas.drawBitmap(sourceBmp, 0, 0, null);

效果图如下:

3.2 剪切区的图形运算

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

public static enum Op 
    DIFFERENCE,//差集
    INTERSECT,//交集
    REPLACE, // 就比path多了这个而已
    REVERSE_DIFFERENCE, //反差集
    UNION,//并集
    XOR//补集

Op.DIFFERENCE:计算 A 和 B 的差集范围,即 A-B,只有在此范围内的绘制内容才会被显示。如下图所示:

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

Op.INTERSECT:即 A 和 B 的交集范围,只有在此范围内的绘制内容才会被显示。如下图所示:

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

Op.UNION:A 和 B 的并集范围,两者所包括的范围的绘制内容都会被显示。如下图所示:

Op.XOR: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可以add各种形状

下面我们在上一个案例的基础上稍作修改,先创建一个矩形剪切区,再创建一个 Path 剪切区(Path 内添加了一个圆),添加第二个剪切区时做 Op.UNION 运算(您也可以替换成其他运算),运行结果显示剪切区是由一个矩形和圆构成的。

@Override
protected void onDraw(Canvas canvas) 
    canvas.drawColor(Color.BLACK);
    Bitmap sourceBmp = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher);
    // 绘制原图
    canvas.drawBitmap(sourceBmp, 0, 0, null);
    // 平移坐标,让新的绘制内容在原图下面
    canvas.translate(以上是关于Android绘图使用 Graphics2D 实现动态效果的主要内容,如果未能解决你的问题,请参考以下文章

java GUI Graphics2D 绘图

Java 实现图片水印

java 实现图片水印

使用Java的Graphics类进行绘图的方法详解

Java中的Graphics2D类基本使用教程

十一. 图形图像与多媒体4.Graphics类的绘图方法