Android双SurfaceView实现底部拍照,顶部绘图

本文介绍如何使用双SurfaceView布局实现Android应用中同时进行拍照和在界面上绘制图形的功能,通过自定义SurfaceView类来控制绘制区域的透明度和位置,以及通过Camera API实现在拍照的同时在另一SurfaceView上进行实时绘制。此解决方案适用于需要在拍照界面同时提供绘制功能的应用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

当SurfaceHolder对象的类型设置为SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS时就只能拍照不能绘制了。
为了既能通过SurfaceView拍照又能在上面绘制图形,可以通过双SurfaceView层叠的变通方式如下:
用于绘制的SurfaceView,使其透明并位于顶部:

package com.test;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.Paint.Style;
import android.util.AttributeSet;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

public class SVDraw extends SurfaceView implements SurfaceHolder.Callback {

 private Bitmap bmp;
 private String imgPath = "";
 protected SurfaceHolder sh; // 专门用于控制surfaceView的
 private int width;
 private int height;

 // XML文件解析需要调用View的构造函数View(Context , AttributeSet)
 // 因此自定义SurfaceView中也需要该构造函数
 public SVDraw(Context context, AttributeSet attrs) {
  super(context, attrs);
  // TODO Auto-generated constructor stub
  sh = getHolder();
  sh.addCallback(this);
  sh.setFormat(PixelFormat.TRANSPARENT); // 设置为透明
  setZOrderOnTop(true);// 设置为顶端
 }

 @Override
 public void surfaceChanged(SurfaceHolder arg0, int arg1, int w, int h) {
  // TODO Auto-generated method stub
  width = w;
  height = h;
 }

 @Override
 public void surfaceCreated(SurfaceHolder arg0) {
  // TODO Auto-generated method stub

 }

 @Override
 public void surfaceDestroyed(SurfaceHolder arg0) {
  // TODO Auto-generated method stub

 }

 void clearDraw() {

  Canvas canvas = sh.lockCanvas();
  canvas.drawColor(Color.BLUE);// 清除画布
  sh.unlockCanvasAndPost(canvas);
 }

 /**
  * 绘制
  */
 public void doDraw() {
  if (bmp != null) {
   Canvas canvas = sh.lockCanvas();
   canvas.drawColor(Color.TRANSPARENT);// 这里是绘制背景
   Paint p = new Paint(); // 笔触
   p.setAntiAlias(true); // 反锯齿
   p.setColor(Color.RED);
   p.setStyle(Style.STROKE);
   canvas.drawBitmap(bmp, 0, 0, p);
   canvas.drawLine(width / 2 - 100, 0, width / 2 - 100, height, p);
   canvas.drawLine(width / 2 + 100, 0, width / 2 + 100, height, p);
   // ------------------------ 画边框---------------------
   Rect rec = canvas.getClipBounds();
   rec.bottom--;
   rec.right--;
   p.setColor(Color.GRAY); // 颜色
   p.setStrokeWidth(5);
   canvas.drawRect(rec, p);
   // 提交绘制
   sh.unlockCanvasAndPost(canvas);
  }

 }

 public void drawLine() {

  Canvas canvas = sh.lockCanvas();

  canvas.drawColor(Color.TRANSPARENT);// 这里是绘制背景
  Paint p = new Paint(); // 笔触
  p.setAntiAlias(true); // 反锯齿
  p.setColor(Color.RED);
  p.setStyle(Style.STROKE);
  canvas.drawLine(width / 2 - 100, 0, width / 2 - 100, height, p);
  canvas.drawLine(width / 2 + 100, 0, width / 2 + 100, height, p);

  // 提交绘制
  sh.unlockCanvasAndPost(canvas);
 }

 public String getImgPath() {
  return imgPath;
 }

 public void setImgPath(String imgPath) {
  this.imgPath = imgPath;
  // 根据路径载入目标图像
  bmp = BitmapFactory.decodeFile(imgPath);
 }

}

用于在SurfaceView(使其位于绘制SurfaceView底部)上拍照及预览的Activity:

package com.test;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Iterator;
import java.util.List;

import android.app.Activity;
import android.content.ContentValues;
import android.content.pm.ActivityInfo;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Typeface;

import android.graphics.PixelFormat;
import android.hardware.Camera;
import android.hardware.Camera.PictureCallback;
import android.hardware.Camera.ShutterCallback;
import android.hardware.Camera.Size;

import android.media.AudioManager;
import android.media.ToneGenerator;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.provider.MediaStore;
import android.provider.SyncStateContract.Constants;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.Button;

