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塊中的事情。

 1 @Override
 2 protected void onCreate(Bundle savedInstanceState) {
 3     super.onCreate(savedInstanceState);
 4     setContentView(R.layout.activity_main);
 5     if (savedInstanceState == null) {
 6       FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
 7       ScreenCaptureFragment fragment = new ScreenCaptureFragment();
 8       transaction.replace(R.id.sample_content_fragment, fragment);
 9       transaction.commit();
10     }
11 }  

  經測試發現,利用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(),實現方式分別如下。

1 @Override
2 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
3   return inflater.inflate(R.layout.fragment_screen_capture, container, false);
4 }

  該方法是要告訴主程序類MainActivity存在可以用來替換其組件sample_content_fragment的xml文件fragment_screen_capture。

 

讀取布局中的SurfaceView組件賦給mSurfaceView,並將其Surface成員取出賦給mSurface,將Button組件賦給mButton。

1 @Override
2 public void onViewCreated(View view, Bundle savedInstanceState) {
3     mSurfaceView = (SurfaceView) view.findViewById(R.id.surface);
4     mSurface = mSurfaceView.getHolder().getSurface();
5   mButton = (Button) view.findViewById(R.id.button);
6     mButton.setOnClickListener(this);
7 }

  這段代碼主要作用是提取手機屏幕的分辨率大小矩陣metrics、像素深度mScreenDensity及定義了MediaProjectionManager類對象mMediaProjectionManager。由於ScreenCaptureFragment類並沒有繼承與Activity相關的類,所以在獲取WindowManager類對象及進行后續操作時需要getActivity的協助。

 1 @Override
 2 public void onActivityCreated(Bundle savedInstanceState) {
 3    super.onActivityCreated(savedInstanceState);
 4    Activity activity = getActivity();
 5    DisplayMetrics metrics = new DisplayMetrics();
 6   activity.getWindowManager().getDefaultDisplay().getMetrics(metrics);
 7    mScreenDensity = metrics.densityDpi;
 8    mMediaProjectionManager = (MediaProjectionManager)activity.getSystemService(Context.MEDIA_PROJECTION_SERVICE);
 9 }

  在Android 5.0以前,如果開發者要對Android屏幕進行手機全屏的單幀或實時截圖,往往比較困難。Android 5.0的出現改變了這種現狀,其新增了MediaProjectionManager管理器,可以非常方便地實現屏幕捕捉功能。

  6、上面提及界面上還有一個按鈕,利用其來控制屏幕信息的顯示,第一次按是開始,接着是停止,后續操作如此反復。首先看按鈕的按鍵捕獲與執行代碼,注意開始和停止操作是由一個按鈕實現,其text內容會隨着點擊在START與STOP之間切換。

 1 @Override
 2 public void onClick(View v) {
 3    switch (v.getId()) {
 4       case R.id.button:
 5           if (mVirtualDisplay == null) {
 6                startScreenCapture();
 7           } else {
 8                stopScreenCapture();
 9           }
10           break;
11    }
12 }

  接下來看看開始顯示屏幕信息函數startScreenCapture()做了什么。

 1 private void startScreenCapture() {
 2    Activity activity = getActivity();
 3    if (mSurface == null || activity == null) {
 4        return;
 5    }
 6    if (mMediaProjection != null) {
 7        setUpVirtualDisplay();
 8    } else if (mResultCode != 0 && mResultData != null) {
 9        setUpMediaProjection();
10        setUpVirtualDisplay();
11    } else {
12     // This initiates a prompt dialog for the user to confirm screen projection.
13        startActivityForResult(mMediaProjectionManager.createScreenCaptureIntent(),REQUEST_MEDIA_PROJECTION);
16    }
17 }

  通過以上代碼可以看出,在剛開始,mSurface和activity已經定義,所以程序會繼續往下執行;而MediaProjection對象mMediaProjection沒有定義,為null;同樣的startActivityForResult()方法還沒有執行,不存在用戶返回數據,故mResultCode(int型,記錄返回值,同意或拒絕)為0,mResultData(Intent型,記錄返回后的Intent對象)為null;所以,按照邏輯,會執行最后一個代碼塊,即創建並啟動一個屏幕捕捉的Intent對象。

  注意第二個參數為人為設定的請求碼(整型),數值不限定,主要作用是對用戶操作后的返回值進行判斷。因為發起第一次在手機上發起屏幕截取請求,會彈出用戶授權對話框,具體返回值見下面分析。請求對話框如下:

       

  當用戶選擇拒絕時,應用程序自然就結束了;而選擇同意時,便開始屏幕信息的獲取與顯示了。注意,對話框中間還有一個選項是設置要不要再次提示此請求,如果不夠上,那么每次打開應用請求截屏時均會彈出此對話框。而該機制的實現方式很多,比如用一個配置文件,在配置文件中用一個變量控制是否彈出窗口,比如有一個config文件,里面有一個變量:showDialog=true,如果用戶選擇不在彈出,則復寫config文件為showDialog=false,然后程序每次運行時檢測,為true就顯示,false就不彈窗。

  7、那么用戶選擇同意之后,為什么就可以實時獲取屏幕信息了?關鍵在於后面的方法onActivityResult(),來看其代碼。

 1 @Override
 2 public void onActivityResult(int requestCode, int resultCode, Intent data) {
 3    if (requestCode == REQUEST_MEDIA_PROJECTION) {
 4        if (resultCode != Activity.RESULT_OK) {
 5            return;
 6        }
 7        Activity activity = getActivity();
 8        if (activity == null) {
 9            return;
10        }
11        mResultCode = resultCode;
12        mResultData = data;
13        setUpMediaProjection();
14        setUpVirtualDisplay();
15    }
16 }

  可以看到,當初設定的請求碼在此處發揮作用了,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()。

