1. 程式人生 > >Android實現View截圖並儲存到相簿

Android實現View截圖並儲存到相簿

Android實現View截圖並儲存到相簿

一、目標

對筆記進行截圖,並儲存到相簿。

1. 效果圖

在這裡插入圖片描述

實現第1欄第1個圖示”圖片

“功能,對View進行截圖,並儲存到相簿。

2. 下載地址

神馬筆記最新版本:【神馬筆記 版本1.2.0.apk

二、需求設計

圖片方式是最能保持原有排版的一種方式,能保證完全一致的閱讀體驗。並且還具有一定防止修改的能力。

因此,神馬筆記非常推薦以圖片的方式進行分享。

實現整個功能分成3個步驟:

  1. 對View進行截圖生成Bitmap
  2. 儲存Bitmap圖片到檔案File
  3. 更新相簿相簿

三、準備工作

1. 實現View截圖

對View進行截圖,有2種方式。

  • 通過View#getDrawingCache()獲取快取的Bitmap
  • 通過View#draw()將View繪製到離屏的Bitmap
@Deprecated
public void setDrawingCacheEnabled(boolean enabled)

@Deprecated
public Bitmap getDrawingCache()

@Deprecated
public void buildDrawingCache(boolean autoScale)

@Deprecated
public void destroyDrawingCache()

@Deprecated
public void setDrawingCacheQuality
(@DrawingCacheQuality int quality) @Deprecated public void setDrawingCacheBackgroundColor(@ColorInt int color);
/**
 * @deprecated 
 * The view drawing cache was largely made obsolete with the introduction of
 * hardware-accelerated rendering in API 11. With hardware-acceleration, intermediate cache
 * layers are largely unnecessary and can easily result in a net loss in performance due to the
 * cost of creating and updating the layer. In the rare cases where caching layers are useful,
 * such as for alpha animations, {@link #setLayerType(int, Paint)} handles this with hardware rendering.  
 */
/**
 * For software-rendered snapshots of a small part of the View hierarchy or
 * individual Views it is recommended to create a {@link Canvas} from either a {@link Bitmap} or
 * {@link android.graphics.Picture} and call {@link #draw(Canvas)} on the View.
 */
/**
 * However these
 * software-rendered usages are discouraged and have compatibility issues with hardware-only
 * rendering features such as {@link android.graphics.Bitmap.Config#HARDWARE Config.HARDWARE}
 * bitmaps, real-time shadows, and outline clipping. For screenshots of the UI for feedback
 * reports or unit testing the {@link PixelCopy} API is recommended.
 */

Android 9.0(API 28)已經將所有操作DrawingCache的方法標識為Deprecated,不推薦使用。

因此,我們採用第二種方式,通過View#draw(Canvas)實現截圖。

正如註釋中所描述的,採用軟體渲染的方式,在處理陰影和裁剪時會遇到問題。

開發過程也確實遇到問題,View#draw(Canvas)會丟失elevation及translationZ方式渲染的陰影。

2. 儲存Bitmap到檔案

呼叫Bitmap#compress(CompressFormat format, int quality, OutputStream stream)即可儲存到檔案。

這裡對format及quality兩個引數有些取捨。

public enum CompressFormat {
    JPEG    (0),
    PNG     (1),
    WEBP    (2);

    CompressFormat(int nativeInt) {
        this.nativeInt = nativeInt;
    }
    final int nativeInt;
}
/** 
 * @param quality  Hint to the compressor, 0-100. 0 meaning compress for
 *                 small size, 100 meaning compress for max quality. Some
 *                 formats, like PNG which is lossless, will ignore the
 *                 quality setting
 */

選擇JPEG格式,還是PNG格式呢?

選擇JPEG格式,quality在[0, 100]之間設定多大的數值合適呢?

考慮到圖片用於分享,因此選擇JPEG格式,同時quality設定為50。

3. 更新相簿相簿

理論上,可以把檔案儲存到任何一個位置,但是?

在微信傳送到朋友圈的時候,遇到找不到圖片,無法傳送的問題。

把圖片儲存到系統圖庫目錄,並更新相簿相簿,問題完美解決。

MediaStore為我們提供了一個很好的示例。

public static final String insertImage(ContentResolver cr, Bitmap source,
                                       String title, String description) {
    ContentValues values = new ContentValues();
    values.put(Images.Media.TITLE, title);
    values.put(Images.Media.DESCRIPTION, description);
    values.put(Images.Media.MIME_TYPE, "image/jpeg");

    Uri url = null;
    String stringUrl = null;    /* value to be returned */

    try {
        url = cr.insert(EXTERNAL_CONTENT_URI, values);

        if (source != null) {
            OutputStream imageOut = cr.openOutputStream(url);
            try {
                source.compress(Bitmap.CompressFormat.JPEG, 50, imageOut);
            } finally {
                imageOut.close();
            }

            long id = ContentUris.parseId(url);
            // Wait until MINI_KIND thumbnail is generated.
            Bitmap miniThumb = Images.Thumbnails.getThumbnail(cr, id,
                                                              Images.Thumbnails.MINI_KIND, null);
            // This is for backward compatibility.
            Bitmap microThumb = StoreThumbnail(cr, miniThumb, id, 50F, 50F,
                                               Images.Thumbnails.MICRO_KIND);
        } else {
            Log.e(TAG, "Failed to create thumbnail, removing original");
            cr.delete(url, null, null);
            url = null;
        }
    } catch (Exception e) {
        Log.e(TAG, "Failed to insert image", e);
        if (url != null) {
            cr.delete(url, null, null);
            url = null;
        }
    }

    if (url != null) {
        stringUrl = url.toString();
    }

    return stringUrl;
}

直接呼叫insertImage,我們無法控制最終儲存位置。需要對這段程式碼稍作調整。

四、組合起來

1. Snapshot

Snapshot負責實現View截圖,並根據記憶體使用量,自動調整目標Bitmap的尺寸及模式,以保證顯示完整內容。

public class Snapshot {

    View view;

    float memoryFactor = 0.5f;

    public Snapshot(View view) {
        this(view, 0.5f);
    }

    public Snapshot(View view, float factor) {
        this.view = view;
        this.memoryFactor = (factor > 0.9f || factor < 0.1f)? 0.5f: factor;
    }

    public Bitmap apply() {
        Mode mode = chooseMode(view);
        if (mode == null) {
            return null;
        }

        Bitmap target = Bitmap.createBitmap(mode.mWidth, mode.mHeight, mode.mConfig);
        Canvas canvas = new Canvas(target);
        if (mode.mWidth != mode.mSourceWidth) {
            float scale = 1.f * mode.mWidth / mode.mSourceWidth;
            canvas.scale(scale, scale);
        }
        view.draw(canvas);

        return target;
    }

    Mode chooseMode(View view) {
        Mode mode = chooseMode(view.getWidth(), view.getHeight());
        return mode;
    }

    Mode chooseMode(int width, int height) {

        Mode mode;

        long max = Runtime.getRuntime().maxMemory();
        long total = Runtime.getRuntime().totalMemory();
        long remain = max - total; // 剩餘可用記憶體
        remain = (long)(memoryFactor * remain);

        int w = width;
        int h = height;
        while (true) {

            // 嘗試4個位元組
            long memory = 4 * w * h;
            if (memory <= remain) {
                if (memory <= remain / 3) { // 優先保證儲存後的圖片檔案不會過大,有利於分享
                    mode = new Mode(Bitmap.Config.ARGB_8888, w, h, width, height);

                    break;
                }
            }

            // 嘗試2個位元組
            memory = 2 * w * h;
            if (memory <= remain) {
                mode = new Mode(Bitmap.Config.RGB_565, w, h, width, height);
                break;
            }

            // 判斷是否可以繼續
            if (w % 3 != 0) {
                h = (int)(remain / 2 / w); // 計算出最大高度
                h = h / 2 * 2; // 喜歡偶數

                mode = new Mode(Bitmap.Config.RGB_565, w, h, width, height);
                break;
            }

            // 縮減到原來的2/3
            w = w * 2 / 3;
            h = h * 2 / 3;
        }

        return mode;
    }

    /**
     *
     */
    public static final class Mode {

        Bitmap.Config mConfig;

        int mWidth;
        int mHeight;

