• 分享一种截屏方法


    分享一种截屏方法

     

     

      在任何时候点击手机上的浮动小球(红色圈内)就能完成整个屏幕信息的截取功能,而且最终保存的图像还不会包含该小球,这就是本文将要介绍的方法。

      手机整体屏幕项目的获取方式:

      1、Github

      2、源码打包文件

      以新的视角实现手机屏幕的截取(快照)功能,文章可能比较长,感兴趣的朋友一定得看完,会有收获的哦!若发现有哪些地方存在问题或某些功能有更好的实现方式,欢迎指点,先谢过(我可以尽快改正或完善,以免继续误导别人)。

      关于手机(或平板,以下描述均以手机表示这两类设备)整体屏幕的截取,有的机型默认设置的方式是同时按下电源键和一个音量键(如华硕、诺基亚等,向下音量键+电源键),有的机型是同时按下电源键和Home键(如苹果)。另外,借助一些辅助工具经过特定的设置也是可以完成屏幕快照的获取。

      从打算开发一个与传统截屏方法不同的截屏应用开始,针对Android手机截屏的基本原理、相关知识及Google最新案例,已经在学习与摸索的途中写过两篇文章了,感兴趣的朋友可以通过下面给出的链接去瞧一瞧(这方面知识掌握较好的可以直接忽略)。

      在Win7+Android Studio环境尝试了网络上晒出的很多方式均失败后,带着受打击的心态写了第一篇文章:

      Android手机截屏

      当时的目的是希望实现过的大神能给点有用的建议或意见。当然,一般来说Android应用在Android Studio和Eclipse下都是可以实现的,虽不能将项目代码在两者之间直接进行转换,但如若工作量不是特别大,移植起来也不麻烦,尝试过的朋友应该懂得。

      可能对于截屏应用进行学习或者实现的人不太多,笔者并没有得到什么宝贵性的建议。后面不甘心又进行了一番Google,还是没有直截了当的答案,最后决定静下心来老老实实地分析案例源码(案例没有屏幕数据获取与图片保存的实现)。于是就有了第二篇文章:

      Google最新截屏案例详解。

      简单回顾一下:如果只是想得到正在运行的应用程序自身的界面,那相当简单,真正获取界面信息的代码只有两三句,在第一篇文章给出的例子中有提及。由于在旧版本的API中,Google是将手机截屏相关的方法与接口隐藏的,开发者要想自主实现手机完整屏幕的快照,有很多局限性(必须采用System级别的应用开发,或者在Linux下进行Root、源码编译等操作),这部分内容的总结在第二篇文章中。

      大家都知道一些手机厂商会在自家手机发售前定制、预装一些应用,而这些应用APK有些就是System级的(需要通过源码Build)。也就是说不是没有办法利用隐藏的方法或借口实现手机截屏,而是本文将要寻找一种谁都能自己进行实现的方式,包括初学者。

      俗话说事不过三,今天这篇文章就来说说怎么实现轻松自在地、以一种全新的方式进行手机截屏,希望能给人眼前一亮的感觉。

      本文截屏应用的实现思路大致是这样:

      1、抛弃组合快捷键,采用浮动小图标作为截屏按键(类似于360浮动小球,对其思想和详细的实现方式感兴趣的小伙伴可以见另一篇文章:

    Android浮动小球与开机自启动

    浮动小图标的实现类Service1继承自Service类,这样可以方便地创建只有一个浮动图标按键的布局,在Activity等地方利用startService(Intent intent)方法开启服务。

      2、由于浮动按键本身不是希望截取的屏幕信息,故在开始截屏后将其隐藏,图像保存后面再使其浮现。

      3、图像保存为PNG格式,路径为手机sdcard的Pictures文件目录下,而系统截屏默认的目录是Screenshots。

      4、浮动小球的优先级为一般应用的最顶层,即除了状态栏下拉列表外,小球总是可见的,这要得益于Service类的性质了。虽然在看电视等环境下会比较不适合,但该设计能让用户随时、方便地截取到想要的屏幕图像。

      5、开机自启动功能虽然保留了,但是因为截屏应用需要得到用户的同意,所以在手机重启后由广播机制自动打开的小球并不能完成截屏,还是需要点击应用图标打开应用以进行截取环境的请求。

      文章开头已经给出整个工程的代码(Android Studio版本),所以在介绍过程中就只给出实现截屏的关键代码,感兴趣的可以下载并自己进行实践。一切准备就绪,开始吧。

    一、截屏请求结果数据共享类ShotApplication

      上面已经提到,屏幕获取需要用户同意,初次运行时会有请求对话框,同意之后才能继续,否则程序会终止。既然需要用户选择后的信息,那在发出截屏请求时就不能用简单的startActivity(Intent intent)方法,而是要用startActivityForResult(Intent intent, intresquestCode)方法。而可恨的是Service类中startActivityForResult(Intent intent, int resquestCode)方法不可用,确切的说是不存在可供子类重载的onActivityResult(int resquestCode, int resultCode, Intent data) 方法。但现实是Service1类在实现截屏过程中又要用到后面两个返回值(resultCode与data)来构建MediaProjection类的对象。

      可能有人会有疑问,那截屏过程直接在Activity中进行不就可以了吗?没错,是可以。但是问题在于我们需要在任何想截取屏幕的时候就能快速、方便地进行,即需要借助利用Service实现并浮动在一般性应用窗口之上的小球。而在Activity中实现的话就达不到这种效果了,往往能获取的只能是应用本身界面,或者是将其隐藏后的下一层界面,总之做不到想要即可得的效果。

      所以,首要问题是让类Service1的对象能得到这两个数据。另外得注意,Bundle可以完成一般数据的加载并赋给Intent类对象,然后一起发送给目标类,但参数data本身就是Intent类型的。虽然Intent类存在putExtras(Intent src)方法,但为了体现面向对象数据封装的思想,这里采取的是数据共享的思路,利用继承自Application类的子类ShotApplication,然后定义需要共享的成员变量(有些是其他类的对象)。类代码很简单(不包括import *):

     1 public class ShotApplication extends Application {
     2     private int result;
     3     private Intent intent;
     4     private MediaProjectionManager mMediaProjectionManager;
     5 
     6     public int getResult(){
     7         return result;
     8     }
     9 
    10     public Intent getIntent(){
    11         return intent;
    12     }
    13 
    14     public MediaProjectionManager getMediaProjectionManager(){
    15         return mMediaProjectionManager;
    16     }
    17 
    18     public void setResult(int result1){
    19         this.result = result1;
    20     }
    21 
    22     public void setIntent(Intent intent1){
    23         this.intent = intent1;
    24     }
    25 
    26     public void setMediaProjectionManager(MediaProjectionManager mMediaProjectionManager){
    27         this.mMediaProjectionManager = mMediaProjectionManager;
    28     }
    29 }

          其中MediaProjectionManager类对象在发送截屏请求和构建MediaProjection类对象时均会用到,至于成员值的设置及获取很直观,就不解释了。那么数据的传递就明朗了:先从主程序类MainActivity中存入共享类ShotApplication,然后服务类Service1从共享类ShotApplication中提取出来。

    二、主程序类MainActivity所做4件事

      先给出代码(不包括import *),然后分析到底是哪4件事:

     1 public class MainActivity extends ActionBarActivity {
     2     private int result = 0;
     3     private Intent intent = null;
     4     private int REQUEST_MEDIA_PROJECTION = 1;
     5     private MediaProjectionManager mMediaProjectionManager;
     6 
     7     @TargetApi(Build.VERSION_CODES.LOLLIPOP)
     8     @Override
     9     protected void onCreate(Bundle savedInstanceState) {
    10         super.onCreate(savedInstanceState);
    11         setContentView(R.layout.activity_main);
    12 
    13         mMediaProjectionManager = (MediaProjectionManager)getApplication().getSystemService(Context.MEDIA_PROJECTION_SERVICE);
    14         startIntent();
    15     }
    16 
    17     @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    18     private void startIntent(){
    19         if(intent != null && result != 0){
    20             ((ShotApplication)getApplication()).setResult(result);
    21             ((ShotApplication)getApplication()).setIntent(intent);
    22             Intent intent = new Intent(getApplicationContext(), Service1.class);
    23             startService(intent);
    24         }else{
    25             startActivityForResult(mMediaProjectionManager.createScreenCaptureIntent(), REQUEST_MEDIA_PROJECTION);
    26             ((ShotApplication)getApplication()).setMediaProjectionManager(mMediaProjectionManager);
    27         }
    28     }
    29 
    30     @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    31     @Override
    32     public void onActivityResult(int requestCode, int resultCode, Intent data) {
    33         if (requestCode == REQUEST_MEDIA_PROJECTION) {
    34             if (resultCode != Activity.RESULT_OK) {
    35                 return;
    36             }else if(data != null && resultCode != 0){
    37                 result = resultCode;
    38                 intent = data;
    39                 ((ShotApplication)getApplication()).setResult(resultCode);
    40                 ((ShotApplication)getApplication()).setIntent(data);
    41                 Intent intent = new Intent(getApplicationContext(), Service1.class);
    42                 startService(intent);
    43 
    44                 finish();
    45             }
    46         }
    47     }
    48 }

      向用户提出截屏请求的代码就是下面这句:

    1 startActivityForResult(mMediaProjectionManager.createScreenCaptureIntent(), REQUEST_MEDIA_PROJECTION);

      这正是类MainActivity做的第1件事。当然,在这之前需要获取类MediaProjectionManager实例,代码为:

    1 mMediaProjectionManager = (MediaProjectionManager)getApplication().getSystemService(Context.MEDIA_PROJECTION_SERVICE);

      由于onCreate()方法是应用开启后自动调用的(startIntent随即被调用),所以这一行截屏请求代码也会自动执行。如果是应用安装后第一次开启,那么就会弹出截屏权限允许对话框,需要用户授权。如图:

      说到这,既可以解释上面提到思路的第5条了,开机自启动后能够开启服务,但不能执行截屏请求操作。原因很简单:一开机就莫名其妙弹出截屏请求对话框不符合用户使用习惯,再者无论是在广播还是服务中调用sratActivityForResult()方法都是不太现实的。

      类MainActivity做的第2件事就是将用户操作所返回的值和初始获取的类MediaProjectionManager实例写入数据共享类ShotApplication中了,代码如下:

    1 //截屏请求对话框用户操作返回数据result和intent
    2 ((ShotApplication)getApplication()).setResult(result);
    3 ((ShotApplication)getApplication()).setIntent(intent);
    4 //类MediaProjectionManager对象mMediaProjectionManager
    5 ((ShotApplication)getApplication()).setMediaProjectionManager(mMediaProjectionManager);

      类MainActivity做的第3件事就是肯定是开启截屏服务了,代码如下:

    1 Intent intent = new Intent(getApplicationContext(), Service1.class);
    2 startService(intent);

      注意自定义方法startIntent()时在onCreate()方法被调用,其在不同时期作用不同。如果在此次被调用之前用户已经允许过截屏操作,那么直接开启截屏服务;而如果没有允许过,则向用户请求,即做上述第1件事。

      类MainActivity做的第4件事是将自身销毁,之后的控制权就交给服务类Service1的浮动小球(这即是该类整个界面)了。

    1 finish();

    三、服务类Service1完成整体屏幕截取

      终于到了关键的类Service1了,同样先给出代码(不包括import *):

      1 public class Service1 extends Service
      2 {
      3     private LinearLayout mFloatLayout = null;
      4     private WindowManager.LayoutParams wmParams = null;
      5     private WindowManager mWindowManager = null;
      6     private LayoutInflater inflater = null;
      7     private ImageButton mFloatView = null;
      8 
      9     private static final String TAG = "MainActivity";
     10 
     11     private SimpleDateFormat dateFormat = null;
     12     private String strDate = null;
     13     private String pathImage = null;
     14     private String nameImage = null;
     15 
     16     private MediaProjection mMediaProjection = null;
     17     private VirtualDisplay mVirtualDisplay = null;
     18 
     19     public static int mResultCode = 0;
     20     public static Intent mResultData = null;
     21     public static MediaProjectionManager mMediaProjectionManager1 = null;
     22 
     23     private WindowManager mWindowManager1 = null;
     24     private int windowWidth = 0;
     25     private int windowHeight = 0;
     26     private ImageReader mImageReader = null;
     27     private DisplayMetrics metrics = null;
     28     private int mScreenDensity = 0;
     29 
     30     @Override
     31     public void onCreate()
     32     {
     33         // TODO Auto-generated method stub
     34         super.onCreate();
     35 
     36         createFloatView();
     37 
     38         createVirtualEnvironment();
     39     }
     40 
     41     @Override
     42     public IBinder onBind(Intent intent)
     43     {
     44         // TODO Auto-generated method stub
     45         return null;
     46     }
     47 
     48     private void createFloatView()
     49     {
     50         wmParams = new WindowManager.LayoutParams();
     51         mWindowManager = (WindowManager)getApplication().getSystemService(getApplication().WINDOW_SERVICE);
     52         wmParams.type = LayoutParams.TYPE_PHONE;
     53         wmParams.format = PixelFormat.RGBA_8888;
     54         wmParams.flags = LayoutParams.FLAG_NOT_FOCUSABLE;
     55         wmParams.gravity = Gravity.LEFT | Gravity.TOP;
     56         wmParams.x = 0;
     57         wmParams.y = 0;
     58         wmParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
     59         wmParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
     60         inflater = LayoutInflater.from(getApplication());
     61         mFloatLayout = (LinearLayout) inflater.inflate(R.layout.float_layout, null);
     62         mWindowManager.addView(mFloatLayout, wmParams);
     63         mFloatView = (ImageButton)mFloatLayout.findViewById(R.id.float_id);
     64 
     65         mFloatLayout.measure(View.MeasureSpec.makeMeasureSpec(0,
     66                 View.MeasureSpec.UNSPECIFIED), View.MeasureSpec
     67                 .makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
     68 
     69         mFloatView.setOnTouchListener(new OnTouchListener() {
     70             @Override
     71             public boolean onTouch(View v, MotionEvent event) {
     72                 // TODO Auto-generated method stub
     73                 wmParams.x = (int) event.getRawX() - mFloatView.getMeasuredWidth() / 2;
     74                 wmParams.y = (int) event.getRawY() - mFloatView.getMeasuredHeight() / 2 - 25;
     75                 mWindowManager.updateViewLayout(mFloatLayout, wmParams);
     76                 return false;
     77             }
     78         });
     79 
     80         mFloatView.setOnClickListener(new OnClickListener() {
     81 
     82             @Override
     83             public void onClick(View v) {
     84                 // hide the button
     85                 mFloatView.setVisibility(View.INVISIBLE);
     86 
     87                 Handler handler1 = new Handler();
     88                 handler1.postDelayed(new Runnable() {
     89                     public void run() {
     90                         //start virtual
     91                         startVirtual();
     92                     }
     93                 }, 500);
     94 
     95                 Handler handler2 = new Handler();
     96                 handler2.postDelayed(new Runnable() {
     97                     public void run() {
     98                         //capture the screen
     99                         startCapture();
    100                     }
    101                 }, 1500);
    102 
    103                 Handler handler3 = new Handler();
    104                 handler3.postDelayed(new Runnable() {
    105                     public void run() {
    106                         mFloatView.setVisibility(View.VISIBLE);
    107                         //stopVirtual();
    108                     }
    109                 }, 1000);
    110             }
    111         });
    112 
    113         Log.i(TAG, "created the float sphere view");
    114     }
    115 
    116     private void createVirtualEnvironment(){
    117         dateFormat = new SimpleDateFormat("yyyy_MM_dd_hh_mm_ss");
    118         strDate = dateFormat.format(new java.util.Date());
    119         pathImage = Environment.getExternalStorageDirectory().getPath()+"/Pictures/";
    120         nameImage = pathImage+strDate+".png";
    121         mMediaProjectionManager1 = (MediaProjectionManager)getApplication().getSystemService(Context.MEDIA_PROJECTION_SERVICE);
    122         mWindowManager1 = (WindowManager)getApplication().getSystemService(Context.WINDOW_SERVICE);
    123         windowWidth = mWindowManager1.getDefaultDisplay().getWidth();
    124         windowHeight = mWindowManager1.getDefaultDisplay().getHeight();
    125         metrics = new DisplayMetrics();
    126         mWindowManager1.getDefaultDisplay().getMetrics(metrics);
    127         mScreenDensity = metrics.densityDpi;
    128         mImageReader = ImageReader.newInstance(windowWidth, windowHeight, 0x1, 2); //ImageFormat.RGB_565
    129 
    130         Log.i(TAG, "prepared the virtual environment");
    131     }
    132 
    133     @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    134     public void startVirtual(){
    135         if (mMediaProjection != null) {
    136             Log.i(TAG, "want to display virtual");
    137             virtualDisplay();
    138         } else {
    139             Log.i(TAG, "start screen capture intent");
    140             Log.i(TAG, "want to build mediaprojection and display virtual");
    141             setUpMediaProjection();
    142             virtualDisplay();
    143         }
    144     }
    145 
    146     @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    147     public void setUpMediaProjection(){
    148         mResultData = ((ShotApplication)getApplication()).getIntent();
    149         mResultCode = ((ShotApplication)getApplication()).getResult();
    150         mMediaProjectionManager1 = ((ShotApplication)getApplication()).getMediaProjectionManager();
    151         mMediaProjection = mMediaProjectionManager1.getMediaProjection(mResultCode, mResultData);
    152         Log.i(TAG, "mMediaProjection defined");
    153     }
    154 
    155     @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    156     private void virtualDisplay(){
    157         mVirtualDisplay = mMediaProjection.createVirtualDisplay("screen-mirror",
    158                 windowWidth, windowHeight, mScreenDensity, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
    159                 mImageReader.getSurface(), null, null);
    160         Log.i(TAG, "virtual displayed");
    161     }
    162 
    163     @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    164     private void startCapture(){
    165         strDate = dateFormat.format(new java.util.Date());
    166         nameImage = pathImage+strDate+".png";
    167 
    168         Image image = mImageReader.acquireLatestImage();
    169         int width = image.getWidth();
    170         int height = image.getHeight();
    171         final Image.Plane[] planes = image.getPlanes();
    172         final ByteBuffer buffer = planes[0].getBuffer();
    173         int pixelStride = planes[0].getPixelStride();
    174         int rowStride = planes[0].getRowStride();
    175         int rowPadding = rowStride - pixelStride * width;
    176         Bitmap bitmap = Bitmap.createBitmap(width+rowPadding/pixelStride, height, Bitmap.Config.ARGB_8888);
    177         bitmap.copyPixelsFromBuffer(buffer);
    178         bitmap = Bitmap.createBitmap(bitmap, 0, 0,width, height);
    179         image.close();
    180         Log.i(TAG, "image data captured");
    181 
    182         if(bitmap != null) {
    183             try{
    184                 File fileImage = new File(nameImage);
    185                 if(!fileImage.exists()){
    186                     fileImage.createNewFile();
    187                     Log.i(TAG, "image file created");
    188                 }
    189                 FileOutputStream out = new FileOutputStream(fileImage);
    190                 if(out != null){
    191                     bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
    192                     out.flush();
    193                     out.close();
    194                     Intent media = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
    195                     Uri contentUri = Uri.fromFile(fileImage);
    196                     media.setData(contentUri);
    197                     this.sendBroadcast(media);
    198                     Log.i(TAG, "screen image saved");
    199                 }
    200             }catch(FileNotFoundException e) {
    201                 e.printStackTrace();
    202             }catch (IOException e){
    203                 e.printStackTrace();
    204             }
    205         }
    206     }
    207 
    208     @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    209     private void tearDownMediaProjection() {
    210         if (mMediaProjection != null) {
    211             mMediaProjection.stop();
    212             mMediaProjection = null;
    213         }
    214         Log.i(TAG,"mMediaProjection undefined");
    215     }
    216 
    217     private void stopVirtual() {
    218         if (mVirtualDisplay == null) {
    219             return;
    220         }
    221         mVirtualDisplay.release();
    222         mVirtualDisplay = null;
    223         Log.i(TAG,"virtual display stopped");
    224     }
    225 
    226     @Override
    227     public void onDestroy()
    228     {
    229         // to remove mFloatLayout from windowManager
    230         super.onDestroy();
    231         if(mFloatLayout != null)
    232         {
    233             mWindowManager.removeView(mFloatLayout);
    234         }
    235         tearDownMediaProjection();
    236         Log.i(TAG, "application destroy");
    237     }
    238 }

      由于类Service1中大部分代码和之前文章中给出的相差不大,接下来会先将类中各方法简单罗列一遍,然后更加着重介绍改进的地方。

      从onCreate()方法开始,其调用了两个方法:createFloatView()和createVirtualEnvironment();createFloatView()方法负责浮动小球的生成、拖动及其点击事件的响应;createVirtualEnvironment()方法定义截屏所需的变量(包括屏幕信息、图像格式、保存格式等等)。另外,各个方法利用Log日志类输出了运行过程中的状态信息,便于观察代码执行过程。

      关键之处就在于对浮动小球点击事件的响应实现,而拖动只是附带的一个辅助功能而已。浮动小球点击事件的响应代码也完成了4件事情,下面一一进行分析。

      1、隐藏小球

    1 mFloatView.setVisibility(View.INVISIBLE);

      2、初始化截屏环境

     1 public void startVirtual(){
     2   if (mMediaProjection != null) {
     3     Log.i(TAG, "want to display virtual");
     4     virtualDisplay();
     5   } else {
     6     Log.i(TAG, "start screen capture intent");
     7     Log.i(TAG, "want to build mediaprojection and display virtual");
     8     setUpMediaProjection();
     9     virtualDisplay();
    10   }
    11 }

      可以看出,这个过程先是对MediaProjection类实例mMediaProjection的值进行了判断,若之前没有初始化(即值为null),则调用setUpMediaProjection()方法获取共享数据并对其进行赋值;若已初始化,则直接调用virtualDisplay()方法利用之前定义的变量对截屏环境进行初始化,而真正执行最终操作的方法为createVirtualDisplay()。

      3、屏幕截取

          截屏环境初始化完成之后,便可以开始获取屏幕信息了,所以接下来调用的是startCapture()方法。该方法的实现和之前不同,也是容易出错的地方在于以下两句代码:

    1  int rowPadding = rowStride - pixelStride * width;
    2 Bitmap bitmap = Bitmap.createBitmap(width+rowPadding/pixelStride, height, Bitmap.Config.ARGB_8888);

          值得注意的是调用方法createBitmap()创建Bitmap对象时所用的第1、3个参数,分别对应于图像的宽度、格式。实现过程中发现,只有将格式设置为ARGB_8888才能获取想要的图像质量;而对于宽度,后面会解释为什么要为其设置偏移值。

      4、显示小球

    1 mFloatView.setVisibility(View.VISIBLE);

    四、总结

      至于服务类Service1类中的其他代码,以及用于开机自启动服务的广播子类BootBroadcastReceiver这里就不打算介绍了。

      下面来看看不同情况下的效果图,这里指的不同情况跟上述创建位图的两个参数有关。

      1、图像格式

      如果创建位图时用的格式不是ARGB_8888,比如RGB_565,虽然屏幕信息的获取没有问题,但是在将信息转化为图像并保存的过程中出现了严重的偏差。如下图:

      2、宽度值

      之前介绍过调用createBitmap()方法设置其宽度参数时添加了偏移信息,如果不这么做,获取的屏幕截图会出现左边部分缺失的情况(右边会以黑色补全)。如下图:

      而图像的宽度和格式参数设置正确后的截屏结果图如下:

      不用怀疑,这是本文方法获取的截屏图片,而非手机自带的电源键+音量键获得的^_^。

  • 相关阅读:
    DS博客作业05--查找
    DS博客作业04--图
    DS博客作业03--树
    栈和队列
    第六次作业
    第五次作业
    第四次作业
    第三次作业
    java任务
    第三周-自主学习任务-面向对象基础与类的识别
  • 原文地址:https://www.cnblogs.com/tgyf/p/4851092.html
Copyright © 2020-2023  润新知