1 private void setUpMediaProjection() {
2   mMediaProjection = mMediaProjectionManager.getMediaProjection(mResultCode, mResultData);
3 }

雖然只有一句代碼,但定義了在外漂泊已久的mMediaProjection,有了該對象才可以獲取被捕獲的屏幕信息。

9、然后是方法setUpVirtualDisplay(),從名字也可以看出屏幕信息終於要dispaly了。

1 private void setUpVirtualDisplay() {
2   mVirtualDisplay = mMediaProjection.createVirtualDisplay(
3     "ScreenCapture",mSurfaceView.getWidth(), mSurfaceView.getHeight(), mScreenDensity, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
4      mSurface, null, null);
5   mButton.setText(R.string.stop);
6 }

  總共兩句代碼,第一句實現了將屏幕信息顯示在SurfaceView組件中,第二句將按鈕文本設為"STOP",因為開始時是"START",文本內容表示的含義是點擊后會進行的操作,而不是程序當前的執行狀態。到此時,界面上左下角的組件SurfaceView應該在實時顯示屏幕信息,若狀態欄的時間等信息變化了,可以達到流暢的播放效果。當然,由於不斷地截取屏幕信息,會出現雙面鏡反射效果,即自身無限包含直到一個點(在理發店也可以體驗^_^)。

  實時顯示屏幕界面如下:

       

  10、要讓其停止,只需要再次點擊按鈕,這回輪到stopScreenCapture()方法表現了。

1 private void stopScreenCapture() {
2   if (mVirtualDisplay == null) {
3      return;
4   }
5   mVirtualDisplay.release();
6   mVirtualDisplay = null;
7   mButtonToggle.setText(R.string.start);
8 }

  代碼很簡單,將對象mVirtualDisplay釋放,同時將按鈕文本設置為"START"。

  11、停止獲取屏幕信息后,若結束應用,在完全退出之前要讓其自動完成一項任務——停止對象mMediaProjection,需要重載onDestory()方法。

1 @Override
2 public void onDestroy() {
3   super.onDestroy();
4   tearDownMediaProjection();
5 }

  內部調用的方法tearDownMediaProjection()實現如下。

1 private void tearDownMediaProjection() {
2     if (mMediaProjection != null) {
3     mMediaProjection.stop();
4     mMediaProjection = null;
5     }
6 }

  12、最后介紹兩個重載函數onCreate()與onPause(),先看后者。

1 @Override
2 public void onPause() {
3   super.onPause();
4   stopScreenCapture();
5 }

  可以看出,在應用停止(比如其他應用突然在activity頂層運行、按了Back或Home鍵等)時,若當前正處在獲取屏幕信息的狀態下,調用此函數可以先將當前獲取操縱停止,避免不必要的資源浪費。操作過就會發現,若在截屏請求時同意並勾選不再彈出對話框,那之后運行(包括從activity隊列中重獲新生與重新運行程序)就不會出現了。

  來看onCreate()做了什么。

1 @Override
2 public void onCreate(Bundle savedInstanceState) {
3   super.onCreate(savedInstanceState);
4   if (savedInstanceState != null) {
5       mResultCode = savedInstanceState.getInt(STATE_RESULT_CODE);
6       mResultData =savedInstanceState.getParcelable(STATE_RESULT_DATA);
7   }
8 }

  剛開始就會判斷數據承載對象savedInstanceState是否為空,若不是則取出mResultCode與mResultData。而這兩個值前面已經說明,會記錄用戶的選擇結果。

  回到截屏開始函數,startScreenCapture(),其中有一個分支如下。

1 if (mResultCode != 0 && mResultData != null) {
2   setUpMediaProjection();
3   setUpVirtualDisplay();
4 } 

  即如果條件成立,就直接進行屏幕信息的獲取並顯示,不再需要發送帶屏幕信息獲取請求的Intent對象了,也就不會有那個對話框了。

 

  對案例的分析就到這里了,歡迎感興趣的朋友一起交流,共同進步!!!

 


免責聲明!

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



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