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块中的事情。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (savedInstanceState == null) {
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
ScreenCaptureFragment fragment = new ScreenCaptureFragment();
transaction.replace(R.id.sample_content_fragment, fragment);
9 transaction.commit();
}
}
经测试发现,利用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(),实现方式分别如下。
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_screen_capture, container, false);
}
该方法是要告诉主程序类MainActivity存在可以用来替换其组件sample_content_fragment的xml文件fragment_screen_capture。
读取布局中的SurfaceView组件赋给mSurfaceView,并将其Surface成员取出赋给mSurface,将Button组件赋给mButton。
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
mSurfaceView = (SurfaceView) view.findViewById(R.id.surface);
mSurface = mSurfaceView.getHolder().getSurface();
mButton = (Button) view.findViewById(R.id.button);
mButton.setOnClickListener(this);
}
这段代码主要作用是提取手机屏幕的分辨率大小矩阵metrics、像素深度mScreenDensity及定义了MediaProjectionManager类对象mMediaProjectionManager。由于ScreenCaptureFragment类并没有继承与Activity相关的类,所以在获取WindowManager类对象及进行后续操作时需要getActivity的协助。
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
Activity activity = getActivity();
DisplayMetrics metrics = new DisplayMetrics();
activity.getWindowManager().getDefaultDisplay().getMetrics(metrics);
mScreenDensity = metrics.densityDpi;
mMediaProjectionManager = (MediaProjectionManager)activity.getSystemService(Context.MEDIA_PROJECTION_SERVICE);
}
在Android 5.0以前,如果开发者要对Android屏幕进行手机全屏的单帧或实时截图,往往比较困难。Android 5.0的出现改变了这种现状,其新增了MediaProjectionManager管理器,可以非常方便地实现屏幕捕捉功能。
6、上面提及界面上还有一个按钮,利用其来控制屏幕信息的显示,第一次按是开始,接着是停止,后续操作如此反复。首先看按钮的按键捕获与执行代码,注意开始和停止操作是由一个按钮实现,其text内容会随着点击在START与STOP之间切换。
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.button:
if (mVirtualDisplay == null) {
startScreenCapture();
} else {
stopScreenCapture();
}
break;
}
}
接下来看看开始显示屏幕信息函数startScreenCapture()做了什么。
private void startScreenCapture() {
Activity activity = getActivity();
if (mSurface == null || activity == null) {
return;
}
if (mMediaProjection != null) {
setUpVirtualDisplay();
} else if (mResultCode != 0 && mResultData != null) {
setUpMediaProjection();
setUpVirtualDisplay();
} else {
// This initiates a prompt dialog for the user to confirm screen projection.
startActivityForResult(mMediaProjectionManager.createScreenCaptureIntent(),REQUEST_MEDIA_PROJECTION);
}
}
通过以上代码可以看出,在刚开始,mSurface和activity已经定义,所以程序会继续往下执行;而MediaProjection对象mMediaProjection没有定义,为null;同样的startActivityForResult()方法还没有执行,不存在用户返回数据,故mResultCode(int型,记录返回值,同意或拒绝)为0,mResultData(Intent型,记录返回后的Intent对象)为null;所以,按照逻辑,会执行最后一个代码块,即创建并启动一个屏幕捕捉的Intent对象。
注意第二个参数为人为设定的请求码(整型),数值不限定,主要作用是对用户操作后的返回值进行判断。因为发起第一次在手机上发起屏幕截取请求,会弹出用户授权对话框,具体返回值见下面分析。请求对话框如下:
当用户选择拒绝时,应用程序自然就结束了;而选择同意时,便开始屏幕信息的获取与显示了。注意,对话框中间还有一个选项是设置要不要再次提示此请求,如果不够上,那么每次打开应用请求截屏时均会弹出此对话框。而该机制的实现方式很多,比如用一个配置文件,在配置文件中用一个变量控制是否弹出窗口,比如有一个config文件,里面有一个变量:showDialog=true,如果用户选择不在弹出,则复写config文件为showDialog=false,然后程序每次运行时检测,为true就显示,false就不弹窗。
7、那么用户选择同意之后,为什么就可以实时获取屏幕信息了?关键在于后面的方法onActivityResult(),来看其代码。
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_MEDIA_PROJECTION) {
if (resultCode != Activity.RESULT_OK) {
return;
}
Activity activity = getActivity();
if (activity == null) {
return;
}
mResultCode = resultCode;
mResultData = data;
setUpMediaProjection();
setUpVirtualDisplay();
}
}
可以看到,当初设定的请求码在此处发挥作用了,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()。
private void setUpMediaProjection() {
mMediaProjection = mMediaProjectionManager.getMediaProjection(mResultCode, mResultData);
}
虽然只有一句代码,但定义了在外漂泊已久的mMediaProjection,有了该对象才可以获取被捕获的屏幕信息。
9、然后是方法setUpVirtualDisplay(),从名字也可以看出屏幕信息终于要dispaly了。
private void setUpVirtualDisplay() {
mVirtualDisplay = mMediaProjection.createVirtualDisplay(
"ScreenCapture",mSurfaceView.getWidth(), mSurfaceView.getHeight(), mScreenDensity, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
mSurface, null, null);
mButton.setText(R.string.stop);
}
总共两句代码,第一句实现了将屏幕信息显示在SurfaceView组件中,第二句将按钮文本设为"STOP",因为开始时是"START",文本内容表示的含义是点击后会进行的操作,而不是程序当前的执行状态。到此时,界面上左下角的组件SurfaceView应该在实时显示屏幕信息,若状态栏的时间等信息变化了,可以达到流畅的播放效果。当然,由于不断地截取屏幕信息,会出现双面镜反射效果,即自身无限包含直到一个点(在理发店也可以体验^_^)。
实时显示屏幕界面如下:
10、要让其停止,只需要再次点击按钮,这回轮到stopScreenCapture()方法表现了。
private void stopScreenCapture() {
if (mVirtualDisplay == null) {
return;
}
mVirtualDisplay.release();
mVirtualDisplay = null;
mButtonToggle.setText(R.string.start);
}
代码很简单,将对象mVirtualDisplay释放,同时将按钮文本设置为"START"。
11、停止获取屏幕信息后,若结束应用,在完全退出之前要让其自动完成一项任务——停止对象mMediaProjection,需要重载onDestory()方法。
@Override
public void onDestroy() {
super.onDestroy();
tearDownMediaProjection();
}
内部调用的方法tearDownMediaProjection()实现如下。
private void tearDownMediaProjection() {
if (mMediaProjection != null) {
mMediaProjection.stop();
mMediaProjection = null;
}
}
12、最后介绍两个重载函数onCreate()与onPause(),先看后者。
@Override
public void onPause() {
super.onPause();
stopScreenCapture();
}
可以看出,在应用停止(比如其他应用突然在activity顶层运行、按了Back或Home键等)时,若当前正处在获取屏幕信息的状态下,调用此函数可以先将当前获取操纵停止,避免不必要的资源浪费。操作过就会发现,若在截屏请求时同意并勾选不再弹出对话框,那之后运行(包括从activity队列中重获新生与重新运行程序)就不会出现了。
来看onCreate()做了什么。
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
mResultCode = savedInstanceState.getInt(STATE_RESULT_CODE);
mResultData =savedInstanceState.getParcelable(STATE_RESULT_DATA);
}
}
刚开始就会判断数据承载对象savedInstanceState是否为空,若不是则取出mResultCode与mResultData。而这两个值前面已经说明,会记录用户的选择结果。
回到截屏开始函数,startScreenCapture(),其中有一个分支如下。
if (mResultCode != 0 && mResultData != null) {
setUpMediaProjection();
setUpVirtualDisplay();
}
即如果条件成立,就直接进行屏幕信息的获取并显示,不再需要发送带屏幕信息获取请求的Intent对象了,也就不会有那个对话框了。
对案例的分析就到这里了,欢迎感兴趣的朋友一起交流,共同进步!!!
Google最新截屏案例详解的更多相关文章
- 第7.20节 案例详解:Python抽象类之真实子类
第7.20节 案例详解:Python抽象类之真实子类 上节介绍了Python抽象基类相关概念,并介绍了抽象基类实现真实子类的步骤和语法,本节结合一个案例进一步详细介绍. 一. 案例说明 本节定义 ...
- 第7.18节 案例详解:Python类中装饰器@staticmethod定义的静态方法
第7.18节 案例详解:Python类中装饰器@staticmethod定义的静态方法 上节介绍了Python中类的静态方法,本节将结合案例详细说明相关内容. 一. 案例说明 本节定义了类Sta ...
- 第7.13节 案例详解:Python类变量
第7.13节 案例详解:Python类变量 上节介绍了类变量的定义和使用方法,并举例进行了说明.本节将通过一个更完整的例子来说明. 一. 定义函数dirp def dirp(iter): ret ...
- 第7.24节 Python案例详解:使用property函数定义属性简化属性访问代码实现
第7.24节 Python案例详解:使用property函数定义属性简化属性访问代码实现 一. 案例说明 本节将通过一个案例介绍怎么使用property定义快捷的属性访问.案例中使用Rectan ...
- spring的IOC,DI及案例详解
一:spring的基本特征 Spring是一个非常活跃的开源框架:它是一个基于Core来架构多层JavaEE系统的框架,它的主要目的是简化企业开发.Spring以一种非侵入式的方式来管理你的代码,Sp ...
- 深入浅出 spring-data-elasticsearch - 基本案例详解(三
『 风云说:能分享自己职位的知识的领导是个好领导. 』运行环境:JDK 7 或 8,Maven 3.0+技术栈:SpringBoot 1.5+, Spring Data Elasticsearch ...
- 用一个开发案例详解Oracle临时表
用一个开发案例详解Oracle临时表 2016-11-14 bisal ITPUB  一.开发需求 最近有一个开发需求,大致需要先使用主表,或主表和几张子表关联查询出ID(主键)及一些主表字段 ...
- http500:服务器内部错误案例详解(服务器代码语法错误或者逻辑错误)
http500:服务器内部错误案例详解(服务器代码语法错误或者逻辑错误) 一.总结 服务器内部错误可能是服务器中代码运行的时候的语法错误或者逻辑错误 二.http500:服务器内部错误案例详解 只是一 ...
- spss进行判别分析步骤_spss判别分析结果解释_spss判别分析案例详解
spss进行判别分析步骤_spss判别分析结果解释_spss判别分析案例详解 1.Discriminant Analysis判别分析主对话框 如图 1-1 所示 图 1-1 Discriminant ...
随机推荐
- mongoDB怎样拷贝一个collection从一个数据库到另一个在同一个主机上
new_database是目的数据库 db.<collection_name>.find().forEach(function(d){ db.getSiblingDB('<new_d ...
- Mysql数据库的通用安装方法
安装方式简介 Mysql数据库也时不时的用过一段时间,具体使用的功能都比较浅显,没有具体深入学习.最近一段在公司部署iNeedle系统时经常避免不了要安装apache和Mysql数据库.一般Mysql ...
- VC++ Debug编译方式
字节填充 VC++在Debug编译方式下,new的内存用0xcd(助记词为Cleared Data)填充,防止未初始化: delete后,内存用0xdd(Dead Data)填充,防止再次被使用. 这 ...
- 错误“Unexpected namespace prefix "xmlns" found for tag LinearLayout”的解决方法(转)
(转自:http://blog.csdn.net/bombzhang/article/details/12676789) 有一次升级开发工具后发现xml脚本出现错误“Unexpected namesp ...
- ELF Format 笔记(二)—— ELF Header
ilocker:关注 Android 安全(新入行,0基础) QQ: 2597294287 以 32 位的 ELF header 数据结构为例: #define EI_NIDENT 16 typede ...
- 理解 QEMU/KVM 和 Ceph(2):QEMU 的 RBD 块驱动(block driver)
本系列文章会总结 QEMU/KVM 和 Ceph 之间的整合: (1)QEMU-KVM 和 Ceph RBD 的 缓存机制总结 (2)QEMU 的 RBD 块驱动(block driver) (3)存 ...
- java 知识点随记
JAVA 读取配置文件: Properties props= new Properties();//文件在src目录下,编译会被加载到classpath下. Props.load(Test.class ...
- Ubuntu 14.04 下 Chromium 出现 未安装Adobe Flash Player 问题解决
Ubuntu 14.04 中,其他浏览器在安装Adobe Flash插件后可以播放视频及音乐,但是Chromium浏览器则会提示缺少Adobe Flash 插件. 原因:之前Chromium使用Net ...
- 希尔排序(c++)
希尔排序(Shell Sort)是插入排序的一种.也称缩小增量排序,是直接插入排序算法的一种更高效的改进版本.希尔排序是非稳定排序算法.该方法因DL.Shell于1959年提出而得名. 希尔排序是把记 ...
- poj2580 Super Memmo
Description Your friend, Jackson is invited to a TV show called SuperMemo in which the participant i ...