1.前言
逛街的时候,看到一篇Android Canvas 方法总结,这篇文章将Canvas一些基本操作介绍的很详细。从零开始的朋友可以先去刷点经验,剩下的同学拿起手术刀,我们一起来将Canvas血腥解剖吧。
2.Canvas简介
官方文档介绍如下
The Canvas class holds the "draw" calls. To draw something, you need 4 basic components:
a Bitmap to hold the pixels,
a Canvas to host the draw calls (writing into the bitmap),
a drawing primitive (e.g. Rect, Path, text, Bitmap),
a paint (to describe the colors and styles for the drawing).
用人话说大概是这样
一个Canvas类对象有四大基本要素
1、用来保存像素的Bitmap
2、用来在Bitmap上进行绘制操作的Canvas
3、要绘制的东西
4、绘制用的画笔Paint
Bitmap和Canvas的关系类似于画板与画布,不理解没有关系,后面还会详细介绍的。
3.Canvas基本绘制
一开始,先来点简单的基础题。因为太懒了不想从头写起,我就假设大家都看过了Android Canvas 方法总结,来写一些这里面没有介绍的方法。
3.1 圆角矩形
画笔初始化
mPaint = new Paint();
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(10);
mPaint.setColor(Color.RED);
在onDraw()
中进行绘制,参数二、三越大,圆角半径越大
private void drawRoundRect(Canvas canvas) {
RectF r = new RectF(100, 100, 400, 500);
//x-radius ,y-radius圆角的半径
canvas.drawRoundRect(r, 80, 80, mPaint);
}
效果图如下
圆角矩形.png3.2 圆角矩形路径
那么有的时候,我不需要那么整齐的圆角,该怎么办呢
private void drawRoundRectPath(Canvas canvas) {
RectF r = new RectF(100, 100, 400, 500);
Path path = new Path();
float radii[] = {80, 80, 80, 80, 80, 80, 20, 20};
path.addRoundRect(r, radii, Path.Direction.CCW);
canvas.drawPath(path, mPaint);
}
这里的radii就定义了四个角的弧度,接着使用Path就可以将其绘制出来。事实上,Path是无所不能的,我们将在以后单独介绍他。
圆角矩形路径.png3.3 Region区域
Region是区域的意思,它表示的Canvas图层上的一块封闭的区域。
来看看它的构造方法
/** Create an empty region
*/
public Region() {
this(nativeConstructor());
}
/** Return a copy of the specified region
*/
public Region(Region region) {
this(nativeConstructor());
nativeSetRegion(mNativeRegion, region.mNativeRegion);
}
/** Return a region set to the specified rectangle
*/
public Region(Rect r) {
mNativeRegion = nativeConstructor();
nativeSetRect(mNativeRegion, r.left, r.top, r.right, r.bottom);
}
/** Return a region set to the specified rectangle
*/
public Region(int left, int top, int right, int bottom) {
mNativeRegion = nativeConstructor();
nativeSetRect(mNativeRegion, left, top, right, bottom);
}
看上去挺万金油的,什么都能传进去,那么Region能干嘛呢?我们可以用它来进行一些交并补集的操作,比如下面代码就能展示两个region的交集
region1.op(region2, Region.Op.INTERSECT);//交集部分 region1是调用者A,region2是求交集的B
去源码里看看这个Op,发现是个枚举类
public enum Op {
DIFFERENCE(0),
INTERSECT(1),
UNION(2),
XOR(3),
REVERSE_DIFFERENCE(4),
REPLACE(5);
...
}
可见交并补的类型还挺多,我们用一张图来介绍吧
Region_op.png
那么这里就出现了一个问题,这些Op过后的region都是不规则的了,系统要如何将他们绘制出来呢?
嘿嘿嘿,请回忆起当年被微积分支配的恐惧吧!
private void drawRegion(Canvas canvas){
RectF r = new RectF(100, 100, 400, 500);
Path path = new Path();
float radii[] = {80, 80, 80, 80, 80, 80, 20, 20};
path.addRoundRect(r, radii, Path.Direction.CCW);
//创建一块矩形的区域
Region region = new Region(100, 100, 600, 800);
Region region1 = new Region();
region1.setPath(path, region);//path的椭圆区域和矩形区域进行交集
//结合区域迭代器使用(得到图形里面的所有的矩形区域)
RegionIterator iterator = new RegionIterator(region1);
Rect rect = new Rect();
mPaint.setStrokeWidth(1);
while (iterator.next(rect)) {
canvas.drawRect(rect, mPaint);
}
}
看看代码,这里通过迭代器用一个个小矩形填满整个region控件,为了方便展示,我们把paint的类型设成stroke,效果图如下
region绘制.png解释下,这里首先绘制了最大的那个矩形,然后在极限的距离上缩小矩形,再通过这些小矩形将剩下的部分都填充起来。
4.Canvas基本变换
4.1 Canavas坐标系
Canvas里面牵扯两种坐标系:Canvas自己的坐标系、绘图坐标系
Canvas的坐标系,它就在View的左上角,从坐标原点往右是X轴正半轴,往下是Y轴的正半轴,有且只有一个,唯一不变
绘图坐标系,它不是唯一不变的,它与Canvas的Matrix有关系,当Matrix发生改变的时候,绘图坐标系对应的进行改变,同时这个过程是不可逆的(通过相反的矩阵还原),而Matrix又是通过我们设置translate、rotate、scale、skew来进行改变的
上面这段话还是挺好理解的,我们用代码来验证下
private void drawMatrix(Canvas canvas){
// 绘制坐标系
RectF r = new RectF(0, 0, 400, 500);
mPaint.setColor(Color.GREEN);
canvas.drawRect(r, mPaint);
// 第一次绘制坐标轴
canvas.drawLine(0,0,canvas.getWidth(),0,mPaint);// X 轴
mPaint.setColor(Color.BLUE);
canvas.drawLine(0,0,0,canvas.getHeight(),mPaint);// Y 轴
//平移--即改变坐标原点
canvas.translate(50, 50);
// 第二次绘制坐标轴
mPaint.setColor(Color.GREEN);
canvas.drawLine(0,0,canvas.getWidth(),0,mPaint);// X 轴
mPaint.setColor(Color.BLUE);
canvas.drawLine(0,0,0,canvas.getHeight(),mPaint);// Y 轴
canvas.rotate(45);
// 第三次绘制坐标轴
mPaint.setColor(Color.GREEN);
canvas.drawLine(0,0,canvas.getWidth(),0,mPaint);// X 轴
mPaint.setColor(Color.BLUE);
canvas.drawLine(0,0,0,canvas.getHeight(),mPaint);// Y 轴
}
运行结果如下
Canvas坐标系剩下的translate、rotate、scale、skew等方法,在之前推荐的那篇文章里就有,这里就不再重复劳动了
5.Canvas状态保存
5.1 状态栈
状态栈通过save、 restore方法来保存和还原变换操作Matrix以及Clip剪裁,也可以通过restoretoCount直接还原到对应栈的保存状态。需要注意的是,一开始canvas就是在栈1的位置,执行一次save就进栈一次(此时为2),执行一次restore就出栈一次。
private void saveRestore(Canvas canvas){
RectF r = new RectF(0, 0, 400, 500);
mPaint.setColor(Color.GREEN);
canvas.drawRect(r, mPaint);
canvas.save();
//平移
canvas.translate(50, 50);
mPaint.setColor(Color.BLUE);
canvas.drawRect(r, mPaint);
canvas.restore();
mPaint.setColor(Color.YELLOW);
r = new RectF(0, 0, 200, 200);
canvas.drawRect(r, mPaint);
}
效果图如下
状态栈有的同学可能会把状态栈理解为图层,其实这是不对滴。我们来看下save方法的源码
/**
* Saves the current matrix and clip onto a private stack.
* <p>
* Subsequent calls to translate,scale,rotate,skew,concat or clipRect,
* clipPath will all operate as usual, but when the balancing call to
* restore() is made, those calls will be forgotten, and the settings that
* existed before the save() will be reinstated.
*
* @return The value to pass to restoreToCount() to balance this save()
*/
public int save() {
return native_save(mNativeCanvasWrapper, MATRIX_SAVE_FLAG | CLIP_SAVE_FLAG);
}
注释上说的很明白,save只是保存了matrix和clip的状态,并没有保存一个真正的图层。真正的图层是在Layer栈中保存的。
5.2 Layer栈
Layer栈通过saveLayer新建一个透明的图层,并且会将saveLayer之前的一些Canvas操作延续过来,后续的绘图操作都在新建的layer上面进行,当我们调用restore或者 restoreToCount 时更新到对应的图层和画布上。
下面这段代码要仔细看
private void saveLayer(Canvas canvas) {
RectF rectF = new RectF(0, 0, 400, 500);
Paint paint = new Paint();
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(10);
paint.setColor(Color.GREEN);
canvas.drawRect(rectF, paint);
canvas.translate(50, 50);
canvas.saveLayer(0, 0, canvas.getWidth(), canvas.getHeight(), null, Canvas.ALL_SAVE_FLAG);
canvas.drawColor(Color.BLUE);// 通过drawColor可以发现saveLayer是新建了一个图层,
paint.setColor(Color.YELLOW);
canvas.drawRect(rectF, paint);
canvas.restore();
RectF rectF1 = new RectF(0, 0, 300, 400);
paint.setColor(Color.RED);
canvas.drawRect(rectF1, paint);
}
先上效果图
saveLayer我们慢慢解释。一开始画了个绿色的框,之后移动了(50,50)的距离,再通过saveLayer新建了一个图层。注意这里用到了Canvas.ALL_SAVE_FLAG,别的FLAG还有只保存Matrix的,只保存Clip的等等,大家可以自己去看。接着在新的图层上画了蓝色背景和黄色的矩形,从结果可以看出之前的一些Canvas操作会被延续到新的图层。调用restore后,两个图层合二为一,由于图层2是蓝色背景,因此就把图层1的绿色边框覆盖了。最后再绘制另一个红色的框,此时只有一个图层了,所以就绘制在当前图层的最上方。
文章开头说Bitmap和Canvas的关系类似于画板与画布,就是因为每次Canvas执行saveLayer时都会新建一个透明的图层,与之前的图层叠加后更新到Bitmap上,从而将绘制的内容展示出来。
这些大概就是save与saveLayer的区别所在了,如果觉得自己明白了,就去看看给女朋友化妆系列的代码,看看是否可以理解其中的save与saveLayer操作,以及为什么要这样做。
6.Drawable与Canvas
6.1 Drawable简介
下面我们将用一个例子来加深学习效果,不过开始前还需要将Drawable这位兄弟介绍给大家。首先上一段官方注释:
A Drawable is a general abstraction for "something that can be drawn."
顾名思义,Drawable就是可以被画出来的一个东西,这是一个抽象类,继承自它的实现类如下(windows中查询类继承关系的快捷键是Ctrl+H)
drawable继承结构.png是不是发现有些熟悉的字眼?比如layer、shape、color等等。对啦,就是可以在drawable文件夹中进行定义的xml资源文件,系统会将这些xml转换成都解析成相应的drawable对象。
由于drawable是可绘制的对象,canvas是绘制的画纸,因此这两位是密不可分的好基友。除去上图中系统实现的drawable外,我们还可以根据需要自定义drawable。事实上自定义drawable才是日常会用到的东西,下面一起来看看这种基本操作。
6.1 基本操作
这个自定义控件的功能不太好描述,先展示下效果图
效果图.png最外层是一个HorizontalScrollView,里面包裹着许多ImageView,可以拖动,中间选中的区域呈现灰色,其余的显示彩色。这些灰色、彩色的图是两套资源。
要实现上述功能,我们需要两个控件,一个是外层控制触摸事件的ViewGroup,可以通过继承HorizontalScrollView来完成。另一个是用来注入ImageView变换颜色的Drawable,这就需要自定义来实现了。
6.1.1 RevealDrawable
创建RevealDrawable继承自Drawable,需要重写public void draw(@NonNull Canvas canvas)
方法。
对于每一部分的图片而言,会有以下几种绘制情况:
1.灰色
2.彩色
3.左灰右彩
4.左彩右灰
灰色和彩色好办,直接将两种图片当做参数传入RevealDrawable中,在需要时通过draw(canvas)
绘制出来即可。那么混合色该怎么做?又要如何去判断左右或者说灰彩各占的比例呢?
对于问题一,我们可以用之前所说的canvas裁剪来完成,需要注意save()
和restore()
的调用;至于问题二,Drawable源码中有这么一行参数:private int mLevel = 0;
,很显然,Google早已考虑到Drawable的这种使用场景,而mLevel
就是用来确定比例的,其值为0~10000,可以由我们在外层动态去设置。
总结一下,整体思路就是HorizontalScrollView根据Scroll的距离为RevealDrawable动态设置level,而RevealDrawable则根据被设置的level展示出不同的图像效果。剩下的就是数学问题了。
这里就展示其核心的绘制方法,注释都有,完整的代码等闲了整理下一起放到大型同性交友平台上。
@Override
public void draw(Canvas canvas) {
// 绘制
int level = getLevel();//from 0 (minimum) to 10000
//三个区间
//右边区间和左边区间--设置成灰色
if(level == 10000|| level == 0){
mUnselectedDrawable.draw(canvas);
}
else if(level==5000){//全部选中--设置成彩色
mSelectedDrawable.draw(canvas);
}else{
//混合效果的Drawable
/**
* 将画板切割成两块-左边和右边
*/
final Rect r = mTmpRect;
//得到当前自身Drawable的矩形区域
Rect bounds = getBounds();
{
//1.先绘制灰色部分
//level 0~5000~10000
//比例
float ratio = (level/5000f) - 1f;
int w = bounds.width();
if(mOrientation==HORIZONTAL){
w = (int) (w* Math.abs(ratio));
}
int h = bounds.height();
if(mOrientation==VERTICAL){
h = (int) (h* Math.abs(ratio));
}
int gravity = ratio < 0 ? Gravity.LEFT : Gravity.RIGHT;
//从一个已有的bounds矩形边界范围中抠出一个矩形r
Gravity.apply(
gravity,//从左边还是右边开始抠
w,//目标矩形的宽
h, //目标矩形的高
bounds, //被抠出来的rect
r);//目标rect
canvas.save();//保存画布
canvas.clipRect(r);//切割
mUnselectedDrawable.draw(canvas);//画
canvas.restore();//恢复之前保存的画布
}
{
//2.再绘制彩色部分
//level 0~5000~10000
//比例
float ratio = (level/5000f) - 1f;
int w = bounds.width();
if(mOrientation==HORIZONTAL){
w -= (int) (w* Math.abs(ratio));
}
int h = bounds.height();
if(mOrientation==VERTICAL){
h -= (int) (h* Math.abs(ratio));
}
int gravity = ratio < 0 ? Gravity.RIGHT : Gravity.LEFT;
//从一个已有的bounds矩形边界范围中抠出一个矩形r
Gravity.apply(
gravity,//从左边还是右边开始抠
w,//目标矩形的宽
h, //目标矩形的高
bounds, //被抠出来的rect
r);//目标rect
canvas.save();//保存画布
canvas.clipRect(r);//切割
mSelectedDrawable.draw(canvas);//画
canvas.restore();//恢复之前保存的画布
}
}
}
6.1.2 GalleryHorizontalScrollView
外层的GalleryHorizontalScrollView继承自HorizontalScrollView,需要处理滑动事件与level设置,别忘了ScrollView的子View必须是ViewGroup
private void init() {
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
);
container = new LinearLayout(getContext());
container.setLayoutParams(params);
setOnScrollChangeListener(this);
}
onLayout()
的作用是在控件初始化时设置Padding值,以便于一开始,将第一个ImageView展示在正中间的位置(为了好看,没什么特别的用处)
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
View v = container.getChildAt(0);
icon_width = v.getWidth();//单个图片的宽度
centerX = getWidth() / 2;//整个sv的宽度
centerX = centerX - icon_width/2;
container.setPadding(centerX, 0, centerX, 0);
}
起始图.png
起初触摸事件是在touch
方法中完成的,后来发现这个方法精度太高,滑动抖动明显,因此换在scroll
方法中执行。
private void reveal() {
// 渐变效果
//得到hzv滑出去的距离
int scrollX = getScrollX();
Log.d(TAG, "reveal: "+scrollX);
//找到两张渐变的图片的下标--左,右
int index_left = scrollX/icon_width;
int index_right = index_left + 1;
//设置图片的level
for (int i = 0; i < container.getChildCount(); i++) {
if(i==index_left||i==index_right){
//变化
//比例:
float ratio = 5000f/icon_width;
ImageView iv_left = (ImageView) container.getChildAt(index_left);
//scrollX%icon_width:代表滑出去的距离
//滑出去了icon_width/2 icon_width/2%icon_width
iv_left.setImageLevel(
(int)(5000-scrollX%icon_width*ratio)
);
//右边
if(index_right<container.getChildCount()){
ImageView iv_right = (ImageView) container.getChildAt(index_right);
//scrollX%icon_width:代表滑出去的距离
//滑出去了icon_width/2 icon_width/2%icon_width
iv_right.setImageLevel(
(int)(10000-scrollX%icon_width*ratio)
);
}
}else{
//灰色
ImageView iv = (ImageView) container.getChildAt(i);
iv.setImageLevel(0);
}
}
}
最后是添加图片的方法
public void addImageViews(Drawable[] revealDrawables){
for (int i = 0; i < revealDrawables.length; i++) {
ImageView img = new ImageView(getContext());
img.setImageDrawable(revealDrawables[i]);
container.addView(img);
if(i==0){
img.setImageLevel(5000);
}
}
addView(container);
}
7.Canvas缓存
在上面的例子中我们介绍了Canvas与自定义Drawable的配合使用,接下来我们上一个Canvas与Bitmap混合实现缓存的效果。其实这句话是废话,因为创建Canvas时就必须传入Bitmap为参,毕竟Bitmap才是真正保存像素的地方。
缓存的思想就是先将像素保存到CacheCanvas的CacheBitmap中,再将这个CacheBitmap保存到View的Canvas上。
我们在初始化方法中创建缓存对象。
private void init() {
//创建一个与该VIew相同大小的缓冲区
cacheBitmap = Bitmap.createBitmap(VIEW_WIDTH, VIEW_HEIGHT, Bitmap.Config.ARGB_8888);
//创建缓冲区Cache的Canvas对象
cacheCanvas = new Canvas();
path = new Path();
//设置cacheCanvas将会绘制到内存的bitmap上
cacheCanvas.setBitmap(cacheBitmap);
paint = new Paint();
paint.setColor(Color.RED);
paint.setFlags(Paint.DITHER_FLAG);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(5);
paint.setAntiAlias(true);
paint.setDither(true);//防抖动,比较清晰
}
接着onTouch()
中根据手势进行绘制,注意是绘制到缓存canvas上
@Override
public boolean onTouchEvent(MotionEvent event) {
//获取拖动时间的发生位置
float x = event.getX();
float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
path.moveTo(x, y);
preX = x;
preY = y;
break;
case MotionEvent.ACTION_MOVE:
path.quadTo(preX, preY, x, y);//绘制圆滑曲线
preX = x;
preY = y;
break;
case MotionEvent.ACTION_UP:
//这是是调用了cacheBitmap的Canvas在绘制
cacheCanvas.drawPath(path, paint);
path.reset();
break;
}
invalidate();//在UI线程刷新VIew
return true;
}
invalidate()
会回调draw()
方法,此时再将缓存bitmap绘制到View的canvas中。
@Override
protected void onDraw(Canvas canvas) {
Paint p = new Paint();
//将cacheBitmap绘制到该View
canvas.drawBitmap(cacheBitmap, 0, 0, p);
}
这样一来,手指滑动轨迹就会略有延迟后再绘制到用户界面上,类似于写字板的效果。
cache缓存.png8.总结
关于canvas的介绍就到此为止,了解canvas的基本绘制,知道它的两个坐标系,学会和drawable、bitmap混合使用基本就差不多了。希望能给大家带来好运。
前段时间秋招找工作实习什么的断更好久,从今天开始,立个FLAG在此,一天一篇!
网友评论