简述SurfaceView及常见问题
Posted sparrowlhl
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了简述SurfaceView及常见问题相关的知识,希望对你有一定的参考价值。
在android开发中,SurfaceView平常并不常用,但是遇到一些视频播放或者拍照等情况,就需要用到。下面对该控件进行简单的介绍,并列举出使用过程中遇到的问题进行QA形式的解答!
声明: 关于SurfaceView的原理的介绍主要参考文章:https://blog.csdn.net/luoshengyang/article/details/8661317
一、运用场景:
普通的Android控件,它们的UI都是在应用程序的主线程中进行绘制的。而应用程序除了绘制外,还需要及时响应用户输入,否则,会引起ANR。为了避免ANR,对于一些游戏画面,摄像预览、视频播放等UI比较复杂,而且要求能够进行高效绘制的视图,它们的UI就不适合在应用程序的主线程中进行绘制。这时需要给此类视图生成一个独立的绘图表面,并使用一个独立的线程来绘制这些视图的UI,此时就必须使用SurfaceView进行开发。
二、SurfaceView是什么?
1、定义:
继承自View,该类内嵌了一个专门用于绘制的Surface。你可以控制这个Surface的格式和尺寸。SurfaceView控制这个Surface的绘制位置。
Surface是纵深排序(Z-ordered)的,这表明它总在自己所在窗口的后面。Surfaceview提供了一个可见区域,只有在这个可见区域内 的surface部分内容才可见,可见区域外的部分不可见。Surface的排版显示受到视图层级关系的影响,它的兄弟视图结点会在顶端显示。这意味者 surface的内容会被它的兄弟视图遮挡,这一特性可以用来放置遮盖物(overlays)(例如,文本和按钮等控件)。注意,如果surface上面 有透明控件,那么它的每次变化都会引起框架重新计算它和顶层控件的透明效果,这会影响性能。
2、负责绘制UI的SurfaceFlinger:
讲到Surface,在此需要简要介绍一下SurfaceFlinger,一个负责绘制Android应用程序UI的服务。SurfaceFlinger服务运行在Android系统的System进程中,它负责管理Android系统的帧缓冲区(Frame Buffer)。Android应用程序为了能够将自己的UI绘制在系统的帧缓冲区上,就必须要与SurfaceFlinger服务进行通信.具体流程概括为:
(1) Android应用程序请求SurfaceFlinger服务创建Surface;
(2) Surface创建后,Android应用程序在上面绘制自己的UI;
(3) 再请求SurfaceFlinger服务将已经绘制好UI的Surface渲染到设备显示屏上。
一般来说,每一个窗口在SurfaceFlinger服务中都对应有一个Layer,用来描述它的绘图表面。对于那些具有SurfaceView的窗口来说,每一个SurfaceView在SurfaceFlinger服务中还对应有一个独立的Layer或者LayerBuffer,用来单独描述它的绘图表面,以区别于它的宿主窗口的绘图表面。
由于拥有独立的绘图表面,因此SurfaceView的UI就可以在一个独立的线程中进行绘制。又由于不会占用主线程资源,SurfaceView一方面 可以实现复杂而高效的UI,另一方面又不会导致用户输入得不到及时响应。
3、单独介绍SurfaceView的一个成员变量:(仅做了解)
SurfaceView类的成员变量mRequestedType描述的是SurfaceView的绘图表面的类型,有以下四个值:摘自源码。
SURFACE_TYPE_NORMAL:用RAM缓存原生数据的普通Surface
SURFACE_TYPE_HARDWARE:适用于DMA(Direct memory access )引擎和硬件加速的Surface
SURFACE_TYPE_GPU:适用于GPU加速的Surface
SURFACE_TYPE_PUSH_BUFFERS:表明该Surface不包含原生数据,Surface用到的数据由其他对象提供,在Camera图像预览中就使用该类型的Surface,有Camera负责提供给预览Surface数据,这样图像预览会比较流畅。如果设置这种类型则就不能调用lockCanvas来获取Canvas对象了。
需要说明的是,虽然SurfaceHolder中已经不建议使用setType()方法了,我们自己写demo也可以看到该方法已经被声明为@Deprecated
/** * Sets the surface‘s type. * * @deprecated this is ignored, this value is set automatically when needed. */ @Deprecated public void setType(int type);
但是,在使用自定义的SurfaceView实现视频播放时,还为了兼容低版本,仍需要设置setType:
//设置SurfaceHolder类型,为了兼容低版本,需要设置此类型,否则播放时,只有声音,而没有图像 getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
4、关于View的setWillNotDraw():
查看SurfaceView的源码可知,在其构造函数中调用了setWillNotDraw(true);该方法会导致draw()、onDraw()都不执行。
/** * If this view doesn‘t do any drawing on its own, set this flag to * allow further optimizations. By default, this flag is not set on * View, but could be set on some View subclasses such as ViewGroup. * * Typically, if you override @link #onDraw(android.graphics.Canvas) * you should clear this flag. * * @param willNotDraw whether or not this View draw on its own */ public void setWillNotDraw(boolean willNotDraw) setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
自定义SurfaceView,运行结果如下:
(1)默认执行结果:
(2)在CommSurfaceView的构造函数中,手动设置setWillNotDraw(false),运行结果如下:
三、SurfaceView的实现过程:(详情请参看原文)
SurfaceView的整个实现过程分为三步:绘图表面的创建、在宿主窗口设置一块透明区域用于显示、在独立线程中进行自己UI的绘制。
1.、创建独立的绘图表面;
2、在宿主窗口上设置一块透明区域来显示自己(使绘制的UI可见);【SurfaceView的窗口类型所表示的Z轴位置小于Activity窗口的Z轴位置】
3、在独立的线程中进行其UI绘制。
四、SurfaceView的使用:
由于SurfaceView的底层实现过程已经进行了封装,并为开发者提供了上层使用接口,即:系统给SurfaceView提供了一个专门绘图的Surface,嵌入在了SurfaceView视图层中,画面在Surface中绘制完成,在SurfaceView中通过获得SurfaceHolder的对象,管理并展示Surface的数据内容。所以,开发时只需按照下述步骤即可:
1、一般都是重新自定义SurfaceView,起到封装的作用:
(1)继承SurfaceView;
(2)利用getHolder()获取SurfaceHolder对象;
(3)给SurfaceHolder对象添加实现SurfaceHolder.Callback接口的对象,即添加回调函数SurfaceHolder.addCallback(callback);
(4)重写Callback的三个方法:surfaceChanged,surfaceCreated,surfaceDestroyed;
(5)利用SurfaceHolder对象设置其类型、格式、大小等。
(一)在SurfaceView上进行UI绘制的流程如下:
(1). 在绘图表面的基础上建立一块画布,即获得一个Canvas对象。
(2). 利用Canvas类提供的绘图接口在前面获得的画布上绘制任意的UI。
(3). 将已经填充好UI数据的画布缓冲区提交给SurfaceFlinger服务,以便SurfaceFlinger服务可以将它合成到屏幕上去。
代码格式:
Canvas c=surfaceHolder.lockCanvas();
.....具体画图操作......
surfaceHolder.unlockCanvasAndPost(c);
(二)利用SurfaceView播放视频:
Android中有三种实现形式,均可实现视频的播放:
-
- 原生VideoView(继承SurfaceView)
- 直接使用SurfaceView
- 自定义播放控件(继承SurfaceView)
具体视频的播放均是使用的MediaPlayer,只是在自定义及VideoView中,被封装在各自的类中,对调用者不可见。
视频的画面一般由两部分组成:视频内容画面+上层操作布局(标题、播放进度、时长、暂停等控件)。
为了配合上层操作布局,一般都会实现MediaPlayerControl接口(当然,完全可以自定义),获取视频播放相关信息。
以原生VideoView为例,展现代码格式: VideoView nativeVideoView = (VideoView) findViewById(R.id.nativeVideoView); MediaController nativeMediaController = new MediaController(this);
//设置播放路径:实际内部封装了MediaPlayer的创建及数据源、监听事件、播放类型、画面显示等的设置,即视频播放前的准备工作,
//其中 mMediaPlayer.setDisplay(mSurfaceHolder);即是画面相关的设置 nativeVideoView.setVideoPath(path); nativeVideoView.setMediaController(nativeMediaController); nativeVideoView.requestFocus(); nativeVideoView.start();//内部执行mediaPlayer.start()
五、常见问题:(以VideoView为例)
1、在播放视频时,滑动进度,会出现闪屏(画面一直在变)?
原因:原生VideoView使用SeekBar,在滑动进度的过程中,在OnSeekBarChangeListener的onProgressChanged()中调用了MediaPlayerControl的seekTo(),即实时更新视频画面,出现闪屏。
1 源码:frameworks\\base\\core\\java\\android\\widget\\MediaController.java 2 3 private final OnSeekBarChangeListener mSeekListener = new OnSeekBarChangeListener() 4 @Override 5 public void onStartTrackingTouch(SeekBar bar) 6 show(3600000); 7 8 mDragging = true; 9 10 // By removing these pending progress messages we make sure 11 // that a) we won‘t update the progress while the user adjusts 12 // the seekbar and b) once the user is done dragging the thumb 13 // we will post one of these messages to the queue again and 14 // this ensures that there will be exactly one message queued up. 15 removeCallbacks(mShowProgress); 16 17 18 @Override 19 public void onProgressChanged(SeekBar bar, int progress, boolean fromuser) 20 if (!fromuser) 21 // We‘re not interested in programmatically generated changes to 22 // the progress bar‘s position. 23 return; 24 25 26 long duration = mPlayer.getDuration(); 27 long newposition = (duration * progress) / 1000L; 28 mPlayer.seekTo( (int) newposition); //实时更新播放画面 29 if (mCurrentTime != null) 30 mCurrentTime.setText(stringForTime( (int) newposition)); 31 32 33 @Override 34 public void onStopTrackingTouch(SeekBar bar) 35 mDragging = false; 36 setProgress(); 37 updatePausePlay(); 38 show(sDefaultTimeout); 39 40 // Ensure that progress is properly updated in the future, 41 // the call to show() does not guarantee this because it is a 42 // no-op if we are already showing. 43 post(mShowProgress); 44 45 ;
处理:解决此问题,可将视频画面的更新放在滑动结束(即onStopTrackingTouch()中)即可。
引入新的问题:若用户看视频,需要从30分钟开始播放,但是具体位置不确定,因此需要在30分钟附近查看,此时则需要随着用户的滑动,画面切换,
但是,又不能出现上述闪屏问题。
新问题的解决方案:可以监听两次滑动的时间间隔,然后画面更新(例:两次间隔500ms更新一次画面)
测试结果:证实可以。具体为在滑动过程中即onProgressChanged()中进行如下操作:
//startTouchTime:首次为拖动开始的时间,之后为每次更新时重新赋值更新时的时间
//changeTouchTime:时刻获取滑动变化的时间
if(mDragging && changeTouchTime - startTouchTime >= 500)
Log.e(TAG, "prepare fromuser onProgressChanged ++++++++++++++ ");
startTouchTime = changeTouchTime; //重置,最近一次修改
mPlayer.seekTo(newPosition);
if (currentTime != null)
currentTime.setText(stringForTime(newPosition));
2、横竖屏切换时,视频从头播放,不会连续。
原因:默认情况下, Activity在横竖屏切换时会重新创建,因此视频重播.结合是否设置属性对Activity的生命周期的影响,即可明白。
解决方案:横竖屏切换必须在AndroidManifest.xml中设置属性android:configChanges="screenSize|orientation"。
3、播放完成,停止时,播放进度未显示在最后?(该问题是我的项目中出现的,并非通用问题)
现象:由于视频播放完成,做退出处理,一般看不到此现象。
原因:根据视频“剪辑”时,进度可以完全显示。查找原因,发现剪辑视频时,整个过程中进度条可见,一直在实时更新界面进度,因此可完全显示;而普通播放视频时,进度条等播放布局是可隐藏的,而代码中在MoviePlayer的setProgress()中,判断当进度等布局不可见时,直接返回,而不更新界面显示进度,因此,在播放完成时,界面显示的是最近一次布局可见时的进度(一般不在最后)。
4、视频播放过程中,操作暂停播放后点击Home键退回桌面,再次进入会出现黑屏?【注:播放情况下正常是由于播放再次进入执行了继续播放,即刷新了界面】
原因:点击Home键时,SurfaceView中的Surface被销毁(从执行回调函数surfaceDestroy()即可看出),播放画面显示为黑色。
测试现象:
(1) 看到黑屏效果,是因为Activity背景被设置为黑色,在去掉背景色之后,发现仅有播放视频区域显示黑色;
(2)将窗体设置为白色,按上述操作,发现仅视频播放区域为黑色;将主题设置为android:Theme.Translucent,按上述操作,发现仅视频播放区域为透明,即可看见MainActivity。这也可以解释SurfaceView的实现原理中的第二条,在宿主窗口中设置一块透明区域显示自己。运行效果图如下:MainActivity界面(左侧)、右侧为视频播放界面。
得出结论:Surface被销毁后,播放区域显示的背景为当前主题Theme中的配置。
5、其他问题:SurfaceView、GLSurfaceView、SurfaceTexture、TextureView的区别:
SurfaceView:Android1.0就有,继承自View,因为有单独的Surface【不在View hierachy】中,它的显示也不受View的属性控制,所以不能进行平移,缩放等变换,也不能放在其它ViewGroup中,一些View中的特性也无法使用。
GLSurfaceView:Android1.5引入,继承自SurfaceView,在SurfaceView的基础上,它加入了EGL的管理,并自带了渲染线程。另外它定义了用户需要实现的Render接口,提供了用Strategy pattern更改具体Render行为的灵活性。作为GLSurfaceView的Client,只需要将实现了渲染函数的Renderer的实现类设置给GLSurfaceView即可。
SurfaceTexture:Android 3.0(API level 11)加入。单独的类,和SurfaceView不同的是,它对图像流的处理并不直接显示,而是转为GL外部纹理,因此可用于图像流数据的二次处理(如Camera滤镜,桌面特效等)。比如Camera的预览数据,变成纹理后可以交给GLSurfaceView直接显示,也可以通过SurfaceTexture交给TextureView作为View heirachy中的一个硬件加速层来显示。
TextureView:在4.0(API level 14)中引入。继承自View,它可以将内容流直接投影到View中,可以用于实现Live preview【实时预览】等功能.和SurfaceView不同,它不会在WMS中单独创建窗口,而是作为View hierachy中的一个普通View,因此可以和其它普通View一样进行移动,旋转,缩放,动画等变化。值得注意的是TextureView必须在硬件加速的窗口中。它显示的内容流数据可以来自App进程或是远端进程。
六、总结:
SurfaceView是android系统中特殊的视图,继承自View。它具有独立的绘图表面,但是,由于其窗口类型所表示的Z轴位置小于Activity窗口的Z轴位置,因此,为了使其可见,需要在宿主窗口申请设置一块透明区域来显示。一切条件就绪后,就可以在独立的线程中进行复杂的UI绘制,且不会影响应用程序的主线程响应用户输入。
以上是关于简述SurfaceView及常见问题的主要内容,如果未能解决你的问题,请参考以下文章
Android SurfaceView 绘图覆盖刷新及脏矩形刷新方法
Android笔记:SurfaceView与SurfaceHolder对象
自己定义控件(2.2):SurfaceView和SurfaceHolder
Android游戏开发十九(必看篇)SurfaceView运行机制详解—剖析Back与Home按键及切入后台等异常处理!