Android自定义控件开发入门与实战(15)SurfaceView

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();

}

}

}

}).start();

}

在每次画完图后,让线程休眠800ms

效果如下:

在这里插入图片描述

emm书上不是说的有三块画布的咩?怎么这里只显示了两块??

书上写的是 google给出的定义Surface中的缓冲画布的数量是根据需求动态分配的,如果用户获取画布的频率比较慢那就分两块画布,否则就会分配3的倍数块画布。

总的来讲,Surface肯定会被分配大于等于2个缓冲区域的。

(2)双缓冲技术局部更新原理

其实,SurfaceView是支持局部更新的,我们可以通过Canvas. lockCanvas(Rect dirty)函数指定获取画布的区域和大小。画布以外的地方会将现在屏幕上的内容复制过来,以保持与屏幕一致。而画布以内的区域则保持原画不变。前面我们一直使用lockCanvas()函数来获取画布,这两个函数的区别如下:

  • lockCanvas():用于获取整屏画布,屏幕内容不会被更新到画布上,画布保持原画布内容

  • lockCanvas(Rect dirty):用于获取指定区域的画布,画布以外的区域会保持与屏幕内容一致。

我们来自定义一个控件RectView,派生自View:

@Override

protected void onDraw(Canvas canvas) {

super.onDraw(canvas);

//画大方

mPaint.setColor(Color.RED);

canvas.drawRect(new Rect(0, 0, 600, 600), mPaint);

//画中方

mPaint.setColor(Color.GREEN);

canvas.drawRect(new Rect(30, 30, 570, 570), mPaint);

//画小方

mPaint.setColor(Color.BLUE);

canvas.drawRect(new Rect(60, 60, 540, 540), mPaint);

//画圆形

mPaint.setColor(Color.argb(0x3f, 0xff, 0xff, 0xff));

canvas.drawCircle(300, 300, 100, mPaint);

//写数字

mPaint.setColor(Color.GREEN);

canvas.drawText(“6”, 300, 300, mPaint);

}

很简单的图形,效果如下

在这里插入图片描述

从效果图中可以看到是一层层的叠加效果,如果我们将这些层次分明的图形利用SurfaceView来绘制,那么效果是怎样的呢?

