请参考教材,全面理解和完成本章节内容... ...
记录办公室陋习时,如果能以现场照片佐证,问题解决起来就会容易很多。接下来的两章,使用系统自带的Camera API,为CriminalIntent应用添加拍摄作案现场照片的功能。
Camera API功能虽然强大,但要用好它并不容易。不仅要编写大量的实现代码,还要苦苦挣扎着学习和理解一大堆全新概念。因此,很容易产生的一个疑问就是:“只是拍张快照,难道就没有便捷的标准接口可以使用吗?”
答案是肯定的。我们可以通过隐式intent与照相机进行交互。大多数Android设备都会内置相机应用。相机应用会自动侦听由MediaStore.ACTION_IMAGE_CAPTURE创建的intent。第21章将介绍如何使用隐式的intent。
本章将要创建一个基于fragment的activity,然后使用SurfaceView类配合相机硬件来实时展示现场的视频预览,如图19-1所示。
图19-1 viewfinder中的相机实时预览
同样,我们可以参考图19-2预览稍后会创建的新对象。
图19-2 CriminalIntent应用相机部分的对象图解
Camera实例提供了对设备相机硬件级别的调用。相机是一种独占性资源:一次只能有一个activity能够调用相机。
本章中,SurfaceView实例是相机的取景器。SurfaceView是一种特殊的视图,可直接将要显示的内容渲染输出到设备的屏幕上。
首先,我们会创建CrimeCameraFragment视图的布局、CrimeCameraFragment类及CrimeCameraActivity类。然后,在CrimeCameraFragment类中创建并管理一个用来拍照的取景器。最后,配置CrimeFragment启动CrimeCameraActivity实例。
19.1 创建 Fragment 布局
首先, 在strings.xml中,为我们即将添加的拍照按钮的添加字符串资源,如代码清单19-1所示。
代码清单19-1 为相机按钮添加字符串资源(strings.xml)
接下来,以FrameLayout为根元素,创建一个名为fragment_crime_camera.xml的布局文件。然后参照图19-3完成各组件的添加。
图19-3 CrimeCameraFragment的布局(fragment_crime_camera.xml)
可以看到,新建布局文件中,FrameLayout只包含唯一一个LinearLayout子元素,这会导致Android Lint报出没有用处的LinearLayout警告信息。暂时忽略它,第20章将为FrameLayout添加第二个子视图。
在SurfaceView组件定义中,我们使用layout_width与layout_weight的属性组合来布置它的子视图。因为设置的android:layout_width="wrap_content"属性值,Button组件仅占用了自己所需的空间,而按照android:layout_width="0dp"的属性值,SurfaceView组件不占用任何空间。不过从剩余空间的角度来说,因为使用了layout_weight属性,所以SurfaceView组件使用了Button组件以外的全部空间。
图19-4展示了新建布局的预览界面。
图19-4 取景器与按钮界面
19.2 创建 CrimeCameraFragment
以android.support.v4.app.Fragment为超类,创建一个名为CrimeCameraFragment的新类。在随后打开的CrimeCameraFragment.java中,增加如代码清单19-2所示的变量。然后,覆盖onCreateView(...)方法,实例化布局并引用各组件。
注: android.hardware.Camera已经过时,import android. hardware.Camera;;
现在,先为按钮设置一个事件监听器,用户点击按钮时,退出当前托管activity并回到列表项明细界面。
代码清单19-2 初始相机fragment类(CrimeCameraFragment.java)
19.3 创建 CrimeCameraActivity
以SingleFragmentActivity
为超类,创建一个名为CrimeCameraActivity
的新类。在CrimeCameraActivity.java中,覆盖createFragment()
方法返回一个CrimeCameraFragment
,如代码清单19-3所示。
代码清单19-3 创建相机的activity类(CrimeCameraActivity.java)
声明activity并添加允许调用相机设置
接下来,还需要在配置文件中增加uses-permission
元素节点以获得使用相机的权限。参照代码清单19-4,更新AndroidManifest.xml配置文件。
代码清单19-4 声明相机的activity并添加允许调用相机设置(AndroidManifest.xml)
uses-feature
元素用来指定应用使用的某项特色设备功能。通过android. hardware.camera特色功能的设置,可以保证只有那些配备相机功能的设备才能够看到你发布在Google Play上的应用。
注意,在activity的声明中,为了防止用户在调整角度取景拍照时,设备屏幕随意旋转,我们使用android:screenOrientation
属性强制activity界面总是以水平模式展现。
属性android:screenOrientation
还有很多其他可选属性值。例如,可以设置activity与其父类保持一致的显示方位,也可以选择在设备处于不同方向时,根据设备感应器感应只以水平模式显示。可以查看开发文档的<activity>
元素获取更多其他可用属性值信息。
19.4 使用相机 API
目前为止,我们一直在处理基本的activity创建工作。现在,是时候学习理解相机相关的概念并着手使用相机类了。
19.4.1 打开并释放相机
首先,来进行相机资源的管理。我们已经在CrimeCameraFragment
中添加了一个Camera
实例。相机是一种系统级别的重要资源,因此,很关键的一点就是,需要时使用,用完及时释放。如果忘记释放,除非重启设备,否则其他应用将无法使用相机。
以下是将要用来管理Camera
实例的方法:
public static Camera open(int cameraId)
public static Camera open()
public final void release()
其中open(int)
方法是在API级别第9级引入的,因此,如果设备的API级别小于第9级,那么就只能使用不带参数的open()
方法。
在CrimeCameraFragment
生命周期中,我们应该在onResume()
和onPause()
回调方法中打开和释放相机资源。这两个方法可确定用户能够同fragment视图交互的时间边界,只有在用户能够同fragment视图交互时,相机才可以使用。(注意,即使fragment首次开始出现在屏幕上,onResume()
方法也会被调用。)
在CrimeCameraFragment.onResume()
方法中,使用Camera.open(int)
静态方法来初始化相机。然后传入参数0打开设备可用的第一相机(通常指的是后置相机)。如果设备没有后置相机(如Nexus 7机型),那么前置相机将会打开。
对于API级别第8级的设备来说,需要调用不带参数的Camera.open()
方法。针对onResume()
方法使用@TargetApi
注解保护,然后检查设备的编译版本,根据设备不同的版本号确定是调用Camera.open(0)
方法还是Camera.open()
方法,如代码清单19-5所示。
代码清单19-5 在onResume()
方法中打开相机(CrimeCameraFragment.java)
Fragment
被销毁时,应该及时释放相机资源,以便于其他应用需要时可以使用。覆盖onPause()
方法释放相机资源,如代码清单19-6所示。
代码清单19-6 实现生命周期方法(CrimeCameraFragment.java)
注意,调用release()
方法之前,首先要确保存在Camera
实例。调用相机相关代码前,都应该作这样的检查。要知道,即使是请求获取相机的使用权限,相机也可能无法获得。比如,另一个activity正在使用它,或者因为是虚拟设备,相机根本就不存在。总之,不管是什么原因,相机实例不存在时,空值检查可以防止应用意外崩溃。
19.4.2 SurfaceView
、SurfaceHolder
与Surface
SurfaceView是什么?
SurfaceView也是一种视图,他与普通View主要区别是,View是在UI的主线程中更新画面,而SurfaceView是在一个新线程中更新画面。View的特性决定了其不适合做动画,因为如果更新画面时间过长,那么主UI线程就会被正在画的函数阻塞。所以Android中通常用SurfaceView显示动画效果。
通常,SurfaceView就是一个绘图容器,它可以在主线程之外的线程中向屏幕绘图上。这样可以避免画图任务繁重的时候造成主线程阻塞,从而提高了程序的反应速度。在游戏开发中多用到SurfaceView,游戏中的背景、人物、动画等等尽量在画布canvas中画出。同样,我们也可以利用SurfaceView显示相机Camera捕获的图像。
SurfaceView
类实现了SurfaceHolder
接口。在CrimeCameraFragment.java中,增加以下代码获取SurfaceView
的SurfaceHolder
实例,如代码清单19-7所示。
代码清单19-7 获取SurfaceHolder
实例(CrimeCameraFragment.java)
setType(...)
方法和SURFACE_TYPE_PUSH_BUFFERS
常量都已被弃用,因此,对于废弃代码。android studio也会将弃用代码打上删除线标示出来。
SurfaceHolder
是我们与Surface
对象联系的纽带。Surface
对象代表着原始像素数据的缓冲区。
Surface
对象也有生命周期:SurfaceView
出现在屏幕上时,会创建Surface
;SurfaceView
从屏幕上消失时,Surface
随即被销毁。Surface
不存在时,必须保证没有任何内容要在它上面绘制。
图19-5 SurfaceView
、SurfaceHolder
及Surface
不像其他视图对象,SurfaceView
及其协同工作对象都不会自我绘制内容。对于任何想将内容绘制到Surface
缓冲区的对象,我们称其为Surface
的客户端。在CrimeCameraFragment
类中,Camera
实例是Surface
的客户端。
记住,Surface
不存在时,必须保证没有任何内容要在Surface
的缓冲区中绘制。图19-6展示了需要处理的两种可能情况,Surface
创建完成后,需要将Camera
连接到SurfaceHolder
上;Surface
销毁后,再将Camera
从SurfaceHolder
上断开。
图19-6 理想的工作状态
为完成以上任务,SurfaceHolder
提供了另一个接口:SurfaceHolder.Callback
。该接口监听Surface
生命周期中的事件,这样就可以控制Surface
与其客户端协同工作。
在CrimeCameraFragment.java中,实现SurfaceHolder.Callback
接口,使得相机预览与Surface
生命周期方法能够协同工作,如代码清单19-8所示。
代码清单19-8 实现SurfaceHolder.Callback
接口(CrimeCameraFragment.java)
注意,预览启动失败时,我们通过异常控制机制释放了相机资源。任何时候,打开相机并完成任务后,必须记得及时释放它,即使是在发生异常时。
在surfaceChanged(...)
实现方法中,我们设置相机预览大小为空。在确定可接受的预览大小前,这只是一个临时赋值。相机的预览大小不能随意设置,如果设置了不可接受的值,应用将会抛出异常。
19.4.3 确定预览界面大小
首先,通过Camera.Parameters
嵌套类获取系统支持的相机预览尺寸列表。Camera.Parameters
类包括下列方法:
public List<Camera.Size> getSupportedPreviewSizes()
该方法返回android.hardware.Camera.Size
类实例的一个列表,每个实例封装了一个具体的图片宽高尺寸。
要找到适合Surface
的预览尺寸,可以将列表中的预览尺寸与传入surfaceChanged()
方法的Surface
的宽、高进行比较。
在CrimeCameraFragment
类中,添加代码清单19-9所示的方法。该方法接受一组预览尺寸,然后找出具有最大数目像素的尺寸。要说明的是,这里计算最佳尺寸的实现代码并不优雅,但它能够很好地满足我们的使用需求。
代码清单19-9 找出设备支持的最佳尺寸(CrimeCameraFragment.java)
在surfaceChanged(...)
方法里调用该方法设置预览尺寸,如代码清单19-10所示。
代码清单19-10 调用getBestSupportedSize(...)
方法(CrimeCameraFragment.java)
19.4.4 启动 CrimeCameraActivity
要使用取景器,需要在CrimeFragment
用户界面添加一个相机调用按钮。单击相机按钮,CrimeFragment
将启动一个CrimeCameraActivity
实例。图19-7展示了已添加相机按钮(红框内)的CrimeFragment
视图界面。
图19-7 添加了相机按钮的CrimeFragment
要实现图19-7所示的组件重排后的用户界面,需要添加三个LinearLayout
和一个ImageButton
。参照图19-8分别修改reslayout和reslayout-land下面的CrimeFragment
布局文件fragment_crime.xml。
图19-8 添加相机按钮并重新布置布局(layout/fragment_crime.xml)
参照图19-9完成类似的水平布局调整。
图19-9 添加相机按钮并重新布置布局(layout-land/fragment_crime.xml)
在CrimeFragment
类中,新增一个成员变量,通过资源ID引用图片按钮,然后为其设置OnClickListener
,启动CrimeCameraActivity
,如代码清单19-11所示。
代码清单19-11 启动CrimeCameraActivity
(CrimeFragment.java)
对于不带相机的设备,拍照按钮(mPhotoButton
)应该禁用。可以查询PackageManager
确认设备是否带有相机。在onCreateView(...)
方法中,针对没有相机的设备,添加禁用拍照按钮的代码,如代码清单19-12所示。
代码清单19-12 检查设备是否带有相机(CrimeFragment.java)
获取到PackageManager
后,调用hasSystemFeature(String)
方法并传入表示设备特色功能的常量。FEATURE_CAMERA
常量代表后置相机,而FEATURE_CAMERA_FRONT
常量代表前置相机。对于没有相机的设备,调用ImageButton
按钮的setEnabled(false)
属性方法。
运行CriminalIntent应用。查看某项Crime明细记录,然后点击拍照按钮查看相机实时预览画面。但现在并不能拍照和保存,此类功能将会在下一章完成,现在点击「取证」按钮将返回CrimeFragment
视图,如图19-10所示。
图19-10 来自相机的实时预览画面
前面我们已经在配置文件中强制CrimeCameraActivity
界面总是以水平模式展现。尝试旋转设备,可以看到,即使设备处于竖直模式,预览和拍照按钮都被锁定以水平模式展现了。
隐藏状态栏和操作栏
如图19-10所示,activity的操作栏和状态栏遮挡了部分取景器窗口。一般来说,用户只关注取景器中的画面,而且也不会在拍照界面停留很久,因此,操作栏和状态栏不仅没有什么用处,甚至还会妨碍拍照取景,如果能隐藏它们那最好不过了。
有趣的是,我们只能在CrimeCameraActivity
中而不能在CrimeCameraFragment
中隐藏操作栏和状态栏。打开CrimeCameraActivity.java文件,参照代码清单19-13,在onCreate(Bundle)
方法中添加隐藏功能代码。
代码清单19-13 配置activity(CrimeCameraActivity.java)
为什么必须在activity中实现隐藏呢?在调用Activity.setContentView(...)
方法(该方法是在CrimeCameraActivity
类的onCreate(Bundle)
超类版本方法中被调用的。)创建activity
视图之前,就必须调用requestWindowFeature(...)
方法及addFlags(...)
方法。而fragment无法在其托管activity视图创建之前添加,因此,必须在activity里调用隐藏操作栏和状态栏的相关方法。
再次运行CriminalIntent应用。现在看到的是一个没有遮挡的取景器窗口,如图19-11所示。
图19-11 隐藏了状态栏和操作栏的activity画面
下一章将介绍更多camera API相关内容,实现在本地保存图像文件并在CrimeFragment视图中显示。