canvas.drawPath(mPath, mPaint);
Log.d(“Rikka”,“onDraw”);
}
上面的代码的执行结果是一片黑屏。
为什么同样的代码,派生自View就可以画图,而SurfaceView却不行?
我们调用了上面打印的LOG,在SurfaceView上点击滑动时,发现日志如下:
虽然走了TouchEvent的postInvalidate()方法,但是压根就不会执行onDraw()。
这时候,我们的init()函数中有一行setWillNotDraw(false);被注释了,如果我们让它执行,会怎么样呢?
(2)setWillNotDraw(boolean willNotDraw)
这个函数存在在View类中,它主要用在View派生子类的初始化中,如果参数willNotDraw取true,则表示当前控件没有绘制内容。当屏幕重绘的时候,这个控件不需要绘制,所以在重绘的时候也不会调用该控件的onDraw()函数。
相反如果为false,每次重绘都要执行onDraw()。
可以看出setWillNotDraw()是一个优化策略,它让控件显式的告诉系统,在重绘重绘时,哪个控件需要重绘,哪个控件不需要重绘。这样可以大大提高重绘效率。
一般而言,对于LinearLayout、RelativeLayout而言,他们的主要功能布局其中的控件,所以它们本身是没有东西需要绘制的,所以它们的构造方法在显式的设置 setWillNotDraw(true)
之所以我们上面列子中SurfaceView一开始黑屏,不重绘,真是因为它也 默认的设置setWillNotDraw为true了
所以从这里也看出,SurfaceView的设计人员其实不想让我们通过重写onDraw()函数来绘制SurfaceView的控件函数。
(3)总结:
-
SurfaceView派生自View
-
当SurfaceView需要使用View的onDraw()函数来重绘控件时,需要在初始化的时候执行 setWillNotDraw(false)
-
View中的所有方法都是执行在UI线程的,所以并不建议SurfaceView重写View的onDraw方法来实现自定义控件,而要使用SurfaceView特有的双缓冲机制。
2、使用缓冲Canvas绘图
之前讲了SurfaceView是自带画布的,具有双缓冲技术。这是SurfaceView建议使用的绘图方式。那么我们应该如何拿到这块自画布来绘图呢?
SurfaceHolder surfaceHolder = getHolder();
Canvas canvas1 = surfaceHolder.lockCanvas();
//绘图操作
…
surfaceHolder.unlockCanvasAndPost(canvas1);
我们先通过surfaceHolder.lockCanvas()函数得到SurfaceView的自带缓冲画布,并将这个画布加锁,防止它被别的线程更改。
当绘制完后,我们通过surfaceHolder.unlockCanvasAndPost(canvas)来将缓冲画布释放,并将所画的内容更新到主线程的画布上,显示的显示在屏幕上。
Q:为什么得到画布时要加锁?
A:SurfaceView的缓冲画布时可以在线程中更新的,这是它的一大特点,而如果我们有多个线程同时更新画布,那么这个画布岂不是被画的乱七八糟?所以我们需要加锁。
而加锁会产生另一个问题,当画布被其他线程锁定的时候或者缓存的Canvas没有被创建的时候,surfaceHolder.lockCanvas()一定会返回null,如果继续使用canvas,必须要做判空处理,也需要在画布为空的时候添加重试策略。
学到这些,我们更改之前的捕捉手势的代码:
@Override
public boolean onTouchEvent(MotionEvent event) {
。。。
else if (event.getAction() == MotionEvent.ACTION_MOVE) {
mPath.lineTo(x, y);
}
drawCanvas();
return super.onTouchEvent(event);
}
private void drawCanvas() {
SurfaceHolder surfaceHolder = getHolder();
Canvas canvas = surfaceHolder.lockCanvas();
canvas.drawPath(mPath, mPaint);
surfaceHolder.unlockCanvasAndPost(canvas);
}
我们把postInvalidate()去掉,改成用缓冲画布来绘图,发现屏幕也变白了。
那其实,onTouchEvent方法是执行在主线程中的,所以在onTouchEvent中绘图跟直接重写View的onDraw()函数也没有什么区别了,那我们为什么还要用SurfaceView呢 = =!
其实SurfaceView的正确用法是在子线程中更新画布,我们在上述的代码中修改:
private void drawCanvas() {
new Thread(new Runnable() {
@Override
public void run() {
SurfaceHolder surfaceHolder = getHolder();
Canvas canvas = surfaceHolder.lockCanvas();
canvas.drawPath(mPath, mPaint);
surfaceHolder.unlockCanvasAndPost(canvas);
}
}).start();
}
3、监听Surface生命周期
上面我们简单的介绍了如何使用SurfaceView缓冲画布,其实与SurfaceView相关的有三个概念:Surface、SurfaceView、SurfaceHolder。
其实,这三个概念是典型的MVC模式。M是数据模型,在这里是Surface,Surface中保存着缓冲画布与绘图内容相关的各种信息,View即视图,代表用户交互界面,在这里就是SurfaceView,负责将Surface中存储的数据展示在View上。SurfaceHolder就是C,用它来操控Surface的数据。
既然我们知道SurfaceView的缓存Canvas是保存在Surface中的,那么,必然需要Surface存在的时候,才能够操作缓存Canvas,否则很容易获取到的Canvas是空的。
所以Android提供了SurfaceView的生命周期:
SurfaceHolder surfaceHolder = getHolder();
surfaceHolder.addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
});
-
surfaceCreated:但Surface对象被创建后,该函数就会被立刻调用
-
surfaceChanged:当Surface发生任何结构性的变化时,该函数就会被立刻回调
-
surfaceDestroyed:当Surface对象将要销毁时,该函数就会被立刻回调。
也就是说我们一般放在surfaceCreated函数中开启线程来绘图,而在在Destroyed方法中结束线程。
示例
我们这里用SurfaceView来实现一个动态背景效果的控件,让一个图作为显示不全的背景图,并且会左右移动将不全的地方显示出来。
(1)我们要让背景图片的宽度变成屏幕宽度的3/2,这样才能让他左右移动:
mSurfaceWidth = getWidth();
mSurfaceHeight = getHeight();
int mWidth = (int) (mSurfaceWidth * 3 / 2);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_jojo3);
bitmapBg = Bitmap.createScaledBitmap(bitmap, mWidth, (int) mSurfaceHeight, true);
(2)如何在屏幕上只画出图像的一部分?
Canvas::drawBitmap中有这样一个函数:
public void drawBitmap(Bitmap bitmap,float left,float top,Paint paint)
这个函数可以指定开始绘制图片的左上角位置。其中left、top就是指从Bitmap的哪个左上角点开始绘制,这样我们就可以指定绘制图片的一部分了。
(3)如何实现Bitmap的左右移动?
我们默从Bitmap的左上角(0,0)开始绘制,然后根据每次的步近距离向右移动,当移动到底时,再返回向左移动,核心代码如下:
//开始绘制的图片的x坐标
private int mBitposX;
//背景移动状态
private enum State {
LEFT, RIGHT
}
//默认为向左
private State state = State.LEFT;
//背景画布移动步伐,设置为1表示每次只移动1px,越大表明移动的越快
private final int BITMAP_STEP = 1;
private void DrawView() {
mCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
mCanvas.drawBitmap(bitmapBg, mBitposX, 0, null);
//滚动效果
switch (state) {
case LEFT:
//画布左移
mBitposX -= BITMAP_STEP;
break;
case RIGHT:
mBitposX += BITMAP_STEP;
break;
default:
break;
}
if (mBitposX <= -mSurfaceWidth / 2) {
state = State.RIGHT;
}
if (mBitposX >= 0) {
state = State.LEFT;
}
}
然后我们需要在初始化的时候就让背景开始运动,所以要添加Surface监听,用flag作为开始、结束动画的标识,在生命周期中使用:
public AnimationSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
surfaceHolder = getHolder();
surfaceHolder.addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {
flag = true;
startAnimation();
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
flag = false;
}
});
}
然后startAnimation函数用来打开动画:
private void startAnimation() {
mSurfaceWidth = getWidth();
mSurfaceHeight = getHeight();
int mWidth = (int) (mSurfaceWidth * 3 / 2);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_jojo3);
bitmapBg = Bitmap.createScaledBitmap(bitmap, mWidth, (int) mSurfaceHeight, true);
thread = new Thread(new Runnable() {
@Override
public void run() {
while (flag) {
mCanvas = surfaceHolder.lockCanvas();
DrawView();
surfaceHolder.unlockCanvasAndPost(mCanvas);
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
thread.start();
}
为了减轻主线程的计算负担,我们单独开启一个线程来执行绘图操作;绘图完成后,我们延缓了50ms再进行下次绘图,这样从效果上来看就是一步步移动的。
3、SurfaceView双缓冲技术
(1)概述
SurfaceView的双缓冲技术需要两个图形缓冲区支持,一个是前端缓冲区,一个是后端缓冲区。
前端区对应当前屏幕正在显示的内容,后端缓冲区是接下来渲染的图形缓冲区。
**我们通过surfaceHolder.lockCanvas()函数获得的缓冲区是后端缓冲区。
当绘图完成后,调用surfaceHolder.unlockCanvasAndPost(mCanvas)函数将后端缓冲区与前端缓冲区交换,后端缓冲区变前端缓冲区。**
而原来的前端缓冲区则变成后端缓冲区,等待下一次srufaceHolder.lockCanvas()函数调用返回给用户使用,如此往复。
上面的机制让绘制的效率大大的提高,但这样也产生了一个问题:两块画布上的内容肯定会存在不一致的情况,尤其是在多线程的情况下。
试想一下,我们利用一个线程操作A、B两个画布,A是屏幕画布,B是缓冲画布,我们拿到的一定是B画布,当我们绘制完,让B更新到屏幕上时,继续绘制时,将拿到A画布,但如果A画布和B画布的内容不一样,那么在A画布上继续作画,则会产生预想不到的情况的。
下面举一个栗子,每获取一次画布写一个数字,循环10次,代码如下:
private void init() {
mPaint = new Paint();
mPaint.setColor(Color.RED);
mPaint.setTextSize(30);
getHolder().addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {
drawText(holder);
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
});
}
private void drawText(SurfaceHolder holder) {
for (int i = 0; i < 10; i++) {
Canvas canvas = holder.lockCanvas();
if (canvas != null) {
canvas.drawText(i + “”, i * 30, 50, mPaint);
}
holder.unlockCanvasAndPost(canvas);
}
}
效果如图:
shit 怎么就打了 0 3 6 9
讲道理,我们每次获取一次画布然后在上面写数字,如果有两块画布,那应该是是1 3 6 7 9,因为最后写入的数字,那么按照逻辑往前推,9必然和1 3 5 7 在同一块画布上,其它数字都在另一块画布上,。
这是因为这里有三块缓冲画布。
如果我们在绘图时使用单独的线程,而且每次绘图完成以后,让线程休眠一段时间,就可以明显地看到每次所绘制的数字了。
private void drawText(final SurfaceHolder holder) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
Canvas canvas = holder.lockCanvas();
if (canvas != null) {
canvas.drawText(i + “”, i * 30, 50, mPaint);
}
holder.unlockCanvasAndPost(canvas);
try {
Thread.sleep(800);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级安卓工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新Android移动开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
holder.unlockCanvasAndPost(canvas);
try {
Thread.sleep(800);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级安卓工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新Android移动开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-5LYglVPp-1710921284771)]
[外链图片转存中…(img-VZvvDI9F-1710921284772)]
[外链图片转存中…(img-u5i2cC6W-1710921284772)]
[外链图片转存中…(img-cMzMBSBy-1710921284773)]
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
[外链图片转存中…(img-mwFc0fHm-1710921284773)]