Android——简易照相机
实验名称:Android多媒体应用-拍照
实验目的:设计一个简易照相机
实验内容:利用Camera类设计一个简易照相机,通过SurfaceView组件实现取景预览,拍照保存等功能,拍照保存时按camera+序号.jpg的方式保存照片,序号自动增加,确保拍照保存不会覆盖前面的照片。
实验日期:2019年5月
目录
一、实验原理
该相机程序,需要应用surfaceview组件来预览摄像头拍摄到的景物,再使用回调接口surfaceholder.callback监控取景视图。使用照片服务类Camera实现拍照功能,并通过imageview组件显示。最后通过onPicturetaken方法,将拍摄的照片保存至手机。编写getFile和checkFileName方法,用来对文件名进行处理,实现了实验对于文件名的要求。
二、实验过程记录
2.1 布局文件
为实现基本的相机程序效果,使用最简单的线性布局,通过组建的嵌套生成需要的布局。首先指定最外层的线性布局对齐方式为垂直布局,之后再编写textview组件显示程序的标题,然后嵌套一层水平对齐方式的线性布局,在其中加入两个button组件用于防止“拍照”和“退出”按钮。之后在第一层线性布局中加入ImageView组件用于相片的显示,最后加入surfaceView组件用于取景预览。
2.2 控制文件
2.2.1 实例化对象
在MainActivity.java文件中修改文件,以实现对程序的控制。首先在公共类 “MainActivity” 扩展 “Activity” 之后,添加 “implements SurfaceHolder.Callback” ,实现Callback接口,以处理取景预览功能。之后,如下图所示实例化所需的对象,并声明用于保存图片文件的路径。
在上图2.1中,分别实例化了用于拍照功能的“Camera”对象“mCamera”、用于取景预览的“SurfaceView”对象“surfaceView”、用于回调图片参数的“SurfaceHolder”对象“holder”、用于显示图片的“ImageView”对象“mImageView”、用于拍照退出的按钮对象“cameraBtn”和“exitBtn”,以及用于记录图片保存路径的对象“path”。
2.2.2 重载构造函数
对构造函数进行修改,使其实现“关联布局文件和控制文件,注册回调监听器”的功能。如图2.2所示:
首先,通过findViewById方法关联图1中的相关组件,并为“cameraBtn”和“exitBtn”分别设置监听事件,最后设置SurfaceHolder对象的相关内容。创建SurfaceHolder对象“holder”,注册回调监听器并设置SurfaceHolder的类型。
2.2.3 编写mClick类
为按钮的监听事件编写mClick类,以实现拍照和退出功能。如图2.3所示:
上图中,构造了一个继承于OnClickListener的mClick类。通过判断点击按钮传入的参数v,可以对拍照和退出进行区分,拍照使用takePicture方法,退出则调用exit()函数,对相机资源进行重置。
2.2.4 编写jpegCallback类
在图3编写的mClick类中,takePicture方法用到了获取照片事件的回调接口jpegCallback,现在需要对jpegCallback类进行编写。如图2.4所示:
在上图中,编写了继承于Camera. PictureCallback的类“jpegCallback” ,通过重载onPictureTaken函数,实现了拍照、显示图片以及保存图片的功能。
2.2.5重载surfaceCreated方法
创建相机对象之后,会默认调用三个构造函数,分别为surfaceCreated、surfaceChanged和surfaceDestroyed,首先重载surfaceCreated函数,如图2.5所示。
上图中,首先释放掉相机资源,然后开启摄像头。之后尝试进行相机预览,如果捕获到预览错误信息,则输出报错。其中的ReleaseCamera()函数用来重置相机,释放相机资源。
2.2.6重载surfaceChanged方法
相机画面发生变化时,将触发surfaceChanged方法,如下图2.6所示重载surfaceChanged方法。
重载的surfaceChanged方法会调用initCamera函数,重新设置预览的画面。重置包括预览画面的格式、大小、预览框大小等信息。最后调用startPreview方法开始预览。
2.2.7重载surfaceDestroyed方法
关闭相机时,会触发surfaceDestroyed方法,如下图2.7所示重载surfaceDestroyed方法。
可以看到,重载的surfaceDestroyed方法并没有实现具体的功能。
2.3添加权限
由于程序中用到了SD卡读写和相机的调用,所以需要在程序文件中声明需要申请的权限。包括SD卡读取、写入权限,以及相机调用权限。如下图2.8所示,在AndroidManifest.xml文件中添加下列信息,以申请权限。
2.4安装APK测试
由于AndroidStudio模拟器在电脑上经常卡顿,所以我决定使用安卓手机进行测试。首先将编译好的项目打包成APK文件,然后将该APK文件发送至手机端安装。安装截图如图2.9所示,可以看到安装时会提示该APP将获取的手机权限。
点击安装即可完成APK文件的安装。
三、实验中存在的问题及解决方案
3.1 预览倾斜问题及解决过程
安装完成,运行该APP程序,点击拍照按钮,可以看到如图3.1所示的手机界面。其中上面的小画面是imageView组件显示的拍完的照片,下面的大图片是surfaceView组件显示的相机预览照片。
拍摄完成之后,推迟该APP,并在手机自带的文件管理界面,找到test文件夹(如图3.2),这是刚刚图片拍摄之后,程序生成的文件夹,用于保存图片文件。在test文件夹下,可以看到刚刚生成的camera.jpg文件(如图3.3所示) ,浏览该文件,可以查看之前拍摄的图片(如图3.4) 。
查看该图片,发现拍摄出来的图片,与正常视角相差了90°,而且在图片拍摄和预览过程中,图片和正常视角都是不相符的,都旋转了90°。
经过查阅资料发现,主要是由于相机传感器像素坐标信息,与手机显示屏像素坐标不相符导致的。图3.5展示了两像素坐标的不同之处。
(图片引用于:https://blog.csdn.net/xx326664162/article/details/53350551)
相机传感器获取得到图像,就确定了这幅图像每个坐标的像素值,但是要显示到手机屏幕上,就需要按照屏幕的坐标系来显示,于是就需要将相机传感器的坐标系逆时针旋转90度,才能显示到屏幕的坐标系上。
在安卓程序中,提供了相关的setRotation()方法和setDisplayOrientation()方法。其中,setRotation()方法的作用是将相机传感器获取到的位图坐标旋转一定的角度,而setDisplayOrientation()方法的作用是将预览时的画面旋转一定的角度。通过调用这两个方法,我们就可以实现图片的正常拍摄和预览了。如图3.6所示,在原有程序的基础上添加该方法, 再生成APK文件进行测试。
重新安装APK文件,测试运行,运行结果如图3.7所示。可以看到,预览的图片已经可以正常显示了。然后再到test文件夹中查看camera.jpg文件(如图3.8) ,同样,拍摄的图片也是可以正常显示了。
3.2 文件命名问题及解决过程
目前的程序所能实现的功能,只能拍摄一张照片,如果拍摄多张照片,由于文件名相同,后面的文件就会将前面已存在的文件覆盖掉,很不方便。本次实验要求的是“拍照保存时按camera+序号.jpg的方式保存照片,序号自动增加”。
3.2.1问题分析
为了实现这个功能,需要在每次文件保存使,都要考虑我们需要生成的文件名是什么。如果企图保存当前程序生成的图片数量,在关闭APP之后,数据不易保存。所以我想的解决办法是:每次生成的文件名都需要经历两次函数,第一个函数需要返回所需文件夹中的所有文件,第二个函数需要判断指定的文件在该文件夹中是否存在,如果存在则返回一个我们需要的、正确的文件名。
3.2.2测试程序
① getFile()函数
为了不破坏原有的安卓程序,我在Eclipse中进行相关函数的编写和测试。首先编写第一个函数getFile(),该函数需要输入文件夹的路径字符串,输出该文件夹下的所有文件名。程序文件如图3.9所示:
该程序,首先通过path参数获取指定的文件对象,再通过listFiles()方法列出该文件夹下,所有的文件,包括文件夹、普通文件等等,并将其保存在array数组中。之后逐个判断是否为普通文件,将文件名保存在list中,最后返回list。
现在,将path指定为桌面上的android文件夹,调用getFiles方法,查看程序输出。
图3.10是android文件夹下的文件信息,图11是程序输出的信息。
可以看到,除了临时文件,普通文件均可以正常输出。
② checkFileName方法
下面实现checkFileName函数,该方法的作用是:在文件名字符串中,判断所需要的字符串是否存在,如果存在则返回原文件名;不存在则重新调用,利用index参数,实现文件名的递增过程。最后返回一个正确的、合理的文件名。
checkFileName方法的实现过程是:首先将传入的文件名参数根据“.”进行分割,分成“.”之前的字符串+起始索引数字index+“.”字符+“.”之后的字符串,然后判断整个文件名是否包含于长文件名参数中,如果存在则再次调用该方法,同时起始索引数字加一;如果不存在,就直接返回上面连接好的文件名。
现在将checkFileName方法和getFile进行合并,通过main函数进行调用测试。首先在桌面的android目录下新建一个mydir目录,在mydir目录下新建一个test.txt文件,然后运行java程序进行测试。
上图3.12展示了当前的mydir目录以及Java程序的运行结果。由于起始索引参数index设置的是0,而文件夹下只有一个test.txt文件,所以程序返回的结果“test.txt”是正确的。为了测试的准确性,我在mydir目录下又新建了一个test0.txt文件,如果程序功能正常,应该是会返回test1.txt。下面运行程序测试:
可以看到,程序运行结果正确。
3.2.3安卓测试
现在可以放心地将代码移植到AndroidStudio程序中去了。在原有的onPictureTaken()方法的基础上,增加红色框中的内容,如图3.14所示。其含义为:通过getFile方法查找手机"/sdcard/test"目录下的所有文件,将结果返回至list中。然后通过调用checkFileName方法,检查list中是否有符合filename的文件名(filename已在之前定义),checkFileName方法会返回合理的文件名,再通过字符串连接,将完整的路径保存到path字符串中,作为完整的文件名路径。
为了更清晰的显示文件保存的路径和文件名信息,我在布局文件中增加了一个textview组件,在path生成之后,将path输出到textview显示,这样就会对文件的路径信息更加直观。
最后,将getFile和checkFileName方法都移植到java文件中,就可以运行测试了。
同样重新生成APK文件测试运行,进行多次拍摄,下面是我拍摄第8次时的界面截图(图3.15)。
因为文件名起始索引为0,所以第8次生成的文件名应该是camera7.jpg,可以从APP界面中看到文件的保存路径。现在在test文件夹下,会有8张我刚刚拍摄的图片,命名方式为camera+i。图3.16展示了test文件夹下的文件信息,文件的保存也是正常的。
3.3 预览画面卡顿问题及解决过程
之前的多次测试中,我发现该程序的一个bug:在每次点击“拍照“按钮之后,预览画面就会卡住(静止且没有响应),而且如果此时再次点击”拍照“或”退出“按钮,程序就会意外退出。
经过查阅资料,我了解到,该问题是由以下原因造成的:
在执行拍照命令时,会调用camera.takePicture()方法,该方法在执行过程中会调用camera.stopPreview来获取拍摄帧数据,从而进行数据回调。而调用camera.stopPreview方法,就会暂停相机的预览,并表现为预览画面卡住。如果此时用户点击了按钮的话,也就会再次调用camera.takepicture方法,由于相机还没有开始预览,没有进行相关进程的初始化,就会出现之前遇到的意外退出问题。
解决的方法也很简单,既然camera.takePicture () 方法调用了camera.stopPreview来停止预览,那么只要在camera.takePicture()方法结束之后,手动调用一次camera.startPreview方法,来开启相机预览就可以了。
于是进行添加图3.17红框中的部分:
再次生成APK文件进行测试,图3.18为测试结果。
这是我拍摄第9张图片之后的界面。上方的imageview组件显示的是第9张的图片,而下面的surfaceView显示的是此时的预览画面。此时如果我连续拍摄,继续点击“拍照“按钮,程序也不会因为bug而意外退出了。实现了连续拍照的功能。
四、实验结果
现在,程序已经实现了实验所要求的功能,我将目前程序所能实现的效果录制成GIF图片并进行上传,程序的效果如下所示:http://47.95.13.239/Study/Android/test.gif
五、源代码
我已经将整理过的该项目的源代码上传到了GitHub:
https://github.com/ZHJ0125/AndroidSimpleCameraApp
5.1布局文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/LinearLayout1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_gravity="center_horizontal"
android:gravity="center_horizontal"
android:text="拍照测试"
android:textSize="20dp"
tools:context=".MainActivity"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/test"
android:textSize="15dp"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:gravity="center_horizontal">
<Button
android:id="@+id/btn1"
android:layout_width="110dp"
android:layout_height="wrap_content"
android:text="拍照"/>
<Button
android:id="@+id/btn2"
android:layout_width="110dp"
android:layout_height="wrap_content"
android:text="退出"/>
</LinearLayout>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/imageView1"
android:layout_gravity="center" />
<SurfaceView
android:id="@+id/surfaceView1"
android:layout_width="320dp"
android:layout_height="240dp"
android:layout_gravity="center_horizontal"/>
</LinearLayout>
5.2程序控制文件
package zhj.com.simplecamera;
import android.os.Bundle;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.PixelFormat;
import android.hardware.Camera;
import android.hardware.Camera.PictureCallback;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.widget.Button;
import android.widget.ImageView;
import android.app.Activity;
import android.widget.TextView;
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.File;
import java.util.ArrayList;
public class MainActivity extends Activity implements SurfaceHolder.Callback{
Camera mCamera = null;
SurfaceView surfaceView;
SurfaceHolder holder;
ImageView mImageView;
Button cameraBtn, exitBtn;
TextView textView;
int i = 0;
String filename = "camera.jpg"; //图片文件名
String path = ""; //图片保存路径
/**
* Override the onCreate
* function:重载构造函数,关联布局文件和控制文件,注册回调监听器
**/
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//关联ID
mImageView = (ImageView)findViewById(R.id.imageView1);
cameraBtn = (Button)findViewById(R.id.btn1);
exitBtn = (Button)findViewById(R.id.btn2);
cameraBtn.setOnClickListener(new mClick()); //设置监听事件
exitBtn.setOnClickListener(new mClick());
surfaceView = (SurfaceView)findViewById(R.id.surfaceView1);
textView = (TextView)findViewById(R.id.test);
//System.out.println("begin to holder...");
holder = surfaceView.getHolder();
holder.addCallback(this);
holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
}
/**
* class:mClick
* function:设置按键监听事件,对应拍照和退出按钮
* */
class mClick implements OnClickListener{
@Override
public void onClick(View v) {
if (v == cameraBtn){
mCamera.takePicture(null, null, new jpegCallback()); //拍照
//surfaceCreated(holder); //调用构造函数
}
else if (v == exitBtn){
exit(); //退出程序
}
}
}
void exit(){
mCamera.release();
mCamera = null;
}
/**
* class:jpegCallback
* function:实现拍照和保存图片功能
* */
public class jpegCallback implements PictureCallback{
@Override
public void onPictureTaken (byte[] data, Camera camera){
Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
ArrayList<String> list = getFile("/sdcard/test");
path = "/sdcard/test/" + checkFileName(list, filename, 0);
textView.setText("文件保存路径:" + path);
try{
BufferedOutputStream outStream = new BufferedOutputStream(new FileOutputStream(path));
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outStream);
outStream.flush();
outStream.close();
mImageView.setImageBitmap(bitmap); //在ImageView显示拍照的图片
mCamera.startPreview();
}catch (Exception e){
Log.e("error", e.getMessage());
}
}
}
private static ArrayList<String> getFile(String path){ //检测路径下所有的文件,并返回文件名列表
File file = new File(path); // 获得指定文件对象
File[] array = file.listFiles(); // 获得该文件夹内的所有文件
ArrayList<String> list = new ArrayList<String>();
for(int i=0;i<array.length;i++){
if(array[i].isFile()){ //如果是文件,将文件名保存在列表中
list.add(array[i].getName());
}
}
return list; //返回储存文件名的列表
}
private static String checkFileName(ArrayList<String> names,String name,int index) { //检测文件是否存在,并返回合理的文件名
if(names.contains(name.substring(0,name.indexOf("."))+index+name.substring(name.indexOf("."),name.length()))) {
//System.out.println(name.substring(0,name.indexOf("."))+index+name.substring(name.indexOf("."),name.length())+ "Hello");
//name.substring(0,name.indexOf(".")) --> 返回"."之前的字符串
//文件存在,再次调用checkFileName方法
name = checkFileName(names,name,index+1);
} else {
//文件不存在,返回合理的文件名
return name.substring(0,name.indexOf("."))+index+name.substring(name.indexOf("."),name.length());
}
return name;
}
/**
* Override the surfaceCreated
* function:创建相机时触发,开启相机预览功能
* */
@Override
public void surfaceCreated(SurfaceHolder holder){
if (mCamera != null){
ReleaseCamera(); //首先释放相机资源
}
mCamera = Camera.open(); //开启摄像头
//System.out.println("
Camera.open() is OK !!!
");
//mCamera = android.hardware.Camera.open();
//mCamera = Camera.open(Camera.CameraInfo.CAMERA_FACING_BACK);
//System.out.println("Camera is OK!");
try{
mCamera.setPreviewDisplay(holder); //设置相机预览
}catch (IOException e){
System.out.println("预览错误");
}
}
private void ReleaseCamera() //重置相机
{
if(mCamera != null)
{
mCamera.release();
mCamera = null;
}
}
/**
* Override the surfaceChanged
* function:当画面发生改变时触发,重置相机参数
* */
@Override
public void surfaceChanged (SurfaceHolder holder, int format, int width, int height){
//System.out.println("Camera is going to ready...");
initCamera(); //重置相机参数
}
//设置相机参数
public void initCamera(){
//System.out.println("here1....");
Camera.Parameters parameters = mCamera.getParameters();
parameters.setPictureFormat(PixelFormat.JPEG); //照片格式
parameters.setPreviewSize(320, 240); //预览规格大小
parameters.setPictureSize(320, 240); //图片大小
parameters.setRotation(90); //设置照片数据旋转90°
mCamera.setParameters(parameters);
mCamera.setDisplayOrientation(90); //设置浏览画面水平转90°
mCamera.startPreview(); //开始预览
}
/**
* Override the surfaceDestroyed
* function:关闭相机时触发,空
* */
@Override
public void surfaceDestroyed(SurfaceHolder holder){ } //消灭相机时触发
}
六、待解决问题和设想
6.1 待解决问题
①功能单一
该APP还有很多不完善的地方,功能非常单一,只能实现简单的拍照功能。
②版本兼容问题
本次实验中所有的APP测试均在安卓7.1.2版本的手机上进行,在新建工程时,我将最底支持版本选择为API 14和Android 4.0,但是在Android版本为4.4.2的手机上测试时,依然会出现很多问题,导致程序异常结束。程序还未在更高Android版本的手机上测试过,尚不清楚将会有什么bug。版本的兼容问题是亟待解决的关键问题之一。
③关于连拍功能
报告中已经说明了连续拍照的实现过程,到目前为止,程序已经实现该过程:点击“拍照“按钮 拍摄照片 imageView显示照片 surfaceView启动预览 循环拍摄。但在调试过程中我发现,如果点击”拍摄“按钮过快,可能导致相机预览camera.startPreview还未开始,就点击了”拍摄“按钮,于是就会导致跟之前相同的问题,让程序意外结束。这也是待解决的问题之一。
6.2 设想
①版本问题
现在版本问题是最严重的一个问题,因为大家使用的Android手机,其Android版本必然不同,如果不能解决该问题,可能导致很多手机无法正常运行该程序,或者会出现很多bug。现在需要查阅一下Android版本的资料,并进行兼容性的改进。
②加功能
现在功能很单一,以后希望能加上摄像等功能,做到比安卓自带的相机功能更加完善(笑)。
七、实验总结
整个实验项目进行的还算顺利,一开始还有很多问题,最后基本上都通过查阅资料以及和同学的交流,解决了问题。
问题还算挺多的,主要是一些细节问题导致的,需要对整个工程的结构,以及各个函数的功能都理清楚,才能更好地解决问题。
调试也有很多技巧,最简便的是使用输出语句判断程序的执行,还有就是通过注释部分功能,判断出问题的位置等等。
八、部分参考资料
- 关于androidcamera相机的方向的理解http://blog.sina.com.cn/s/blog_68f23d9f0102y2cc.html
- Java检测文件名是否重复 https://blog.csdn.net/u014804332/article/details/80385217
- JAVA中方法的调用 https://www.imooc.com/article/13423
- 预览卡住解决办法思路引导
https://www.jianshu.com/p/586af3a2dc8d?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation - Android相机开发和遇到的坑 https://blog.csdn.net/xx326664162/article/details/53350551