Google最新截屏案例详解
Google从Android 5.0 开始,给出了截屏案例ScreenCapture,在同版本的examples的Media类别中可以找到。给需要开发手机或平板截屏应用的小伙伴提供了非常有意义的参考资料,由于以前版本的API是隐藏的,要想开发一个截屏应用需要费一番心思且有局限性。当然了,这里说的截屏不是应用程序本身,而是包括状态栏在内的整个屏幕,不管当前运行的是什么程序,效果同按下手机自带截屏快捷键一样。
整个案例的源码就不在这里显摆了,相信感兴趣的朋友一定能找得到,其实整个工程很简单,而且在AndroidManifest.xml中也不需要添加什么用户权限。因为该案例并没有将屏幕数据转化为某一种类型图片并保存,而只是将信息显示在界面上的某一个组件中,注意是实时显示,即不断地在播放屏幕上的内容。不过这不是什么问题,我们只要在此基础上稍加改进就能读取出屏幕信息,生成图片保存下来,这时必须记得添加存储卡读写的权限。
下面对案例中关键的代码进行解析,一方面是想在学习与总结的过程中巩固知识,更重要的是希望有大神指出讲解错误或不足的地方。
1、在布局上,采取的方式是将主界面MainActivity中的FrameLayout布局组件替换为视图显示类ScreenCaptureFragment的LinearLayout布局组件,其中MainActivity继承自SampleActivityBase(SampleActivityBase继承自FragmentActivity),ScreenCaptureFragment类继承自Fragment ,负责显示屏幕信息的组件是SurfaceView,和控制是否显示屏幕信息的Button组件一起包含在上述的LinearLayout中。即布局文件有两个,分别作为MainActivity类和ScreenCaptureFragment类的显示视图(View)。
2、对于Java文件,真正发挥作用的是ScreenCaptureFragment类,因为SampleActivityBase类做的事情是继承FragmentActivity和添加日志,MainActivity类在完成布局设置及组件替换之后,便将控制权交给了ScreenCaptureFragment类。那究竟是怎么做到将屏幕信息取出,并不断地进行显示的呢?
3、先来看MainActivity中的onCreate()方法,可以说除了默认生成的代码,该类只做了下面if块中的事情。
1 @Override 2 protected void onCreate(Bundle savedInstanceState) { 3 super.onCreate(savedInstanceState); 4 setContentView(R.layout.activity_main); 5 if (savedInstanceState == null) { 6 FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); 7 ScreenCaptureFragment fragment = new ScreenCaptureFragment(); 8 transaction.replace(R.id.sample_content_fragment, fragment); 9 transaction.commit(); 10 } 11 }
经测试发现,利用FragmentTransaction类事务对象transaction的方法替换主界面中的FrameLayout显示内容时,replace()和add()的效果没有明显的区别。下面是Google官方给出的函数原型及解释:
public abstract FragmentTransaction add (int containerViewId, Fragment fragment, String tag)
Add a fragment to the activity state. This fragment may optionally also have its view (if Fragment.onCreateView returns non-null) into a container view of the activity.
add是把一个fragment添加到一个容器container里。
public abstract FragmentTransaction replace (int containerViewId, Fragment fragment, String tag)
Replace an existing fragment that was added to a container. This is essentially the same as calling remove(Fragment) for all currently added fragments that were added with the same containerViewId and then add(int, Fragment, String) with the same arguments given here.
replace是先remove掉相同id的所有fragment,然后在add当前的这个fragment。
值得注意的是,add和replace影响的只是界面,而控制回退的,是事务。
4、每一个事务都是同时要执行的一套变化。可以在一个给定的事务中设置你想执行的所有变化,使用诸如add()、remove()及 replace()。然后,要给activity应用事务,必须调用 commit()。在调用commit()之前,你可能想调用addToBackStack(),将事务添加到一个fragment事务的back stack。这个back stack由activity管理,并允许用户通过按下 BACK 按键返回到前一个fragment状态。
如果添加多个变化到事务(例如add()或remove())并调用addToBackStack(), 然后在你调用commit()之前的所有应用的变化会被作为一个单个事务添加到后台堆栈,BACK按键会将它们一起回退。调用commit()并不立即执行事务。恰恰相反,它将事务安排排期, 一旦准备好, 就在activity的UI线程上运行(主线程)。如果有必要,无论如何, 你可以从你的UI线程调用 executePendingTransactions()来立即执行由commit()提交的事务。但这么做通常不必要,除非事务是其他线程中的job的一个从属。
警告:你只能在activity保存它的状态(当用户离开activity)之前使用commit()提交事务。
在事务提交,即transaction.commit()执行之后,控制权就交给了MainActivity的主UI。在此案例中,其实就是将控制权交给了新添加的Fragment成分,即LinearLayout布局,其中包括一个SurfaceView和Button,具体操作为按下Button按钮即开始在SurfaceView中显示屏幕信息,再次按下停止显示。
5、对于ScreenCaptureFragment类,做的事情主要包括对继承方法进行重载,新定义手机屏幕获取与显示方法。在MainActivity启动及进行事务提交这段时间里面,其实ScreenCaptureFragment已经完成的操作包括onCreate(),onCreateView(),onViewCreated(),onActivityCreated(),它们都是在类对象构建过程中自动执行的,但要想在其中实现额外的功能,必须进行重载,案例中比较关键的是onCreateView(),onViewCreated(),onActivityCreated(),实现方式分别如下。
1 @Override 2 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 3 return inflater.inflate(R.layout.fragment_screen_capture, container, false); 4 }
该方法是要告诉主程序类MainActivity存在可以用来替换其组件sample_content_fragment的xml文件fragment_screen_capture。
读取布局中的SurfaceView组件赋给mSurfaceView,并将其Surface成员取出赋给mSurface,将Button组件赋给mButton。
1 @Override 2 public void onViewCreated(View view, Bundle savedInstanceState) { 3 mSurfaceView = (SurfaceView) view.findViewById(R.id.surface); 4 mSurface = mSurfaceView.getHolder().getSurface(); 5 mButton = (Button) view.findViewById(R.id.button); 6 mButton.setOnClickListener(this); 7 }
这段代码主要作用是提取手机屏幕的分辨率大小矩阵metrics、像素深度mScreenDensity及定义了MediaProjectionManager类对象mMediaProjectionManager。由于ScreenCaptureFragment类并没有继承与Activity相关的类,所以在获取WindowManager类对象及进行后续操作时需要getActivity的协助。
1 @Override 2 public void onActivityCreated(Bundle savedInstanceState) { 3 super.onActivityCreated(savedInstanceState); 4 Activity activity = getActivity(); 5 DisplayMetrics metrics = new DisplayMetrics(); 6 activity.getWindowManager().getDefaultDisplay().getMetrics(metrics); 7 mScreenDensity = metrics.densityDpi; 8 mMediaProjectionManager = (MediaProjectionManager)activity.getSystemService(Context.MEDIA_PROJECTION_SERVICE); 9 }
在Android 5.0以前,如果开发者要对Android屏幕进行手机全屏的单帧或实时截图,往往比较困难。Android 5.0的出现改变了这种现状,其新增了MediaProjectionManager管理器,可以非常方便地实现屏幕捕捉功能。
6、上面提及界面上还有一个按钮,利用其来控制屏幕信息的显示,第一次按是开始,接着是停止,后续操作如此反复。首先看按钮的按键捕获与执行代码,注意开始和停止操作是由一个按钮实现,其text内容会随着点击在START与STOP之间切换。
1 @Override 2 public void onClick(View v) { 3 switch (v.getId()) { 4 case R.id.button: 5 if (mVirtualDisplay == null) { 6 startScreenCapture(); 7 } else { 8 stopScreenCapture(); 9 } 10 break; 11 } 12 }
接下来看看开始显示屏幕信息函数startScreenCapture()做了什么。
1 private void startScreenCapture() { 2 Activity activity = getActivity(); 3 if (mSurface == null || activity == null) { 4 return; 5 } 6 if (mMediaProjection != null) { 7 setUpVirtualDisplay(); 8 } else if (mResultCode != 0 && mResultData != null) { 9 setUpMediaProjection(); 10 setUpVirtualDisplay(); 11 } else { 12 // This initiates a prompt dialog for the user to confirm screen projection. 13 startActivityForResult(mMediaProjectionManager.createScreenCaptureIntent(),REQUEST_MEDIA_PROJECTION); 16 } 17 }
通过以上代码可以看出,在刚开始,mSurface和activity已经定义,所以程序会继续往下执行;而MediaProjection对象mMediaProjection没有定义,为null;同样的startActivityForResult()方法还没有执行,不存在用户返回数据,故mResultCode(int型,记录返回值,同意或拒绝)为0,mResultData(Intent型,记录返回后的Intent对象)为null;所以,按照逻辑,会执行最后一个代码块,即创建并启动一个屏幕捕捉的Intent对象。
注意第二个参数为人为设定的请求码(整型),数值不限定,主要作用是对用户操作后的返回值进行判断。因为发起第一次在手机上发起屏幕截取请求,会弹出用户授权对话框,具体返回值见下面分析。请求对话框如下:
当用户选择拒绝时,应用程序自然就结束了;而选择同意时,便开始屏幕信息的获取与显示了。注意,对话框中间还有一个选项是设置要不要再次提示此请求,如果不够上,那么每次打开应用请求截屏时均会弹出此对话框。而该机制的实现方式很多,比如用一个配置文件,在配置文件中用一个变量控制是否弹出窗口,比如有一个config文件,里面有一个变量:showDialog=true,如果用户选择不在弹出,则复写config文件为showDialog=false,然后程序每次运行时检测,为true就显示,false就不弹窗。
7、那么用户选择同意之后,为什么就可以实时获取屏幕信息了?关键在于后面的方法onActivityResult(),来看其代码。
1 @Override 2 public void onActivityResult(int requestCode, int resultCode, Intent data) { 3 if (requestCode == REQUEST_MEDIA_PROJECTION) { 4 if (resultCode != Activity.RESULT_OK) { 5 return; 6 } 7 Activity activity = getActivity(); 8 if (activity == null) { 9 return; 10 } 11 mResultCode = resultCode; 12 mResultData = data; 13 setUpMediaProjection(); 14 setUpVirtualDisplay(); 15 } 16 }
可以看到,当初设定的请求码在此处发挥作用了,requestCode == REQUEST_MEDIA_PROJECTION,而用户选择同意之后的结果值为Activity.RESULT_OK,看一下其在源码中(Activity.java)的定义:同意之后的返回值为-1,拒绝的话就为0。
/** Standard activity result: operation canceled. */
public static final int RESULT_CANCELED = 0;
/** Standard activity result: operation succeeded. */
public static final int RESULT_OK = -1;
8、给mResultCode与mResultData赋于返回结果值之后,主角真正登场了,首先是方法setUpMediaProjection()。
1 private void setUpMediaProjection() { 2 mMediaProjection = mMediaProjectionManager.getMediaProjection(mResultCode, mResultData); 3 }
虽然只有一句代码,但定义了在外漂泊已久的mMediaProjection,有了该对象才可以获取被捕获的屏幕信息。
9、然后是方法setUpVirtualDisplay(),从名字也可以看出屏幕信息终于要dispaly了。
1 private void setUpVirtualDisplay() { 2 mVirtualDisplay = mMediaProjection.createVirtualDisplay( 3 "ScreenCapture",mSurfaceView.getWidth(), mSurfaceView.getHeight(), mScreenDensity, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, 4 mSurface, null, null); 5 mButton.setText(R.string.stop); 6 }
总共两句代码,第一句实现了将屏幕信息显示在SurfaceView组件中,第二句将按钮文本设为"STOP",因为开始时是"START",文本内容表示的含义是点击后会进行的操作,而不是程序当前的执行状态。到此时,界面上左下角的组件SurfaceView应该在实时显示屏幕信息,若状态栏的时间等信息变化了,可以达到流畅的播放效果。当然,由于不断地截取屏幕信息,会出现双面镜反射效果,即自身无限包含直到一个点(在理发店也可以体验^_^)。
实时显示屏幕界面如下:
10、要让其停止,只需要再次点击按钮,这回轮到stopScreenCapture()方法表现了。
1 private void stopScreenCapture() { 2 if (mVirtualDisplay == null) { 3 return; 4 } 5 mVirtualDisplay.release(); 6 mVirtualDisplay = null; 7 mButtonToggle.setText(R.string.start); 8 }
代码很简单,将对象mVirtualDisplay释放,同时将按钮文本设置为"START"。
11、停止获取屏幕信息后,若结束应用,在完全退出之前要让其自动完成一项任务——停止对象mMediaProjection,需要重载onDestory()方法。
1 @Override 2 public void onDestroy() { 3 super.onDestroy(); 4 tearDownMediaProjection(); 5 }
内部调用的方法tearDownMediaProjection()实现如下。
1 private void tearDownMediaProjection() { 2 if (mMediaProjection != null) { 3 mMediaProjection.stop(); 4 mMediaProjection = null; 5 } 6 }
12、最后介绍两个重载函数onCreate()与onPause(),先看后者。
1 @Override 2 public void onPause() { 3 super.onPause(); 4 stopScreenCapture(); 5 }
可以看出,在应用停止(比如其他应用突然在activity顶层运行、按了Back或Home键等)时,若当前正处在获取屏幕信息的状态下,调用此函数可以先将当前获取操纵停止,避免不必要的资源浪费。操作过就会发现,若在截屏请求时同意并勾选不再弹出对话框,那之后运行(包括从activity队列中重获新生与重新运行程序)就不会出现了。
来看onCreate()做了什么。
1 @Override 2 public void onCreate(Bundle savedInstanceState) { 3 super.onCreate(savedInstanceState); 4 if (savedInstanceState != null) { 5 mResultCode = savedInstanceState.getInt(STATE_RESULT_CODE); 6 mResultData =savedInstanceState.getParcelable(STATE_RESULT_DATA); 7 } 8 }
刚开始就会判断数据承载对象savedInstanceState是否为空,若不是则取出mResultCode与mResultData。而这两个值前面已经说明,会记录用户的选择结果。
回到截屏开始函数,startScreenCapture(),其中有一个分支如下。
1 if (mResultCode != 0 && mResultData != null) { 2 setUpMediaProjection(); 3 setUpVirtualDisplay(); 4 }
即如果条件成立,就直接进行屏幕信息的获取并显示,不再需要发送带屏幕信息获取请求的Intent对象了,也就不会有那个对话框了。
对案例的分析就到这里了,欢迎感兴趣的朋友一起交流,共同进步!!!