Android 5.0后截屏,无需root

本文介绍了Android 5.0及更高版本中如何使用官方API实现无root设备的截屏功能。主要涉及MediaProjectionManager、ImageReader等类的使用,包括启动截屏服务、新建虚拟屏幕以及处理虚拟屏幕图像的步骤。同时对比了需要root权限的shell方式截屏。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Android5.0截屏,无需root

关于截屏这一块,Android在5.0之后提供了官方的API,关于截屏也不需要使用adb,root,以及去模拟按键又或者去撸隐藏代码了。
主要用到以下几个类:
1. MediaProjection
2. MediaProjectionManager
3. MediaProjection.Callback
4. DisplayManager
5. VirtualDisplay
6. VirtualDisplay.Callback
7. ImageReader
8. Image
9. Bitmap
10. PixelFormat

实现步骤

拿到屏幕上的实时信息

先请求截屏的服务,然后拿到返回来的Intent数据.
这里实现打开一个服务,跟一般的服务不一样,这里的服务会转换为一个MediaProjectionManager,看命名是一个管理器,这个管理器持有MediaProjection,还有一个请求截屏的Intent。有两种方式拿到这个MediaProjectionManager.

Context.getSystemService(Context.MEDIA_PROJECTION_SERVICE);
or
Context.getSystemService(MediaProjectionManager.class);

拿到MediaProjectionManager后,就可以拿到它名下的一个Intent,通过启动这个Intent,我们可以拿到另外一个Intent。所以我们必须用 startActivityForResult这种方式来启动这个Intent。
启动这个Intent,系统会向用户申请权限,告知用户接下来会有截屏操作,同时也开始截屏的准备工作了。因为我们是以startActivityForResult方式启动的,所以在onActivityResult里面会返回实时的屏幕的信息(这里的信息不是以图像的形式出现,所以我们拿到信息后还需要自己处理转换成我们需要的图像信息)。(录屏也是这样做的)

    public void requestScreenShot() {
        if (Build.VERSION.SDK_INT >= 21) {
//            MediaProjectionManager mediaProjectionManager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);  //方式一
            MediaProjectionManager mediaProjectionManager = getSystemService(MediaProjectionManager.class);  //方式二
            startActivityForResult(mediaProjectionManager.createScreenCaptureIntent(), REQUEST_MEDIA_PROJECTION);//REQUEST_MEDIA_PROJECTION是我们自己定义的一个int,随便给可以。
        } else {
            Toast.makeText(MainActivity.this, "版本过低,无法截屏", Toast.LENGTH_SHORT).show();
        }
    }

onActivityResult方法里面可以拿到返回的数据,Intent类型。

@Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        switch (requestCode) {
            case REQUEST_MEDIA_PROJECTION: {
                if (resultCode == -1 && data != null) {//这里的resultCode必须为-1(Activity.RESULT_OK),后面也会用到-1(系统返回的-1,只需要记住就可以了);
                    this.data = data;  //记录这里拿到data,是一个Intent,实际上这里记录的只是一个引用,里面的东西是实时在改变的(因为里面记录的是屏幕信息),信息存储在intent里面的bundle,bundle里面记录的是一个用Android专用序列化方式Parcelable序列化过的一个对象。
                }
            }
        }
    }

拿到屏幕信息后,我们需要处理,将其转换成我们需要的PNG或者bitmap格式。
处理分为大抵分为两步,第一步,新建一个虚拟屏幕,将之前拿到的信息显示在虚拟屏幕上;第二步,拿到屏幕上的图像。

新建一个虚拟屏幕

这里还是需要借助MediaProjectionManager,上面有说过,MediaProjectionManager持有MediaProjection,还有一个请求截屏的Intent,Intent我们已经用过了,这里会用到MediaProjection。我们要拿到这个MediaProjection,方法里面有两个参数(一个是上面说的-1(Activity.RESULT_OK),一个是上面拿到的屏幕信息Intent)

if(null==mMediaProjection)
    mMediaProjection = mediaProjectionManager.getMediaProjection(Activity.RESULT_OK,data);
 //这里的mediaProjectionManager 跟上面的不一定是同一个对象,可以自己通过Context重新请求一个MediaProjectionManager

