随着手机的发展,现在几乎每个应用都会用到摄像头功能,设置头像的时候需要拍照,发朋友圈的时候可以拍照,扫一扫也用到了摄像头。大部分应用只需要调用Android原生的拍照程序就可以满足要求了,但像扫一扫这样的功能就需要自己定义预览界面了。今天就来总结下Android下的拍照功能吧。
1.使用系统相机进行拍照
首先绘制一个简单的布局:两个按钮和一个ImageView
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginBottom="10dp"
android:layout_marginTop="10dp"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/ll_btns"
android:layout_width="match_parent"
android:orientation="horizontal"
android:layout_alignParentBottom="true"
android:layout_height="50dp">
<Button
android:id="@+id/btn_default"
android:layout_width="0dp"
android:layout_weight="1"
android:text="default"
android:layout_marginRight="5dp"
android:layout_height="match_parent" />
<Button
android:id="@+id/btn_custom"
android:text="custom"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_marginLeft="5dp"
android:layout_height="match_parent" />
</LinearLayout>
<ImageView
android:id="@+id/iv_result"
android:layout_above="@id/ll_btns"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>
然后在Activity中先给按钮添加监听事件
public class MainActivity extends Activity implements View.OnClickListener{
private static final int REQUEST_TAKE_PHOTO = 1;
private static final String TAG = "MainActivity";
private ImageView imageView;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.btn_default).setOnClickListener(this);
findViewById(R.id.btn_custom).setOnClickListener(this);
imageView= (ImageView) findViewById(R.id.iv_result);
}
@Override
public void onClick(View v) {
switch (v.getId()){
case R.id.btn_default:
Intent intentDef=new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
startActivityForResult(intentDef, REQUEST_TAKE_PHOTO);
break;
case R.id.btn_custom:
break;
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if(requestCode==REQUEST_TAKE_PHOTO &&resultCode==RESULT_OK){
imageView.setImageBitmap((Bitmap) data.getExtras().get("data"));
}
}
}
从上面的代码我们可以看到,调用系统照相机只需要两行语句就可以了
Intent intentDef=new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
startActivityForResult(intentDef, REQUEST_TAKE_PHOTO);
这是一个隐式意图,系统可以根据传入的Action自动找到所需的Activity,拍完照后系统会将照片存放在Bundle中,取出来显示即可。下面我们来看一下运行效果吧。
一眼就可以看出上图的那种AV画质绝对不可能是手机拍出的原图的,不相信的小伙伴可以自己拍照对比下。事实上这种方式获取的位图的确不是原图,这种缩略图只能做个头像,放大了就全是马赛克了。那么Android为什么要返回缩略图呢,直接返回原图不好吗?当然不好,因为系统并不知道你需要多大的图片,如果你只需要一个头像却返回一个原图那不是白白浪费了好多内存吗?这里提到了内存,加载位图是相当消耗内存的,到底消耗多少呢?我们稍后再说,我们先说一下获取原图的方法。其实也很简单,就是拍照的时候给系统相机一个输出路径。
Intent intentDef=new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
File photoFile = null;
try {
photoFile = createImageFile();
} catch (IOException ex) {
ex.printStackTrace();
}
if (photoFile != null) {
intentDef.putExtra(MediaStore.EXTRA_OUTPUT,
Uri.fromFile(photoFile));
startActivityForResult(intentDef, REQUEST_TAKE_PHOTO);
}
上面的代码就是创建了一个文件,然后将文件的Uri放到intentDef中,这样拍完的照片就保存在文件中了。
String mCurrentPhotoPath;
private File createImageFile() throws IOException{
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
String imageFileName = "JPEG_" + timeStamp + "_";
File storageDir = Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES);
if(!storageDir.exists())
storageDir.mkdirs();
File image = File.createTempFile(
imageFileName, /* prefix */
".jpg", /* suffix */
storageDir /* directory */
);
// Save a file: path for use with ACTION_VIEW intents
mCurrentPhotoPath =image.getAbsolutePath();
return image;
}
如果常去Android Developer查文档的童鞋可能已经发现了,我上面的这个方法是抄袭Android Developer上的文章了,是的,整篇文章都是根据Android Developer写的(来咬我啊)。下面给出原文链接,有兴趣的也可以直接阅读原文Android Camera
事实上,官网教程是有些地方不对的,也可能是我哪里弄错了。
就拿上面那个方法来说吧,如果不加上
if(!storageDir.exists())
storageDir.mkdirs();
的话,在5.0以上的系统运行会出错的,因为storageDir的路径在运行的时候不存在,所以无法在该目录创建文件,这个问题在努比亚Z9上测试是存在的,但是在酷派的某机器上就没问题。努比亚是21,酷派是19。
还有一个问题是官网的路径是在文件路径前面加了一个”file”字符串,我一直不明白是为什么,加上后就会出错。
下面该试试图片展示了
Bitmap bitmap=BitmapFactory.decodeFile(mCurrentPhotoPath);
imageView.setImageBitmap(bitmap);
那么上面的代码运行后能不能把图片展示出来呢,这个得看机型,努比亚的是可以展示的,酷派的则不行。如果你的手机也展示不出来的话,那就打开log日志,一定有这么一句话
Bitmap too large to be uploaded into a texture (2736x4864, max=4096x4096)
这个错误的原因是Android系统底层渲染图像太大导致的,也是一个和机型有关的问题。
Bitmap bitmap=BitmapFactory.decodeFile(mCurrentPhotoPath);
int bw=bitmap.getWidth();
int bh=bitmap.getHeight();
int iw=imageView.getWidth();
int ih=imageView.getHeight();
Log.e(TAG, "onActivityResult: bitmap的宽度"+bw);
Log.e(TAG, "onActivityResult: bitmap的高度"+bh );
Log.e(TAG, "onActivityResult: imageview的宽度"+iw);
Log.e(TAG, "onActivityResult: imageview的高度"+ih);
imageView.setImageBitmap(bitmap);
运行结果:
onActivityResult: bitmap的宽度2736
onActivityResult: bitmap的高度4864
onActivityResult: imageview的宽度656
onActivityResult: imageview的高度1090
原图的尺寸比imageview所需尺寸大了20多倍,相信很多刚步入Android开发的人经常是直接将原图展示出来的,这样的做法是极其浪费内存,而且也容易出现OOM。
我们来算一下要展示原图大概需要多少内存吧,Bitmap默认展示的的方式是ARGB8888的方式,即一个像素点占用4个字节。
总像素点=2736*4864=13307904
占用内存=13307904*4=53231616B=50.77MB
难易想象吧,一张图片就占用50MB的内存,而且我的手机像素只能算是一般,好点的手机拍出来的图片更占内存。
那Android系统给每个应用分配了多少内存呢,可能很多人认为是16Mb,如果真实16Mb,那我的手机早就OOM了。早期的时候,确实是16Mb,但是现在早就不适用了,现在手机给应用分配的内存大小跟系统等级和屏幕大小都是有关系,具体大小有兴趣的可以自己测一下,反正我的两款手机都是在200Mb以上的。
扯了这么多没用的,还是快点把图片展示出来吧
private void setPic() {
// Get the dimensions of the View
int targetW = imageView.getWidth();
int targetH = imageView.getHeight();
Log.e(TAG, "setPic: "+targetH+" "+targetW );
// Get the dimensions of the bitmap
BitmapFactory.Options bmOptions = new BitmapFactory.Options();
bmOptions.inJustDecodeBounds = true;
BitmapFactory.decodeFile(mCurrentPhotoPath, bmOptions);
int photoW = bmOptions.outWidth;
int photoH = bmOptions.outHeight;
// Determine how much to scale down the image
int scaleFactor = Math.min(photoW/targetW, photoH/targetH);
// Decode the image file into a Bitmap sized to fill the View
bmOptions.inJustDecodeBounds = false;
bmOptions.inSampleSize = scaleFactor;
bmOptions.inPurgeable = true;
Bitmap bitmap = BitmapFactory.decodeFile(mCurrentPhotoPath, bmOptions);
imageView.setImageBitmap(bitmap);
}
嗯,就是这个方法,官方文档里面的代码我就直接用了,作用一看就知道了:压缩图片嘛。最后在来一张效果图吧
2.自定义相机拍照
下面来说一下自定义相机吧,使用系统的相机的缺点就是界面不可变,功能也不能自己控制,所以对于某些App就需要我们自己去写拍照功能了。
首先要在AndroidManifest.xml中添加权限
<uses-permission android:name="android.permission.CAMERA"/>
当我们用Intent的方式访问相机的时候是不需要此权限的,因为这个权限已经在系统相机应用中申请好了,但是自定义相机的时候是需要自己去获取并控制Android Camera的,没有权限就会出错。
然后我们另写一个页面
<?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="vertical">
<FrameLayout
android:id="@+id/camera_preview"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_weight="1" />
<RelativeLayout
android:layout_gravity="center"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button
android:id="@+id/button_capture"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="Capture" />
<Button
android:id="@+id/cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
android:text="cancel"
/>
<Button
android:id="@+id/ensure"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ensure"
android:visibility="gone"
android:layout_alignParentRight="true"
/>
</RelativeLayout>
</LinearLayout>
这个页面中有一个FrameLayout来显示预览界面,三个按钮分别控制,拍照,取消和确定。
预览页面用的是SurfaceView,SurfaceView是Android系统专门用来绘制图像的控件,主要应用于游戏吧,这个控件也是继承View的,但是与View不同的是,View控件的绘制是在UI线程绘制的,如果我们需要绘制的界面特别复杂的话就会阻塞UI线程,预览界面由于里面的图像都是实时刷新的,所以只能使用SurfaceView了。关于SurfaceView的机制和详细介绍可移步官方文档Android SurfaceView。
我们新建一个类CameraPreview继承SurfaceView。
public class CameraPreview extends SurfaceView {
private static final String TAG = "CameraPreview";
private Context mContext;
public CameraPreview(Context context){
super(context);
mContext=context;
}
}
如果我们要拍照最基本的就是要有Camera对象吧,所以我们先定义一个Camera对象,然后在构造方法中初始化。
mCamera=getCameraInstance();
打开Camera调用的是Camera.open(),这个调用是有可能失败的,因为Android系统的摄像头最多就只有两个,如果你申请摄像头的时候摄像头正被其他应用占用着就会导致申请失败
public static Camera getCameraInstance(){
Camera c = null;
try {
c = Camera.open(); // attempt to get a Camera instance
}
catch (Exception e){
e.printStackTrace();
}
return c; // returns null if camera is unavailable
}
然后我们还需要创建一个SurfaceHolder,SurfaceHolder是一个接口,它的作用就是为我们提供了一个方式去访问SurfaceView底层负责绘制的类Surface,我们只需要调用getHolder()便可以得到SurfaceHolder对象,通过SurfaceHolder我们可以控制绘制的大小和格式。
mHolder=getHolder();
mHolder.addCallback(this);
上述代码中,我们获取了SurfaceHolder,还为它添加了一个监听器,所以我们需要实现SurfaceHolder.Callback中的三个方法:surfaceCreated,surfaceChanged和surfaceDestroyed。这三个方法从名字上也很好理解,分别是预览创建,预览改变和预览销毁。
在surfaceCreated中我们做一些Camera的初始化工作
try {
Camera.Parameters params = mCamera.getParameters();
if (params.getSupportedFocusModes().contains(
Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)) {
params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
}
mCamera.setParameters(params);
mCamera.setDisplayOrientation(90);
mCamera.setPreviewDisplay(holder);
mCamera.startPreview();
} catch (IOException e) {
Log.d(TAG, "Error setting camera preview: " + e.getMessage());
}
在这里我们设置了自动对焦,并设置了它旋转了90度。因为默认情况下,预览界面是横屏的。
在surfaceDestroyed方法中我们要释放资源
if (mCamera != null){
mCamera.stopPreview();
mCamera.release();
mCamera = null;
}
最后一个方法是SurfaceChange,这个方法有四个参数,当这四个参数有变化的时候就会调用。对于预览界面我们并不需要对它做处理,这里只做一个默认实现
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
if (mHolder.getSurface() == null){
return;
}
try {
mCamera.stopPreview();
} catch (Exception e){
e.printStackTrace();
}
try {
mCamera.setPreviewDisplay(mHolder);
mCamera.startPreview();
} catch (Exception e){
Log.d(TAG, "Error starting camera preview: " + e.getMessage());
}
}
然后我们再在CameraActivity的OnCreate中初始化CameraPreview
mPreview = new CameraPreview(this);
FrameLayout preview = (FrameLayout) findViewById(R.id.camera_preview);
preview.addView(mPreview);
这样之后我们就可以运行了,已经可以预览了,接下来的工作就是拍照了。
拍照调用Camera的takePicture方法就可以了,这个方法的完整参数有四个,具体作用如下:
shutter:the callback for image capture moment, or null
raw :the callback for raw (uncompressed) image data, or null
postview:callback with postview image data, may be null
jpeg:the callback for JPEG image data, or null
我们需要获取拍完照片的图像,所以需要实现最后一个接口,其他传null就可以了。
private Camera.PictureCallback mPicture = new Camera.PictureCallback() {
@Override
public void onPictureTaken(byte[] data, Camera camera) {
File pictureFile = null;
try {
pictureFile = createImageFile();
} catch (IOException e) {
e.printStackTrace();
}
if (pictureFile == null){
Log.e(TAG, "Error creating media file, check storage permissions: ");
return;
}
try {
FileOutputStream fos = new FileOutputStream(pictureFile);
fos.write(data);
fos.close();
} catch (FileNotFoundException e) {
Log.d(TAG, "File not found: " + e.getMessage());
} catch (IOException e) {
Log.d(TAG, "Error accessing file: " + e.getMessage());
}
}
};
这里面所做的工作就两个:新建一个文件和把图像数据写入文件。so easy
下面我们完成拍照的方法
public void takePicture(){
if(mCamera!=null){
mCamera.takePicture(null, null, mPicture);
}
}
到这里,自定义拍照的部分也结束了。其实也不算结束,比如拍完照后点击取消怎么继续预览?点击确定怎么把图片传给其他的页面?如果在预览的时候手机熄屏了,再次亮屏的时候怎么继续预览?这些问题我还是希望读者可以自己思考一下(如果有读者的话)。
第一篇博客就这样了吧,整体上感觉写的不怎么样,好久好久都不写作文了,文字表达功力一落千丈啊,希望以后可以做的更好吧。