| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> 移动开发 -> Android自定义控件开发入门与实战(15)SurfaceView,看完就能找到工作 -> 正文阅读 |
|
[移动开发]Android自定义控件开发入门与实战(15)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(mPa
th, 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函数中开启线程来绘图,而在在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()函数来获取画布,这两个函数的区别如下:
我们来自定义一个控件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策略,我们这次拿的画布应该是A了。我们通过lockCanvas(new Rect(200, 200, 400, 400)) 方法获取了比蓝色画布中还要小的一部分。 那么问题来了:我们这次画的是半透明的白色圆,而画布以外的区域是从屏幕上复制过来的,那屏幕内的画布用的是哪块画布呢? |
|
移动开发 最新文章 |
Vue3装载axios和element-ui |
android adb cmd |
【xcode】Xcode常用快捷键与技巧 |
Android开发中的线程池使用 |
Java 和 Android 的 Base64 |
Android 测试文字编码格式 |
微信小程序支付 |
安卓权限记录 |
知乎之自动养号 |
【Android Jetpack】DataStore |
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 | -2024/11/24 9:49:08- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |