扩展自定义相机应用程序
在我看来,Android 上的内置相机应用程序缺少几个基本特征。其中之一是,延迟一小段时间,10或者30秒,之后进行拍摄。此种功能对于那些可以安装在三脚架上的相机来说,通常很实用。它提供了这样的功能,摄影师设置好镜头,设定好计时器,然后自己跑到镜头里。
虽然对于移动电话而言,可能不是很常用。但在某些特殊场景,却非常有用的。例如,当我想要和同伴一起拍照时,就非常喜欢这个功能。目前当我尝试这样做时,因为反对着屏幕,看不见触屏界面,拍照就变得非常麻烦。在屏幕里到处摸索乱按,希望能碰巧按下快门按钮。
建立一个基于计时器的相机应用程序
为了扭转刚才所述的情况,我们可以为拍摄增加一个延迟时间。让我们更新我们的SnapShot示例,拍摄动作在按下按键10秒后进行。为了实现这个目标,我们需要使用某些类似 java.util.Timer 的东西。不幸的是,在 android 系统,使用计时器比较复杂,它会引入单独的线程。而单独线程要与UI进行交互,需要通过Handler,才能让主线程执行某一动作。
Handler的另一个用法是,调度某个动作,在未来发生。有了Handler的这一功能,就不必使用Timer了。
若要创建一个Handler对象,在将来执行某些动作,我们只需构造一个通用对象:
Handler timerHandler = new Handler();
然后,我们必须创建一个Runnable对象。Runnable将要执行的动作,放到它的run方法中。在我们的例子里,我们想要在10秒以后,执行图片拍摄:
Runnable timerTask = new Runnable()
{
public void run()
{
camera.takePicture(null,null,null,TimerSnapShot.this);
}
};
这就够了。现在当我们按下按钮时,我们只需要做好调度:
timerHandler.postDelayed(timerTask, 10000);
这会告诉 timerHandler 在10秒(10000 毫秒)后调用我们的timerTask。在下面的示例中,我们创建一个Handler,让它每隔1秒,就调用某个方法。以这种方式,我们可以为用户在屏幕上提供倒计时。
package com.apress.proandroidmedia.ch2.timersnapshot;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Iterator;
import java.util.List;
import android.app.Activity;
import android.content.ContentValues;
import android.content.res.Configuration;
import android.hardware.Camera;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.provider.MediaStore.Images.Media;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
public class TimerSnapShot extends Activity implements OnClickListener,
SurfaceHolder.Callback, Camera.PictureCallback {
SurfaceView cameraView;
SurfaceHolder surfaceHolder;
Camera camera;
这个 activity 非常类似我们的 SnapShot activity。我们打算添加一个 Button 来触发的倒计时, 和一个 TextView 来显示倒计时。
Button startButton;
TextView countdownTextView;
我们还需要一个 Handler,本例中名为 timerUpdateHandler,一个布尔量(timerRunning),帮助我们记录是否启动了计时器,还有一个整数(currentTime),记录倒计时读数。
Handler timerUpdateHandler;
boolean timerRunning = false;
int currentTime = 10;
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
cameraView = (SurfaceView)this.findViewById(R.id.CameraView);
surfaceHolder = cameraView.getHolder();
surfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
surfaceHolder.addCallback(this);
下一步,我们将取得新UI元素(在布局XML中定义)的引用,并使我们的 activity 成为 Button 的 OnClickListener。我们可以这样做,是因为我们的 activity 实现了 OnClickListener。
countdownTextView = (TextView) findViewById(R.id.CountDownTextView);
startButton = (Button) findViewById(R.id.CountDownButton);
startButton.setOnClickListener(this);
最后,在我们onCreate方法中, 要做的是实例化Handler对象。
timerUpdateHandler = new Handler();
}
我们的onClick方法在按下startButton按钮时被调用。我们会检查timerRunning,看定时器例程是否已经运行,如果没有,我们通过Handler对象timerUpdateHandler,非延迟调用 Runnable timerUpdateTask。
public void onClick(View v)
{
if (!timerRunning)
{
timerRunning = true;
timerUpdateHandler.post(timerUpdateTask);
}
}
这是我们的 Runnable 对象 timerUpdateTask。它包含run方法,由我们的timerUpdateHandler对象触发。
private Runnable timerUpdateTask = new Runnable()
{
public void run()
{
如果记录倒计时计数的整数currentTime大于1,则递减之,并让Handler在1秒后再度调用本Runnable。
if (currentTime > 1)
{
currentTime--;
timerUpdateHandler.postDelayed(timerUpdateTask, 1000);
}
else
{
如果currentTime不大于1,我们将让相机进行拍照并重置所有的记录变量。
camera.takePicture(null,null ,TimerSnapShot.this);
timerRunning = false;
currentTime = 10;
}
不管结果如何,我们将更新 TextView 来显示当前的剩余时间。
countdownTextView.setText(""+currentTime);
}
};
本 activity 的其余部分,与前述的SnapShot示例基本一样。
public void surfaceChanged(SurfaceHolder holder, int format, int w, int h)
{
camera.startPreview();
}
public void surfaceCreated(SurfaceHolder holder)
{
camera = Camera.open();
try {
camera.setPreviewDisplay(holder);
Camera.Parameters parameters = camera.getParameters();
if (this.getResources().getConfiguration().orientation
!= Configuration.ORIENTATION_LANDSCAPE)
{
parameters.set("orientation", "portrait");
// Android 2.2 及以上版本
camera.setDisplayOrientation(90);
// Android 2.0 及以上版本
parameters.setRotation(90);
}
camera.setParameters(parameters);
}
catch (IOException exception)
{
camera.release();
}
}
public void surfaceDestroyed(SurfaceHolder holder)
{
camera.stopPreview();
camera.release();
}
public void onPictureTaken(byte[] data, Camera camera)
{
Uri imageFileUri = getContentResolver()
.insert(Media.EXTERNAL_CONTENT_URI, new ContentValues());
try
{
OutputStream imageFileOS = getContentResolver()
.openOutputStream(imageFileUri);
imageFileOS.write(data);
imageFileOS.flush();
imageFileOS.close();
Toast t = Toast.makeText(this,"Saved JPEG!",Toast.LENGTH_SHORT);
t.show();
}
catch (FileNotFoundException e)
{
Toast t = Toast.makeText(this,e.getMessage(), Toast.LENGTH_SHORT);
t.show();
}
catch (IOException e)
{
Toast t = Toast.makeText(this,e.getMessage(),Toast.LENGTH_SHORT);
t.show();
}
camera.startPreview();
}
}
XML 布局有点不同。在此应用程序中,我们用于显示相机预览的 SurfaceView 包含在一个FrameLayout中,与之并列的还有 LinearLayout,其包含了用于显示倒计时计数的 TextView 和 触发倒计时的 Button。FrameLayout 让所有子项以左上角对齐,彼此之间顶部对齐。这样 TextView 和 Button 出现在相机预览顶部。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<FrameLayout android:id="@+id/FrameLayout01"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<SurfaceView android:id="@+id/CameraView"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
</SurfaceView>
<LinearLayout android:id="@+id/LinearLayout01"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView android:id="@+id/CountDownTextView"
android:text="10"
android:textSize="100dip"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|center_horizontal|center">
</TextView>
<Button android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/CountDownButton"
android:text="Start Timer">
</Button> </LinearLayout>
</FrameLayout>
</LinearLayout>
最后,我们需要确保我们的 AndroidManifest.xml 文件包含Camera权限。
<uses-permission android:name="android.permission.CAMERA">
</uses-permission>
图 2-5. 带倒计时相机
建立一个定时摄影应用程序
我们都看到过漂亮的定时摄影例子。就是在一段时间内,拍摄多张照片,每次间隔相同的时间。可以是每分钟一张,每小时一张,甚至每星期一张。通过一系列定时拍摄的照片,我们可以看到事物随时间的变化,比如观察正在建造的建筑物,记录一朵花如何生长和开放。
现在,我们已建立一个基于计时器的相机应用程序,将它升级为一个定时程序是相当简单。首先我们会更改了一些实例变量和添加一个常量。
...
public class TimelapseSnapShot extends Activity implements OnClickListener,
SurfaceHolder.Callback, Camera.PictureCallback {
SurfaceView cameraView;
SurfaceHolder surfaceHolder;
Camera camera;
我们把Button重命名为startStopButton,因为它现在会处理两个操作。另外对其他变量的名字也做些小的修改。
Button startStopButton;
TextView countdownTextView;
Handler timerUpdateHandler;
boolean timelapseRunning = false;
整数currentTime将以秒为单位,记录照片的时间间隔, 而不是从总延时往下递减,如在前面的例子中那样。常数 SECONDS_BETWEEN_PHOTOS 设置为 60。如同它的名字所暗示,这将用于确定照片之间的等待时间。
int currentTime = 0;
public static final int SECONDS_BETWEEN_PHOTOS = 60; // 一分钟
onCreate方法大部分保持不变 - 只是使用新的变量名。
@Override
public void onCreate(Bundle savedInstanceState)
{ super.onCreate(savedInstanceState);
setContentView(R.layout.main);
cameraView = (SurfaceView) this.findViewById(R.id.CameraView);
surfaceHolder = cameraView.getHolder();
surfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
surfaceHolder.addCallback(this);
countdownTextView = (TextView)findViewById(R.id.CountDownTextView);
startStopButton = (Button) findViewById(R.id.CountDownButton);
startStopButton.setOnClickListener(this);
timerUpdateHandler = new Handler();
}
从基于计时器的应用程序,变为一个定时器应用程序,大部分变化来自 onClick 方法 和 Runnable 方法。前者在按钮被按下时触发,后者由Handler进行调度。onClick 方法首先检查定时进程是否已经开始(Button 已经按过),如果没有,它将其设置为运行态,并以 Runnable 为参数,调用 Handler 的post方法。如果是在定时过程中,按下按钮意味着停止定时,从而 timerUpdateHandler 的 removeCallbacks 方法被调用。这将清除任何挂起的Runnable对象。
public void onClick(View v)
{
if (!timelapseRunning)
{
startStopButton.setText("Stop");
timelapseRunning = true;
timerUpdateHandler.post(timerUpdateTask);
}
else
{
startStopButton.setText("Start");
timelapseRunning = false;
timerUpdateHandler.removeCallbacks(timerUpdateTask);
}
}
我们用一个Handler来做调度,当时间到了之后,Handler将调用Runnable。在我们Handler的run方法中,我们先检查整数currentTime是否小于我们照片间隔秒数 (SECONDS_BETWEEN_PHOTOS)。如果是,我们只需增加currentTime。如果currentTime不小于等待周期,我们告诉Camera执行拍照,并将currentTime设置回 0,继续计数。每次循环之后,我们以新currentTime的值,更新TextView显示,并调度下一次循环。
private Runnable timerUpdateTask = new Runnable()
{
public void run()
{
if (currentTime < SECONDS_BETWEEN_PHOTOS)
{
currentTime++;
}
else
{
camera.takePicture(null,null,null,TimelapseSnapShot.this);
currentTime = 0;
} timerUpdateHandler.postDelayed(timerUpdateTask, 1000);
countdownTextView.setText(""+currentTime);
}
};
本例的res/layout/main.xml 接口,当然还有AndroidManifest.xml 跟单计时器版相同。
摘要
正如你所看到的,有众多原因我们可能想要建立我们自己的基于相机的应用程序,而不是只在我们的应用程序中使用内置的Camera应用。没有什么能够限制你能做的,从简单地创建一个倒计时拍照应用程序,到建立你自己的定时系统,以及更多。继续前进,我们看看我们能对捕获的图像做些什么。