Android之MediaProjectionManager实现手机截屏总结
比較好的文章:
Android中使用代碼截圖的各種方法總結
http://blog.csdn.net/woshinia/article/details/11520403
手機截屏:
http://www.cnblogs.com/tgyf/p/4655507.html
轉載的地方:
http://www.cnblogs.com/tgyf/p/4851092.html
分享一種截屏方法
在任何時候點擊手機上的浮動小球(紅色圈內)就能完成整個屏幕信息的截取功能,而且最終保存的圖像還不會包含該小球,這就是本文將要介紹的 方法。手機整體屏幕獲取項目下載鏈接:http://files.cnblogs.com/files/tgyf/CaptureScreen.rar。以新的視角實現手機屏幕的截取(快照)功能,文章可能比較長,感興趣的朋友一定得看完,會有收獲的哦!若發現有哪些地方存在問題或某些功能有更好的實現方式,歡迎指點,先謝過(我可以盡快改正或完善,以免繼續誤導別人)。
關于手機(或平板,以下描述均以手機表示這兩類設備)整體屏幕的截取,有的機型默認設置的方式是同時按下電源鍵和一個音量鍵(如華碩、諾基亞等,向下音量鍵+電源鍵),有的機型是同時按下電源鍵和Home鍵(如蘋果)。另外,借助一些輔助工具經過特定的設置也是可以完成屏幕快照的獲取。
從打算開發一個與傳統截屏方法不同的截屏應用開始,針對Android手機截屏的基本原理、相關知識及Google最新案例,已經在學習與摸索的途中寫過兩篇文章了,感興趣的朋友可以通過下面給出的鏈接去瞧一瞧(這方面知識掌握較好的可以直接忽略)。
在Win7+Android Studio環境嘗試了網絡上曬出的很多方式均失敗后,帶著受打擊的心態寫了第一篇文章:
“Android手機截屏”——http://www.cnblogs.com/tgyf/p/4655507.html。
當時的目的是希望實現過的大神能給點有用的建議或意見。當然,一般來說Android應用在Android Studio和Eclipse下都是可以實現的,雖不能將項目代碼在兩者之間直接進行轉換,但如若工作量不是特別大,移植起來也不麻煩,嘗試過的朋友應該懂得。
可能對于截屏應用進行學習或者實現的人不太多,筆者并沒有得到什么寶貴性的建議。后面不甘心又進行了一番Google,還是沒有直截了當的答案,最后決定靜下心來老老實實地分析案例源碼(案例沒有屏幕數據獲取與圖片保存的實現)。于是就有了第二篇文章:
“Google最新截屏案例詳解”——http://www.cnblogs.com/tgyf/p/4675082.html。
簡單回顧一下:如果只是想得到正在運行的應用程序自身的界面,那相當簡單,真正獲取界面信息的代碼只有兩三句,在第一篇文章給出的例子中有提及。由于在舊版本的API中,Google是將手機截屏相關的方法與接口隱藏的,開發者要想自主實現手機完整屏幕的快照,有很多局限性(必須采用System級別的應用開發,或者在Linux下進行Root、源碼編譯等操作),這部分內容的總結在第二篇文章中。
大家都知道一些手機廠商會在自家手機發售前定制、預裝一些應用,而這些應用APK有些就是System級的(需要通過源碼Build)。也就是說不是沒有辦法利用隱藏的方法或借口實現手機截屏,而是本文將要尋找一種誰都能自己進行實現的方式,包括初學者。
俗話說事不過三,今天這篇文章就來說說怎么實現輕松自在地、以一種全新的方式進行手機截屏,希望能給人眼前一亮的感覺。
本文截屏應用的實現思路大致是這樣:
1、拋棄組合快捷鍵,采用浮動小圖標作為截屏按鍵(類似于360浮動小球,對其思想和詳細的實現方式感興趣的小伙伴可以見另一篇文章:“Android浮動小球與開機自啟動”——http://www.cnblogs.com/tgyf/p/4665401.html)。浮動小圖標的實現類Service1繼承自Service類,這樣可以方便地創建只有一個浮動圖標按鍵的布局,在Activity等地方利用startService(Intent intent)方法開啟服務。
2、由于浮動按鍵本身不是希望截取的屏幕信息,故在開始截屏后將其隱藏,圖像保存后面再使其浮現。
3、圖像保存為PNG格式,路徑為手機sdcard的Pictures文件目錄下,而系統截屏默認的目錄是Screenshots。
4、浮動小球的優先級為一般應用的最頂層,即除了狀態欄下拉列表外,小球總是可見的,這要得益于Service類的性質了。雖然在看電視等環境下會比較不適合,但該設計能讓用戶隨時、方便地截取到想要的屏幕圖像。
5、開機自啟動功能雖然保留了,但是因為截屏應用需要得到用戶的同意,所以在手機重啟后由廣播機制自動打開的小球并不能完成截屏,還是需要點擊應用圖標打開應用以進行截取環境的請求。
文章開頭已經給出整個工程的代碼(Android Studio版本),所以在介紹過程中就只給出實現截屏的關鍵代碼,感興趣的可以下載并自己進行實踐。這里再給出鏈接為:http://files.cnblogs.com/files/tgyf/CaptureScreen.rar。一切準備就緒,開始吧。
?
一、截屏請求結果數據共享類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 *):
public class ShotApplication extends Application {private int result;private Intent intent;private MediaProjectionManager mMediaProjectionManager;public int getResult(){return result;}public Intent getIntent(){return intent;}public MediaProjectionManager getMediaProjectionManager(){return mMediaProjectionManager;}public void setResult(int result1){this.result = result1;}public void setIntent(Intent intent1){this.intent = intent1;}public void setMediaProjectionManager(MediaProjectionManager mMediaProjectionManager){this.mMediaProjectionManager = mMediaProjectionManager;} }其中MediaProjectionManager類對象在發送截屏請求和構建MediaProjection類對象時均會用到,至于成員值的設置及獲取很直觀,就不解釋了。那么數據的傳遞就明朗了:先從主程序類MainActivity中存入共享類ShotApplication,然后服務類Service1從共享類ShotApplication中提取出來。
?
二、主程序類MainActivity所做4件事
先給出代碼(不包括import *),然后分析到底是哪4件事:
public class MainActivity extends ActionBarActivity {private int result = 0;private Intent intent = null;private int REQUEST_MEDIA_PROJECTION = 1;private MediaProjectionManager mMediaProjectionManager;@TargetApi(Build.VERSION_CODES.LOLLIPOP)@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);mMediaProjectionManager = (MediaProjectionManager)getApplication().getSystemService(Context.MEDIA_PROJECTION_SERVICE);startIntent();}@TargetApi(Build.VERSION_CODES.LOLLIPOP)private void startIntent(){if(intent != null && result != 0){((ShotApplication)getApplication()).setResult(result);((ShotApplication)getApplication()).setIntent(intent);Intent intent = new Intent(getApplicationContext(), Service1.class);startService(intent);}else{startActivityForResult(mMediaProjectionManager.createScreenCaptureIntent(), REQUEST_MEDIA_PROJECTION);((ShotApplication)getApplication()).setMediaProjectionManager(mMediaProjectionManager);}}@TargetApi(Build.VERSION_CODES.LOLLIPOP)@Overridepublic void onActivityResult(int requestCode, int resultCode, Intent data) {if (requestCode == REQUEST_MEDIA_PROJECTION) {if (resultCode != Activity.RESULT_OK) {return;}else if(data != null && resultCode != 0){result = resultCode;intent = data;((ShotApplication)getApplication()).setResult(resultCode);((ShotApplication)getApplication()).setIntent(data);Intent intent = new Intent(getApplicationContext(), Service1.class);startService(intent);finish();}}} }向用戶提出截屏請求的代碼就是下面這句: startActivityForResult(mMediaProjectionManager.createScreenCaptureIntent(), REQUEST_MEDIA_PROJECTION);
這正是類MainActivity做的第1件事。當然,在這之前需要獲取類MediaProjectionManager實例,代碼為:
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件事就是肯定是開啟截屏服務了,代碼如下:
Intent intent = new Intent(getApplicationContext(), Service1.class); 2 startService(intent); <span style="font-size: 16px;"> 注意自定義方法startIntent()時在onCreate()方法被調用,其在不同時期作用不同。如果在此次被調用之前用戶已經允許過截屏操作,那么直接開啟截屏服務;而如果沒有允許過,則向用戶請求,即做上述第1件事。</span><p><span style="font-size: 16px;"> 類MainActivity做的第4件事是將自身銷毀,之后的控制權就交給服務類Service1的浮動小球(這即是該類整個界面)了。</span></p><pre name="code" class="html">1 finish();
三、服務類Service1完成整體屏幕截取
終于到了關鍵的類Service1了,同樣先給出代碼(不包括import *):
public class Service1 extends Service {private LinearLayout mFloatLayout = null;private WindowManager.LayoutParams wmParams = null;private WindowManager mWindowManager = null;private LayoutInflater inflater = null;private ImageButton mFloatView = null;private static final String TAG = "MainActivity";private SimpleDateFormat dateFormat = null;private String strDate = null;private String pathImage = null;private String nameImage = null;private MediaProjection mMediaProjection = null;private VirtualDisplay mVirtualDisplay = null;public static int mResultCode = 0;public static Intent mResultData = null;public static MediaProjectionManager mMediaProjectionManager1 = null;private WindowManager mWindowManager1 = null;private int windowWidth = 0;private int windowHeight = 0;private ImageReader mImageReader = null;private DisplayMetrics metrics = null;private int mScreenDensity = 0;@Overridepublic void onCreate(){// TODO Auto-generated method stubsuper.onCreate();createFloatView();createVirtualEnvironment();}@Overridepublic IBinder onBind(Intent intent){// TODO Auto-generated method stubreturn null;}private void createFloatView(){wmParams = new WindowManager.LayoutParams();mWindowManager = (WindowManager)getApplication().getSystemService(getApplication().WINDOW_SERVICE);wmParams.type = LayoutParams.TYPE_PHONE;wmParams.format = PixelFormat.RGBA_8888;wmParams.flags = LayoutParams.FLAG_NOT_FOCUSABLE;wmParams.gravity = Gravity.LEFT | Gravity.TOP;wmParams.x = 0;wmParams.y = 0;wmParams.width = WindowManager.LayoutParams.WRAP_CONTENT;wmParams.height = WindowManager.LayoutParams.WRAP_CONTENT;inflater = LayoutInflater.from(getApplication());mFloatLayout = (LinearLayout) inflater.inflate(R.layout.float_layout, null);mWindowManager.addView(mFloatLayout, wmParams);mFloatView = (ImageButton)mFloatLayout.findViewById(R.id.float_id);mFloatLayout.measure(View.MeasureSpec.makeMeasureSpec(0,View.MeasureSpec.UNSPECIFIED), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));mFloatView.setOnTouchListener(new OnTouchListener() {@Overridepublic boolean onTouch(View v, MotionEvent event) {// TODO Auto-generated method stubwmParams.x = (int) event.getRawX() - mFloatView.getMeasuredWidth() / 2;wmParams.y = (int) event.getRawY() - mFloatView.getMeasuredHeight() / 2 - 25;mWindowManager.updateViewLayout(mFloatLayout, wmParams);return false;}});mFloatView.setOnClickListener(new OnClickListener() {@Overridepublic void onClick(View v) {// hide the buttonmFloatView.setVisibility(View.INVISIBLE);Handler handler1 = new Handler();handler1.postDelayed(new Runnable() {public void run() {//start virtualstartVirtual();}}, 500);Handler handler2 = new Handler();handler2.postDelayed(new Runnable() {public void run() {//capture the screenstartCapture();}}, 1500);Handler handler3 = new Handler();handler3.postDelayed(new Runnable() {public void run() {mFloatView.setVisibility(View.VISIBLE);//stopVirtual();}}, 1000);}});Log.i(TAG, "created the float sphere view");}private void createVirtualEnvironment(){dateFormat = new SimpleDateFormat("yyyy_MM_dd_hh_mm_ss");strDate = dateFormat.format(new java.util.Date());pathImage = Environment.getExternalStorageDirectory().getPath()+"/Pictures/";nameImage = pathImage+strDate+".png";mMediaProjectionManager1 = (MediaProjectionManager)getApplication().getSystemService(Context.MEDIA_PROJECTION_SERVICE);mWindowManager1 = (WindowManager)getApplication().getSystemService(Context.WINDOW_SERVICE);windowWidth = mWindowManager1.getDefaultDisplay().getWidth();windowHeight = mWindowManager1.getDefaultDisplay().getHeight();metrics = new DisplayMetrics();mWindowManager1.getDefaultDisplay().getMetrics(metrics);mScreenDensity = metrics.densityDpi;mImageReader = ImageReader.newInstance(windowWidth, windowHeight, 0x1, 2); //ImageFormat.RGB_565Log.i(TAG, "prepared the virtual environment");}@TargetApi(Build.VERSION_CODES.LOLLIPOP)public void startVirtual(){if (mMediaProjection != null) {Log.i(TAG, "want to display virtual");virtualDisplay();} else {Log.i(TAG, "start screen capture intent");Log.i(TAG, "want to build mediaprojection and display virtual");setUpMediaProjection();virtualDisplay();}}@TargetApi(Build.VERSION_CODES.LOLLIPOP)public void setUpMediaProjection(){mResultData = ((ShotApplication)getApplication()).getIntent();mResultCode = ((ShotApplication)getApplication()).getResult();mMediaProjectionManager1 = ((ShotApplication)getApplication()).getMediaProjectionManager();mMediaProjection = mMediaProjectionManager1.getMediaProjection(mResultCode, mResultData);Log.i(TAG, "mMediaProjection defined");}@TargetApi(Build.VERSION_CODES.LOLLIPOP)private void virtualDisplay(){mVirtualDisplay = mMediaProjection.createVirtualDisplay("screen-mirror",windowWidth, windowHeight, mScreenDensity, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,mImageReader.getSurface(), null, null);Log.i(TAG, "virtual displayed");}@TargetApi(Build.VERSION_CODES.LOLLIPOP)private void startCapture(){strDate = dateFormat.format(new java.util.Date());nameImage = pathImage+strDate+".png";Image image = mImageReader.acquireLatestImage();int width = image.getWidth();int height = image.getHeight();final Image.Plane[] planes = image.getPlanes();final ByteBuffer buffer = planes[0].getBuffer();int pixelStride = planes[0].getPixelStride();int rowStride = planes[0].getRowStride();int rowPadding = rowStride - pixelStride * width;Bitmap bitmap = Bitmap.createBitmap(width+rowPadding/pixelStride, height, Bitmap.Config.ARGB_8888);bitmap.copyPixelsFromBuffer(buffer);bitmap = Bitmap.createBitmap(bitmap, 0, 0,width, height);image.close();Log.i(TAG, "image data captured");if(bitmap != null) {try{File fileImage = new File(nameImage);if(!fileImage.exists()){fileImage.createNewFile();Log.i(TAG, "image file created");}FileOutputStream out = new FileOutputStream(fileImage);if(out != null){bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);out.flush();out.close();Intent media = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);Uri contentUri = Uri.fromFile(fileImage);media.setData(contentUri);this.sendBroadcast(media);Log.i(TAG, "screen image saved");}}catch(FileNotFoundException e) {e.printStackTrace();}catch (IOException e){e.printStackTrace();}}}@TargetApi(Build.VERSION_CODES.LOLLIPOP)private void tearDownMediaProjection() {if (mMediaProjection != null) {mMediaProjection.stop();mMediaProjection = null;}Log.i(TAG,"mMediaProjection undefined");}private void stopVirtual() {if (mVirtualDisplay == null) {return;}mVirtualDisplay.release();mVirtualDisplay = null;Log.i(TAG,"virtual display stopped");}@Overridepublic void onDestroy(){// to remove mFloatLayout from windowManagersuper.onDestroy();if(mFloatLayout != null){mWindowManager.removeView(mFloatLayout);}tearDownMediaProjection();Log.i(TAG, "application destroy");} } 由于類Service1中大部分代碼和之前文章中給出的相差不大,接下來會先將類中各方法簡單羅列一遍,然后更加著重介紹改進的地方。從onCreate()方法開始,其調用了兩個方法:createFloatView()和createVirtualEnvironment();createFloatView()方法負責浮動小球的生成、拖動及其點擊事件的響應;createVirtualEnvironment()方法定義截屏所需的變量(包括屏幕信息、圖像格式、保存格式等等)。另外,各個方法利用Log日志類輸出了運行過程中的狀態信息,便于觀察代碼執行過程。
關鍵之處就在于對浮動小球點擊事件的響應實現,而拖動只是附帶的一個輔助功能而已。浮動小球點擊事件的響應代碼也完成了4件事情,下面一一進行分析。
1、隱藏小球
1 mFloatView.setVisibility(View.INVISIBLE);2、初始化截屏環境
public void startVirtual(){if (mMediaProjection != null) {Log.i(TAG, "want to display virtual");virtualDisplay();} else {Log.i(TAG, "start screen capture intent");Log.i(TAG, "want to build mediaprojection and display virtual");setUpMediaProjection();virtualDisplay();} }
可以看出,這個過程先是對MediaProjection類實例mMediaProjection的值進行了判斷,若之前沒有初始化(即值為null),則調用setUpMediaProjection()方法獲取共享數據并對其進行賦值;若已初始化,則直接調用virtualDisplay()方法利用之前定義的變量對截屏環境進行初始化,而真正執行最終操作的方法為createVirtualDisplay()。
3、屏幕截取
????? 截屏環境初始化完成之后,便可以開始獲取屏幕信息了,所以接下來調用的是startCapture()方法。該方法的實現和之前不同,也是容易出錯的地方在于以下兩句代碼:
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、顯示小球
mFloatView.setVisibility(View.VISIBLE);四、總結
至于服務類Service1類中的其他代碼,以及用于開機自啟動服務的廣播子類BootBroadcastReceiver這里就不打算介紹了。
下面來看看不同情況下的效果圖,這里指的不同情況跟上述創建位圖的兩個參數有關。
1、圖像格式
如果創建位圖時用的格式不是ARGB_8888,比如RGB_565,雖然屏幕信息的獲取沒有問題,但是在將信息轉化為圖像并保存的過程中出現了嚴重的偏差。如下圖:
2、寬度值之前介紹過調用createBitmap()方法設置其寬度參數時添加了偏移信息,如果不這么做,獲取的屏幕截圖會出現左邊部分缺失的情況(右邊會以黑色補全)。如下圖:
而圖像的寬度和格式參數設置正確后的截屏結果圖如下:?
總結
以上是生活随笔為你收集整理的Android之MediaProjectionManager实现手机截屏总结的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android之Intent.ACTIO
- 下一篇: Android之用SingleTask和