截屏与WebView长图分享经验总结

【背景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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,409评论 25 707
  • 其实我们很辛福,只是对我们所拥有的事物视而不见,习以为常,并且认为这些理所当然的。只是因为熟悉的地方,没有风景。才...
    黎叔ai阅读 255评论 0 1
  • 快过年了,到处都很热闹。 可是,自己随着年纪的增大,却越发的觉得过年是十分孤独的,总觉得自己越来越不能融入进这个年...
    一片云儿阅读 258评论 0 0