Android实现手机摇晃截图

今天来介绍个这段时间做的功能,手机摇晃截图。因为这个功能比较常见,所以记录一下以便于之后用到时查阅。

这里说上去是一个功能,其实是有两个需求,即摇晃&截图

摇晃

首先介绍这个摇晃,也就是我们常说的“手机摇一摇”。这个看起来高端,其实实现起来很简单,就是通过SensorManager来检测手机的重力感应,进而判读用户是否在主动的摇晃手机。

核心Service类

我们选择建立一个Service,在里面创建我们的SensorManager,并为其注册SensorEventListener,在监听里实现摇晃的判断。具体代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
package com.chen.screenshot;

import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.IBinder;
import android.util.Log;

public class ShakerWatchService extends Service {
private static final String LOG_TAG = ShakerWatchService.class.getName();
private SensorManager mSensorManager;
private SensorEventListener mSensorEventListener = new SensorEventListener() {
private static final float SENSITIVITY_THRESHOLD = 16; //set the sensitivity of accelerometer
private static final int BUFFER = 5; //Buffer used to average the acceleration
private static final int SHAKE_DIMENSION = 3; //movement direction
private float[] gravity = new float[SHAKE_DIMENSION];
private float average = 0;
private int fill = 0;
@Override
public void onSensorChanged(SensorEvent event) {
final float alpha = 0.8F;

for (int i = 0; i < SHAKE_DIMENSION; i++) {
gravity[i] = alpha * gravity[i] + (1 - alpha) * event.values[i];
}

float x = event.values[0] - gravity[0];
float y = event.values[1] - gravity[1];
float z = event.values[2] - gravity[2];

if (fill <= BUFFER) {
average += Math.abs(x) + Math.abs(y) + Math.abs(z);
fill++;
} else {
if (average / BUFFER >= SENSITIVITY_THRESHOLD) {
handleShakeAction();
}
average = 0;
fill = 0;
}
}

@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
}
};

@Override
public void onCreate() {
super.onCreate();
mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// 第一个参数是Listener,第二个参数是所得传感器类型,第三个参数值获取传感器信息的频率
mSensorManager.registerListener(mSensorEventListener, mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER), SensorManager.SENSOR_DELAY_NORMAL);
return super.onStartCommand(intent, flags, startId);
}

@Override
public IBinder onBind(Intent intent) {
return null;
}

@Override
public void onDestroy() {
mSensorManager.unregisterListener(mSensorEventListener);
super.onDestroy();
}

private void handleShakeAction() {
Log.d(LOG_TAG, "handle shake action");
}
}

这样,当service启动时,只要用户摇晃,我们就可以监听到,并触发handleShakeAction()方法。在该方法里写入我们所需要的逻辑即可。

功能调用

接着我们在想要使用的时候只需start这个service,想要关闭的时候只要stop这个service就可以了。在Demo里,我在主Activity中使用了一个Switch开关控件来控制摇晃功能的开启与关闭。

布局文件如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">

<Switch
android:id="@+id/switch_shaker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_margin="20dp"
android:switchPadding="20dp"
android:text="start shaker" />
</LinearLayout>

最后是Activity中的调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package com.chen.screenshot;

import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.widget.CompoundButton;
import android.widget.Switch;

public class MainActivity extends AppCompatActivity {

private Switch mShakerSwitch;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mShakerSwitch = findViewById(R.id.switch_shaker);
mShakerSwitch.setChecked(false);
mShakerSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
Intent intent = new Intent(MainActivity.this,ShakerWatchService.class);
if (isChecked) {
startService(intent);
} else {
stopService(intent);
}
}
});
}

@Override
protected void onDestroy() {
Intent intent = new Intent(MainActivity.this,ShakerWatchService.class);
stopService(intent);
super.onDestroy();
}
}

至此我们的“手机摇一摇”功能就算是完工了。

截图

接下来就是要完成手机截图了。截图的方法有很多种,这里介绍最具代表性的三种。

方法一:View截屏

1
2
3
4
5
public Bitmap capture(Activity activity) {
activity.getWindow().getDecorView().setDrawingCacheEnabled(true);
Bitmap bmp = activity.getWindow().getDecorView().getDrawingCache();
return bmp;
}

这里的思路很清晰,就是获取Activity的Decor View,然后通过View的getDrawingCache()方法获取View的截屏。这种方法不需额外权限,也不需要Root。但是它不能截取到状态栏,也不能截取SurfaceView及WebView。

方法二:adb截屏

这个方法用起来更加简单,只需要在代码中执行一行截屏的adb shell命令即可。麻烦的一点是,需要Root权限。

1
2
// 命令格式: adb shell screencap -p + 文件路径 + 文件名
adb shell screencap -p /sdcard/sreenshot1.png

方法三:虚拟桌面截屏

鉴于上面两种方法的局限性,我们实际项目中采取了第三种方法,利用Android 5.0开放的录屏API来进行截屏。说到这可能你也发现了,这个方法也是有局限性的,就是只能在Android 5.0以上的手机使用。不过手机更新越来越快,4.X的手机也越来越少,5.0其实也并不算一个很过分的门槛。

该方法的原理是启动屏幕捕捉,使用MediaProjectionImageReader创建一个虚拟桌面,然后将捕捉的数据传递到虚拟桌面,最后使用ImageReader截取虚拟桌面的一帧画面。

先建立一个AutoScreenShot类,里面写入截屏的核心方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
package com.chen.screenshot;

import android.annotation.TargetApi;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
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.AsyncTask;
import android.os.Build;
import android.os.Handler;
import android.util.Log;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;

public class AutoScreenShot {
private static final String LOG_TAG = AutoScreenShotActivity.class.getName();
private static final int CAPTURE_DELAY = 800;

private ImageReader mImageReader;
private MediaProjection mMediaProjection;
private VirtualDisplay mVirtualDisplay;
private OnShotListener mOnShotListener;
private String mAutoScreenShotPath;

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public AutoScreenShot(Context context, int reqCode, Intent data) {
mMediaProjection = ((MediaProjectionManager) context.getSystemService(Context.MEDIA_PROJECTION_SERVICE)).
getMediaProjection(reqCode, data);

mImageReader = ImageReader.newInstance(
Resources.getSystem().getDisplayMetrics().widthPixels,
Resources.getSystem().getDisplayMetrics().heightPixels,
PixelFormat.RGBA_8888,
1);

// 内部存储,无需额外申请权限。路径为:Android/data/com.chen.screenshot/files/screenshot/xxxx.png
mAutoScreenShotPath = context.getExternalFilesDir("screenshot").getAbsoluteFile() + "/" +
System.currentTimeMillis() + ".png";
}

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public void startScreenShot(OnShotListener onShotListener) {
Log.d(LOG_TAG, "start screen shot");
mOnShotListener = onShotListener;
mVirtualDisplay = mMediaProjection.createVirtualDisplay("screen-mirror",
Resources.getSystem().getDisplayMetrics().widthPixels,
Resources.getSystem().getDisplayMetrics().heightPixels,
Resources.getSystem().getDisplayMetrics().densityDpi,
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
mImageReader.getSurface(), null, null);

// Add this delay for not capture permission dialog shadow
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
Image image = mImageReader.acquireLatestImage();
new SaveImageTask().execute(image);
}
}, CAPTURE_DELAY);
}

private class SaveImageTask extends AsyncTask<Image, Void, Bitmap> {
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
protected Bitmap doInBackground(Image... params) {
if (params == null || params.length < 1 || params[0] == null) {
return null;
}

Image image = params[0];
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();
File fileImage = null;
if (bitmap != null) {
try {
fileImage = new File(mAutoScreenShotPath);
if (!fileImage.exists()) {
fileImage.createNewFile();
}
FileOutputStream out = new FileOutputStream(fileImage);
if (out != null) {
bitmap.compress(Bitmap.CompressFormat.PNG, 90, out);
out.flush();
out.close();
}
} catch (FileNotFoundException e) {
e.printStackTrace();
fileImage = null;
} catch (IOException e) {
e.printStackTrace();
fileImage = null;
}
}
if (fileImage != null) {
return bitmap;
}
return null;
}

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
protected void onPostExecute(Bitmap bitmap) {
super.onPostExecute(bitmap);
if (bitmap != null && !bitmap.isRecycled()) {
bitmap.recycle();
}
if (mVirtualDisplay != null) {
mVirtualDisplay.release();
}
if (mMediaProjection != null) {
mMediaProjection.stop();
}
if (mImageReader != null) {
mImageReader.close();
}
if (mOnShotListener != null) {
mOnShotListener.onFinish(mAutoScreenShotPath);
}
}
}

public interface OnShotListener {
void onFinish(String autoScreenShotPath);
}
}

接着,创建一个透明的Dialog Activity,用来显示截屏权限的dialog。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package com.chen.screenshot;

import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.ColorDrawable;
import android.media.projection.MediaProjectionManager;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.util.Log;
import android.view.Window;
import android.widget.Toast;

/**
* This activity is transparent.
* Only use for showing auto screen shot permission dialog.
* Auto screen shot need OS version not below Android 5.0 (SDK level 21)
*/
public class AutoScreenShotActivity extends Activity{
private static final String LOG_TAG = AutoScreenShotActivity.class.getName();
private static final int REQUEST_MEDIA_PROJECTION = 10000;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d(LOG_TAG, "AutoScreenShotActivity onCreate()");
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setBackgroundDrawable(new ColorDrawable(android.graphics.Color.TRANSPARENT));
getWindow().setDimAmount(0f);

requestScreenShot();
}

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void requestScreenShot() {
Log.d(LOG_TAG, "requestScreenShot()");
Intent intent = ((MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE)).createScreenCaptureIntent();
startActivityForResult(intent, REQUEST_MEDIA_PROJECTION);
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_MEDIA_PROJECTION) {
if(resultCode == RESULT_OK && data != null) {
AutoScreenShot autoScreenShot = new AutoScreenShot(this, resultCode, data);
autoScreenShot.startScreenShot(new AutoScreenShot.OnShotListener() {
@Override
public void onFinish(String autoScreenShotPath) {
Log.d(LOG_TAG, "Auto screen shot success");
Toast.makeText(AutoScreenShotActivity.this, "shot success", Toast.LENGTH_SHORT).show();
}
});
} else if (resultCode == RESULT_CANCELED) {
Log.d(LOG_TAG, "Not give permission");
Toast.makeText(AutoScreenShotActivity.this, "shot cancel , please give permission.", Toast.LENGTH_SHORT).show();
}
finish();
}
}
}

AndroidManifest里记得设置theme为Theme.Dialog。

1
2
3
4
<activity
android:name=".AutoScreenShotActivity"
android:exported="true"
android:theme="@android:style/Theme.Dialog"/>

总结

这样,摇晃手机截屏的基础功能就算是基本完成了。你可以根据项目的特地需求,在这个基础上再做调整修改。

Chen wechat
欢迎扫描二维码,订阅我的博客公众号MiracleChen