1. 程式人生 > >Android普通截圖(包括狀態列內容)

Android普通截圖(包括狀態列內容)

從 Android 5.0(API 21) 起,開放了截圖錄屏的API,使用到的類有MediaProjectionManager、MediaProjection、VirtualDisplay、ImageReader。

普通截圖大致步驟

① 獲取螢幕的長寬高以及densityDpi等,並初始化ImageReader的例項[ 注意獲取長寬高的時機 ]

    ImageReader imageReader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 2);

② 獲取MediaProjectionManager的例項

    MediaProjectionManager manager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);

③ 獲取截圖的Intent

    Intent intent = manager.createScreenCaptureIntent();

④ 啟動截圖頁面[ MediaProjectionPermissionActivity ]

    startActivityForResult(intent, 200);

⑤ 在onActivityResult方法中獲取MediaProjection例項

    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (resultCode == RESULT_OK && requestCode == 200) {
           MediaProjection projection = manager.getMediaProjection(RESULT_OK, data);          
        }
    }
⑥ 獲取虛擬顯示器VirtualDisplay的例項
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (resultCode == RESULT_OK && requestCode == 200) {
           MediaProjection projection = manager.getMediaProjection(RESULT_OK, data);
           VirtualDisplay display = projection.createVirtualDisplay("ScreenShot",
                    width, height, dpi, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
                    imageReader.getSurface(), null, null);
        }
    }
⑦ 獲取截圖,bitmap即為所得 [ 注意獲取image的時機,我採用了handler.postDelay的方式 ]
    Image image = imageReader.acquireLatestImage();
    int width = image.getWidth();
    int height = image.getHeight();
    Image.Plane[] planes = image.getPlanes();
    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();

完整示例


我要做的工作是,在Activity建立時就擷取螢幕,所以在onCreate方法中就需要獲取螢幕的一些相關引數,這裡我使用了如何合理地獲取螢幕的寬高中的第二種方法,採用handler.postDelayed(Runnable r, long delayMillis) 方法。

import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.PixelFormat;
import android.hardware.display.DisplayManager;
import android.hardware.display.VirtualDisplay;
import android.media.Image;
import android.media.ImageReader;
import android.media.projection.MediaProjection;
import android.media.projection.MediaProjectionManager;
import android.os.Bundle;
import android.os.Handler;
import android.util.DisplayMetrics;
import android.view.Display;

import com.katherine.du.everydaystudy.BaseActivity;
import com.katherine.du.everydaystudy.R;

import java.nio.ByteBuffer;

public class MediaProjectionActivity extends BaseActivity {

    private MediaProjectionManager manager;
    private int height;
    private int width;
    private int dpi;
    private ImageReader imageReader;
    private VirtualDisplay display;
    private MediaProjection projection;
    static Handler handler = new Handler();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_empty);
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                //獲取螢幕的寬高和DPI
                Display display = getWindowManager().getDefaultDisplay();
                DisplayMetrics metrics = new DisplayMetrics();
                display.getMetrics(metrics);
                width = metrics.widthPixels;
                height = metrics.heightPixels;
                dpi = metrics.densityDpi;
                //初始化ImageReader例項
                imageReader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 2);
                //獲取MediaProjectionManager例項
                manager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
                //啟動截圖Activity【com.android.systemui.media.MediaProjectionPermissionActivity】
                startActivityForResult(manager.createScreenCaptureIntent(), 200);
            }
        }, 1000);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (resultCode == RESULT_OK && requestCode == 200) {
            //獲取MediaProjection例項
            projection = manager.getMediaProjection(RESULT_OK, data);
            //獲取虛擬顯示器VirtualDisplay的例項
            display = projection.createVirtualDisplay("ScreenShot",
                    width, height, dpi,
                    DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
                    imageReader.getSurface(),
                    null, null);
            //獲取截圖
            handler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    //bitmap即為所得
                    Bitmap bitmap = takeScreenShot();
                }
            }, 1000);
        }
    }

    private Bitmap takeScreenShot() {
        Bitmap bitmap = null;
        try {
            Image image = imageReader.acquireLatestImage();
            int width = image.getWidth();
            int height = image.getHeight();
            Image.Plane[] planes = image.getPlanes();
            ByteBuffer buffer = planes[0].getBuffer();
            int pixelStride = planes[0].getPixelStride();
            int rowStride = planes[0].getRowStride();
            int rowPadding = rowStride - pixelStride * width;
            bitmap = Bitmap.createBitmap(width + rowPadding / pixelStride,
                    height, Bitmap.Config.ARGB_8888);
            bitmap.copyPixelsFromBuffer(buffer);
            bitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height);
            image.close();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (imageReader != null) {
                imageReader.close();
            }
            if (projection != null) {
                projection.stop();
            }
            if (display != null) {
                display.release();//釋放
            }
        }
        return bitmap;
    }
}

