Android(安卓)5.0后截屏,无需root
- Android50截屏无需root
- 实现步骤
- 拿到屏幕上的实时信息
- 新建一个虚拟屏幕
- 处理虚拟屏幕上的图像
- 实现步骤
- Android截屏shell方式需要root对版本没要求
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里面最多存储几张图像(多了耗费内存),这里格式必须是ImageFormat或PixelFormat里面的,当然也并不是说里面的所有格式都支持,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 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 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 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 * - if isNeedResultMsg is false, {@link CommandResult#successMsg} is null and * {@link CommandResult#errorMsg} is null.
* - if {@link CommandResult#result} is -1, there maybe some excepiton.
*
*/ 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 * * - {@link CommandResult#result} means result of command, 0 means normal, else means error, same to excute in * linux shell
* - {@link CommandResult#successMsg} means success message of command result
* - {@link CommandResult#errorMsg} means error message of command result
*
* * @author Trinea 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; } }}
更多相关文章
- Android(安卓)开发的常用工具类(一)——ScreenUtils 可用于获取屏
- Android获取屏幕分辨率及DisplayMetrics简介
- android 获取屏幕高度,宽度,状态栏高度
- Android(安卓)如何在xmL 里面动态设置padding
- Android界面布局基本知识简述
- Android实现屏幕旋转方法总结
- Android(安卓)实现变色状态栏
- android EditText中inputType的属性列表
- Android获取屏幕分辨率及DisplayMetrics简介