public class SurfaceViewDraw extends Activity implements
  SurfaceHolder.Callback, Camera.PictureCallback {
 /** Called when the activity is first created. */
 private SVDraw svDraw = null;
 private SurfaceView svCamera = null;
 protected SurfaceHolder mSurfaceHolder;

 private Button btnClear;
 private Button btnOpen;
 private Button btnClose;
 private Button btnTakePic;
 private Button btnDraw;

 private Camera mCamera; // 这个是hardware的Camera对象
 private boolean isOpen = false;// 相机是否打开
 private ToneGenerator tone;
 private String imgPath;

 private int width;
 private int height;

 @Override
 public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  hideStatusBar();
  setContentView(R.layout.main);
  setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);// 强制为横屏

  svDraw = (com.test.SVDraw) findViewById(R.id.svDraw);
  svCamera = (SurfaceView) findViewById(R.id.svCamera);

  btnClear = (Button) findViewById(R.id.btnClear);
  btnOpen = (Button) findViewById(R.id.btnOpen);
  btnClose = (Button) findViewById(R.id.btnClose);
  btnTakePic = (Button) findViewById(R.id.btnTakePic);
  btnDraw = (Button) findViewById(R.id.btnDraw);

  btnClear.setOnClickListener(new ClickEvent());
  btnOpen.setOnClickListener(new ClickEvent());
  btnClose.setOnClickListener(new ClickEvent());
  btnTakePic.setOnClickListener(new ClickEvent());
  btnDraw.setOnClickListener(new ClickEvent());

  mSurfaceHolder = svCamera.getHolder();
  mSurfaceHolder.addCallback(this);
  // 当设置为SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS后就不能绘图了
  mSurfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);

 }

 class ClickEvent implements View.OnClickListener {

  @Override
  public void onClick(View v) {
   // TODO Auto-generated method stub
   if (v == btnClear) {

    stopPreview(); // 停止预览后清屏速度会快一点
    svDraw.setVisibility(View.INVISIBLE);
    startPreview();// 清屏后启动预览

   } else if (v == btnOpen) {

    initCamera();

   } else if (v == btnClose) {

    closeCamera();

   } else if (v == btnTakePic) {
    if (isOpen) {
     startPreview();// 防止异常
     mCamera.takePicture(mShutterCallback, null, null,
       mjpegCallback);
     svDraw.setVisibility(View.VISIBLE);
     svDraw.drawLine();// 拍照后绘制测线
    }
   } else if (v == btnDraw) {

    svDraw.setVisibility(View.VISIBLE);
    svDraw.doDraw();
   }
  }

 }

 ShutterCallback mShutterCallback = new ShutterCallback() {
  @Override
  public void onShutter() {
   // TODO Auto-generated method stub
   if (tone == null)
    // 发出提示用户的声音
    tone = new ToneGenerator(AudioManager.STREAM_MUSIC,
      ToneGenerator.MAX_VOLUME);
   tone.startTone(ToneGenerator.TONE_PROP_BEEP);

  }
 };
 /**
  * Jpeg格式压缩
  */
 PictureCallback mjpegCallback = new PictureCallback() {
  @Override
  // 取得拍照图片
  public void onPictureTaken(byte[] data, Camera camera) {
   // TODO Auto-generated method stub
   // 拍照前关闭预览
   mCamera.stopPreview();
   // 取得图像路径
   imgPath = saveFile2(data);
   svDraw.setImgPath(imgPath);

  }

 };

 /**
  * draw information on the picture
  *
  * @param imgPath
  */
 public void drawInfo(String imgPath) {

  Bitmap bmp = BitmapFactory.decodeFile(imgPath);

  if (bmp != null) {
   Bitmap drawBmp = Bitmap.createBitmap(640, 480, Config.ARGB_8888);
   Canvas c = new Canvas(drawBmp);
   Paint p = new Paint();
   c.drawBitmap(bmp, 0, 0, p);
   String familyName = "Arial";
   Typeface font = Typeface.create(familyName, Typeface.NORMAL);
   p.setColor(Color.RED);
   p.setTypeface(font);
   p.setTextSize(20);
   p.setStyle(Paint.Style.STROKE);
   SimpleDateFormat dateFormat = new SimpleDateFormat(
     "yyyy-MM-dd hh:mm:ss");
   String strDate = dateFormat.format(new Date());
   c.drawText(strDate, 10, 30, p);

   try {
    saveBmp(drawBmp, imgPath);
   } catch (IOException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
   }
  }

 }

 /**
  * save bmp as jpg by path
  *
  * @param bmpPath
  * @param bmp
  * @throws IOException
  */
 public void saveBmp(Bitmap bmp, String fileName) throws IOException {

  File f = new File(fileName);
  f.createNewFile();
  FileOutputStream fOut = null;
  try {
   fOut = new FileOutputStream(f);
  } catch (FileNotFoundException e) {
   e.printStackTrace();
  }

  bmp.compress(Bitmap.CompressFormat.JPEG, 100, fOut);
  try {
   fOut.flush();
  } catch (IOException e) {
   e.printStackTrace();
  }
  try {
   fOut.close();
  } catch (IOException e) {
   e.printStackTrace();
  }
 }

 /**
  * return imgFilePath
  *
  * @param data
  * @return
  */
 private String saveFile2(byte[] data) {
  File imgFileDir = getDir();
  if (!imgFileDir.exists() && !imgFileDir.mkdirs()) {
   Log.v("directory", "Can't create directory to save image.");
   return null;
  }
  // 图像名称
  SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddhhmmss");
  String strDate = dateFormat.format(new Date());
  String imgFileName = "img_" + strDate + ".jpg";
  // 图像路径
  String imgFilePath = imgFileDir.getPath() + File.separator
    + imgFileName;
  File imgFile = new File(imgFilePath);
  try {
   FileOutputStream fos = new FileOutputStream(imgFile);
   fos.write(data);
   fos.close();
   Log.v("directory", "New Image saved:" + imgFile);

  } catch (Exception error) {
   Log.d(Constants.ACCOUNT_NAME,
     imgFileName + " not saved: " + error.getMessage());

  }
  //绘制拍照日期等
  drawInfo(imgFilePath);
        return imgFilePath;
 }

 /**
  *
  * @return
  */
 private File getDir() {
  File sdDir = Environment
    .getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
  // 创建图像需要保存的文件夹
  return new File(sdDir, "Photo");

 }

 @Override
 public void onPictureTaken(byte[] data, Camera camera) {
  // TODO Auto-generated method stub
  // data是一个原始的JPEG图像数据,
  // 在这里我们可以存储图片,很显然可以采用MediaStore
  // 注意保存图片后,再次调用stopPreview()停止预览,等待测量
  Uri imageUri = this.getContentResolver().insert(
    MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
    new ContentValues());
  try {
   OutputStream os = this.getContentResolver().openOutputStream(
     imageUri);
   os.write(data);
   os.flush();
   os.close();
  } catch (Exception e) {
   // TODO: handle exception
   e.printStackTrace();
  }
  // 拍照后停止预览
  mCamera.stopPreview();
 }

 @Override
 public void surfaceCreated(SurfaceHolder holder) {
  // TODO Auto-generated method stub

 }

 @Override
 public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
  // TODO Auto-generated method stub
  width = w;
  height = h;
 }

 @Override
 public void surfaceDestroyed(SurfaceHolder holder) {
  // TODO Auto-generated method stub

 }

 /**
  * 关闭相机
  */
 public void closeCamera() {
  if (isOpen) {
   mCamera.stopPreview();
   mCamera.release();
   mCamera = null;
   isOpen = false;
  }
 }

 /**
  * 停止拍照预览
  */
 public void stopPreview() {
  if (isOpen) {
   mCamera.stopPreview();
  }
 }

 /**
  * 启动拍照预览
  */
 public void startPreview() {
  if (isOpen) {
   mCamera.startPreview();
  }
 }

 /**
  * 初始化相机
  */
 public void initCamera() {
  if (!isOpen) {
   mCamera = Camera.open();
  }
  if (mCamera != null && !isOpen) {
   try {
    Camera.Parameters mParameters = mCamera.getParameters();
    mParameters.setPictureFormat(PixelFormat.JPEG); // 设置照片格式
    List<Size> sizes = mParameters.getSupportedPreviewSizes();
    Size optimalSize = getOptimalPreviewSize(sizes, width, height);
    mParameters.setPreviewSize(optimalSize.width,
      optimalSize.height); // 大小
    mParameters.setPictureSize(optimalSize.width,
      optimalSize.height);

    mParameters.set("jpeg-quality", 100);// 照片质量
    // 首先获取系统设备支持的所有颜色特效,有复合我们的,则设置;否则不设置
    List<String> colorEffects = mParameters
      .getSupportedColorEffects();
    Iterator<String> colorItor = colorEffects.iterator();
    while (colorItor.hasNext()) {
     String currColor = colorItor.next();
     if (currColor.equals(Camera.Parameters.EFFECT_SOLARIZE)) {
      mParameters
        .setColorEffect(Camera.Parameters.EFFECT_SOLARIZE);
      break;
     }
    }
    mCamera.setParameters(mParameters);
    mCamera.setPreviewDisplay(mSurfaceHolder);
    mCamera.startPreview();
   } catch (IOException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
   }
   isOpen = true;
  }
 }

 private Size getOptimalPreviewSize(List<Size> sizes, int w, int h) {
  final double ASPECT_TOLERANCE = 0.05;
  double targetRatio = (double) w / h;
  if (sizes == null)
   return null;

  Size optimalSize = null;
  double minDiff = Double.MAX_VALUE;

  int targetHeight = h;

  // Try to find an size match aspect ratio and size
  for (Size size : sizes) {
   double ratio = (double) size.width / size.height;
   if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE)
    continue;
   if (Math.abs(size.height - targetHeight) < minDiff) {
    optimalSize = size;
    minDiff = Math.abs(size.height - targetHeight);
   }
  }

  // Cannot find the one match the aspect ratio, ignore the requirement
  if (optimalSize == null) {
   minDiff = Double.MAX_VALUE;
   for (Size size : sizes) {
    if (Math.abs(size.height - targetHeight) < minDiff) {
     optimalSize = size;
     minDiff = Math.abs(size.height - targetHeight);
    }
   }
  }
  return optimalSize;
 }

 @Override
 protected void onDestroy() {
  // TODO Auto-generated method stub
  super.onDestroy();
  if (isOpen) {
   closeCamera();
  }
 }

 // 在 Activity.setCurrentView()之前调用
 public void hideStatusBar() {
  // 隐藏标题
  requestWindowFeature(Window.FEATURE_NO_TITLE);
  // 定义全屏参数
  int flag = WindowManager.LayoutParams.FLAG_FULLSCREEN;
  // 获得窗口对象
  Window curWindow = this.getWindow();
  // 设置Flag标示
  curWindow.setFlags(flag, flag);
 }
}