MediaProjectionManager解讀

獲取例項的方法,兩種:

Instances of this class must be obtained using Context.getSystemService(Class) with the argumentMediaProjectionManager.class

or Context.getSystemService(String) with the argumentContext.MEDIA_PROJECTION_SERVICE.

成員方法,兩個:

    Intent createScreenCaptureIntent()

Returns an Intent that must passed to startActivityForResult() in order to start screen capture. The activity will prompt the user whether to allow screen capture. The result of this activity should be passed to getMediaProjection.

這個方法會返回一個Intent。通過startActivityForResulet() 啟動這個intent來開始捕捉螢幕。這個Intent相關的Activity會提示使用者是否允許擷取螢幕。如果允許的話,這個Activity會返回結果給getMediaProjection作為引數。

    MediaProjection getMediaProjection (int resultCode, Intent resultData)

Retrieve the MediaProjection obtained from a succesful screen capture request. Will be null if the result from the startActivityForResult() is anything other than RESULT_OK.

上面那個方法提到,截圖成功後會返回一個intent作為結果,即這裡的resultData。但如果resultCode不是RESULT_OK的話,此方法將返回null。

實現流程解讀

我們來看一下這個流程,先從這個createScreenCaptureIntent()方法說起,看一下這個方法原始碼的實現:

    public Intent createScreenCaptureIntent() {
        Intent i = new Intent();
        i.setClassName("com.android.systemui",
                "com.android.systemui.media.MediaProjectionPermissionActivity");
        return i;
    }
可以看到返回了MediaProjectionPermissionActivity的intent。

接著我們使用了startActivityForResulet() 啟動這個intent,我們來看看MediaProjectionPermissionActivity.java類

當我們首次跳轉到這個Activity時,會彈出一個彈窗詢問你是否允許截圖,介面如下所示:


我們來看看原始碼是怎樣的,MediaProjectionPermissionActivity中的onCreate方法:

    public void onCreate(Bundle icicle) {
        super.onCreate(icicle);
        ......
        try {
            if (mService.hasProjectionPermission(mUid, mPackageName)) {
                setResult(RESULT_OK, getMediaProjectionIntent(mUid, mPackageName,
                        false /*permanentGrant*/));
                finish();
                return;
            }
        } catch (RemoteException e) {
            Log.e(TAG, "Error checking projection permissions", e);
            finish();
            return;
        }
        ......
        mDialog = new AlertDialog.Builder(this)
                .setIcon(aInfo.loadIcon(packageManager))
                .setMessage(message)
                .setPositiveButton(R.string.media_projection_action_text, this)
                .setNegativeButton(android.R.string.cancel, this)
                .setView(R.layout.remember_permission_checkbox)
                .setOnCancelListener(this)
                .create();
        ......
        mDialog.show();//彈出詢問框
    }
當我們點選確定按鈕的時候,我們繼續看onClick中的點選事件:
    @Override
    public void onClick(DialogInterface dialog, int which) {
        try {
            if (which == AlertDialog.BUTTON_POSITIVE) {
                setResult(RESULT_OK, getMediaProjectionIntent(mUid, mPackageName, mPermanentGrant));
            }
        } catch (RemoteException e) {
            ......
        } finally {
            ......
        }
    }

    private Intent getMediaProjectionIntent(int uid, String packageName, boolean permanentGrant)
            throws RemoteException {
        IMediaProjection projection = mService.createProjection(uid, packageName,
                 MediaProjectionManager.TYPE_SCREEN_CAPTURE, permanentGrant);
        Intent intent = new Intent();
        intent.putExtra(MediaProjectionManager.EXTRA_MEDIA_PROJECTION, projection.asBinder());
        return intent;
    }
注意到,點選確定以後,將執行 setResult(RESULT_OK, getMediaProjectionIntent(mUid, mPackageName, mPermanentGrant));方法。

而getMediaProjectionIntent方法,就返回了上面講到的getMediaProjection方法所需要的Intent。

詢問框裡還有一個不再顯示的CheckBox,我們看一下它的點選事件:

    @Override
    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
        mPermanentGrant = isChecked;
    }
如果你選擇不再顯示,且點選立即開始後,getMediaProjectionIntent就會將permanentGrant置為true。所以再回頭看一下oncreate裡頭的mService.hasProjectionPermission(mUid, mPackageName)就會每次得到真值,此後每次進入這個Activity時就不會再彈出詢問框了。
    if (mService.hasProjectionPermission(mUid, mPackageName)) {
        setResult(RESULT_OK, getMediaProjectionIntent(mUid, mPackageName, false /*permanentGrant*/));
        finish();
        return;
    }
不過,如果直接點選了取消按鈕,或者是先選中不再顯示再點選取消,都是先關閉詢問框,然後finish掉MediaProjectionPermissionActivity自己。

接著就是獲取MediaProjection例項啦,通過getMediaProjection方法,看看實現:

    public MediaProjection getMediaProjection(int resultCode, @NonNull Intent resultData) {
        if (resultCode != Activity.RESULT_OK || resultData == null) {
            return null;
        }
        IBinder projection = resultData.getIBinderExtra(EXTRA_MEDIA_PROJECTION);
        if (projection == null) {
            return null;
        }
        return new MediaProjection(mContext, IMediaProjection.Stub.asInterface(projection));
    }
如果resultCode不等於RESULT_OK或者resultData為空的時候都獲取不到MediaProjection的例項。

然後就是獲取虛擬顯示器,我們來看一下MediaProjection.java中的createVirtualDisplay方法:

    public VirtualDisplay createVirtualDisplay(@NonNull String name,
            int width, int height, int dpi, int flags, @Nullable Surface surface,
            @Nullable VirtualDisplay.Callback callback, @Nullable Handler handler) {
        DisplayManager dm = (DisplayManager) mContext.getSystemService(Context.DISPLAY_SERVICE);
        return dm.createVirtualDisplay(
                    this, name, width, height, dpi, surface, flags, callback, handler);
    }
引數分別是:名字,寬,高,dpi,DisplayManager的flags,virtualDisplay狀態改變時的回撥,呼叫callback的handler。

需要注意的是:這個方法將虛擬顯示器的內容渲染到應用提供的Surface上,當虛擬顯示器不再使用時,應該呼叫released方法將其釋放。

接下來就該獲取截圖了,前面最開始的時候我們初始化了ImageReader的例項:

    imageReader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 2);
引數分別是:指定生成影象的寬,高,影象格式[必須是ImageFormat或者PixelFormat格式的],允許同時產生的圖片的數量[儘可能小,但要大於0]

使用ImageReader的acquireLatestImage方法從ImageReader的佇列裡獲取最新的圖片並刪掉舊的圖片。

    public Image acquireLatestImage() {
        Image image = acquireNextImage();
        if (image == null) {
            return null;
        }
        try {
            for (;;) {
                Image next = acquireNextImageNoThrowISE();
                if (next == null) {
                    Image result = image;
                    image = null;
                    return result;
                }
                image.close();
                image = next;
            }
        } finally {
            if (image != null) {
                image.close();
            }
        }
    }

這裡要注意的是,使用handler.postDelayed(Runnable r, long delayMillis)進行獲取,否則image很可能是null[原因暫不詳]。

最後就是image轉Bitmap了:

    Image image = imageReader.acquireLatestImage();
    int width = image.getWidth();
    int height = image.getHeight();
    Image.Plane[] planes = image.getPlanes();
    ByteBuffer buffer = planes[0].getBuffer();
    int pixelStride = planes[0].getPixelStride();
    int rowStride = planes[0].getRowStride();
    int rowPadding = rowStride - pixelStride * width;
    bitmap = Bitmap.createBitmap(width + rowPadding / pixelStride,
            height, Bitmap.Config.ARGB_8888);
    bitmap.copyPixelsFromBuffer(buffer);
    bitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height);
    image.close();

差不多就到這裡辣=====

效果:


對啦,個人公眾號開通啦,哈哈,純屬娛樂~~~