private void init() {

mPaint = new Paint();

mPaint.setColor(Color.argb(0x1f, 0xff, 0xff, 0xff));

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(final SurfaceHolder holder) {

new Thread(new Runnable() {

@Override

public void run() {

//先进行清屏操作

while (true) {

Rect dirty = new Rect(0, 0, 1, 1);

Canvas canvas = holder.lockCanvas(dirty);

Rect canvasRect = canvas.getClipBounds();

if (getWidth() == canvasRect.width() && getHeight() == canvasRect.height()) {

canvas.drawColor(Color.BLACK);

holder.unlockCanvasAndPost(canvas);

} else {

holder.unlockCanvasAndPost(canvas);

break;

}

}

//画图

for (int i = 0; i < 5; i++) {

//画大方

if (i == 0) {

Canvas canvas = holder.lockCanvas(new Rect(0, 0, 600, 600));

canvas.drawColor(Color.RED);

holder.unlockCanvasAndPost(canvas);

}

//画中方

if (i == 1) {

Canvas canvas = holder.lockCanvas(new Rect(30, 30, 570, 570));

canvas.drawColor(Color.GREEN);

holder.unlockCanvasAndPost(canvas);

}

//画小方

if (i == 2) {

Canvas canvas = holder.lockCanvas(new Rect(60, 60, 540, 540));

canvas.drawColor(Color.BLUE);

holder.unlockCanvasAndPost(canvas);

}

//画圆

if (i == 3) {

Canvas canvas = holder.lockCanvas(new Rect(200, 200, 400, 400));

mPaint.setColor(Color.argb(0x3f, 0xff, 0xff, 0xff));

canvas.drawCircle(300, 300, 100, mPaint);

holder.unlockCanvasAndPost(canvas);

}

//画数字

if (i == 4) {

Canvas canvas = holder.lockCanvas(new Rect(250, 250, 350, 350));

mPaint.setColor(Color.RED);

canvas.drawText(i + “”, 300, 300, mPaint);

holder.unlockCanvasAndPost(canvas);

}

try {

Thread.sleep(800);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

}).start();

}

我们在drawText()函数中利用线程执行绘图操作。

代码分成两部分,第一部分利用while进行清屏操作,第二部分是利用for循环获取缓冲画布绘图。

有关清屏操作的代码我们之后再讲。

效果如下:

在这里插入图片描述

从效果图看出第二部分的代码就是根据多缓冲机制来实现的。而且外围的三个颜色框和之前的View是相同的,而最后画圆和写数字则是不一样的。我们以缓冲机制来分析一下。

前三个方框的绘制过程如下:

从绘制数字部分可以看出,手机上默认分配了三块缓冲画布。一块在屏幕上,另一块待分配。

①缓冲画布A在第一次画大方时获取,对画布中画了红色,而画布以外的区域通过holder.lockCanvas(new Rect(0, 0, 600, 600)) 拿到了屏幕上的之前的画布,也就是全部黑色。

②缓冲画布B在画中方时获取,它对指定的画布涂成绿色,而画布以外的地方是从A拿过来的,也就是红色边框+黑色背景

③缓冲画布C在画小方时获取,他跟上述的步骤一样。

到这里我们总结出:

  • 缓冲画布时根据LRU(先进先出)策略被存取使用的

  • 使用holder.lockCanvas(rect)函数获取到的画布区域,在通过unlockCanvasAndPost(canvas)函数提交到屏幕上,指定区域的内容是我们自己的绘图结果,指定区域外的内容是从屏幕上复制过来的。

④根据LRU策略,我们这次拿的画布应该是A了。我们通过lockCanvas(new Rect(200, 200, 400, 400)) 方法获取了比蓝色画布中还要小的一部分。

那么问题来了:我们这次画的是半透明的白色圆,而画布以外的区域是从屏幕上复制过来的,那屏幕内的画布用的是哪块画布呢?

答案是:屏幕内的画布用的使我们拿到的画布本身!这里拿到的是画布A,所以画圆就是在A上叠起来画的。

所以在效果图上可以看到,有圆的那个区域其实是红色透明的~

⑤写数字同理,根据LRU策略,拿到B画布的区域,是绿色的,然后在上面写数字。

为什么要去清屏

上面每次绘图时,我们都要做清屏操作,为什么要这么做呢,假如我们把while去掉,那么效果是怎么样的呢?

在这里插入图片描述

在没有清屏的时候,通过lockCanvas(rect)函数拿到的画布区域是不是还是指定的区域。

在不清屏的情况下,把每次得到的rect区域打印出来:

private void dumpCanvasRect(Canvas canvas) {

if (canvas != null) {

Rect rect = canvas.getClipBounds();

Log.d(“Rikka”, “left:” + rect.left + " top:" + rect.top + " right:" + rect.right + " bottom:" + rect.bottom);

}

}

得到:

在这里插入图片描述

发现:第一次获取到的画布区域并不是我们指定的区域,而是SurfaceView所占的全屏,把清屏去掉后

在这里插入图片描述

清屏后,就可以拿到指定的区域。

因为这里有两块缓冲画布,有一块画布初始化地被显示在屏幕上,已经被默认填充为黑色了,虽然我们指定为画布区域,但是系统认为整个区域为脏区域,需要全部画一遍才行。所以一开始指定的画布区域其实是全部。

根据这个特性,我们在一开始的时候,可以指定一个极小的区域:

Rect dirty = new Rect(0, 0, 1, 1)

Canvas canvas = holder.lockCanvas(dirty);

然后这个屏幕还没有被画过,那么它应该返回一个与当前控件一样大小的区域,这时我们就可以给它画上默认的黑色。

Rect canvasRect = canvas.getClipBounds();

if (getWidth() == canvasRect.width() && getHeight() == canvasRect.height()) {

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

尾声

最后,我再重复一次,如果你想成为一个优秀的 Android 开发人员,请集中精力,对基础和重要的事情做深度研究。

对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。 整理的这些架构技术希望对Android开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。

最后想要拿高薪实现技术提升薪水得到质的飞跃。最快捷的方式,就是有人可以带着你一起分析,这样学习起来最为高效,所以为了大家能够顺利进阶中高级、架构师,我特地为大家准备了一套高手学习的源码和框架视频等精品Android架构师教程,保证你学了以后保证薪资上升一个台阶。

当你有了学习线路,学习哪些内容,也知道以后的路怎么走了,理论看多了总要实践的。

进阶学习视频

附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题 (含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!**

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

尾声

最后,我再重复一次,如果你想成为一个优秀的 Android 开发人员,请集中精力,对基础和重要的事情做深度研究。

对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。 整理的这些架构技术希望对Android开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。

最后想要拿高薪实现技术提升薪水得到质的飞跃。最快捷的方式,就是有人可以带着你一起分析,这样学习起来最为高效,所以为了大家能够顺利进阶中高级、架构师,我特地为大家准备了一套高手学习的源码和框架视频等精品Android架构师教程,保证你学了以后保证薪资上升一个台阶。

当你有了学习线路,学习哪些内容,也知道以后的路怎么走了,理论看多了总要实践的。

进阶学习视频

[外链图片转存中…(img-GytkpcX1-1712595716871)]

附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题 (含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

[外链图片转存中…(img-LfDqXYIg-1712595716871)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值