初始化一个ImageReader对象,这个对象会在虚拟化屏幕里面用到,这个ImageReader实际上是屏幕上面的画面。初始化一个ImageReader用到四个参数会。newInstance (int width, int height, int format, int maxImages)分别代表图像的宽、高、图像色彩格式、imagereader里面最多存储几张图像(多了耗费内存),这里格式必须是ImageFormatPixelFormat里面的,当然也并不是说里面的所有格式都支持,ImageFormat.NV21就不支持。

mImageReader = ImageReader.newInstance(
                    getScreenWidth(),  //真实屏幕宽度
                    getScreenHeight(),  //真实屏幕高度
                    PixelFormat.RGBA_8888,// a pixel两节省一些内存 个2个字节 此处RGBA_8888 必须和下面 buffer处理一致的格式
                    1);  //最多存储一张图像

下面是一些获取真实屏幕参数的方法:

//获取真实屏幕宽度(单位px)
private int getScreenWidth() {
        return Resources.getSystem().getDisplayMetrics().widthPixels;
    }
//获取真实屏幕高度(单位px)
private int getScreenHeight() {
        return Resources.getSystem().getDisplayMetrics().heightPixels;
    }
//获取状态栏高度(单位px)
private int getStatusBatHeight(){
        /**
         * 获取状态栏高度——方法1
         * */
       //获取status_bar_height资源的ID
        int resourceId = getContext().getResources().getIdentifier("status_bar_height", "dimen", "android");
        if (resourceId > 0) {
            //根据资源ID获取响应的尺寸值
            statusBarHeight1 = getContext().getResources().getDimensionPixelSize(resourceId);
            float scale = getContext().getResources().getDisplayMetrics().density;
            statusBarHeight1= (int) (statusBarHeight1*scale+0.5f);
            return statusBarHeight1;
        }
        return 0;
    }

然后就是新建虚拟屏幕了,用之前拿到的mMediaProjection,这个对象下面有一个方法createVirtualDisplay ,这个方法就是创建一个虚拟屏幕。下面介绍一下各个参数的意义。

VirtualDisplay createVirtualDisplay (String name, //虚拟屏幕的名字,不能为空,可以随便取
int width, //虚拟屏幕的宽度
int height, //虚拟屏幕的高度
int dpi, //虚拟屏幕的DPI
int flags, //虚拟屏幕的显示标志,必须为DisplayManager下面的int常量
Surface surface, //存放虚拟屏幕图像的UI
VirtualDisplay.Callback callback, //虚拟屏幕状态发生改变的回调
Handler handler) //上面回调所运行的线程,为null上面回调会运行在主线程里面

mVirtualDisplay = mMediaProjection.createVirtualDisplay("screen-mirror",
                getScreenWidth(),
                getScreenHeight(),
                getScreenDpi(),
                DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
                mImageReader.getSurface(), null, null);

处理虚拟屏幕上的图像

从存储虚拟屏幕的ImageReader对象上,拿到里面的image图像,这里就可以得到image的字节数组信息,在新建一个bitmap对象,将字节信息传给bitmap,就可以拿到我们需要的图像,这个bitmap就是我们的屏幕截图了。需要注意的是,bitmap的色彩格式要和上面给ImageReader设置的一样

            Image image = mImageReader.acquireLatestImage();
            while(null==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);//这里的bitmap为最终的截图
            image.close();
            File fileImage = null;
            if (bitmap != null) {
                try {
                    fileImage = new File(mLocalUrl);//mLocalURL为存储的路径
                    if (!fileImage.exists()) {
                        fileImage.createNewFile();
                    }
                    FileOutputStream out = new FileOutputStream(fileImage);
                    if (out != null) {
                        bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
                        out.flush();
                        out.close();
                    }
                } catch (FileNotFoundException e) {
                    e.printStackTrace();
                    fileImage = null;
                } catch (IOException e) {
                    e.printStackTrace();
                    fileImage = null;
                }
            }
            if (bitmap != null && !bitmap.isRecycled()) {
                bitmap.recycle();
            }
        }

        if (mVirtualDisplay != null) {
            mVirtualDisplay.release();
        }

        if (mOnShotListener != null) {
            mOnShotListener.onFinish();
        }
        if(null!=mMediaProjection){
            mMediaProjection.stop();
        }

