雜家前文曾寫過一篇關於僅僅拍攝特定區域圖片的demo。僅僅是比較簡陋。在坐標的換算上不是非常嚴謹,並且沒有完畢預覽界面四周暗中間亮的效果,深以為憾。今天把這個補齊了。
在上代碼之前首先交代下,這里面存在着換算的兩種模式。第一種,是以屏幕上的矩形區域為基准進行換算。舉個樣例。屏幕中間一個 矩形框為100dip*100dip.這里一定要使用dip為單位,否則在不同的手機上屏幕呈現的矩形框大小不一樣。
先將這個dip換算成px。然后依據屏幕的寬和高的像素計算出矩形區域,傳給Surfaceview上鋪的一層View,這里叫MaskView(蒙板),讓MaskView進行繪制。然后拍照時。通過屏幕矩形框的大小和屏幕的大小與終於拍攝圖片的PictureSize進行換算。得到圖片里的矩形區域圖片,然后截取保存。另外一種模式是,預先知道想要的圖片的長寬,如我就是想截400*400(單位為px)大小的圖片。
那就以此為基准,換算出屏幕上呈現的Rect的長寬,然后讓MaskView繪制。
到底用哪一種模式,按需選擇。本文以第一種模式演示樣例。以下上代碼:
在雜家的前文基礎上進行封裝。首先封裝一個MaskView,用來繪制四周暗中間亮的效果,或者你能夠加一個滾動欄。這都不是事。
一、MaskView.java
package org.yanzi.ui;
import org.yanzi.util.DisplayUtil;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Point;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.Log;
import android.widget.ImageView;
public class MaskView extends ImageView {
private static final String TAG = "YanZi";
private Paint mLinePaint;
private Paint mAreaPaint;
private Rect mCenterRect = null;
private Context mContext;
public MaskView(Context context, AttributeSet attrs) {
super(context, attrs);
// TODO Auto-generated constructor stub
initPaint();
mContext = context;
Point p = DisplayUtil.getScreenMetrics(mContext);
widthScreen = p.x;
heightScreen = p.y;
}
private void initPaint(){
//繪制中間透明區域矩形邊界的Paint
mLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mLinePaint.setColor(Color.BLUE);
mLinePaint.setStyle(Style.STROKE);
mLinePaint.setStrokeWidth(5f);
mLinePaint.setAlpha(30);
//繪制四周陰影區域
mAreaPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mAreaPaint.setColor(Color.GRAY);
mAreaPaint.setStyle(Style.FILL);
mAreaPaint.setAlpha(180);
}
public void setCenterRect(Rect r){
Log.i(TAG, "setCenterRect...");
this.mCenterRect = r;
postInvalidate();
}
public void clearCenterRect(Rect r){
this.mCenterRect = null;
}
int widthScreen, heightScreen;
@Override
protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub
Log.i(TAG, "onDraw...");
if(mCenterRect == null)
return;
//繪制四周陰影區域
canvas.drawRect(0, 0, widthScreen, mCenterRect.top, mAreaPaint);
canvas.drawRect(0, mCenterRect.bottom + 1, widthScreen, heightScreen, mAreaPaint);
canvas.drawRect(0, mCenterRect.top, mCenterRect.left - 1, mCenterRect.bottom + 1, mAreaPaint);
canvas.drawRect(mCenterRect.right + 1, mCenterRect.top, widthScreen, mCenterRect.bottom + 1, mAreaPaint);
//繪制目標透明區域
canvas.drawRect(mCenterRect, mLinePaint);
super.onDraw(canvas);
}
}
說明例如以下:
1、為了讓這個MaskView有更好的適配型,里面設置變量mCenterRect,這個矩陣的坐標就是已經換算好的。對屏幕的尺寸進行適配過的,以全屏下的屏幕寬高為坐標系,不須要再換算了。
2、當然這個MaskView是全屏的,這里改動下PlayCamera_V1.0.0中的一個小問題,我將它的布局換成例如以下:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".CameraActivity" >
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent" >
<org.yanzi.camera.preview.CameraSurfaceView
android:id="@+id/camera_surfaceview"
android:layout_width="0dip"
android:layout_height="0dip" />
<org.yanzi.ui.MaskView
android:id="@+id/view_mask"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
<ImageButton
android:id="@+id/btn_shutter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:layout_marginBottom="10dip"
android:background="@drawable/btn_shutter_background" />
</RelativeLayout>
更改的地方是讓FrameLayout直接全屏。不要設置成wrap_content,假設設它為wrap。代碼里調整Surfaceview的大小,而MaskView設為wrap的話,它會覺得MaskView的長寬也是0.另外,讓Framelayout全屏。在日后16:9和4:3切換時,能夠通過設置Surfaceview的margin來調整預覽布局的大小,所以預覽的母布局FrameLayout必須全屏。
3.關於繪制陰影區域的代碼里的+1 -1這幾個小地方盡量不要錯,按本文寫就不會錯。順序是先繪制最上面、最以下、左側、右側四個區域的陰影。
//繪制四周陰影區域
canvas.drawRect(0, 0, widthScreen, mCenterRect.top, mAreaPaint);
canvas.drawRect(0, mCenterRect.bottom + 1, widthScreen, heightScreen, mAreaPaint);
canvas.drawRect(0, mCenterRect.top, mCenterRect.left - 1, mCenterRect.bottom + 1, mAreaPaint);
canvas.drawRect(mCenterRect.right + 1, mCenterRect.top, widthScreen, mCenterRect.bottom + 1, mAreaPaint);
二、在CameraActivity.java里封裝兩個函數:
/**生成拍照后圖片的中間矩形的寬度和高度
* @param w 屏幕上的矩形寬度,單位px
* @param h 屏幕上的矩形高度。單位px
* @return
*/
private Point createCenterPictureRect(int w, int h){
int wScreen = DisplayUtil.getScreenMetrics(this).x;
int hScreen = DisplayUtil.getScreenMetrics(this).y;
int wSavePicture = CameraInterface.getInstance().doGetPrictureSize().y; //由於圖片旋轉了,所以此處寬高換位
int hSavePicture = CameraInterface.getInstance().doGetPrictureSize().x; //由於圖片旋轉了。所以此處寬高換位
float wRate = (float)(wSavePicture) / (float)(wScreen);
float hRate = (float)(hSavePicture) / (float)(hScreen);
float rate = (wRate <= hRate) ? wRate : hRate;//也能夠依照最小比率計算
int wRectPicture = (int)( w * wRate);
int hRectPicture = (int)( h * hRate);
return new Point(wRectPicture, hRectPicture);
}
/**
* 生成屏幕中間的矩形
* @param w 目標矩形的寬度,單位px
* @param h 目標矩形的高度,單位px
* @return
*/
private Rect createCenterScreenRect(int w, int h){
int x1 = DisplayUtil.getScreenMetrics(this).x / 2 - w / 2;
int y1 = DisplayUtil.getScreenMetrics(this).y / 2 - h / 2;
int x2 = x1 + w;
int y2 = y1 + h;
return new Rect(x1, y1, x2, y2);
}
各自是生成圖片的中間矩形的寬和高組成的一個Point,生成屏幕中間的矩形區域。兩個函數的輸入參數都是px為單位的屏幕中間矩形的寬和高。這里有個條件:矩形以屏幕中心為中心,否則的話計算公式要適當變換下。
三、在開啟預覽后,就能夠讓MaskView繪制了
@Override
public void cameraHasOpened() {
// TODO Auto-generated method stub
SurfaceHolder holder = surfaceView.getSurfaceHolder();
CameraInterface.getInstance().doStartPreview(holder, previewRate);
if(maskView != null){
Rect screenCenterRect = createCenterScreenRect(DisplayUtil.dip2px(this, DST_CENTER_RECT_WIDTH)
,DisplayUtil.dip2px(this, DST_CENTER_RECT_HEIGHT));
maskView.setCenterRect(screenCenterRect);
}
} 這里有個注意事項:由於camera.open的時候是放在一個單獨線程里的。open之后進行回調到cameraHasOpened()這里,那這個函數的運行時在主線程和子線程?答案也是在子線程,即子線程的回調還是在子線程里運行。正因此。在封裝MaskView時set矩陣后用的是postInvalidate()進行刷新的。
public void setCenterRect(Rect r){
Log.i(TAG, "setCenterRect...");
this.mCenterRect = r;
postInvalidate();
}四、最后就是告訴拍照的回調了
private class BtnListeners implements OnClickListener{
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
switch(v.getId()){
case R.id.btn_shutter:
if(rectPictureSize == null){
rectPictureSize = createCenterPictureRect(DisplayUtil.dip2px(CameraActivity.this, DST_CENTER_RECT_WIDTH)
,DisplayUtil.dip2px(CameraActivity.this, DST_CENTER_RECT_HEIGHT));
}
CameraInterface.getInstance().doTakePicture(rectPictureSize.x, rectPictureSize.y);
break;
default:break;
}
}
}上面是拍照的監聽,在CameraInterface里重寫一個doTakePicture函數: int DST_RECT_WIDTH, DST_RECT_HEIGHT;
public void doTakePicture(int w, int h){
if(isPreviewing && (mCamera != null)){
Log.i(TAG, "矩形拍照尺寸:width = " + w + " h = " + h);
DST_RECT_WIDTH = w;
DST_RECT_HEIGHT = h;
mCamera.takePicture(mShutterCallback, null, mRectJpegPictureCallback);
}
}這里出來個mRectJpegPictureCallback,它相應的類:/**
* 拍攝指定區域的Rect
*/
PictureCallback mRectJpegPictureCallback = new PictureCallback()
//對jpeg圖像數據的回調,最重要的一個回調
{
public void onPictureTaken(byte[] data, Camera camera) {
// TODO Auto-generated method stub
Log.i(TAG, "myJpegCallback:onPictureTaken...");
Bitmap b = null;
if(null != data){
b = BitmapFactory.decodeByteArray(data, 0, data.length);//data是字節數據,將其解析成位圖
mCamera.stopPreview();
isPreviewing = false;
}
//保存圖片到sdcard
if(null != b)
{
//設置FOCUS_MODE_CONTINUOUS_VIDEO)之后,myParam.set("rotation", 90)失效。 //圖片居然不能旋轉了,故這里要旋轉下 Bitmap rotaBitmap = ImageUtil.getRotateBitmap(b, 90.0f); int x = rotaBitmap.getWidth()/2 - DST_RECT_WIDTH/2; int y = rotaBitmap.getHeight()/2 - DST_RECT_HEIGHT/2; Log.i(TAG, "rotaBitmap.getWidth() = " + rotaBitmap.getWidth() + " rotaBitmap.getHeight() = " + rotaBitmap.getHeight()); Bitmap rectBitmap = Bitmap.createBitmap(rotaBitmap, x, y, DST_RECT_WIDTH, DST_RECT_HEIGHT); FileUtil.saveBitmap(rectBitmap); if(rotaBitmap.isRecycled()){ rotaBitmap.recycle(); rotaBitmap = null; } if(rectBitmap.isRecycled()){ rectBitmap.recycle(); rectBitmap = null; } } //再次進入預覽 mCamera.startPreview(); isPreviewing = true; if(!b.isRecycled()){ b.recycle(); b = null; } } };
注意事項:
1、為了讓截出的區域和屏幕上顯示的全然一致,這里首先要滿足PreviewSize長寬比、PictureSize長寬比、屏幕預覽Surfaceview的長寬比為同一比例,這是個先決條件。然后再將屏幕矩形區域長寬換算成圖片矩形區域時:
/**生成拍照后圖片的中間矩形的寬度和高度
* @param w 屏幕上的矩形寬度,單位px
* @param h 屏幕上的矩形高度,單位px
* @return
*/
private Point createCenterPictureRect(int w, int h){
int wScreen = DisplayUtil.getScreenMetrics(this).x;
int hScreen = DisplayUtil.getScreenMetrics(this).y;
int wSavePicture = CameraInterface.getInstance().doGetPrictureSize().y; //由於圖片旋轉了,所以此處寬高換位
int hSavePicture = CameraInterface.getInstance().doGetPrictureSize().x; //由於圖片旋轉了。所以此處寬高換位
float wRate = (float)(wSavePicture) / (float)(wScreen);
float hRate = (float)(hSavePicture) / (float)(hScreen);
float rate = (wRate <= hRate) ?
wRate : hRate;//也能夠依照最小比率計算
int wRectPicture = (int)( w * wRate);
int hRectPicture = (int)( h * hRate);
return new Point(wRectPicture, hRectPicture);
}
原則上wRate 是應該等於hRate 的。。!!
。!!!
!!
2、我對CamParaUtil里的getPropPreviewSize和getPropPictureSize進行了更新。曾經是以width進行推斷的,這里改成了以height進行推斷。
由於在讀取參數時得到的是800*480(寬*高)這樣的類型,一般高是略微小的,所以以height進行推斷。而這個高在終於顯示和保存時經過旋轉又成了寬。
public Size getPropPictureSize(List<Camera.Size> list, float th, int minHeight){
Collections.sort(list, sizeComparator);
int i = 0;
for(Size s:list){
if((s.height >= minHeight) && equalRate(s, th)){
Log.i(TAG, "PictureSize : w = " + s.width + "h = " + s.height);
break;
}
i++;
}
if(i == list.size()){
i = 0;//假設沒找到,就選最小的size
}
return list.get(i);
}最后來看下效果吧。我設定屏幕上顯示的矩形尺寸為200dip*200dip, Camera預覽的參數是以屏幕的比例進行自己主動尋找,預覽尺寸的height不小於400,PictureSize的height不小於1300. //設置PreviewSize和PictureSize
Size pictureSize = CamParaUtil.getInstance().getPropPictureSize(
mParams.getSupportedPictureSizes(),previewRate, 1300);
mParams.setPictureSize(pictureSize.width, pictureSize.height);
Size previewSize = CamParaUtil.getInstance().getPropPreviewSize(
mParams.getSupportedPreviewSizes(), previewRate, 400);
mParams.setPreviewSize(previewSize.width, previewSize.height);
