Android开发经验总结篇

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

2018-03-21  本文已影响21人  zhengLH

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

上一篇下一篇

猜你喜欢

热点阅读