玩轉Android Camera開發(四):預覽介面四周暗中間亮,只拍攝矩形區域圖片(附完整原始碼)

yanzi1225627發表於2014-06-26

雜家前文曾寫過一篇關於只拍攝特定區域圖片的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);

可以看到單純的擷取是不改變影象解析度的,注意真正的解析度的概念並不等於xxx * xxx,圖片放的越大越不清楚。稍後推出矩形區域可以移動、且可拉伸的,拍攝任意位置的特定區域圖片demo。
-------------------------------本文系原創,轉載請註明作者:yanzi1225627
程式碼下載連結:






相關文章