自定义控件|从画布裁剪来说过度绘制
自定义View一直是安卓开发中比较困难的技术点,实现一个优秀的自定义View控件不仅涉及到View的定位、测量、绘制等知识体系,还涉及到控件的绘制效率、是否存在过度绘制、是否存在绘制时间超长、是否存在内存泄漏等问题。
过度绘制又是布局优化中很重要的一个环节,有部分过度绘制是因为视图中View层级太多,背景层次太多,还有部分是因为View本身在同一块区域进行了多次绘制导致。关于视图层级,有经验的开发者都会在构造XML文件时进行处理,这点比较好注意到,也比较好优化。而关于View本身的重复绘制,可能不是很好处理,特别是在使用第三方控件时,需要通过修改源码来优化。比较经典的一个例子就是自定义扑克牌控件,下面,我们一步步来看下如何对这种控件进行优化。
准备工作 在查看View的过度绘制状态时,我们一般会打开手机的GPU过度绘制调试开关,位于设备的开发者选项里:
文章图片
他会将屏幕中的View的过度绘制状态以不同的颜色填充,具体为:
文章图片
接下来我们就需要实现扑克牌控件了。
实现控件 我们将几张扑克牌绘制在一个自定义View中,按照从左到右的顺序,右边一张牌盖住左边一张牌的部分。实现效果应该如下图:
文章图片
为了达到比较好的效果,这边准备了54张扑克牌的的素材。
文章图片
【自定义控件|从画布裁剪来说过度绘制】接下来,我们来实现控件,需要注意的几点是:
- 计算扑克牌被盖住的部分宽度
- 获取扑克牌的
Bitmap
对象 - 获取每张扑克牌的绘制区域
- 绘制扑克牌
/**
* 扑克相叠视图
*/
public class PokerView extends View {/**
* 默认一行刚好能排列4张扑克
*/
private final static int DEFAULT_COUNT = 4;
/**
* 扑克资源引用,用来随机发牌
*/
private final static List POKER_LIST = new ArrayList<>();
static {
POKER_LIST.add(R.drawable.p1);
POKER_LIST.add(R.drawable.p2);
......省略部分代码
POKER_LIST.add(R.drawable.p53);
POKER_LIST.add(R.drawable.p54);
}private int count = DEFAULT_COUNT;
private Paint mPaint;
/**
* 当前扑克的Bitmap列表
*/
private Map mCurBitmaps = new HashMap<>();
......省略构造方法以及初始化画笔方法init/**
* 发牌,重绘视图
*/
public void shuffle(int count) {
this.count = count;
randomPoker();
invalidate();
}private void init() {
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setStrokeWidth(0);
mPaint.setStyle(Paint.Style.FILL);
}@Override protected void onAttachedToWindow() {
super.onAttachedToWindow();
randomPoker();
}
/**
* 绘制扑克牌
*/
@Override protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int measuredWidth = getMeasuredWidth();
int defaultPokerWidth = measuredWidth / DEFAULT_COUNT;
int pokerHeight = getMeasuredHeight();
// 一般绘制,存在过度绘制问题
overlayDraw(canvas, pokerHeight, defaultPokerWidth);
// 优化绘制,不存在过度绘制问题
clipDraw(canvas, pokerHeight, defaultPokerWidth);
}@Override protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mCurBitmaps.clear();
}/**
* 获取随机N张牌
*/
private void randomPoker() {
mCurBitmaps.clear();
while (mCurBitmaps.size() < count) {
int random = (int) (Math.random() * 53 + 1);
int rp = POKER_LIST.get(random);
if (!mCurBitmaps.containsKey(rp)) {
mCurBitmaps.put(rp, BitmapFactory.decodeResource(getResources(), rp));
}
}
}
}
可以看出,核心就是重写View的
onDraw
方法,上例中的overlayDraw
方法是普通绘制策略,存在过度绘制问题;clipDraw
是优化的绘制策略,不存在过度绘制问题。下面进行详述。一般方案
上述
overlayDraw
方法的逻辑是确定好每张牌的绘制区域后,进行整个区域的绘制工作,这时,对于被后一张牌盖住的部分,其实也进行了绘制,事实上,这部分不可见的区域完全是不需要绘制的,这也就导致了过度绘制。/**
* 过度绘制
*/
private void overlayDraw(Canvas canvas, int pokerHeight, int defaultWidth) {
int pokerWidth = defaultWidth * 3 / (count - 1);
Iterator iterator = mCurBitmaps.values().iterator();
for (int i = 0;
i < count;
i++) {
Rect rect = new Rect();
rect.left = pokerWidth * i;
rect.bottom = pokerHeight;
rect.top = 0;
rect.right = rect.left + defaultWidth;
if (iterator.hasNext()) {
canvas.drawBitmap(iterator.next(), null, rect, mPaint);
}
}
}
我们来看下过度绘制状态:
文章图片
可以看到梅花Q的左边部分过度绘制显示浅红色,也就是三层绘制;梅花Q的右边部分过度绘制显示淡绿色,也就是两层绘制。
整个扑克视图绝大多数部分都存在过度绘制问题。
优化方案
上一种方案,我们绘制每张扑克的整个区域,事实上,除了最后一张扑克牌显示完全,其他扑克显示都是不完全的,不可见的部分其实就是没必要去绘制的,这样就可以去除过度绘制了。所以,我们需要对绘制区域进行裁剪。具体工具就是
canvas
的裁剪方法。主要涉及到clipRect
和clipOutRect
两个方法(api26+)。我们来看下试图裁剪到底是怎么回事。我们在
onDraw
方法中绘制两个有公共部分的正方形,View背景设置浅灰色,左上方正方形背景设置绿色,右下方正方形背景设置红色,我们来测试下效果。以下测试均在API26以上进行
第一组代码:
Rect leftRect = new Rect(0,0,300,300);
Rect rightRect = new Rect(150,150,450,450);
mPaint.setColor(Color.GREEN);
canvas.drawRect(leftRect,mPaint);
mPaint.setColor(Color.RED);
canvas.drawRect(rightRect,mPaint);
效果:
文章图片
第二组代码:
Rect leftRect = new Rect(0,0,300,300);
Rect rightRect = new Rect(150,150,450,450);
canvas.clipRect(leftRect);
mPaint.setColor(Color.GREEN);
canvas.drawRect(leftRect,mPaint);
mPaint.setColor(Color.RED);
canvas.drawRect(rightRect,mPaint);
效果:
文章图片
第三组代码:
Rect leftRect = new Rect(0,0,300,300);
Rect rightRect = new Rect(150,150,450,450);
canvas.clipRect(leftRect);
canvas.clipRect(rightRect);
mPaint.setColor(Color.GREEN);
canvas.drawRect(leftRect,mPaint);
mPaint.setColor(Color.RED);
canvas.drawRect(rightRect,mPaint);
效果:
文章图片
第四组代码:
Rect leftRect = new Rect(0,0,300,300);
Rect rightRect = new Rect(150,150,450,450);
canvas.clipOutRect(leftRect);
canvas.clipRect(rightRect);
mPaint.setColor(Color.GREEN);
canvas.drawRect(leftRect,mPaint);
mPaint.setColor(Color.RED);
canvas.drawRect(rightRect,mPaint);
效果:
文章图片
从以上测试结果可以看出上述两个方法的作用:
clipRect
方法是裁剪出要绘制的画布clipOutRect
方法是裁剪掉不需要绘制的画布
clipRect
对两块区域同时裁剪时,最终的绘制区域为公共部分clipRect
只裁剪一块区域时,最终绘制区域为裁剪区域clipOutRect
裁剪时,最终绘制区域不包括裁剪的区域clipOutRect
和clipRect
同时使用时,最终绘制区域为:clipRect
裁剪区域,并且排除掉clipOutRect
裁剪的区域
我们看下
clipDraw
方法的实现/**
* 非过度绘制
*/
private void clipDraw(Canvas canvas, int pokerHeight, int defaultWidth) {
int pokerWidth = defaultWidth * 3 / (count - 1);
Rect lastRect = null;
Iterator iterator = mCurBitmaps.values().iterator();
for (int i = count - 1;
i >= 0;
i--) {
canvas.save();
Rect rect = new Rect();
rect.left = pokerWidth * i;
rect.bottom = pokerHeight;
rect.top = 0;
rect.right = rect.left + defaultWidth;
if (lastRect != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
canvas.clipOutRect(lastRect);
}else {
// TODO: api 26 以下适配
}
}
canvas.clipRect(rect);
if (iterator.hasNext()) {
canvas.drawBitmap(iterator.next(), null, rect, mPaint);
}
lastRect = rect;
canvas.restore();
}
}
上述代码是从右边往左边绘制的。我们在绘制时,会先将上一张牌的区域裁减掉,然后在剩下的区域中裁剪出需要绘制的牌的区域。我们可以看下过度绘制状态
文章图片
很明显,所有的区域都是蓝色的,也就是说,只绘制了一次。明显优于第一种绘制方案。
回顾 可以看到,我们通过简单的裁剪策略就避免了多重的区域绘制,本节主要是介绍了过度绘制检测、画布裁剪、自定义View的一些技术点。旨在为需要的读者提供一种解决问题的思路。
github地址
推荐阅读
- Docker应用:容器间通信与Mariadb数据库主从复制
- 一个人的碎碎念
- 我从来不做坏事
- 从蓦然回首到花开在眼前,都是为了更好的明天。
- 西湖游
- SpringBoot调用公共模块的自定义注解失效的解决
- python自定义封装带颜色的logging模块
- 改变自己,先从自我反思开始
- leetcode|leetcode 92. 反转链表 II
- 从我的第一张健身卡谈传统健身房