分享一種截屏方法

在任何時候點擊手機上的浮動小球(紅色圈內)就能完成整個屏幕信息的截取功能,而且最終保存的圖像還不會包含該小球,這就是本文將要介紹的方法。
手機整體屏幕項目的獲取方式:
1、Github
2、源碼打包文件
以新的視角實現手機屏幕的截取(快照)功能,文章可能比較長,感興趣的朋友一定得看完,會有收獲的哦!若發現有哪些地方存在問題或某些功能有更好的實現方式,歡迎指點,先謝過(我可以盡快改正或完善,以免繼續誤導別人)。
關於手機(或平板,以下描述均以手機表示這兩類設備)整體屏幕的截取,有的機型默認設置的方式是同時按下電源鍵和一個音量鍵(如華碩、諾基亞等,向下音量鍵+電源鍵),有的機型是同時按下電源鍵和Home鍵(如蘋果)。另外,借助一些輔助工具經過特定的設置也是可以完成屏幕快照的獲取。
從打算開發一個與傳統截屏方法不同的截屏應用開始,針對Android手機截屏的基本原理、相關知識及Google最新案例,已經在學習與摸索的途中寫過兩篇文章了,感興趣的朋友可以通過下面給出的鏈接去瞧一瞧(這方面知識掌握較好的可以直接忽略)。
在Win7+Android Studio環境嘗試了網絡上曬出的很多方式均失敗后,帶着受打擊的心態寫了第一篇文章:
當時的目的是希望實現過的大神能給點有用的建議或意見。當然,一般來說Android應用在Android Studio和Eclipse下都是可以實現的,雖不能將項目代碼在兩者之間直接進行轉換,但如若工作量不是特別大,移植起來也不麻煩,嘗試過的朋友應該懂得。
可能對於截屏應用進行學習或者實現的人不太多,筆者並沒有得到什么寶貴性的建議。后面不甘心又進行了一番Google,還是沒有直截了當的答案,最后決定靜下心來老老實實地分析案例源碼(案例沒有屏幕數據獲取與圖片保存的實現)。於是就有了第二篇文章:
簡單回顧一下:如果只是想得到正在運行的應用程序自身的界面,那相當簡單,真正獲取界面信息的代碼只有兩三句,在第一篇文章給出的例子中有提及。由於在舊版本的API中,Google是將手機截屏相關的方法與接口隱藏的,開發者要想自主實現手機完整屏幕的快照,有很多局限性(必須采用System級別的應用開發,或者在Linux下進行Root、源碼編譯等操作),這部分內容的總結在第二篇文章中。
大家都知道一些手機廠商會在自家手機發售前定制、預裝一些應用,而這些應用APK有些就是System級的(需要通過源碼Build)。也就是說不是沒有辦法利用隱藏的方法或借口實現手機截屏,而是本文將要尋找一種誰都能自己進行實現的方式,包括初學者。
俗話說事不過三,今天這篇文章就來說說怎么實現輕松自在地、以一種全新的方式進行手機截屏,希望能給人眼前一亮的感覺。
本文截屏應用的實現思路大致是這樣:
1、拋棄組合快捷鍵,采用浮動小圖標作為截屏按鍵(類似於360浮動小球,對其思想和詳細的實現方式感興趣的小伙伴可以見另一篇文章:
浮動小圖標的實現類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()方法設置其寬度參數時添加了偏移信息,如果不這么做,獲取的屏幕截圖會出現左邊部分缺失的情況(右邊會以黑色補全)。如下圖:

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

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