[转载]玩转Android Camera开发(四):预览界面四周暗中间亮,只拍摄矩形区域图片(附完整源码)
杂家前文曾写过一篇关于只拍摄特定区域图片的demo,只是比较简陋,在坐标的换算上不是很严谨,而且没有完成预览界面四周暗中间亮的效果,深以为憾,今天把这个补齐了。
在 上代码之前首先交代下,这里面存在着换算的两种模式。第一种,是以屏幕上的矩形区域为基准进行换算。举个例子,屏幕中间一个 矩形框为100dip*100dip.这里一定要使用dip为单位,否则在不同的手机上屏幕呈现的矩形框大小不一样。先将这个dip换算成px,然后根据 屏幕的宽和高的像素计算出矩形区域,传给Surfaceview上铺的一层View,这里叫MaskView(蒙板),让MaskView进行绘制。然后 拍照时,通过屏幕矩形框的大小和屏幕的大小与最终拍摄图片的PictureSize进行换算,得到图片里的矩形区域图片,然后截取保存。第二种模式是,预 先知道想要的图片的长宽,如我就是想截400*400(单位为px)大小的图片。那就以此为基准,换算出屏幕上呈现的Rect的长宽,然后让 MaskView绘制。究竟用哪一种模式,按需选择。本文以第一种模式示例。下面上代码:
在杂家的前文基础上进行封装,首先封装一个MaskView,用来绘制四周暗中间亮的效果,或者你可以加一个滚动条,这都不是事。
一、MaskView.java
- <span style="font-family:Comic Sans MS;font-size:18px;">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);
- }
- }
- </span>
说明如下:
1、为了让这个MaskView有更好的适配型,里面设置变量mCenterRect,这个矩阵的坐标就是已经换算好的,对屏幕的尺寸进行适配过的,以全屏下的屏幕宽高为坐标系,不需要再换算了。
2、当然这个MaskView是全屏的,这里修改下PlayCamera_V1.0.0中的一个小问题,我将它的布局换成如下:
- <span style="font-family:Comic Sans MS;font-size:18px;"><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></span>
更 改的地方是让FrameLayout直接全屏,不要设置成wrap_content,如果设它为wrap,代码里调整Surfaceview的大小,而 MaskView设为wrap的话,它会认为MaskView的长宽也是0.另外,让Framelayout全屏,在日后16:9和4:3切换时,可以通 过设置Surfaceview的margin来调整预览布局的大小,所以预览的母布局FrameLayout必须全屏。
3.关于绘制阴影区域的代码里的+1 -1这几个小地方尽量不要错,按本文写就不会错。顺序是先绘制最上面、最下面、左侧、右侧四个区域的阴影。
- <span style="font-family:Comic Sans MS;font-size:18px;">//绘制四周阴影区域
- 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);</span>
二、在CameraActivity.java里封装两个函数:
- <span style="font-family:Comic Sans MS;font-size:18px;"> /**生成拍照后图片的中间矩形的宽度和高度
- * @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);
- }</span>
分别是生成图片的中间矩形的宽和高组成的一个Point,生成屏幕中间的矩形区域。两个函数的输入参数都是px为单位的屏幕中间矩形的宽和高。这里有个条件:矩形以屏幕中心为中心,否则的话计算公式要适当变换下。
三、在开启预览后,就可以让MaskView绘制了
- <span style="font-family:Comic Sans MS;font-size:18px;"> @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);
- }
- }</span>
这里有个注意事项:因为camera.open的时候是放在一个单独线程里的,open之后进行回调到cameraHasOpened()这里,那这个函数的执行时在主线程和子线程?答案也是在子线程,即子线程的回调还是在子线程里执行。正因此,在封装MaskView时set矩阵后用的是postInvalidate()进行刷新的。
- <span style="font-family:Comic Sans MS;font-size:18px;"> public void setCenterRect(Rect r){
- Log.i(TAG, "setCenterRect...");
- this.mCenterRect = r;
- postInvalidate();
- }</span>
四、最后就是告诉拍照的回调了
- <span style="font-family:Comic Sans MS;font-size:18px;">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;
- }
- }
- }</span>
上面是拍照的监听,在CameraInterface里重写一个doTakePicture函数:
- <span style="font-family:Comic Sans MS;font-size:18px;"> 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);
- }
- }</span>
这里出来个mRectJpegPictureCallback,它对应的类:
- <span style="font-family:Comic Sans MS;font-size:18px;">/**
- * 拍摄指定区域的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;
- }
- }
- };</span>
注意事项:
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进行判断。而 这个高在最终显示和保存时经过旋转又成了宽。
- <span style="font-family:Comic Sans MS;font-size:18px;"> 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);
- }</span>
最后来看下效果吧,我设定屏幕上显示的矩形尺寸为200dip*200dip, Camera预览的参数是以屏幕的比例进行自动寻找,预览尺寸的height不小于400,PictureSize的height不小于1300.
- <span style="font-family:Comic Sans MS;font-size:18px;"> //设置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);</span>