Android自定义控件(十)——SurfaceView实战实现天气APP背景移动效果
SurfaceView与View区别
前面我们所有的讲解基本都是自定义View来实现各种Android的自定义控件,但编写过相机的Android程序员,肯定对SurfaceView不陌生,那什么时候该用SurfaceView呢?
我们先来看一个概念,在Android中屏幕的刷新时间为16ms,如果View能够在16ms内完成所有的执行的绘图操作,那么在视觉上,界面是流畅的;否则APP就会卡顿,我们经常会看到如果View的逻辑非常复杂,Android Studio都会提示以下日志:
Skipped 60 frames! The application maybe doing too much work on its main thread
- 1
之所以会提示这个警告,是因为我们在自定义View的绘图操作中,执行了非常复杂的逻辑运算,导致16s内并没有完成绘制,所以当出现在自定义View中非常复杂的耗时的逻辑运算时,就需要使用SurfaceView。
SurfaceView在两个方面改进了View的绘图操作:
1.使用了双缓冲技术
2.自带画布,支持在子线程中更新画布内容
这里说的双缓冲技术,就是多加了一块缓冲画布,当需要执行绘图操作的时候,先在缓冲画布上绘制,绘制好后直接将缓冲画布的内部更新到主画布之中。这样,在屏幕更新的时候,只需要把缓冲画布上的内容照搬过来就可以了,就不会存在耗时的逻辑问题,也解决了超时绘制。
使用缓冲的Canvas绘图
前面我们已经介绍了,SurfaceView时自带画布的,具有双缓冲技术,那么问题来了,我们怎么才能拿到这块画布呢?直接先上代码:
SurfaceHolder surfaceHolder=getHolder();
Canvas canvas=surfaceHodler.lockCanvas();
//中间执行绘图操作
surfaceHolder.unlockCanvasAndPost(canvas);
- 1
- 2
- 3
- 4
我们这里直接通过surfaceHolder.lockCanvas()获取到了缓冲画布,并且将画布上锁,防止被其他线程篡改,当绘图完成之后释放锁,通过surfaceHolder.unlockCanvasAndPost(canvas)进行释放,这段代码不仅释放锁,还将缓冲画布的内容更新到主线程的画布上,从而显示到屏幕中。
这里上锁是防止其他线程同时更新缓冲画布,造成缓冲画布乱七八糟,所以我们需要加锁,至于什么是线程锁,死锁,释放锁等知识,这是Java多线程的知识,详情参考Java多线程书籍或者操作系统,这属于基础,篇幅有限,这里就不赘述了。
SurfaceView生命周期
在讲解SurfaceView生命周期之前,我们先要理解三个概念:Surface,SurfaceView,SurfaceHolder。有过MVC开发经验的小伙伴应该会非常熟悉,SurfaceView就是视图V,Surface中保存了缓冲画布和绘制内容相关的各种数据,也就是模型M,SurfaceHolder很明显就是MVC中的C控制器。
所以,当我们需要操作SurfaceView的时候,必然需要Surface存在,所以Android专门提供了监听Surface生命周期的函数:
public class DemoSurfaceView extends SurfaceView { private SurfaceHolder surfaceHolder; public DemoSurfaceView(Context context) { super(context); } public DemoSurfaceView(Context context, AttributeSet attrs) { super(context, attrs); this.surfaceHolder=getHolder(); this.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) { } }); } public DemoSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); }
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
上面也是每个自定义SurfaceView的基本使用方式,下面小编解释以下Surface的生命周期。
1.surfaceCreated:当Surface对象被创建后,该函数就会调用。
2.surfaceChanged:当surface发生任何结构性变化时,可以时格式,或者大小变化,该函数就会被立即调用。
3.surfaceDestroyed:当surface将要被销毁时调用。
一般来说,我们需要在类初始化时就立即绘图,那么一般放在surfaceCreated中来开启子线程的绘图操作,以防止没被创建时,缓冲画布时空的,在surfaceDestroyed中观察线程是否执行完成,如果没有执行完成,但surface将要被销毁,必须强制取消线程执行。
实现天气APP背景自动左右循环移动效果
为了实现常用的天气APP自动移动背景效果,我们来看看我们首先需要定义哪些成员变量,根据刚才讲的我们需要观察线程在销毁时,线程是否在执行,所以必须定义个线程是否执行的布尔变量,surfaceHolder控制器当然也需要,左右移动只需要X坐标变化,所以也需要定义变化的X坐标值,代码如下:
private SurfaceHolder surfaceHolder;//控制器
private boolean flag=false;//线程是否能执行
private Bitmap bgBitmap;//背景图片
private float screenWidht,screenHeight;//屏幕宽高
private int mBgX;//绘制的X坐标
private Canvas canvas;//画布
private Thread thread;//线程
//定义一个枚举类型,判断移动的方向
private enum State{
LEFT,RIGHT
}
private State state=State.LEFT;//开始向左运动
private final int MOVE_SIZE=1;//每次移动的距离
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
因为时左右循环啊移动,送所以我们还定义了枚举类型判断现在时向左还是向右,同时定义画布,屏幕宽高,以及当前运动方向,线程。
其次,我们需要监控Surface的生民周期,所以在其构造函数中调用如下方法进行监控:
public BgAnimSurfaceView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
this.surfaceHolder=getHolder(); this.surfaceHolder.addCallback(new SurfaceHolder.Callback() { @Override public void surfaceCreated(SurfaceHolder holder) { flag=true;//设置线程可以执行绘图操作 startAnim();//执行动画 } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { } @Override public void surfaceDestroyed(SurfaceHolder holder) { flag=false;//设置线程不可以执行绘图操作 } });
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
接着,我们需要将背景图片宽度放大到屏幕的3/2,高度为屏幕高度,所以,我们首先必须将图片定义到指定的大小,用到前面的Bitmap知识,代码如下:
/***
* 执行动画
*/
private void startAnim(){ this.screenWidht=getWidth();//获取屏幕宽度 this.screenHeight=getHeight();//获取屏幕高度 int enlargeWidht=(int) getWidth()*3/2;//放大的倍数 Bitmap bitmap= BitmapFactory.decodeResource(getResources(),R.drawable.background);//获取源图片 this.bgBitmap=Bitmap.createScaledBitmap(bitmap,enlargeWidht,(int)this.screenHeight,true);//将源图片宽度放大3/2倍,生成新的图片 this.thread=new Thread(new Runnable() { @Override public void run() { while (flag){//如果线程可以执行 canvas=surfaceHolder.lockCanvas(); drawView();//绘制 surfaceHolder.unlockCanvasAndPost(canvas); try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } } } }); this.thread.start();
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
这段代码就是放大图片,然后执行左右移动,这里使用到了本文第二个知识点,如何使用缓冲画布,而我们将绘制的操作放在了drawView()函数中,这里我们50ms执行一次绘图操作,不设置间隔时间,移动可能很快,达不到慢慢移动的效果,接着我们看看drawView()代码实现:
/***
* 开始绘制
*/
private void drawView(){ this.canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);//先清空屏幕 this.canvas.drawBitmap(this.bgBitmap,this.mBgX,0,null);//绘制图片 switch (this.state){//判断现在是向左还是向右移动 case LEFT: this.mBgX-=this.MOVE_SIZE;//向左移动 break; case RIGHT: this.mBgX+=this.MOVE_SIZE;//向右移动 break; default: break; } //如果向左移动了1/2,那么更改为向右移动,本身图片宽度只有3/2都移动了1/2显然已经移动完了 if(this.mBgX<=-this.screenWidht/2){ this.state=State.RIGHT; } //如果X坐标大于0,向左移动 if(this.mBgX>=0){ this.state=State.LEFT; }
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
这样我们就实现了天气APP背景自动移动的效果,代码中的注释已经够详细了,这里就不再赘述了,本文Github下载地址:点击下载
文章来源: liyuanjinglyj.blog.csdn.net,作者:李元静,版权归原作者所有,如需转载,请联系作者。
原文链接:liyuanjinglyj.blog.csdn.net/article/details/103315311
- 点赞
- 收藏
- 关注作者
评论(0)