主界面main.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="horizontal" >
   <!-- 对于自定义控件要指明的控件的包名与空间名,系统自带的控件不需要指定包名 -->
   <FrameLayout
       android:layout_width="640dip"
       android:layout_height="480dip"
       android:orientation="vertical" >
    <SurfaceView
      android:id="@+id/svCamera"
      android:layout_width="fill_parent"
      android:layout_height="fill_parent"/>
   
    <com.test.SVDraw
     android:id="@+id/svDraw"
     android:layout_width="fill_parent"
     android:layout_height="fill_parent"/>
   
   </FrameLayout>
   
    <LinearLayout
      android:id="@+id/LinearLayout01"
      android:layout_width="158dip"
      android:layout_height="fill_parent"
      android:layout_marginLeft="1dip"
      android:layout_marginRight="1dip"
      android:orientation="vertical"
      android:background="@drawable/main_right_bg">
     <Button
    android:id="@+id/btnOpen"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:layout_marginTop="10dip"
    android:text="打开相机"/>
      <Button
    android:id="@+id/btnClose"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:text="关闭相机"/>
      <Button
         android:id="@+id/btnTakePic"
   android:layout_width="fill_parent"
   android:layout_height="wrap_content"
   android:text="拍照"   />
     <Button
   android:id="@+id/btnClear"
   android:layout_width="fill_parent"
   android:layout_height="wrap_content"
   android:text="预览"/>  
     <Button
   android:id="@+id/btnDraw"
   android:layout_width="fill_parent"
   android:layout_height="wrap_content"
   android:text="绘制"/>     
 
    </LinearLayout>
</LinearLayout>

在res下新建文件夹drawable,并在其下面新建面板背景main_right_bg.xml:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
  <gradient
      android:startColor = "#666666"
      android:centerColor="#000FFF"
      android:endColor = "#666666" 
      android:angle = "270"/>
  <corners
      android:radius="4dip"/>
 
</shape>

AndroidManifest.xml:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.test"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk android:minSdkVersion="8" />
   
<!-- 照相机权限 -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
<!-- 在SDCard中创建与删除文件权限 -->
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>
<!-- 往SDCard写入数据权限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        <activity
            android:name=".SurfaceViewDraw"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

开发环境:XP3+Eclipse+Android2.2+JDK6.0
测试环境:Android2.2,5寸屏,分辨率640X480
源代码:http://download.youkuaiyun.com/detail/xinzheng_wang/4409755

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值