相信通过前两个案例的开发,大家对于Android开发相机部分已经有了一定的想法,其实起初开始进行Android相机开发的机缘是要做一个想今天要给大家演示的项目,自定义一个相机应用,这个相机应用的目的是能够像微信扫一扫的功能,只不过我当时负责的只是手机客户端这一部分的开发,整个项目还涉及到一些其他领域的算法及技术,在此不做进一步介绍。我们只来讲一讲如何实现保存预览图像其中的一部分图像,其实原理很简单,就是在上一个案例的基础上通过Bitmap类及其相关的方法来实现对图像的裁剪。
还是你们很喜欢的代码!!!
首先我们应该为程序申请相应的权限,所以在AndroidManifest.xml文件中增加如下代码:
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
然后,我们来创建拍照界面布局,这一次的布局相对于上两个案例的界面就要复杂得多,我们通过设置各个组件的透明度来区分有效区域和无效区域,可以看出我们给其中的取景区域加了一个边框,这一部分是通过编写风格化的.xml文件实现的。
/Activity/res/layout/activity_main.xml
代码清单:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/Framelayout01"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<SurfaceView
android:id="@+id/sfvCamera"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
</SurfaceView>
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical">
<ImageView
android:id="@+id/ImageView01"
android:layout_width="fill_parent"
android:layout_height="120dp"
android:background="#7f000000"/>
<View
android:id="@+id/centerView01"
android:layout_width="fill_parent"
android:layout_height="50dp"
android:background="@layout/backgroud"
/>
<TextView
android:id="@+id/txt_view"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="请让有效图像充满整个取景窗"
android:gravity="center"
android:textSize="18dp"
android:textColor="@android:color/white"
android:background="#7f000000"/>
<EditText
android:id="@+id/txtRearchResult"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:cursorVisible="false"
android:textSize="24dp"
android:textColor="@android:color/white"
android:background="#7f000000"
/>
<RelativeLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="#7f000000"
android:orientation="horizontal">
<TextView
android:id="@+id/button_recamera"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:text="重拍"
android:textColor="@android:color/white"
android:textSize="30dp"/>
<ImageButton
android:id="@+id/button_camera"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:src="@drawable/button"
android:background="#00000000"/>
<TextView
android:id="@+id/button_send"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:text="上传"
android:textColor="@android:color/white"
android:textSize="30dp"/>
</RelativeLayout>
</LinearLayout>
</FrameLayout>
取景区域加的边框方法是在layout文件夹下新建background.xml文件
/Activity/res/layout/background.xml
代码清单:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 背景色 -->
<solid android:color="@android:color/transparent"/>
<!-- 边框色 -->
<stroke android:width="0.5dip" android:color="#ffffffff"/>
</shape>
最后,依旧是最重要的MainActivity.java文件
/Activity/src/org/williamLee/activity/MainActivity.java
代码清单:
packageorg.williamLee.activity;
import java.io.BufferedOutputStream;
importjava.io.ByteArrayOutputStream;
importjava.io.File;
importjava.io.FileInputStream;
import java.io.FileOutputStream;
importjava.io.IOException;
importjava.text.SimpleDateFormat;
importjava.util.Date;
import java.util.Iterator;
importjava.util.List;
importorg.ksoap2.SoapEnvelope;
importorg.ksoap2.serialization.SoapObject;
importorg.ksoap2.serialization.SoapSerializationEnvelope;
importorg.ksoap2.transport.HttpTransportSE;
importandroid.app.Activity;
importandroid.content.pm.ActivityInfo;
importandroid.content.res.Configuration;
importandroid.graphics.Bitmap;
import android.graphics.BitmapFactory;
importandroid.graphics.Matrix;
importandroid.hardware.Camera;
import android.hardware.Camera.CameraInfo;
importandroid.hardware.Camera.PictureCallback;
importandroid.hardware.Camera.Size;
importandroid.os.Bundle;
importandroid.os.Environment;
importandroid.util.Base64;
importandroid.util.DisplayMetrics;
import org.kobjects.base64.*;
importorg.williamLee.activity.R;
importandroid.util.Log;
importandroid.view.MotionEvent;
importandroid.view.SurfaceHolder;
import android.view.SurfaceView;
importandroid.view.View;
importandroid.view.View.OnClickListener;
importandroid.view.WindowManager;
import android.widget.Button;
importandroid.widget.EditText;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.LinearLayout;
importandroid.widget.TextView;
importandroid.widget.Toast;
public classMainActivity extends Activity implementsSurfaceHolder.Callback {
//拍照预览
private SurfaceView surfaceView;//定义视图控件
private SurfaceHolder surfaceHolder;//定义控件
//照相机
private Camera mCamera;
//图片地址
private ImageButton btn_camera;
private TextView txt_view;
private TextView btn_recamera;
private TextView btn_send;
private View view;
private EditText edit_txt;
private byte[] buffer = null;
private String s;
private String Url,filename;
int[] pos=new int[2];
int surfWidth,surfHeigth;
public voidonCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//设置布局
setContentView(R.layout.activity_main);
//禁止自动锁屏
getWindow().setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON,WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LOCKED);
//从SurfaceView中获得SurfaceHolder
surfaceHolder = surfaceView.getHolder();
surfaceHolder.addCallback(this);
surfaceHolder.setType(surfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
mCamera = isCameraAvailiable();
if(mCamera==null) Toast.makeText(getApplicationContext(),"相机设备被占用", Toast.LENGTH_SHORT).show();
//设置事件监听
surfaceCreated(surfaceHolder);
ButtonOnClickListener onClickListener = newButtonOnClickListener();
btn_recamera.setOnClickListener(onClickListener);
btn_camera.setOnClickListener(onClickListener);
btn_send.setOnClickListener(onClickListener);
btn_camera.setVisibility(View.VISIBLE);
btn_recamera.setVisibility(View.INVISIBLE);
btn_send.setVisibility(View.INVISIBLE);
//绑定视图
surfaceView = (SurfaceView)findViewById(R.id.sfvCamera);
btn_camera = (ImageButton)findViewById(R.id.button_camera);
btn_send = (TextView)findViewById(R.id.button_send);
btn_recamera = (TextView)findViewById(R.id.button_recamera);
txt_view = (TextView)findViewById(R.id.txt_view);
view = findViewById(R.id.centerView01);
edit_txt=(EditText)findViewById(R.id.txtRearchResult);
//获取整个屏幕大小
DisplayMetrics dm = newDisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(dm);
surfWidth=dm.widthPixels;
surfHeigth=dm.heightPixels;
}//end of onCreate
//判断照相机设备是否可用
public static CameraisCameraAvailiable(){
Camera object = null;
try {
object = Camera.open();
}
catch(Exception e){
}
return object;
}
class ButtonOnClickListener implementsOnClickListener{
@Override
public voidonClick(View v) {
// TODOAuto-generated method stub
switch (v.getId()){
case R.id.button_camera: {
mCamera.takePicture(null, null, capturedIt);
txt_view.setText("");
btn_camera.setVisibility(View.INVISIBLE);
btn_recamera.setText("重拍");
btn_recamera.setVisibility(View.VISIBLE);
btn_send.setVisibility(View.VISIBLE);
break;}
case R.id.button_recamera:{
mCamera.startPreview();
txt_view.setText("请让有效图像充满整个取景窗");
btn_camera.setVisibility(View.VISIBLE);
btn_recamera.setVisibility(View.INVISIBLE);
btn_send.setVisibility(View.INVISIBLE);
edit_txt.setText("");
break;
}
case R.id.button_send:{
saveImageToFile();
btn_camera.setVisibility(View.INVISIBLE);
// txt_view.setText("正在解析图片,请稍候···");
btn_recamera.setText("开始");
btn_recamera.setVisibility(View.VISIBLE);
btn_send.setVisibility(View.INVISIBLE);
s=getImageRecgResult(Url, filename);
if(s==null){
Toast.makeText(getApplicationContext(),"请开启网络连接,尝试重拍", Toast.LENGTH_SHORT).show();
txt_view.setText("网络出错了···");
}else if(s.length()<=10){
txt_view.setText("图片质量不满足解析最低要求");
Toast.makeText(getApplicationContext(),"亲!你可以尝试把玻璃擦干净,重拍识别",Toast.LENGTH_LONG).show();
}else {
txt_view.setText("");
edit_txt.setText(s);
}
break;
}
}
}
}//end ofclass ButtonOnClickListener
//picture显示
private PictureCallback capturedIt = newPictureCallback() {
@Override
public voidonPictureTaken(byte[] data, Camera camera) {
Bitmap bitmap = BitmapFactory.decodeByteArray(data , 0, data .length);
if(bitmap==null){
}
else
{
//设置剪切长宽位置转换
view.getLocationInWindow(pos);
Matrix m = new Matrix();
m.setRotate(90);
Bitmap bm = Bitmap.createBitmap(bitmap, 0, 0,bitmap.getWidth(),bitmap.getHeight(),m, true);
pos[0]=(int) (pos[0]*(bm.getWidth()/((float)surfWidth)));
pos[1]=(int) (pos[1]*(bm.getHeight()/((float)surfHeigth)));
Bitmap sbm = Bitmap.createBitmap(bm,pos[0],pos[1],(int) (view.getWidth()*(bm.getWidth()/((float)surfWidth))),(int)(view.getHeight()*(bm.getHeight()/((float)surfHeigth))));
buffer = new byte[Bitmap2Bytes(sbm).length];
buffer = Bitmap2Bytes(sbm).clone();}
}
};//end ofclass PictureCallback
public byte[]Bitmap2Bytes(Bitmap bm) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
bm.compress(Bitmap.CompressFormat.JPEG, 100, baos);
return baos.toByteArray();
}//end of B2B
private voidsaveImageToFile(){
File file = getOutFile();
if (file == null){
Toast.makeText(getApplicationContext(), "文件创建失败,请检查SD卡读写权限", Toast.LENGTH_SHORT).show();
return ;
}
if (buffer == null){
Log.i("MyPicture", "自定义相机Buffer: null");
}else{
try{
FileOutputStream fos = newFileOutputStream(file);
fos.write(buffer);
fos.flush();
fos.close();
}catch(IOException e){
e.printStackTrace();
}
}
}//end ofsaveImageToFile
private File getOutFile() {
// TODOAuto-generated method stub
String storageState = Environment.getExternalStorageState();
if(Environment.MEDIA_REMOVED.equals(storageState)){
Toast.makeText(getApplicationContext(), "oh,no, SD卡不存在", Toast.LENGTH_SHORT).show();
return null;
}
File mediaStorageDir = new File(Environment
.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
,"MyPictures");
if (!mediaStorageDir.exists()){
if (!mediaStorageDir.mkdirs()){
mediaStorageDir.getPath());
return null;
}
}
File file = new File(setFilePath(mediaStorageDir));
return file;
}//end of getOutFile
private String setFilePath(FilemediaStorageDir) {
// TODOAuto-generated method stub
String timeStamp =newSimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
filename="IMG_"+timeStamp+".jpg";
String filePath = mediaStorageDir.getPath()+ File.separator;
Url=filePath;
filePath += (timeStamp + ".jpg");
return filePath;
}//end of getFilePath
@Override
public voidsurfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) {
/************************************************************
* 目前只考虑到竖屏使用的情况,所以这一部分不用写,当想加入横屏时须在此处配置参数
*/
}
@Override
public voidsurfaceCreated(SurfaceHolder holder) {
try {
Camera.Parameters parameters = mCamera.getParameters();
if(this.getResources().getConfiguration().orientation!=Configuration.ORIENTATION_LANDSCAPE)
{
parameters.set("orientation", "portrait");
mCamera.setDisplayOrientation(90);
parameters.setRotation(0);
}else {
parameters.set("orientation", "landscape");
//对于Android 2.2及以上版本
mCamera.setDisplayOrientation(0);
//对于Android 2.2及以上版本取消注释
parameters.setRotation(0);
}
Size largestSize =getBestSupportedSize(parameters.getSupportedPreviewSizes());
parameters.setPreviewSize(largestSize.width, largestSize.height);// 设置预览图片尺寸
parameters.setPictureSize(largestSize.width, largestSize.height);
mCamera.setParameters(parameters);
mCamera.setPreviewDisplay(holder);
mCamera.startPreview();
} catch (IOException e) {
mCamera.release();
}
}
@Override
public voidsurfaceDestroyed(SurfaceHolder arg0) {
mCamera.stopPreview();
mCamera.release();
}
@Override
public booleanonTouchEvent(MotionEvent event) {//屏幕触摸事件
if (event.getAction()== MotionEvent.ACTION_DOWN) {//按下时自动对焦
mCamera.autoFocus(null);
//af =true;
}
return true;
}
private SizegetBestSupportedSize(List<Size> sizes) {
// 取能适用的最大的SIZE
Size largestSize = sizes.get(0);
int largestArea = sizes.get(0).height * sizes.get(0).width;
for (Size s : sizes) {
int area = s.width * s.height;
if (area > largestArea) {
largestArea = area;
largestSize = s;
}
}
return largestSize;
}
}
以上就是保存预览图像其中的一部分图像的代码实现,下面我们来讲解一下其中的原理。涉及到图片处理部分的代码如下:
Bitmap bitmap =BitmapFactory.decodeByteArray(data , 0, data .length);
if(bitmap==null){
}
else
{//设置剪切长宽位置转换
view.getLocationInWindow(pos);
Matrix m = new Matrix();
m.setRotate(90);
Bitmap bm = Bitmap.createBitmap(bitmap, 0, 0,bitmap.getWidth(),bitmap.getHeight(),m, true);
pos[0]=(int) (pos[0]*(bm.getWidth()/((float)surfWidth)));
pos[1]=(int) (pos[1]*(bm.getHeight()/((float)surfHeigth)));
Bitmap sbm = Bitmap.createBitmap(bm,pos[0],pos[1],
(int) (view.getWidth()*(bm.getWidth()/((float)surfWidth))),
(int)(view.getHeight()*(bm.getHeight()/((float)surfHeigth))));
//获取整个屏幕大小
DisplayMetrics dm = newDisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(dm);
surfWidth=dm.widthPixels;
surfHeigth=dm.heightPixels;
看到这些新代码你可能瞬间就懵了,没关系我们来解释他的原理:
首先我们要说明一下Android手机屏幕的方向,相机传感器方向以及相机的预览方向;
手机屏幕方向是以屏幕的左上角是坐标系统的原点(0,0)坐标。原点向右延伸是X轴正方向,原点向下延伸是Y轴正方向。
物理摄像头采集到的像:手机相机的图像数据都是来自于摄像头硬件的图像传感器,这个传感器在被固定到手机上后有一个默认的取景方向,坐标原点位于手机横放时的左上角,即与横屏应用的屏幕X方向一致。所以在编辑竖屏应用时要对其进行旋转90°操作才能看到正逻辑的预览图像。
相机的预览方向:在相机API中可以通过setDisplayOrientation()设置相机预览方向。在默认情况下,这个值为0,与图像传感器一致。因此对于横屏应用来说,由于屏幕方向和预览方向一致,预览图像不会颠倒90度。但是对于竖屏应用,屏幕方向和预览方向垂直,所以会出现颠倒90度现象。为了得到正确的预览画面,必须通过API将相机的预览方向旋转90,保持与屏幕方向一致。
以上是手机预览是三个方向之间的问题,参考于博客http://www.androidchina.net/4381.html
参看以上博文你会对Android相机各种方向有一个清醒的认识,所以再次还是要感谢这篇博文的博主啊!
然后我继续来分享我在接下来的设计时的想法,我发现不同的手机默认存储的照片的方向是不一样的,有的手机会随着手机预览视图的调整来存储,而有的手机却始终存储横着的图像(三星手机),为此,我想到的方法是统一横着存储相片,这样能够照顾到所有的机型。那么,我们现在看到的手机频幕上显示的图片是如图1,而实际存储的照片如图
而且,实际存储的照片的尺寸与手机屏幕的尺寸不一样的但比例相等,所以我先将存储的照片通过Bitmap类的相关方法将图片旋转90°,然后按照屏幕尺寸和照片尺寸的比例进行转换,将手机上的有效区域的相关参数(顶点坐标,长,宽)映射到旋转后的照片上从而截取有效的图像存储。
获取屏幕的长和宽
DisplayMetrics dm = newDisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(dm);
surfWidth=dm.widthPixels;
surfHeigth=dm.heightPixels;
以下代码是实现将图片顺时针旋转90°
Matrix m = new Matrix();
m.setRotate(90);
Bitmap bm = Bitmap.createBitmap(bitmap, 0, 0,bitmap.getWidth(),bitmap.getHeight(),m, true);
将屏幕上View有效图像区域的顶点坐标映射到旋转后的图片上
view.getLocationInWindow(pos);
pos[0]=(int) (pos[0]*(bm.getWidth()/((float)surfWidth)));
pos[1]=(int) (pos[1]*(bm.getHeight()/((float)surfHeigth)));
照到图片上的顶点坐标,然后按照映射长宽进行有效图像的截取
Bitmap sbm = Bitmap.createBitmap(bm,pos[0],pos[1],
(int) (view.getWidth()*(bm.getWidth()/((float)surfWidth))),
(int)(view.getHeight()*(bm.getHeight()/((float)surfHeigth))));
这样,就实现了截取预览图像中的有效图像进行保存,通过这个原理,我们只需要都相关的界面布局进行设计,就可以实现类似于扫一扫之类的软件了。
效果图: