【背景1】如何实现截屏?
【核心代码】
【注】以下2种手机截图的方法生成的图片会显示顶部的状态栏、标题栏以及底部的菜单栏.
【法1】
private void screenshot()
{
// 获取屏幕
View dView = getWindow().getDecorView();
dView.setDrawingCacheEnabled(true);
dView.buildDrawingCache();
Bitmap bmp = dView.getDrawingCache();
if (bmp != null)
{
try {
// 获取内置SD卡路径
String sdCardPath = Environment.getExternalStorageDirectory().getPath();
// 图片文件路径
String filePath = sdCardPath + File.separator + "screenshot.png";
File file = new File(filePath);
FileOutputStream os = new FileOutputStream(file);
bmp.compress(Bitmap.CompressFormat.PNG, 100, os);
Drawable drawable = new BitmapDrawable(bmp);
mIvScreenshot.setBackground(drawable);
os.flush();
os.close();
} catch (Exception e) {
}
}
}
【法2】
/**
* 截取屏幕
* @param activity
* @return
*/
public Bitmap captureScreen(Activity activity) {
activity.getWindow().getDecorView().setDrawingCacheEnabled(true);
Bitmap bmp = getWindow().getDecorView().getDrawingCache();
return bmp;
}
二、WebView 生成长图
【注】介绍 web 长图之前,先来说一下单屏图片的生成方案,和手机截图不同的是生成的图片不会显示顶部的状态栏、标题栏以及底部的菜单栏,可以满足不同的业务需求。
【webview 单屏,不包含状态栏,底部栏】
int screenHeight = getWindowManager().getDefaultDisplay().getHeight();
int screenWidth = getWindowManager().getDefaultDisplay().getWidth();
Bitmap shortImage = Bitmap.createBitmap(screenWidth, screenHeight, Bitmap.Config.RGB_565);
Canvas canvas = new Canvas(shortImage);
// 画布的宽高和屏幕的宽高保持一致
Paint paint = new Paint();
canvas.drawBitmap(shortImage, screenWidth, screenHeight, paint);
mWebView.draw(canvas);
Drawable drawable = new BitmapDrawable(shortImage);
mIvScreenshot.setBackground(drawable);
【webview 长图】
有的时候我们需要将一个长 Web 网页生成图片分享出去,相似的例子就是
手机端的各种便签应用,当便签内容超出一屏时,就需要将所有的内容生成一张长图对外分享出去。
WebView 和其他 View 一样,系统都提供了 draw 方法,可以直接将 View 的内容渲染到画布上,有了画布我们就可以在上面绘制其他各种各种的内容,比如底部添加 Logo 图片,画红线框等等。关于 WebView 生成长图网上已经有很多现成的方案和代码,以下代码是经测试过的稳定版本,供参考
// WebView 生成长图,也就是超过一屏的图片,代码中的 longImage 就是最后生成的长图
mWebView.measure(View.MeasureSpec.makeMeasureSpec(View.MeasureSpec.UNSPECIFIED,
View.MeasureSpec.UNSPECIFIED),
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
mWebView.layout(0, 0, mWebView.getMeasuredWidth(), mWebView.getMeasuredHeight());
mWebView.setDrawingCacheEnabled(true);
mWebView.buildDrawingCache();
Bitmap longImage = Bitmap.createBitmap(mWebView.getMeasuredWidth(), mWebView.getMeasuredHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(longImage);
// 画布的宽高和 WebView 的网页保持一致
Paint paint = new Paint();
canvas.drawBitmap(longImage, 0, mWebView.getMeasuredHeight(), paint);
mWebView.draw(canvas);
Drawable drawable = new BitmapDrawable(longImage);
mIvScreenshot.setBackground(drawable);
【背景2】在 Android 原生系统中是没有提供截图的广播或者监听事件的,也就是说代码层面无法获知用户的截屏操作,这样就无法满足用户截屏后跳出分享提示的需求。
【答案】目前比较成熟稳定的方案是监听系统媒体数据库资源的变化,具体方案原理如下:
Android 系统有一个媒体数据库,每拍一张照片,或使用系统截屏截取一 张图片,都会把这张图片的详细信息加入到这个媒体数据库,并发出内容改变通知,我们可以利用内容观察者(ContentObserver)监听媒体数据库的变化,当数据库有变化时,获取最后插入的一条图片数据,如果该图片符合特定的规则,则认为被截屏了。
【注】
(1)考虑到手机存储包括内部存储器和外部存储器,为了增强兼容性,最好同时监听两种储存空间的变化,以下是需要 ContentObserver 监听的资源 URI :
MediaStore.Images.Media.INTERNAL_CONTENT_URI
MediaStore.Images.Media.EXTERNAL_CONTENT_URI
(2)
【核心代码】
/**
* @Author Lee
* @Time 2018/3/21
* @Theme 截屏 和 webView 生成长图分享
*/
public class ScreenshotNLongPicActivity extends AppCompatActivity {
private MediaContentObserver mInternalObserver;
private MediaContentObserver mExternalObserver;
private static final String[] MEDIA_PROJECTIONS = {
MediaStore.Images.ImageColumns.DATA,
MediaStore.Images.ImageColumns.DATE_TAKEN,
};
/** 读取媒体数据库时需要读取的列, 其中 WIDTH 和 HEIGHT 字段在 API 16 以后才有 */
private static final String[] MEDIA_PROJECTIONS_API_16 = {
MediaStore.Images.ImageColumns.DATA,
MediaStore.Images.ImageColumns.DATE_TAKEN,
MediaStore.Images.ImageColumns.WIDTH,
MediaStore.Images.ImageColumns.HEIGHT,
};
/** 截屏依据中的路径判断关键字 */
private static final String[] KEYWORDS = {
"screenshot", "screen_shot", "screen-shot", "screen shot", "screenshots",
"screencapture", "screen_capture", "screen-capture", "screen capture",
"screencap", "screen_cap", "screen-cap", "screen cap"
};
private TextView mTvScreenShot;
private boolean isContain;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activit_screenshot_webview);
checkPermission();
initMediaContentObserver();
mTvScreenShot = findViewById(R.id.tv_screenshot);
}
private void checkPermission() {
DynamicPermissionsUtils.getDynamicPermissions(this,
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE},
1);
}
/**
* 初始化 媒体内容观察者
*/
private void initMediaContentObserver() {
final Handler mUiHandler = new Handler(Looper.getMainLooper());
// 创建内容观察者,包括内部存储和外部存储
mInternalObserver = new MediaContentObserver(MediaStore.Images.Media.INTERNAL_CONTENT_URI, mUiHandler);
mExternalObserver = new MediaContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, mUiHandler);
// 注册内容观察者
this.getContentResolver().registerContentObserver(MediaStore.Images.Media.INTERNAL_CONTENT_URI,
false, mInternalObserver);
this.getContentResolver().registerContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
false, mExternalObserver);
}
/**
* 自定义媒体内容观察者类(观察媒体数据库的改变)
*/
private class MediaContentObserver extends ContentObserver {
private Uri mediaContentUri;
// 需要观察的Uri
public MediaContentObserver(Uri contentUri, Handler handler) {
super(handler);
mediaContentUri = contentUri;
}
@Override
public void onChange(boolean selfChange) {
super.onChange(selfChange);
handleMediaContentChange(mediaContentUri);
}
}
/**
* 处理媒体数据库反馈的数据变化
*
* @param contentUri
*/
private void handleMediaContentChange(Uri contentUri) {
Cursor cursor = null;
try {
// 数据改变时查询数据库中最后加入的一条数据
cursor = this.getContentResolver().query(contentUri, Build.VERSION.SDK_INT < 16 ?
MEDIA_PROJECTIONS : MEDIA_PROJECTIONS_API_16, null,
null, MediaStore.Images.ImageColumns.DATE_ADDED + " desc limit 1");
if (cursor == null) {
return;
}
if (!cursor.moveToFirst()) {
return;
}
// cursor.getColumnIndex获取数据库列索引
int dataIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA);
String data = cursor.getString(dataIndex); // 图片存储地址
int dateTakenIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATE_TAKEN);
long dateTaken = cursor.getLong(dateTakenIndex); // 图片生成时间
int width = 0;
int height = 0;
if (Build.VERSION.SDK_INT >= 16) {
int widthIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.WIDTH);
int heightIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.HEIGHT);
width = cursor.getInt(widthIndex); // 获取图片高度
height = cursor.getInt(heightIndex); // 获取图片宽度
} else {
Point size = getImageSize(data);
// 根据路径获取图片宽和高
width = size.x;
height = size.y;
}
// 处理获取到的第一行数据,分别判断路径是否包含关键词、时间差以及图片宽高和屏幕宽高的大小关系
boolean isCaptrued = handleMediaRowData(data, dateTaken, width, height);
if(isCaptrued){
mTvScreenShot.setText("截屏成功了");
Toast.makeText(ScreenshotNLongPicActivity.this, "截屏成功了", Toast.LENGTH_LONG).show();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (cursor != null && !cursor.isClosed()) {
cursor.close();
}
}
}
/**
* 判断图片是否符合 3大规则(时间, 尺寸,路径)
* 时间: 监听到数据库变化的时间与截图生成的时间不会相差太多(假设阈值为10s)
* 尺寸: 截屏即是获取当前手机屏幕尺寸大小的图片,所以图片宽高大于屏幕宽高的肯定都不是截图产生的。
* 路径: 由于各手机厂家存放截图的文件路径都不太一样,通常图片保存路径都会包含一些常见的关键词,
* 比如 "screenshot"、 "screencapture" 、 "screencap" 、 "截图"、 "截屏"等,
* 每次都检查图片路径信息是否包含这些关键词。
*
* @param data 图片存储地址
* @param dateTaken 图片生成时间
* @param width 图片的宽
* @param height 图片的高
*/
private boolean handleMediaRowData(String data, long dateTaken, int width, int height) {
/*Toast.makeText(this, "data = " + data + " dateTaken=" + dateTaken
+ " width=" + width + " height=" + height, Toast.LENGTH_LONG).show();*/
Log.i("lee", "data = " + data + " dateTaken=" + dateTaken
+ " width=" + width + " height=" + height);
data = data.toLowerCase();
for (String keyWork : KEYWORDS) {
if (data.contains(keyWork)) {
return true;
}
}
if((System.currentTimeMillis() - dateTaken ) < 10 * 1000){
return true;
}
if(width > getWindowManager().getDefaultDisplay().getWidth() ||
height >getWindowManager().getDefaultDisplay().getHeight()){
return false;
}
return false;
}
/**
* 处理图片
*
* @param data
* @return
*/
private Point getImageSize(String data) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(data, options);
return new Point(options.outWidth, options.outHeight);
/* Bitmap bitmap = BitmapFactory.decodeFile(data);
Point point = new Point(bitmap.getWidth(), bitmap.getHeight());
return point;*/
}
}
【xml 布局】
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center">
<TextView
android:id="@+id/tv_screenshot"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="@dimen/textSize_22sp"/>
</LinearLayout>
【运行权限工具类】
/**
* @Author Lee
* @Time 2017/9/11
* @Theme 6.0(sdk23)以后申请动态权限 (静态授权 + 动态申请)
*/
public class DynamicPermissionsUtils {
/*(1) ContextCompat.checkSelfPermission,主要用于检测某个权限是否已经被授予,方法返回值为
PackageManager.PERMISSION_DENIED或者PackageManager.PERMISSION_GRANTED。当返回DENIED就需要进行申请授权了。*/
/* (2) ActivityCompat.requestPermissions,该方法是异步的,第一个参数是Context;
第二个参数是需要申请的权限的字符串数组;第三个参数为requestCode,主要用于回调的时候检测。
可以从方法名requestPermissions以及第二个参数看出,是支持一次性申请多个权限的,
系统会通过对话框逐一询问用户是否授权。*/
/**
*
* @param activity 上下文环境
* @param permissionList 需要动态申请的权限数组(支持一次性申请多个)
* @param requestCode 主要用于回调的时候检测
*/
// 读取手机通讯录
public static void getDynamicPermissions(Activity activity, String[] permissionList, int requestCode){
for(int i =0; i<permissionList.length; i++){
if (ContextCompat.checkSelfPermission(activity,permissionList[i])
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(activity,
permissionList,
requestCode);
}
}
}
}
【传送门】
(1)https://www.jianshu.com/p/8b1bcbbae4e7
(2)http://blog.csdn.net/xietansheng/article/details/52692163