自定义View学习笔记06—Canvas绘制图片

本学习笔记来自GcsSloop的http://www.gcssloop.com/customview/Canvas_PictureText一文;

一、绘制图片
Picture和camera录像功能是类似的,只不过我们Picture录的是Canvas中绘制的内容,camera录制的是现实世界中的内容。

绘制图片,Google官方提供了两种方法,一是drawPicture(矢量图) ,二是 drawBitmap(位图),接下来我们逐一了解学习。
A、Picture(矢量图)

1、Picture.draw():

使用drawPicture方法需要关闭硬件加速,以避免引起不必要的问题,给自己挖坑。关闭硬件加速的方法是:

在AndroidMenifest文件中application节点下添加android:hardwareAccelerated=“false”

如此即可。

Picture的API方法如下:
Picture的API方法

从图表可以看出:beginRecording() 和 endRecording() 两个方法需要成对使用,否则无法实现Picture的录制,两者之间的操作将会存储在Picture中。

录制步骤:
a、准备工作:录制内容,将一些Canvas操作用Picture存储起来,录制的内容是不会直接显示在屏幕上的,只是存储起来了而已。

Picture mPicture  = new Picture();
private void drawPictrue01(){
    Canvas canvas = mPicture.beginRecording(wid, hei);
    canvas.save();
    mPaint.setStrokeWidth(8);
    mPaint.setStyle(Paint.Style.FILL);

    canvas.translate(wid / 2, hei / 2);
    canvas.drawCircle(0, 0, 150, mPaint);
    mPicture.endRecording();
}

b、正式录制,然后在构造方法中调用drawPictrue01():

public PictureCanvas(Context context) {
    super(context);
    mContext = context;
    if (mPaint == null) {
        initPaint();
    }
    drawPictrue01();
}

c、最后在onDraw方法中使用Picture提供的draw方法绘制:

// 将Picture中的内容绘制在Canvas上
mPicture.draw(canvas);

结果如下:
这里写图片描述

注意:
1)、这种方法在低版本的系统上绘制后可能会影响Canvas状态,所以这种方法一般不会使用。
2)、Picture录制的内容不会直接显示,想要将Picture中的内容显示出来就需要手动调用Picture的方法将Picture中的内容绘制出来:
Picture的方法
上面的方法虽然不多,只有三个,但本质上还是有很大的区别:

Picture方法的区别

2、使用Canvas提供的drawPicture方法绘制,官方Api为drawPicture提供了三种方法:

public void drawPicture (Picture picture)
public void drawPicture (Picture picture, Rect dst)
public void drawPicture (Picture picture, RectF dst)

其他内容不变,在onDraw方法中使用Picture提供的drawPicture方法:

canvas.drawPicture(mPicture,new RectF(0,0,400,160));

运行结果与上面相同。

3、我们还可以将Picture包装成为PictureDrawable,使用PictureDrawable的draw方法绘制。

private void drawDrawable(Canvas canvas){
    canvas.translate(wid/2,hei/2);
    // 包装成为Drawable
    PictureDrawable drawable = new PictureDrawable(mPicture);
    // 设置绘制区域 -- 注意此处所绘制的实际内容不会缩放
    drawable.setBounds(0, 0, mPicture.getWidth(), mPicture.getHeight());
    // 绘制
    drawable.draw(canvas);
}

其他内容不变,在onDraw方法中调用drawDrawable()方法,结果如下:
将Picture包装成为PictureDrawable绘制

此处setBounds是设置在画布上的绘制区域,并非根据该区域进行缩放,也不是剪裁Picture,每次都从Picture的左上角开始绘制。

B、Bitmap(位图):
Bitmap是一个费力不讨好的货,因为Bitmap就是很多问题的根源,可能导致内存不足、资源耗费大、OOM、RecyclerView的复用混乱等诸多问题。总之想无坑的使用,很难,需要超高的技术。因此这里记说几个基本的使用方法:
BitMap常用方法

很明显地,上图中的第一个方式不满足我们的开发需求,在此略过;
第2种方式虽然满足我们的要求,但是不推荐使用这种方式,至于为什么在后续详细讲解Drawable的时候会说明,暂时排除;
第3种方式我们会有一种比较熟悉的感觉,这里比较详细的说明一下如何从各个位置获取图片。
通过BitmapFactory从不同位置获取Bitmap:

A、资源文件(drawable/mipmap/raw):

Bitmap bitmap=BitmapFactory.decodeResource(getResources(),R.raw.bitmap);
注:在低版本SDK上要用mContext.getResources()代替getResources(),否则报异常。

B、资源文件(assets):

Bitmap bitmap=null;
try {
    InputStream is = mContext.getAssets().open("bitmap.png");
    bitmap = BitmapFactory.decodeStream(is);
    is.close();
} catch (IOException e) {
    e.printStackTrace();
}

C、内存卡文件:

Bitmap bitmap = BitmapFactory.decodeFile("/sdcard/bitmap.png");

D、网络文件:

// 此处省略了获取网络输入流的代码
Bitmap bitmap = BitmapFactory.decodeStream(is);
is.close();

