截圖功能實現,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();
}
}
最後,讓我們看一下生成的海報:
需求完成!!!
最後,給個建議:如果海報中的東西越多,涉及到的計算會越多,生成速度也會越慢。開個子執行緒去生成,最後回撥一下。