Android Camera Develop: a preview only example

概述

作为系列的第一篇,就当做是Android开发入门了。本文主要讲解怎么在Android Studio下,从最开始慢慢实现一个只能拿来预览(不能拍照)的相机APP。

新建一个空的APP

打开Android Studio,选择New->New Project,然后给你的APP起个名字,比如:

New Project

然后设置支持的最低API等级,我选择的是API 19: Android 4.4

Target Android Devices

再之后选择默认Activity,为了让我们能够更清楚Android的架构,选择Add No Activity

Add an Activity to Mobile

这样我们的空APP就生成了,文件目录大概会是这个样子:

Empty File List

这个APP还不能够运行,如果你尝试运行,Android Studio会报错

添加外观,让APP能够运行

添加一个layout

res下新建一个文件夹layout,在layout下新建一个文件activity_main.xml

Layout File List

activity_main.xml下会出现一个编辑布局的窗口,这时点击左下角的Text,即activity_main.xml的文本编辑模式,在其中加入如下代码:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:text="Hello World!" />
</LinearLayout>

这个activity_main.xml就是一个布局文件,而我们刚才设计了一个布局,布局方式是LinearLayout,其中只有一个TextView,其内容是Hello World!

使用这个layout

新建好一个布局文件后,我们需要让APP使用这个布局。

java->com.polarxiong.camerademo(这个根据你自己写的APP名字来)下新建一个Java Class MainActivity

Main Activity File List

新建后会自动创建一个空的Java类:

public class MainActivity {
}

接下来在这个类中加入一些代码:

import android.app.Activity;
import android.os.Bundle;

public class MainActivity extends Activity {
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

解释一下,Activity类可以创建一个“窗口”,就是APP运行时的那个窗口,而通过Activity这个“窗口”添加布局,就能在窗口中应用这个布局,让用户能够看到;所以MainActivity需要继承Activity,作为这个APP的主窗口。

onCreate()重载了ActivityonCreate(),在窗口被创建时执行这个方法,setContentView()即设置这个窗口的布局,这里我们选择刚才创建的布局R.layout.activity_main。这样当MainActivity这个窗口创建时,就会应用activity_main这个布局。

添加APP入口

我们希望APP在打开时就会显示MainActivity这个窗口,但我们怎么告诉APP呢?答案是manifests下的AndroidManifest.xml文件,这个文件对APP来说是至关重要的,其中定义了关于APP的一些重要信息,AndroidManifest.xml会在APP的代码执行之前就会读取。

AndroidManifest.xml初始时大概是这样:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.polarxiong.camerademo">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">

    </application>

</manifest>

现在我们给APP添加一个入口,也就是一个activity:

<activity android:name=".MainActivity">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

其中activity android:name=".MainActivity"就是指定了刚才的窗口MainActivity,修改后的AndroidManifest.xml像这样:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.polarxiong.camerademo">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

运行

现在点击Android Studio上方的运行按钮就可以运行这个APP啦,你可以选择在真机或模拟器下运行,运行的效果像这样:
TextView Screenshot

让APP显示相机预览

想让相机拍到的画面显示在APP的窗口中,简单来说就是要启动相机,并且将相机内容绑定到窗口。

修改布局

想要相机预览显示在窗口中,实际上就是要显示在布局中,我们首先在布局中给相机预览留个位置,对于这个APP来说的话,就是把全部的位置都留给相机预览了。

修改activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <FrameLayout
        android:id="@+id/camera_preview"
        android:layout_width="0px"
        android:layout_height="fill_parent"
        android:layout_weight="1"
        />
</LinearLayout>

我们会让相机预览填充在FrameLayout中,但因为这个填充会在Java代码中实现,所以目前我们只给它指定一个ID,供以后使用,ID名字就是camera_preview;而剩下的就是在设置这个FrameLayout所占布局大小,设置为占有全部。

添加CameraPreview类

从布局层面来说,我们想要添加相机预览实际上就是在FrameLayout中再添加一个View,这个View可以理解为一个“控件”,就像之前的TextView,也是View中的一种,只不过相机预览这个View的内容是会一直变化的预览帧。因为Android并没有提供相机预览这个View,所以需要我们自己创造一个,而这个View我们就起名叫做CameraPreview

java->com.polarxiong.camerademo(这个根据你自己写的APP名字来)下新建一个Java Class CameraPreview,并修改其内容为:

import android.content.Context;
import android.hardware.Camera;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

import java.io.IOException;

public class CameraPreview extends SurfaceView implements SurfaceHolder.Callback {
    private static final String TAG = "CameraPreview";
    private SurfaceHolder mHolder;
    private Camera mCamera;

    public CameraPreview(Context context) {
        super(context);
        mHolder = getHolder();
        mHolder.addCallback(this);
    }

    private static Camera getCameraInstance() {
        Camera c = null;
        try {
            c = Camera.open();
        } catch (Exception e) {
            Log.d(TAG, "camera is not available");
        }
        return c;
    }

    public void surfaceCreated(SurfaceHolder holder) {
        mCamera = getCameraInstance();
        try {
            mCamera.setPreviewDisplay(holder);
            mCamera.startPreview();
        } catch (IOException e) {
            Log.d(TAG, "Error setting camera preview: " + e.getMessage());
        }
    }

    public void surfaceDestroyed(SurfaceHolder holder) {
        mHolder.removeCallback(this);
        mCamera.setPreviewCallback(null);
        mCamera.stopPreview();
        mCamera.release();
        mCamera = null;
    }

    public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
    }
}

SurfaceView是一个包含有SurfaceView,而Surface用来处理直接呈现在屏幕上的内容,这个我们不细究,可以认为是相机预览的原始数据交给Surface就能转换成呈现在屏幕上的样子,这也是为什么CameraPreview一定要继承SurfaceViewCameraPreview还使用了SurfaceHolder.Callback接口,这个接口包含有三个方法:surfaceChanged()surfaceCreated()surfaceDestroyed(),这三个方法会对应在Surface内容变化、Surface生成和Surface销毁时触发。

成员变量mHolder保存这个Surface的“持有者”(还是Holder顺口),而只有Holder才能对对应的Surface进行修改。成员变量mCamera保存相机Camera的实例。

先看构造函数,构造函数实际就是指定本View(即CameraPreview)是这个SurfaceHolder

对于SurfaceView来说,这个View创建时就会创建Surface,而当Surface创建时就会触发surfaceCreated(),所以我们就要在surfaceCreated()中打开相机、开始预览,并将预览帧交给Surface处理。getCameraInstance()是一个比较安全的获取并打开相机的方法,很简单。CamerasetPreviewDisplay()方法就是告知将预览帧数据交给谁,这里当然就是这个SurfaceHolder了;最后用startPreview()开启相机,这样我们就完成了整个过程,创建好了CameraPreview类。

但我们还要做一些善后处理,相机是共享资源,在APP运行结束后就应当“放弃”相机。我们在APP退出,即surfaceDestroyed()中完成这些善后处理,surfaceDestroyed()中的代码很简单,不需要详细说明,就是构造函数和
surfaceCreated()的逆过程。

我们目前还不需要surfaceChanged(),所以代码留空。

CameraPreview加入到FrameLayout

上一步只是创建了一个View,而现在就是要将这个View添加到activity_main中,因为这个View是实时创建的,当然我们不能直接去修改activity_main.xml,而应当在MainActivity中用代码添加。

修改MainActivity,在onCreate()最后添加:

CameraPreview mPreview = new CameraPreview(this);
FrameLayout preview = (FrameLayout) findViewById(R.id.camera_preview);
preview.addView(mPreview);

代码首先创建一个CameraPreview的实例mPreview,再在布局中通过ID找到FrameLayout,最后在FrameLayout中添加mPreview。修改之后的MainActivity就是:

import android.app.Activity;
import android.os.Bundle;
import android.widget.FrameLayout;

public class MainActivity extends Activity {
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        CameraPreview mPreview = new CameraPreview(this);
        FrameLayout preview = (FrameLayout) findViewById(R.id.camera_preview);
        preview.addView(mPreview);
    }
}

在AndroidManifest中申请和声明相机

现在APP启动时就会调用MainActivity,而MainActivity创建时就会创建CameraPreviewCameraPreview创建时则会调用相机并开启相机预览。现在还存在的一个问题是APP启动后才会调用相机,而很显然我们希望APP在安装时就告知Android需要用到相机,这就是AndroidManifest要干的事情啦。

AndroidManifest.xmlmanifest下加入:

<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" />

AndroidManifest.xmlactivity内加入:

android:screenOrientation="landscape"

最后AndroidManifest.xml像这样:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.polarxiong.camerademo">
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-feature android:name="android.hardware.camera" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity"
            android:screenOrientation="landscape">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

uses-permission是APP申请相机权限,这样在安装APP时就会显示APP需要相机;uses-feature则可以防止APP被安装到没有相机的Android设备上(目前仅Google Play支持)。android:screenOrientation则是设置APP的方向,水平或竖直,这里设置为水平,因为从实际来说,相机预览一直是水平的。

运行

上述步骤完成后整个APP就算开发完毕了,虽然基本没啥功能,但总算是能跑起来了。如下:
Photo

一点唠叨

很显然这个“相机”APP还不能算真正的相机,不能拍照,而且还支持对焦,这个屏幕一篇模糊简直不能用。但如果你能成功写出这个APP,本文的目的也算达到了,至少你已经大体明白了Android开发的步骤,以及Android APP运行的基本过程。随后的系列文章会基于这个APP扩展各方面的功能,欢迎继续阅读。

DEMO

本文实现的相机APP源码都放在GitHub上,如果需要请点击zhantong/AndroidCamera-PreviewOnly

参考