现在开始本文的重点,将Bitmap绘制到画布上。
我们先预览一下drawBitmap的几种常用方法:

// 第一种
public void drawBitmap (Bitmap bitmap, Matrix matrix, Paint paint)
// 第二种
public void drawBitmap (Bitmap bitmap, float left, float top, Paint paint)
// 第三种
public void drawBitmap (Bitmap bitmap, Rect src, Rect dst, Paint paint)
public void drawBitmap (Bitmap bitmap, Rect src, RectF dst, Paint paint)

第一种方法:
后两个参数(matrix, paint)是在绘制的时候对图片进行一些改变,如果只是需要将图片内容绘制出来只需在onDraw()方法运行以下代码就可以了:
注意:屏幕左上角位置为默认坐标原点。

canvas.translate(25, 25);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.zero01);
canvas.drawBitmap(bitmap, new Matrix(), new Paint());//法一

效果如下:
Bitmap位图绘制一

关于Matrix和Paint,暂时略过,这涉及到数学的一些东西,也是吃力不讨好的。

现在我们来看看第二种方式:
就是在绘制时指定了图片左上角的坐标(距离坐标原点的距离):
注意:此处指定的是与坐标原点的距离,并非是与屏幕顶部和左侧的距离, 虽然默认状态下两者是重合的,但是也请注意分别两者的不同。

在onDraw()方法运行以下代码:

Bitmap bitmap2=BitmapFactory.decodeResource(getResources(), R.mipmap.zero04);
canvas.drawBitmap(bitmap2, 50f, 50f, mPaint);//法二

效果如下:
Bitmap位图绘制二

第三种方法:
他提供了两种方法,但大同小异,用法上基本是一致的,在此我们讨论一种方法就好。

该方法是比较有意思的,src和dst两个参数是干什么的呢?
src和dst参数描述

这两个参数的作用的讲解,理解起来有点费力,现在结合代码来说说:

Bitmap bitmap3=BitmapFactory.decodeResource(getResources(), R.mipmap.view10);
Rect src = new Rect(0, 0, bitmap3.getWidth(), bitmap3.getHeight());
//指定图片在屏幕上显示的区域
Rect dst = new Rect(10,10,700,550);
canvas.drawBitmap(bitmap3, src, dst,mPaint);

上面Rect src对象,显示的是view10图片的全部:bitmap3.getWidth(),和bitmap3.getHeight();效果如下:
讲解src

现在我改变一下src里面的参数如下,其他的保持不变:

Rect src = new Rect(0, 0, bitmap3.getWidth() * 3 / 4, bitmap3.getHeight());

运行效果如下:
讲解src

有没有发现此时图片在水平宽度方向比上图少了一部分?没错,少了四分之一,同样的,如果我们把上面的bitmap3.getHeight()也改为bitmap3.getHeight() * 3 / 4,也会发现图片在吹水高度上少四分之一,另外的两个参数也一样。因此:

src参数的作用就是指定图片起始和结束截取区域,以供显示。比如上面的Rect src = new Rect(0, 0, bitmap3.getWidth() * 3 / 4, bitmap3.getHeight()),指定的起始位置是0和0,也就是图片的左上角开始,当然我们也可以指定其他的起始位置。结束终点位置是bitmap3.getWidth() * 3 / 4和bitmap3.getHeight(),当然我们也可以指定其他的终点位置。

理解了src这个参数,相信现在来理解Rect dst参数就简单的多了,他其实就是指定某个大小的矩形框,作为一个容易用来显示(装)前面的bitmap,这跟我们前面学习的Rect是一样的。在此不多说了。

有人或许会说我要现实图片,肯定是显示一个完成的,为毛我要费力的去截取其中一部分来显示,这不是脑子进水么?
客观别急呀,既然花了这么多时间和篇幅来讲解这个,就肯定有他的莫大好处。好处在哪里呢?好处就在于我们做游戏开发或者炫酷的动画的时候很方便。
游戏开发中少不了这种动画效果:
这里写图片描述
每帧只显示其中一小格,然后串联起来形成一个动作连贯的动画效果,用这么一张图片包含了大量的素材,在绘制的时候每次只截取一部分进行绘制,这样可以大大的减少素材数量,而且素材管理起来也很方便。要不然,UI给你这样的小图片20多张,游戏有很多个角色,这样你命个名下来,估计一上午就没有了吧。然而给你一张大图,你自己截取显示,是不是很轻松呢?
好了,现在我们就用上面的方法来完成一个动画效果:
这里写图片描述

所用到的图片素材:
动画素材

自定义View:

public class CanvasCheckres extends View {
private Context mcontext;
private Bitmap bitmap = null;
private Rect areaSrc = null;
private Rect perDst = null;
private int viewX;
private int viewY;
//图片一共13个小部分组成
private int position = -1;
private int canvasItemWid;
private Paint mPaint;
public int getPosition() {
    return position;
}
public void setPosition(int position) {
    this.position = position;
    invalidate();
}
public CanvasCheckres(Context context) {
    super(context);
    mcontext = context;
    bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.checkres);
    canvasItemWid = bitmap.getHeight();
    mPaint = new Paint();
    mPaint.setAntiAlias(true);
    mPaint.setStyle(Paint.Style.FILL);
    mPaint.setColor(0xffFF5317);
}
public CanvasCheckres(Context context, @Nullable AttributeSet attrs) {
    super(context, attrs);
    mcontext = context;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    super.onLayout(changed, left, top, right, bottom);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    viewX = w;
    viewY = h;
}
@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.translate(viewX/2, viewY/2);
    canvas.drawCircle(0, 0, canvasItemWid+30, mPaint);
    areaSrc = new Rect(canvasItemWid * (position - 1), 0, canvasItemWid * position, canvasItemWid);
    perDst = new Rect(-100, -100, 100, 100);
    canvas.drawBitmap(bitmap, areaSrc, perDst, new Paint());
}

}

传递信息的通道:

public class EventCheckerMsg {
    private String  order;
    public String getOrder() {
        return order;
    }
    public void setOrder(String order) {
        this.order = order;
    }
    public EventCheckerMsg(String order) {
        this.order = order;
    }
}

开启线程传递信息,用以替代Timer和TimerTask:

public class MyScheduledExecutor implements Runnable {
    private String flag;
    public MyScheduledExecutor(String flag){
        this.flag = flag;
    }

    @Override
    public void run() {
        EventBus.getDefault().post(new EventCheckerMsg(flag));
    }
}

Activity里面的操作:
A、Activity的xml:

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"    
    tools:context=".operatecheckres.OperateCheckresActivity">

    <LinearLayout
        android:id="@+id/llOperateCheckres"
        android:layout_width="match_parent"
        android:layout_height="320dp"
        android:orientation="horizontal"
        android:background="@drawable/custom_dialog"/>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:orientation="horizontal"
        android:paddingLeft="16dp"
        android:paddingRight="16dp"
        android:layout_marginTop="20dp">

        <TextView
            android:id="@+id/tvCheck"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:text="正序"
            android:textSize="20sp"
            android:gravity="center"
            android:layout_marginRight="3dp"		            
            android:background="@drawable/button_background"/>

        <TextView
            android:id="@+id/tvUnCheck"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:text="倒序"
            android:textSize="20sp"
            android:gravity="center"
            android:layout_marginLeft="3dp"
            android:background="@drawable/button_background"/>
    </LinearLayout>
</LinearLayout>

B、activity的Java代码:

public class OperateCheckresActivity extends AppCompatActivity {
    @BindView(R.id.llOperateCheckres)
    LinearLayout llOperateCheckres;
    @BindView(R.id.tvCheck)
    TextView tvCheck;
    @BindView(R.id.tvUnCheck)
    TextView tvUnCheck;
    CanvasCheckres canvasCheckres;
    int checker = 1;
    int unChecker = 13;
    String checkOrder = "checkOrder";
    String unCheckOrder = "checkReverse";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        this.getSupportActionBar().hide();
        setContentView(R.layout.activity_operate_checkres);
        ButterKnife.bind(this);
        EventBus.getDefault().register(this);
        canvasCheckres = new CanvasCheckres(OperateCheckresActivity.this);
        llOperateCheckres.addView(canvasCheckres);
    }

    @OnClick({R.id.tvCheck, R.id.tvUnCheck, R.id.tvCancel})
    public void onViewClicked(View view) {
        switch (view.getId()) {
            case R.id.tvCheck:
                ScheduledExecutorService ScheduledCheck = 
				                Executors.newScheduledThreadPool(2);
				                
                ScheduledCheck.scheduleAtFixedRate(new 
			                MyScheduledExecutor(checkOrder), 
			                100, 40, TimeUnit.MILLISECONDS);
                ScheduledCheck = null;
                break;
            case R.id.tvUnCheck:
                ScheduledExecutorService SchedulUnedCheck = 
				                Executors.newScheduledThreadPool(2);
				                
				ScheduledCheck.scheduleAtFixedRate(new
								MyScheduledExecutor(unCheckOrder), 
				                100, 40, TimeUnit.MILLISECONDS);
                SchedulUnedCheck = null;
                break;
            default:
                break;
        }
    }

    public void onEventMainThread(EventCheckerMsg eventMsg) {
        if (eventMsg.getOrder().equals(checkOrder)) {
            if (checker > 13) {
                return;
            }
            canvasCheckres.setPosition(checker);
            checker++;
        }

        if (eventMsg.getOrder().equals(unCheckOrder)) {
            if (unChecker < 0) {
                return;
            }
            canvasCheckres.setPosition(unChecker);
            unChecker--;
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        EventBus.getDefault().unregister(this);
    }
}

这样就基本实现了前面的gif动画效果,不过这只是初步实现,要用到项目中的话,还需要优化修改。
另外,这里是通过EventBus和SchedulUnedCheck来实现动画帧的播放的,有伙伴们是通过Handler来实现的,思路都差不多,都是通过延时postDelay来实现的。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值