分享一種截屏方法


分享一種截屏方法

 

 

  在任何時候點擊手機上的浮動小球(紅色圈內)就能完成整個屏幕信息的截取功能,而且最終保存的圖像還不會包含該小球,這就是本文將要介紹的方法。

  手機整體屏幕項目的獲取方式:

  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()方法設置其寬度參數時添加了偏移信息,如果不這么做,獲取的屏幕截圖會出現左邊部分缺失的情況(右邊會以黑色補全)。如下圖:

  而圖像的寬度和格式參數設置正確后的截屏結果圖如下:

  不用懷疑,這是本文方法獲取的截屏圖片,而非手機自帶的電源鍵+音量鍵獲得的^_^。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM