Android——简易照相机

版权声明:本文为博主原创文章,供大家交流学习使用,未经博主允许不得转载。谢谢! https://blog.csdn.net/ZHJ123CSDN/article/details/89880558

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

在上图2.1中,分别实例化了用于拍照功能的“Camera”对象“mCamera”、用于取景预览的“SurfaceView”对象“surfaceView”、用于回调图片参数的“SurfaceHolder”对象“holder”、用于显示图片的“ImageView”对象“mImageView”、用于拍照退出的按钮对象“cameraBtn”和“exitBtn”,以及用于记录图片保存路径的对象“path”。

2.2.2 重载构造函数

对构造函数进行修改,使其实现“关联布局文件和控制文件,注册回调监听器”的功能。如图2.2所示:
在这里插入图片描述

图2.2

首先,通过findViewById方法关联图1中的相关组件,并为“cameraBtn”和“exitBtn”分别设置监听事件,最后设置SurfaceHolder对象的相关内容。创建SurfaceHolder对象“holder”,注册回调监听器并设置SurfaceHolder的类型。

2.2.3 编写mClick类

为按钮的监听事件编写mClick类,以实现拍照和退出功能。如图2.3所示:
在这里插入图片描述

图2.3

上图中,构造了一个继承于OnClickListener的mClick类。通过判断点击按钮传入的参数v,可以对拍照和退出进行区分,拍照使用takePicture方法,退出则调用exit()函数,对相机资源进行重置。

2.2.4 编写jpegCallback类

在图3编写的mClick类中,takePicture方法用到了获取照片事件的回调接口jpegCallback,现在需要对jpegCallback类进行编写。如图2.4所示:
在这里插入图片描述

图2.4

在上图中,编写了继承于Camera. PictureCallback的类“jpegCallback” ,通过重载onPictureTaken函数,实现了拍照、显示图片以及保存图片的功能。

2.2.5重载surfaceCreated方法

创建相机对象之后,会默认调用三个构造函数,分别为surfaceCreated、surfaceChanged和surfaceDestroyed,首先重载surfaceCreated函数,如图2.5所示。
在这里插入图片描述

图2.5

上图中,首先释放掉相机资源,然后开启摄像头。之后尝试进行相机预览,如果捕获到预览错误信息,则输出报错。其中的ReleaseCamera()函数用来重置相机,释放相机资源。

2.2.6重载surfaceChanged方法

相机画面发生变化时,将触发surfaceChanged方法,如下图2.6所示重载surfaceChanged方法。
在这里插入图片描述

图2.6

重载的surfaceChanged方法会调用initCamera函数,重新设置预览的画面。重置包括预览画面的格式、大小、预览框大小等信息。最后调用startPreview方法开始预览。

2.2.7重载surfaceDestroyed方法

关闭相机时,会触发surfaceDestroyed方法,如下图2.7所示重载surfaceDestroyed方法。
在这里插入图片描述

图2.7

可以看到,重载的surfaceDestroyed方法并没有实现具体的功能。

2.3添加权限

由于程序中用到了SD卡读写和相机的调用,所以需要在程序文件中声明需要申请的权限。包括SD卡读取、写入权限,以及相机调用权限。如下图2.8所示,在AndroidManifest.xml文件中添加下列信息,以申请权限。
在这里插入图片描述

图2.8

2.4安装APK测试

由于AndroidStudio模拟器在电脑上经常卡顿,所以我决定使用安卓手机进行测试。首先将编译好的项目打包成APK文件,然后将该APK文件发送至手机端安装。安装截图如图2.9所示,可以看到安装时会提示该APP将获取的手机权限。
在这里插入图片描述

图2.9

点击安装即可完成APK文件的安装。

三、实验中存在的问题及解决方案

3.1 预览倾斜问题及解决过程

安装完成,运行该APP程序,点击拍照按钮,可以看到如图3.1所示的手机界面。其中上面的小画面是imageView组件显示的拍完的照片,下面的大图片是surfaceView组件显示的相机预览照片。
在这里插入图片描述

图3.1

拍摄完成之后,推迟该APP,并在手机自带的文件管理界面,找到test文件夹(如图3.2),这是刚刚图片拍摄之后,程序生成的文件夹,用于保存图片文件。在test文件夹下,可以看到刚刚生成的camera.jpg文件(如图3.3所示) ,浏览该文件,可以查看之前拍摄的图片(如图3.4) 。
在这里插入图片描述

图3.2

在这里插入图片描述

图3.3

在这里插入图片描述

图3.4

查看该图片,发现拍摄出来的图片,与正常视角相差了90°,而且在图片拍摄和预览过程中,图片和正常视角都是不相符的,都旋转了90°。
经过查阅资料发现,主要是由于相机传感器像素坐标信息,与手机显示屏像素坐标不相符导致的。图3.5展示了两像素坐标的不同之处。
在这里插入图片描述

图3.5

(图片引用于:https://blog.csdn.net/xx326664162/article/details/53350551)
相机传感器获取得到图像,就确定了这幅图像每个坐标的像素值,但是要显示到手机屏幕上,就需要按照屏幕的坐标系来显示,于是就需要将相机传感器的坐标系逆时针旋转90度,才能显示到屏幕的坐标系上。
在安卓程序中,提供了相关的setRotation()方法和setDisplayOrientation()方法。其中,setRotation()方法的作用是将相机传感器获取到的位图坐标旋转一定的角度,而setDisplayOrientation()方法的作用是将预览时的画面旋转一定的角度。通过调用这两个方法,我们就可以实现图片的正常拍摄和预览了。如图3.6所示,在原有程序的基础上添加该方法, 再生成APK文件进行测试。
在这里插入图片描述

图3.6

重新安装APK文件,测试运行,运行结果如图3.7所示。可以看到,预览的图片已经可以正常显示了。然后再到test文件夹中查看camera.jpg文件(如图3.8) ,同样,拍摄的图片也是可以正常显示了。
在这里插入图片描述

图3.7

在这里插入图片描述

图3.8

3.2 文件命名问题及解决过程

目前的程序所能实现的功能,只能拍摄一张照片,如果拍摄多张照片,由于文件名相同,后面的文件就会将前面已存在的文件覆盖掉,很不方便。本次实验要求的是“拍照保存时按camera+序号.jpg的方式保存照片,序号自动增加”。

3.2.1问题分析

为了实现这个功能,需要在每次文件保存使,都要考虑我们需要生成的文件名是什么。如果企图保存当前程序生成的图片数量,在关闭APP之后,数据不易保存。所以我想的解决办法是:每次生成的文件名都需要经历两次函数,第一个函数需要返回所需文件夹中的所有文件,第二个函数需要判断指定的文件在该文件夹中是否存在,如果存在则返回一个我们需要的、正确的文件名。

3.2.2测试程序

① getFile()函数
为了不破坏原有的安卓程序,我在Eclipse中进行相关函数的编写和测试。首先编写第一个函数getFile(),该函数需要输入文件夹的路径字符串,输出该文件夹下的所有文件名。程序文件如图3.9所示:
在这里插入图片描述

图3.9

该程序,首先通过path参数获取指定的文件对象,再通过listFiles()方法列出该文件夹下,所有的文件,包括文件夹、普通文件等等,并将其保存在array数组中。之后逐个判断是否为普通文件,将文件名保存在list中,最后返回list。
现在,将path指定为桌面上的android文件夹,调用getFiles方法,查看程序输出。
图3.10是android文件夹下的文件信息,图11是程序输出的信息。
在这里插入图片描述

图3.10

在这里插入图片描述

图3.11

可以看到,除了临时文件,普通文件均可以正常输出。
② checkFileName方法
下面实现checkFileName函数,该方法的作用是:在文件名字符串中,判断所需要的字符串是否存在,如果存在则返回原文件名;不存在则重新调用,利用index参数,实现文件名的递增过程。最后返回一个正确的、合理的文件名。
checkFileName方法的实现过程是:首先将传入的文件名参数根据“.”进行分割,分成“.”之前的字符串+起始索引数字index+“.”字符+“.”之后的字符串,然后判断整个文件名是否包含于长文件名参数中,如果存在则再次调用该方法,同时起始索引数字加一;如果不存在,就直接返回上面连接好的文件名。
现在将checkFileName方法和getFile进行合并,通过main函数进行调用测试。首先在桌面的android目录下新建一个mydir目录,在mydir目录下新建一个test.txt文件,然后运行java程序进行测试。
在这里插入图片描述

图3.12

上图3.12展示了当前的mydir目录以及Java程序的运行结果。由于起始索引参数index设置的是0,而文件夹下只有一个test.txt文件,所以程序返回的结果“test.txt”是正确的。为了测试的准确性,我在mydir目录下又新建了一个test0.txt文件,如果程序功能正常,应该是会返回test1.txt。下面运行程序测试:
在这里插入图片描述

图3.13

可以看到,程序运行结果正确。

3.2.3安卓测试

现在可以放心地将代码移植到AndroidStudio程序中去了。在原有的onPictureTaken()方法的基础上,增加红色框中的内容,如图3.14所示。其含义为:通过getFile方法查找手机"/sdcard/test"目录下的所有文件,将结果返回至list中。然后通过调用checkFileName方法,检查list中是否有符合filename的文件名(filename已在之前定义),checkFileName方法会返回合理的文件名,再通过字符串连接,将完整的路径保存到path字符串中,作为完整的文件名路径。
为了更清晰的显示文件保存的路径和文件名信息,我在布局文件中增加了一个textview组件,在path生成之后,将path输出到textview显示,这样就会对文件的路径信息更加直观。
最后,将getFile和checkFileName方法都移植到java文件中,就可以运行测试了。
在这里插入图片描述

图3.14

同样重新生成APK文件测试运行,进行多次拍摄,下面是我拍摄第8次时的界面截图(图3.15)。
在这里插入图片描述

图3.15

因为文件名起始索引为0,所以第8次生成的文件名应该是camera7.jpg,可以从APP界面中看到文件的保存路径。现在在test文件夹下,会有8张我刚刚拍摄的图片,命名方式为camera+i。图3.16展示了test文件夹下的文件信息,文件的保存也是正常的。
在这里插入图片描述

图3.16

3.3 预览画面卡顿问题及解决过程

之前的多次测试中,我发现该程序的一个bug:在每次点击“拍照“按钮之后,预览画面就会卡住(静止且没有响应),而且如果此时再次点击”拍照“或”退出“按钮,程序就会意外退出。
经过查阅资料,我了解到,该问题是由以下原因造成的:
在执行拍照命令时,会调用camera.takePicture()方法,该方法在执行过程中会调用camera.stopPreview来获取拍摄帧数据,从而进行数据回调。而调用camera.stopPreview方法,就会暂停相机的预览,并表现为预览画面卡住。如果此时用户点击了按钮的话,也就会再次调用camera.takepicture方法,由于相机还没有开始预览,没有进行相关进程的初始化,就会出现之前遇到的意外退出问题。
解决的方法也很简单,既然camera.takePicture () 方法调用了camera.stopPreview来停止预览,那么只要在camera.takePicture()方法结束之后,手动调用一次camera.startPreview方法,来开启相机预览就可以了。
于是进行添加图3.17红框中的部分:
在这里插入图片描述

图3.17

再次生成APK文件进行测试,图3.18为测试结果。
在这里插入图片描述

图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("\n\n\nCamera.open() is OK !!!\n\n\n");

        //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版本的资料,并进行兼容性的改进。
②加功能
现在功能很单一,以后希望能加上摄像等功能,做到比安卓自带的相机功能更加完善(笑)。

七、实验总结

整个实验项目进行的还算顺利,一开始还有很多问题,最后基本上都通过查阅资料以及和同学的交流,解决了问题。
问题还算挺多的,主要是一些细节问题导致的,需要对整个工程的结构,以及各个函数的功能都理清楚,才能更好地解决问题。
调试也有很多技巧,最简便的是使用输出语句判断程序的执行,还有就是通过注释部分功能,判断出问题的位置等等。

八、部分参考资料

  1. 关于androidcamera相机的方向的理解http://blog.sina.com.cn/s/blog_68f23d9f0102y2cc.html
  2. Java检测文件名是否重复 https://blog.csdn.net/u014804332/article/details/80385217
  3. JAVA中方法的调用 https://www.imooc.com/article/13423
  4. 预览卡住解决办法思路引导
    https://www.jianshu.com/p/586af3a2dc8d?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation
  5. Android相机开发和遇到的坑 https://blog.csdn.net/xx326664162/article/details/53350551

猜你喜欢

转载自blog.csdn.net/ZHJ123CSDN/article/details/89880558