        int mSourceWidth;
        int mSourceHeight;

        Mode(Bitmap.Config config, int width, int height, int srcWidth, int srcHeight) {
            this.mConfig = config;

            this.mWidth = width;
            this.mHeight = height;

            this.mSourceWidth = srcWidth;
            this.mSourceHeight = srcHeight;
        }
    }
}

2. SharePictureAction

實現儲存圖片,並更新相簿相簿。分解成3個步驟完成。

  1. 刪除舊檔案
  2. 儲存Bitmap到檔案(儲存在系統Pictures目錄下)
  3. 更新相簿相簿,同時更新縮圖(參考MediaStore#insertImage實現)。
Uri accept(Bitmap bitmap) {
    Uri url = null;

    if (bitmap == null) {
        return url;
    }

    File file = this.targetDir;
    file = new File(file, entity.getName() + ".jpg");

    // delete previous bitmap
    {
        deleteImage(context, file);
    }

    // save file
    try {
        file.createNewFile();
        FileOutputStream fos = new FileOutputStream(file);
        bitmap.compress(Bitmap.CompressFormat.JPEG, 50, fos);
        fos.close();
    } catch (IOException e) {
        e.printStackTrace();

        file.delete();
        file = null;
    }

    // update media store
    if (file != null && file.exists()) {
        String title = entity.getName();
        String description = "";
        int width = bitmap.getWidth();
        int height = bitmap.getHeight();

        url = insertImage(context, file, title, description, width, height);
    }

    return url;
}
static final int deleteImage(Context context, File file) {

    int count = 0;
    ContentResolver resolver = context.getContentResolver();

    try {

        // 刪除舊檔案
        count = resolver.delete(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                                MediaStore.Images.ImageColumns.DATA + "=?",
                                new String[] { file.getAbsolutePath() });

    } catch (Exception e) {

    }

    return count;
}
static final Uri insertImage(Context context,
                             File file,
                             String title,
                             String description,
                             int width,
                             int height) {
    Uri url;

    ContentValues values = new ContentValues();
    ContentResolver cr = context.getContentResolver();

    // insert to media store
    {
        long time = file.lastModified();

        // media provider uses seconds for DATE_MODIFIED and DATE_ADDED, but milliseconds
        // for DATE_TAKEN
        long dateSeconds = time / 1000;

        // mime-type
        String mimeType = "image/jpeg";

        values.put(MediaStore.Images.ImageColumns.TITLE, title);
        values.put(MediaStore.Images.ImageColumns.DISPLAY_NAME, title);
        values.put(MediaStore.Images.ImageColumns.DESCRIPTION, description);

        values.put(MediaStore.Images.ImageColumns.MIME_TYPE, mimeType);
        values.put(MediaStore.Images.ImageColumns.WIDTH, width);
        values.put(MediaStore.Images.ImageColumns.HEIGHT, height);
        values.put(MediaStore.Images.ImageColumns.SIZE, file.length());
        values.put(MediaStore.Images.ImageColumns.DATA, file.getAbsolutePath());

        values.put(MediaStore.Images.ImageColumns.DATE_TAKEN, time);
        values.put(MediaStore.Images.ImageColumns.DATE_ADDED, dateSeconds);
        values.put(MediaStore.Images.ImageColumns.DATE_MODIFIED, dateSeconds);

        url = cr.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
    }

    // generate thumbnail
    {
        long id = ContentUris.parseId(url);

        // Wait until MINI_KIND thumbnail is generated.
        Bitmap miniThumb = MediaStore.Images.Thumbnails.getThumbnail(cr, id,
                                                                     MediaStore.Images.Thumbnails.MINI_KIND, null);
        if (miniThumb != null) {
            miniThumb.recycle();
        }
    }

    return url;
}

五、Final

整個功能遇到的最大問題是第一步——將View轉為Bitmap

後續兩個步驟,MediaStore為我們提供了標準的實現方案。

目前View轉為Bitmap最大的問題是會丟失陰影,當我們使用CardView時,問題會非常明顯。

堅果Pro2通過截圖的方式,可以完美保持陰影。

堅果Pro2通過長截圖的方式,可以完美對View截圖並保持陰影。

~待後續版本進行優化~奈何~奈何~