下面贴出完整的代码,由于是公司的项目,所以就不贴出整个工程了。
这段代码是上面步骤里面的后两步,之前的跳转需要自己在activity里面获取。获取方法如下:

//在oncreate或者onresume里面调用
public void requestScreenShot() {
        if (Build.VERSION.SDK_INT >= 21) {
//            MediaProjectionManager mediaProjectionManager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);  //方式一
            MediaProjectionManager mediaProjectionManager = getSystemService(MediaProjectionManager.class);  //方式二
            startActivityForResult(mediaProjectionManager.createScreenCaptureIntent(), REQUEST_MEDIA_PROJECTION);//REQUEST_MEDIA_PROJECTION是我们自己定义的一个int,随便给可以。
        } else {
            Toast.makeText(MainActivity.this, "版本过低,无法截屏", Toast.LENGTH_SHORT).show();
        }
    }


 @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        switch (requestCode) {
            case REQUEST_MEDIA_PROJECTION: {
                if (resultCode == -1 && data != null) {//这里的resultCode必须为-1(Activity.RESULT_OK),后面也会用到-1(系统返回的-1,只需要记住就可以了);
                    this.data = data;  //记录这里拿到data,是一个Intent,实际上这里记录的只是一个引用,里面的东西是实时在改变的(因为里面记录的是屏幕信息),信息存储在intent里面的bundle,bundle里面记录的是一个用Android专用序列化方式Parcelable序列化过的一个对象。
                }
            }
        }
    }

这段代码最好在一个子线程里面执行,因为我在项目里面已经是在子线程里面执行,所以这里没有用Thread和Handler。

package com.hskj.damnicomniplusvic.wevenation.util;

import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.PixelFormat;
import android.graphics.Rect;
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.Build;
import android.os.SystemClock;
import android.text.TextUtils;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.ref.SoftReference;
import java.nio.ByteBuffer;

/**
 * Created by wei on 16-12-1.
 */
public class Shotter {

    private final SoftReference<Context> mRefContext;
    private ImageReader mImageReader;
    private MediaProjection mMediaProjection;
    private VirtualDisplay mVirtualDisplay;
    int statusBarHeight1 = -1;
    private String mLocalUrl = "";
    private Rect mRect;


//rect是我在项目里面需要截图的区域,一个图标。
    public Shotter(Context context, Intent data, Rect rect) {
        this.mRefContext = new SoftReference<>(context);
        this.mRect=rect;

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            if(null==mMediaProjection)
                mMediaProjection = getMediaProjectionManager().getMediaProjection(Activity.RESULT_OK,
                    data);
            mImageReader = ImageReader.newInstance(
                    getScreenWidth(),
                    getScreenHeight(),
                    PixelFormat.RGBA_8888,// a pixel两节省一些内存 个2个字节 此处RGBA_8888 必须和下面 buffer处理一致的格式
                    1);
            getStatusBatHeight();
        }
    }


    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    private void virtualDisplay() {

        mVirtualDisplay = mMediaProjection.createVirtualDisplay("screen-mirror",
                getScreenWidth(),
                getScreenHeight(),
                getScreenDpi(),
                DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
                mImageReader.getSurface(), null, null);

    }

    public void startScreenShot(String loc_url) {
        mLocalUrl = loc_url;
        startScreenShot();
    }


    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public void startScreenShot() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            virtualDisplay();
            Image image = mImageReader.acquireLatestImage();
            while(null==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);
            if(null!=mRect)
                bitmap = Bitmap.createBitmap(bitmap, mRect.left, mRect.top, mRect.width(), mRect.height());
            else
                bitmap = Bitmap.createBitmap(bitmap,0,0,width,height);
            image.close();
            File fileImage = null;
            if (bitmap != null) {
                try {
                    if (TextUtils.isEmpty(mLocalUrl)) {
                        mLocalUrl = getContext().getExternalFilesDir("screenshot").getAbsoluteFile() + "/" + SystemClock.currentThreadTimeMillis() + ".png";
                    }
                    fileImage = new File(mLocalUrl);

                    if (!fileImage.exists()) {
                        fileImage.createNewFile();
                    }
                    FileOutputStream out = new FileOutputStream(fileImage);
                    if (out != null) {
                        bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
                        out.flush();
                        out.close();
                    }

                } catch (FileNotFoundException e) {
                    e.printStackTrace();
                    fileImage = null;
                } catch (IOException e) {
                    e.printStackTrace();
                    fileImage = null;
                }
            }
            if (bitmap != null && !bitmap.isRecycled()) {
                bitmap.recycle();
            }
        }

        if (mVirtualDisplay != null) {
            mVirtualDisplay.release();
        }

        if(null!=mMediaProjection){
            mMediaProjection.stop();
        }

    }



    private MediaProjectionManager getMediaProjectionManager() {

        return (MediaProjectionManager) getContext().getSystemService(
                Context.MEDIA_PROJECTION_SERVICE);
    }

    private Context getContext() {
        return mRefContext.get();
    }


    private int getScreenWidth() {
        return Resources.getSystem().getDisplayMetrics().widthPixels;
    }

    private int getScreenHeight() {
        return Resources.getSystem().getDisplayMetrics().heightPixels;
    }

    private int getScreenDpi(){
        return Resources.getSystem().getDisplayMetrics().densityDpi;
    }

    private void getStatusBatHeight(){
        /**
         * 获取状态栏高度——方法1
         * */
       //获取status_bar_height资源的ID
        int resourceId = getContext().getResources().getIdentifier("status_bar_height", "dimen", "android");
        if (resourceId > 0) {
            //根据资源ID获取响应的尺寸值
            statusBarHeight1 = getContext().getResources().getDimensionPixelSize(resourceId);
            float scale = getContext().getResources().getDisplayMetrics().density;
            statusBarHeight1= (int) (statusBarHeight1*scale+0.5f);
        }
    }
}

