自定义组件开发三 Graphics 绘制动态效果

概述

上文https://blog.csdn.net/u011733020/article/details/80220513主要介绍了Graphics相关的api的绘图方法。绘制的都是静态的,这里使用Graphics 来实现动态效果绘图,来达到让画面动起来,或者让图案与我们的手指互动。

过去我们在 ImageView 上绘图,这里尝试定义一个继承自 View 的
子类,重写 onDraw(),在该方法中绘图,当 View 显示时会回调 onDraw()方法
展方法,用于绘制组件的外观。

public class MyView extends View{
    public void onDraw(Canvas canvas){

    }
}

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)

先拿小车来演示一下,让小车在 View 的 Canvas 中水平往返移动。
这里写图片描述

package bczm.graphics.view;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;

import bczm.graphics.R;



public class CarMoveView extends View {
    /**
     * 小车的水平位置
     */
    private int x;
    /**
     * 小车的垂直位置,固定为 100
     */
    private static final int y = 100;
    /**
     * 小车的宽度
     */
    private static final int carWidth = 320;

    private static final int COLOR = Color.RED;
    private Paint paint;
    /**
     * 移动的方向
     */
    private boolean direction;
    private Bitmap newBitMap;

    public CarMoveView(Context context) {
        super(context);
    }

    public CarMoveView(Context context, AttributeSet attrs) {
        super(context, attrs);
        //初始化画笔,参数表示去锯齿
        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
//        paint.setColor(COLOR);
        x = 0;

        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.space_art);
        newBitMap = calculate(bitmap);
    }

    public CarMoveView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public Bitmap calculate(Bitmap bm) {
        // 获得图片的宽高
        int width = bm.getWidth();
        int height = bm.getHeight();
        // 设置想要的大小
        int newWidth = 320;
        int newHeight = 320;
        // 计算缩放比例
        float scaleWidth = ((float) newWidth) / width;
        float scaleHeight = ((float) newHeight) / height;
        // 取得想要缩放的matrix参数
        Matrix matrix = new Matrix();
        matrix.postScale(scaleWidth, scaleHeight);
        // 得到新的图片
        Bitmap newbm = Bitmap.createBitmap(bm, 0, 0, width, height, matrix,
                true);
        return newbm;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //根据 x、y 的坐标值 初始小车位置
        canvas.drawBitmap(newBitMap, x, y, paint);
        //改变 x 坐标的值,调用 invalidate()方法后,
        //小车将因 x 的值发生改变而产生移动的效果
        int width = this.getMeasuredWidth();//获取组件的宽度
        if (x <= carWidth) {
            direction = true;
        }
        if (x >= width - carWidth) {
            direction = false;

        }
        x = direction ? x + 5 : x - 5;
    }
}

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    xmlns:android="http://schemas.android.com/apk/res/android">
    <bczm.graphics.view.CarMoveView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/ballview"
        />
</LinearLayout>
public class CarViewActivity extends Activity {
    private CarMoveView ballview;
    @Override
    protected void onCreate( Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_ball_move);
        ballview = (CarMoveView) findViewById(R.id.ballview);
        new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                ballview.postInvalidate();
            }
        }, 200, 50);
    }
}

我们定义了一个 BallMoveView 类,继承自 View,并且重写了 onDraw()方法,该方法是用于在组件上绘图的方法,同时,我们定义了有两个参数的构造方法,如果在布局文件中定义了该组件,则会调用此构造方法来创建对象。

在 onDraw()方法中,画布 Canvas 对象自动传入,第一章我们已经知道该对象的来源,显示在 View 上的内容都最终都必须绘制在该 canvs 对象上。我们调用 canvas 对象的 drawCircle()方法

那么,小车是如何水平移动的呢?在 onDraw()方法中,我们发现该方法的最后几行代码会
根据 direction 来修改成员变量 x 的值,如果 direction 为 true,x 累加 5,否则累加-5。正如上面所说,我们在 Activity 中恰恰是通过定时器周期性调用了 invalidate()方法不断重绘组件,也就是不断调用 onDraw()方法,因为小车的位置由 x 来决定,onDraw()每调用一次,x 的值就会变化一次,小车绘制的位置自然也会跟着一起改变,最后形成了小车移动的效果。

坐标转换

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

public void translate(float dx, float dy)

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

这里写图片描述

下面两段代码是等效的:

代码段 1:canvas.drawPoint(10, 10, paint);
代码段 2: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 表示放大。画布缩放后,绘制在画布上的图形也会等比例缩放。缩放的单位是倍数,比如 sx 为 0.5 时,就是在 x 方向缩小 0.5 倍。

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 度,则 tan45=1,写成:canvas.skew(1, 0)。

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

public int save()
保存现场。

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

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 )

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 剪切区,用于定义更加复杂的区域。

下面用一个例子演示

public class ClipView extends View {

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

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

最终的效果是下面这样:

这里写图片描述

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

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

public static enum Op {
DIFFERENCE,
INTERSECT,
REPLACE,
REVERSE_DIFFERENCE,
UNION,
XOR
}

这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

与剪切区 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)

用Op 的UNION来显示裁剪区代码如下:

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

        Bitmap srcBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.sun);
        //绘制完整原始照片
        canvas.drawBitmap(srcBitmap, 0, 0, null);
        //平移坐标
        canvas.translate(0, 500);
        //定义剪切区
        canvas.clipRect(new Rect(100, 100, 500, 500));
        //定义一个新的剪切区,与上一剪切区做 Op 运算
        Path path = new Path();
        path.addCircle(400, 320, 200, Path.Direction.CCW);
        canvas.clipPath(path, Region.Op.UNION);
        //再次绘制照片
        canvas.drawBitmap(srcBitmap, 0, 0, null);
    }

这里写图片描述

利用剪切区还可以实现帧动画的播放
这里写图片描述

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

这里写图片描述

播放过程中,剪切区(clip)是固定不动的,实际上移动的恰恰是图片,图片每次向左移动一帧。假设图片总长度为 70,显示第一帧时,图片的 left 为 0,然后向左移动一帧,left为-10,向左移动两帧,left 为-20……向左移动 6 帧,left 为-60,此时,整个动画播放完毕。如果要循环播放,将 left 的值重新置 0 即可,具体实现请看下面的代码。
···
public ClipBombView(Context context, AttributeSet attrs) {
super(context, attrs);
bmpBoom = BitmapFactory.decodeResource(getResources(), R.drawable.boom);
}

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    //获取位置的宽度和高度
    int width = bmpBoom.getWidth();
    int height = bmpBoom.getHeight();
    //剪切区
    int frameWidth = width / 7;
    Rect rect = new Rect(0, 0, frameWidth, height);
    canvas.save();
    canvas.translate(100, 100);//平移坐标
    canvas.clipRect(rect);//设置剪切区
    canvas.drawBitmap(bmpBoom, -i * frameWidth, 0, null);//播放一帧
    canvas.restore();
    i++; //i 加 1 以播放下一帧
    if (i == 7) i = 0;//播放完毕后将 i 重置为 0 重新播放
}

···

谢谢认真观读本文的每一位小伙伴,衷心欢迎小伙伴给我指出文中的错误,也欢迎小伙伴与我交流学习。
欢迎爱学习的小伙伴加群一起进步:230274309

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值