1. 程式人生 > >截圖功能實現,Bitmap拼接、合併

截圖功能實現,Bitmap拼接、合併

最近,接到個需求,有個頁面要截圖,然後把截圖得到的圖片,以海報的形式分享出去。
簡單的說,步驟上是2步:1、截圖;2、對拿到的圖片進行處理,得到海報。最後的用三方SDK分享,這裡不做說明。
在說明之前,我們需要先了解點東西:
在這裡插入圖片描述

1、獲取螢幕區域

//獲取螢幕寬高的第一種方法。其中,getWidth和getHeight是過時方法
WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
screenWidth = wm.getDefaultDisplay().getWidth();
screenHeight = wm.getDefaultDisplay().getHeight();

//獲取螢幕寬高的第二種方法
DisplayMetrics metrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metrics);
int[] values={metrics.widthPixels, metrics.heightPixels };

2、獲取應用區域

Rect outRect = new Rect();
getWindow().getDecorView().getWindowVisibleDisplayFrame(outRect);
        
//整個螢幕,除去狀態列的高度
int x=outRect.height();
        
//狀態列的高度
int y=outRect.top;

其中,outRect.top 即是狀態列高度

3、獲取繪製區域

Rect outRect = new Rect();  
activity.getWindow().findViewById(Window.ID_ANDROID_CONTENT).getDrawingRect(outRect);

用繪製區域的outRect.top - 應用區域的outRect.top 即是標題欄的高度

注:不要在onCreate中測量。會出現資料不準的問題

開始寫程式碼:
模擬介面的搭建程式碼,就不展示了,直接看圖就行
在這裡插入圖片描述

中間綠色的,是TextView,一會兒會用到,底部是一個點選按鈕。

activity中的程式碼如下:

import android.app.Activity;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.os.Environment;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;

import java.io.File;
import java.io.FileOutputStream;

public class MainActivity extends Activity {

    private TextView bottom_tv;

    private TextView tv;

    private String savePicPath = Environment.getExternalStorageDirectory().getAbsolutePath()
            + "/Achen/";

    private int countNum = 0;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        bottom_tv = (TextView) findViewById(R.id.bottom_tv);
        tv = (TextView) findViewById(R.id.tv);

        tv.setText(countNum + "");

        bottom_tv.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                screenShot(MainActivity.this);

            }
        });

        tv.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                countNum++;
                tv.setText(countNum + "");
            }
        });

    }

    /**
     * 截圖功能
     */
    private void screenShot(Activity activity) {

        View dView = activity.getWindow().getDecorView();
        dView.setDrawingCacheEnabled(true);
        dView.buildDrawingCache();
        Bitmap bmp = dView.getDrawingCache();

        FileOutputStream fos_1 = null;

        if (bmp != null) {

            File saveFile = new File(savePicPath);
            if (!saveFile.exists()) {
                try {
                    saveFile.mkdirs();
                } catch (Exception e) {

                }
            }

            String picPath = savePicPath + "shot.jpg";

            try {

                File file = new File(picPath);
                fos_1 = new FileOutputStream(file);

                bmp.compress(Bitmap.CompressFormat.JPEG, 100, fos_1);

				//如果有這句話,會報異常:java.lang.IllegalStateException: Can't compress a recycled bitmap
                //bmp.recycle();

                fos_1.flush();
                fos_1.close();

            } catch (Exception e) {

            }

        }

        Toast.makeText(MainActivity.this,"處理結束",Toast.LENGTH_SHORT).show();
    }

}

為什麼定義FileOutputStream的時候,是 fos_1呢,因為下面會有2。

執行起來,我們會看到,手機上展示的情況是:
在這裡插入圖片描述

好,現在我們截圖,完成後,去資料夾下,會找到圖片:
在這裡插入圖片描述

螢幕是截下來了,但是,頂部,狀態列也截下來了。太醜了,現在需要去掉狀態列。

參考文章開頭圖片的說明,修改截圖方法為:

/**
     * 截圖功能
     */
    private void screenShot(Activity activity) {

        View dView = activity.getWindow().getDecorView();
        dView.setDrawingCacheEnabled(true);
        dView.buildDrawingCache();
        Bitmap bmp = dView.getDrawingCache();

        Rect outRect = new Rect();
        activity.getWindow().getDecorView().getWindowVisibleDisplayFrame(outRect);
        int sW = outRect.width();
        int sH = outRect.height();

        Bitmap resultBitmap = null;

        FileOutputStream fos_1 = null;
        FileOutputStream fos_2 = null;

        if (bmp != null) {

            File saveFile = new File(savePicPath);
            if (!saveFile.exists()) {
                try {
                    saveFile.mkdirs();
                } catch (Exception e) {

                }
            }

            String picPath_1 = savePicPath + "shot.jpg";
            String picPath_2 = savePicPath + "result.jpg";

            try {

                File file_1 = new File(picPath_1);
                fos_1 = new FileOutputStream(file_1);
                bmp.compress(Bitmap.CompressFormat.JPEG, 100, fos_1);

                /**
                 * 從bmp上擷取一部分,作為一個新的bitmap。
                 * 擷取的這部分,擷取起點是座標(0,outRect.top)
                 * 擷取寬度是sW,高度是sH
                 */
                resultBitmap = Bitmap.createBitmap(bmp, 0, outRect.top, sW, sH);

                File file_2 = new File(picPath_2);
                fos_2 = new FileOutputStream(file_2);
                resultBitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos_2);

                resultBitmap.recycle();
                //如果有這句話,會報異常:java.lang.IllegalStateException: Can't compress a recycled bitmap
                //bmp.recycle();

                fos_1.flush();
                fos_1.close();

            } catch (Exception e) {

            }

        }

        Toast.makeText(MainActivity.this, "處理結束", Toast.LENGTH_SHORT).show();
    }

現在,我們去目標資料夾裡找result.jpg看看
在這裡插入圖片描述

狀態列沒有了,雖然頂部有一點狀態的剩餘,但是隻要控制擷取座標位置就行,這個沒什麼難度。

需求的第一步截圖完成了嗎?很遺憾的說,沒有。為什麼我中間綠色控制元件要專門設定成TextView呢?繼續看

現在,我做如下操作:
1、進入介面,介面展示數字:0。這個時候,我截圖,找到圖片,看到圖片上是0
2、回到介面,我繼續點選,讓數值增加,假如增加到5,截圖。
這個時候,去找到截圖的圖片,發現上面還是顯示的0,數值沒有變化!!!

其實,從上面bmp.recycle();這句話沒有被註釋,重複截圖報異常,就能猜到了,我釋放了這次的圖片,下次走方法,我重新獲取了一次,卻還是提示我“使用了被釋放的bitmap”

結論:這種方法,只能是一次性的,第一次截圖的時候生成的,以後再怎麼截圖,都還是第一次截圖時候的樣子。

要實時截圖,上面方法不適用。其實實際開發中,上面的方法就沒辦法用。

換個思路。如果我要擷取的,不是整個螢幕,而是一個控制元件呢?或者說,我把控制元件的樣子畫下來,存起來,不就行了?

修改截圖方法為:

    /**
     * 截圖功能
     */
    private void screenShot() {

        //myll,是3個顏色控制元件的總控制元件,我要繪製他們3個
        //建立一個空白的bitmap,他的寬高,就是控制元件myll的寬高
        Bitmap bg = Bitmap.createBitmap(myll.getWidth(), myll.getHeight(), Bitmap.Config.ARGB_8888);
        //初始化一個畫布,並制定畫布背景色
        Canvas canvas = new Canvas(bg);
        /*
         * 這裡要加個白色背景。
         * 如果不加,有時候會展示異常。
         * 如:如果子控制元件有listView,listView添加了head,截圖的時候,head背景就是黑色
         */
        canvas.drawColor(0xffffffff);

        /**
         * Manually render this view (and all of its children) to the given Canvas.
         * The view must have already done a full layout before this function is
         * called.  When implementing a view, implement
         * {@link #onDraw(android.graphics.Canvas)} instead of overriding this method.
         * If you do need to override this method, call the superclass version.
         *
         * 翻譯:
         * 手工將這個檢視(及其所有子檢視)呈現給給定的畫布。
         * 在此函式之前,檢視必須已經完成了完整的佈局
         * 呼叫。實現檢視時,請實現
         * {@link #onDraw(android.graphics.Canvas)},而不是覆蓋這個方法。
         * 如果確實需要重寫此方法,則呼叫超類版本。
         */
        myll.draw(canvas);

        File saveFile = new File(savePicPath);
        if (!saveFile.exists()) {
            try {
                saveFile.mkdirs();
            } catch (Exception e) {

            }
        }

        try{
            String picPath = savePicPath + "chen.jpg";
            File file = new File(picPath);
            FileOutputStream os = new FileOutputStream(file);

            //不建議用這句話,因為生產截圖偏大
            //bg.compress(Bitmap.CompressFormat.PNG, 100, os_1);
            bg.compress(Bitmap.CompressFormat.JPEG, 60, os);
            bg.recycle();
            os.flush();
            os.close();
        }catch (Exception e){
            Log.e("screenShot","e=="+e);
        }

    }

現在,我們去看看產生的截圖:
在這裡插入圖片描述

完美。經測試,反覆截圖,生成的都是實時的。實際Activity中顯示數值是多少,截圖圖中就是多少。

下面,我們需要生成海報了:
設計圖如下:
在這裡插入圖片描述

繪製文字,我們需要知道文字的基本東西:
在這裡插入圖片描述

更具體的,請看一位大神的部落格,我就是從那裡學來的。

好,現在,是建立海報方法。簡化了一下,只有一張圖片(剛才的截圖)和文字。其他的,根據需求自己計算就好。

 	/**
     * 建立海報
     */
    private void createPoster(Activity activity, Bitmap screenShot) {

        try {

            if (screenShot == null || screenShot.getByteCount() == 0) {
                Toast.makeText(MainActivity.this, "圖片生成失敗", Toast.LENGTH_SHORT).show();
                return;
            }

            int[] screenSize = getScreenSize(activity);
            int sWidth = screenSize[0];
            int sHeight = screenSize[1];

            if (sWidth == 0 || sHeight == 0) {
                Toast.makeText(MainActivity.this, "圖片生成失敗", Toast.LENGTH_SHORT).show();
                return;
            }

            //初始化一個空白的bitmap,這個bitmap和螢幕一樣大
            //這裡我說明一下,我們UI給的海報,是展示在手機上的,內容展示在海報中間。我就認為,海報和螢幕一樣大。
            Bitmap bg = Bitmap.createBitmap(sWidth, sHeight, Bitmap.Config.ARGB_8888);
            //初始化一個畫布,並制定畫布背景色
            Canvas canvas = new Canvas(bg);
            canvas.drawColor(0xfff6f6f6);

            /**
             * 要繪製的圖片,是截圖圖片中的哪一部分?
             * 因為我們要把截圖全部繪製到海報上,所以,我們需要如下指定
             */
            Rect shot_rt = new Rect(0, 0, screenShot.getWidth(), screenShot.getHeight());

            // 指定圖片在螢幕(海報)上顯示的區域(位置)
            //引數:左上右下。
            //以下計算數值,按照原型圖來的。原型圖的寬高是:357dp*667dp
            //第一步得到截圖,在海報中要展示的高度
            int showPicH = (int) (sHeight * 450 / 667);
            //根據bitmap的寬高比和上一步得到的高度,計算出截圖在海報中要展示的寬度
            int showPicW = (int) (screenShot.getWidth() * showPicH / screenShot.getHeight());
            //這個矩形,就是海報中展示截圖圖片的區域
            Rect shot_dst = new Rect((sWidth - showPicW) / 2, (int) (sHeight * 42 / 667), sWidth - (sWidth - showPicW) / 2, (int) (sHeight * (42 + 450) / 667));

            //把截圖bitmap:screenShot,中的shot_rt部分,繪製到畫布的shot_dst區域上
            canvas.drawBitmap(screenShot, shot_rt, shot_dst, null);

            //初始化畫筆
            TextPaint textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
            textPaint.setTextAlign(Paint.Align.CENTER);
            textPaint.setColor(Color.RED);
            //文字上坡度
            float ascent;
            //文字下坡度
            float descent;
            //偏移量
            float textOffset;

            //繪製“海報”二字
            textPaint.setTextSize(50);
            textPaint.setColor(0xff333333);
            ascent = textPaint.ascent();
            descent = textPaint.descent();
            textOffset = (ascent + descent) / 2;

            /**
             * 繪製文字
             * 說明:
             * 座標系的方向是:向下,為Y軸正方向。
             * 所以descent為正數,ascent為負數。(descent - ascent) / 2,就是整個文字的高度的一半,正數,單位是畫素。
             * 要想文字座標的Y軸在目標位子,需要減去textOffset(偏移量)
             */
            canvas.drawText("海報", sWidth / 2, (sHeight * (667-70) / 667) - (descent - ascent) / 2 - textOffset, textPaint);


            Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
            paint.setColor(Color.BLACK);
            paint.setStrokeWidth(3);
            paint.setStyle(Paint.Style.STROKE);

            //在文字目標位置,繪製一條線,用於輔助,觀察文字是否在目標位置居中
            canvas.drawLine(0, (sHeight * (667-70) / 667) - (descent - ascent) / 2, sWidth, (sHeight * (667-70) / 667) - (descent - ascent) / 2, paint);

            //最後,把海報bitmap存到本地
            FileOutputStream os = new FileOutputStream(savePicPath + "poster.jpg");
            ByteArrayOutputStream baos = new ByteArrayOutputStream();

            bg.compress(Bitmap.CompressFormat.JPEG, 100, baos);
            byte[] pictureByte = baos.toByteArray();

            os.write(pictureByte, 0, pictureByte.length); // 寫入檔案
            os.close(); // 關閉檔案輸出流
            bg.recycle();


        } catch (Exception e) {
            e.printStackTrace();
        }

    }

最後,讓我們看一下生成的海報:
在這裡插入圖片描述

需求完成!!!

最後,給個建議:如果海報中的東西越多,涉及到的計算會越多,生成速度也會越慢。開個子執行緒去生成,最後回撥一下。