Android截屏,shell方式,需要root,对版本没要求

直接在代码里面执行shell命令,这个命令在电脑上面用adb方式执行无需root也能截图,放在代码里面需要root权限。
在PC上面截图,在DOS窗口输入adb shell screencap -p /sdcard/damn.png就可以截图并保存在sdcard的根目录下的damn.png文件。
在外直接调用doCmds("screencap -p /sdcard/damn.png")
这里不用前面的adb shell是因为,在pc上我们的adb是连接到手机的工具,在手机本身上不用连接这一步。

    /**
     * 执行shell命令函数
     * @param cmd 需要执行的shell命令
     */
    public static void doCmds(String cmd){
        Process process = null;
        try {
            process = Runtime.getRuntime().exec("sh");
            DataOutputStream os = new DataOutputStream(process.getOutputStream());
            os.writeBytes(cmd+"\n");
            os.writeBytes("exit\n");
            os.flush();
            os.close();
            process.waitFor();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

下面提供一个执行shell脚本的工具类,从其他博客搬过来的。

package com.hskj.damnicomniplusvic.wevenation.util;

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.List;

/**
 * Android执行shell命令工具类
 * Created by DAMNICOMNIPLUSVIC on 2017/7/26.
 * (c) 2017 DAMNICOMNIPLUSVIC Inc,All Rights Reserved.
 */

public class ShellUtil {
    private static final String COMMAND_SU       = "su";
    private static final String COMMAND_SH       = "ls";
    private static final String COMMAND_EXIT     = "exit\n";
    private static final String COMMAND_LINE_END = "\n";


    private ShellUtil() {
        throw new AssertionError();
    }


    /**
     * check whether has root permission
     *
     * @return root or not
     */
    public static boolean checkRootPermission() {
        return execCommand("echo root", true, false).result == 0;
    }


    /**
     * execute shell command, default return result msg
     *
     * @param command command
     * @param isRoot whether need to run with root
     * @return the result of execute command
     * @see ShellUtil#execCommand(String[], boolean, boolean)
     */
    public static CommandResult execCommand(String command, boolean isRoot) {
        return execCommand(new String[] {command}, isRoot, true);
    }


    /**
     * execute shell commands, default return result msg
     *
     * @param commands command list
     * @param isRoot whether need to run with root
     * @return the result of execute command
     * @see ShellUtil#execCommand(String[], boolean, boolean)
     */
    public static CommandResult execCommand(List<String> commands, boolean isRoot) {
        return execCommand(commands == null ? null : commands.toArray(new String[] {}), isRoot, true);
    }


    /**
     * execute shell commands, default return result msg
     *
     * @param commands command array
     * @param isRoot whether need to run with root
     * @return the result of execute command
     * @see ShellUtil#execCommand(String[], boolean, boolean)
     */
    public static CommandResult execCommand(String[] commands, boolean isRoot) {
        return execCommand(commands, isRoot, true);
    }


    /**
     * execute shell command
     *
     * @param command command
     * @param isRoot whether need to run with root
     * @param isNeedResultMsg whether need result msg
     * @return the result of execute command
     * @see ShellUtil#execCommand(String[], boolean, boolean)
     */
    public static CommandResult execCommand(String command, boolean isRoot, boolean isNeedResultMsg) {
        return execCommand(new String[] {command}, isRoot, isNeedResultMsg);
    }


    /**
     * execute shell commands
     *
     * @param commands command list
     * @param isRoot whether need to run with root
     * @param isNeedResultMsg whether need result msg
     * @return the result of execute command
     * @see ShellUtil#execCommand(String[], boolean, boolean)
     */
    public static CommandResult execCommand(List<String> commands, boolean isRoot, boolean isNeedResultMsg) {
        return execCommand(commands == null ? null : commands.toArray(new String[] {}), isRoot, isNeedResultMsg);
    }


    /**
     * execute shell commands
     *
     * @param commands command array
     * @param isRoot whether need to run with root
     * @param isNeedResultMsg whether need result msg
     * @return <ul>
     *         <li>if isNeedResultMsg is false, {@link CommandResult#successMsg} is null and
     *         {@link CommandResult#errorMsg} is null.</li>
     *         <li>if {@link CommandResult#result} is -1, there maybe some excepiton.</li>
     *         </ul>
     */
    public static CommandResult execCommand(String[] commands, boolean isRoot, boolean isNeedResultMsg) {
        int result = -1;
        if (commands == null || commands.length == 0) {
            return new CommandResult(result, null, null);
        }


        Process process = null;
        BufferedReader successResult = null;
        BufferedReader errorResult = null;
        StringBuilder successMsg = null;
        StringBuilder errorMsg = null;


        DataOutputStream os = null;
        try {
            process = Runtime.getRuntime().exec(isRoot ? COMMAND_SU : COMMAND_SH);
            os = new DataOutputStream(process.getOutputStream());
            for (String command : commands) {
                if (command == null) {
                    continue;
                }


                // donnot use os.writeBytes(commmand), avoid chinese charset error
                os.write(command.getBytes());
                os.writeBytes(COMMAND_LINE_END);
                os.flush();
            }
            os.writeBytes(COMMAND_EXIT);
            os.flush();


            result = process.waitFor();
            // get command result
            if (isNeedResultMsg) {
                successMsg = new StringBuilder();
                errorMsg = new StringBuilder();
                successResult = new BufferedReader(new InputStreamReader(process.getInputStream()));
                errorResult = new BufferedReader(new InputStreamReader(process.getErrorStream()));
                String s;
                while ((s = successResult.readLine()) != null) {
                    successMsg.append(s);
                }
                while ((s = errorResult.readLine()) != null) {
                    errorMsg.append(s);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (os != null) {
                    os.close();
                }
                if (successResult != null) {
                    successResult.close();
                }
                if (errorResult != null) {
                    errorResult.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }


            if (process != null) {
                process.destroy();
            }
        }
        return new CommandResult(result, successMsg == null ? null : successMsg.toString(), errorMsg == null ? null
                : errorMsg.toString());
    }


    /**
     * result of command
     * <ul>
     * <li>{@link CommandResult#result} means result of command, 0 means normal, else means error, same to excute in
     * linux shell</li>
     * <li>{@link CommandResult#successMsg} means success message of command result</li>
     * <li>{@link CommandResult#errorMsg} means error message of command result</li>
     * </ul>
     *
     * @author <a href="http://www.trinea.cn" target="_blank">Trinea</a> 2013-5-16
     */
    public static class CommandResult {


        /** result of command **/
        public int    result;
        /** success message of command result **/
        public String successMsg;
        /** error message of command result **/
        public String errorMsg;


        public CommandResult(int result) {
            this.result = result;
        }


        public CommandResult(int result, String successMsg, String errorMsg) {
            this.result = result;
            this.successMsg = successMsg;
            this.errorMsg = errorMsg;
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值