请参考教材,全面理解和完成本章节内容... ...
本章将从相机预览里拍摄照片并保存为JPEG格式的本地文件。然后,将照片与Crime关联起来并显示在CrimeFragment的视图中。如果需要,用户也可以选择在DialogFragment中查看大尺寸版本的图片,如图20-1所示。
图20-1 Crime的缩略图以及大尺寸图片展示
20.1 拍摄照片
首先,我们来升级CrimeCameraFragment
的布局,为其添加一个进度指示条组件。相机拍摄照片的过程可能比较耗时,有时需要用户等一会儿,为了不让用户失去耐心,添加进度指示条非常有必要。
在fragment_crime_camera.xml布局文件中,参照图20-2,添加FrameLayout
和ProgressBar
组件。
图20-2 添加FrameLayout
和ProgressBar
组件(fragment_crime_camera.xml)
代替默认的普通大小圆形进度环,@android:style/Widget.ProgressBar.Large
样式将创建一个粗大的圆形旋转进度环,如图20-3所示。
图20-3 旋转的进度环
FrameLayout
(包括它的ProgressBar
子组件)的初始状态设置为不可见。只有在用户点击Take!按钮开始拍照时才可见。
返回到CrimeCameraFragment.java中,为FrameLayout
组件添加成员变量,然后通过资源ID引用它并设置为不可见状态,如代码清单20-1所示。
代码清单20-1 配置使用FrameLayout
视图(CrimeCameraFragment.java)
20.1.1 实现相机回调方法
既然进度环的添加设置已完成,接下来实现从相机的实时预览中捕获一帧图像,然后将它保存为JPEG格式的文件。要拍摄一张照片,需调用以下见名知意的Camera
方法:
public final void takePicture(Camera.ShutterCallback shutter, Camera.PictureCallback raw, Camera.PictureCallback jpeg)
在CriminalIntent应用中,实现ShutterCallback
回调方法以及JPEG版本的PictureCallback
回调方法。图20-4展示了这些对象之间的交互关系。
图20-4 在CrimeCameraFragment
中拍照
下面是需要实现的两个接口,每个接口含有一个待实现的方法:
public static interface Camera.ShutterCallback {
public abstract void onShutter();
}
public static interface Camera.PictureCallback {
public abstract void onPictureTaken (byte[] data, Camera camera);
}
在CrimeCameraFragment.java中,实现Camera.ShutterCallback
接口显示进度环视图,实现Camera.PictureCallback
接口命名并保存已拍摄的JPEG图片文件,如代码清单20-2所示。
代码清单20-2 实现传入takePicture(...)
方法的接口(CrimeCameraFragment.java)
在onPictureTaken(...)
方法中,创建了一个UUID字符串作为图片文件名。然后,使用Java I/O类打开一个输出流,将从Camera
传入的JPEG数据写入文件。如果一切操作顺利,程序会输出一条文件保存成功的日志。
完成了回调方法的处理,接下来就是修改Take!按钮的监听器方法,实现对takePicture(...)
方法的调用。对于没有实现的接收处理原始图像数据的回调方法,记得传入null
值,如代码清单20-3所示。
代码清单20-3 实现takePicture(...)
按钮单击事件方法 (CrimeCameraFragment.java)
20.1.2 设置图片尺寸大小
相机需要知道创建多大尺寸的图片。设置图片尺寸与设置预览尺寸一样。可以调用以下Camera.Parameters
方法获得可用的图片尺寸的列表:
public List<Camera.Size> getSupportedPictureSizes()
在surfaceChanged(...)
方法中,使用getBestSupportedSize(...)
方法获得支持的适用于Surface
的图片尺寸。最后将获得的尺寸设置为相机要创建的图片尺寸,如代码清单20-4所示。
代码清单20-4 调用getBestSupportedSize(...)
方法设置图片尺寸(CrimeCameraFragment.java)
运行CriminalIntent应用,然后点击Take!按钮。在LogCat中,创建一个以CrimeCameraFragment
为标签的过滤器,查看图片文件的保存位置。
目前为止,CrimeCameraFragment
类完全具备了拍照及保存文件的功能。相机API的相关开发工作全部完成了。本章接下来的部分将重点介绍CrimeFragment
类的开发完善,从而将图片与应用的其他部分进行整合。
20.2 返回数据给 CrimeFragment
为了让CrimeFragment
类使用图片,需要将文件名从CrimeCameraFragment
回传给它。图20-5展示了CrimeFragment
与CrimeCameraFragment
之间的交互过程。
图20-5 使用CrimeCameraActivity设置回传信息
首先,CrimeFragment
以接收返回值的方式启动CrimeCameraActivity
。图片拍摄完成后,CrimeCameraFragment
会以图片文件名作为extra创建一个intent,并调用setResult(...)
方法。然后,ActivityManager
会调用onActivityResult(...)
方法将intent转发给CrimePagerActivity
。最后,CrimePagerActivity
的FragmentManager
会调用CrimeFragment.onActivityResult(...)
方法,将intent转发给CrimeFragment
。
20.2.1 以接收返回值的方式启动CrimeCameraActivity
当前,CrimeFragment
只是直接启动CrimeCameraActivity
。在CrimeFragment.java中,新增一个请求码常量,然后修改拍照按钮的监听器方法,以需要接收返回值的方式启动CrimeCameraActivity
,如代码清单20-5所示。
代码清单20-5 以接收返回值的方式启动CrimeCameraActivity
(CrimeFragment.java)
20.2.2 在CrimeCameraFragment中设置返回值
CrimeCameraFragment
会将图片文件名放置在extra中并附加到intent上,然后传入CrimeCameraActivity.setResult(int, Intent)
方法。在CrimeCameraFragment.java中,新增一个extra常量。然后,在onPictureTaken(...)
方法中,判断照片处理状态,如果照片保存成功,就创建一个intent并设置结果代码为RESULT_OK
,反之,则设置结果代码为RESULT_CANCELED
,如代码清单20-6所示。
代码清单20-6 新增照片文件名extra(CrimeCameraFragment.java)
20.2.3 在CrimeFragment
中获取照片文件名
最后,CrimeFragment
会使用照片文件名更新CriminalIntent应用的模型层和视图层。在CrimeFragment.java中,覆盖onActivityResult(...)
方法,检查结果并获取照片文件名。然后,为CrimeFragment
类新增一个用于日志记录的TAG,如果照片文件名获取成功,就输出结果日志,如代码清单20-7所示。
代码清单20-7 获取照片文件名(CrimeFragment.java)
运行CriminalIntent应用。在CrimeCameraActivity
中拍摄一张照片。然后检查LogCat,确认CrimeFragment
成功获取了照片文件名。
有了CrimeFragment
获取的照片文件名,接下来还有一些事情要做。
更新模型层:首先需编写一个封装照片文件名的Photo
类。还需给Crime
类添加一个Photo
类型的mPhoto
属性。CrimeFragment
将使用照片文件名创建一个Photo
对象,然后使用它设置Crime
的mPhoto
属性。
更新CrimeFragment
的视图:需要为CrimeFragment
的布局增加一个ImageView
组件,然后在ImageView
视图上显示Crime
的照片缩略图。
显示全尺寸版的图片:需要创建一个名为ImageFragment
的DialogFragment
子类,然后使用它显示指定路径的照片。
20.3 更新模型层
图20-6展示了CrimeFragment
、Crime
以及Photo
类三者之间的关系。
图20-6 模型层对象与CrimeFragment
20.3.1 新增Photo类
以默认的java.lang.Object
为超类,在com.bignerdranch.android.criminalintent
包中创建一个名为Photo
的新类。
在Photo.java中,参照代码清单20-8添加需要的变量和方法。
代码清单20-8 Photo
新建类(Photo.java)
注意,Photo
类有两个构造方法。第一个构造方法根据给定的文件名创建一个Photo
对象。第二个构造方法是一个JSON序列化方法,在保存以及加载Photo
类型的数据时,Crime
会用到它。
20.3.2 为Crime添加photo属性
现在,我们来更新Crime
类,包含一个Photo
对象并将其序列化为JSON格式,如代码清单20-9所示。
代码清单20-9 Crime照片(Crime.java)
20.3.3 设置photo属性
在CrimeFragment.java中,修改onActivityResult(...)
方法,在其中新建一个Photo
对象并设置给当前的Crime
,如代码清单20-10所示。
代码清单20-10 处理新照片(CrimeFragment.java)
运行CriminalIntent应用并拍摄一张照片。然后查看LogCat,确认Crime
拥有这张新拍的照片。
可能有人会问,为什么要创建一个Photo
类,而不是简单地添加一个文件名属性给Crime
类。直接添加文件名属性虽然可行,但新建Photo
类可以帮助处理更多任务,如显示照片名称或处理触摸事件。显然,要处理这些事情,我们需要一个单独的类。
20.4 更新 CrimeFragment 的视图
完成了模型层的更新,我们来着手更新CrimeFragment
的视图层。特别要提到的是,CrimeFragment
将会在ImageView
上显示照片缩略图。
图20-7 添加了ImageView组件的CrimeFragment
20.4.1 添加ImageView组件
打开layout/fragment_crime.xml布局文件,对照图20-8添加ImageView
组件。
图20-8 添加了ImageView
组件的CrimeFragment布局
我们还需要一个带有ImageView
组件的水平模式布局,如图20-9所示。
图20-9 带有ImageView
组件的水平模式布局(layout-land/fragment_crime.xml
)
在CrimeFragment.java中,创建一个成员变量,然后在onCreateView(...)
方法中以资源ID引用ImageView
视图,如代码清单20-11所示。
代码清单20-11 配置ImageButton
(CrimeFragment.java
)
预览修改后的布局,或者运行CriminalIntent应用,确保ImageView
组件已正确添加。
20.4.2 图像处理
相机拍摄的照片尺寸通常都很大,需要预先处理,然后才能在ImageView
视图上显示。手机制造商每年新推出的手机都带有越来越强大的相机。对用户来说,这是好事。但对于开发者来说,这很让人头痛。
目前,主流Android手机都带有800万像素的照相机组件。大尺寸的图片很容易耗尽应用的内存。因此,加载图片前,需要编写代码缩小图片。图片使用完毕,也需要编写代码清理删除它。
添加处理过的图片到imageview
视图
创建一个名为PictureUtils
的新类,然后,在PictureUtils.java中,添加如代码清单20-12所示的方法,将图片缩放到设备默认的显示尺寸。
代码清单20-12 添加PictureUtils
类(PictureUtils.java)
注意,Display.getWidth()
和Display.getHeight()
方法已被弃用。本章末尾将介绍更多有关代码弃用的知识。
如果能将图片缩放至完美匹配ImageView
视图的尺寸,那自然最好了。然而,我们通常无法及时获得用来显示图片的视图尺寸。例如,在onCreateView(...)
方法中,就无法获得ImageView
视图的尺寸。设备的默认屏幕大小是固定可知的,因此,稳妥起见,可以缩放图片至设备的默认显示屏大小。注意,用来显示图片的视图可能会小于默认的屏幕显示尺寸,但大于屏幕默认的显示尺寸则肯定不行。
接下来,在CrimeFragment
类中,新增一个私有方法,将缩放后的图片设置给ImageView
视图,如代码清单20-13所示。
代码清单20-13 添加showPhoto()
方法(CrimeFragment.java)
在CrimeFragment.java中,新增onStart()
实现方法,只要CrimeFragment
的视图一出现在屏幕上,就调用showPhoto()
方法显示图片,如代码清单20-14所示。
代码清单20-14 加载图片(CrimeFragment.java)
在CrimeFragment.onActivityResult(...)
方法中,同样调用showPhoto()
方法,以确保用户从CrimeCameraActivity
返回后,ImageView
视图可以显示用户所拍照片,如代码清单20-15所示。
代码清单20-15 在onActivityResult(...)
方法中调用showPhoto()
方法(CrimeFragment.java)
卸载图片
在PictureUtils
类中添加清理方法,清理ImageView
的BitmapDrawable
,如代码清单20-16所示。
代码清单20-16 清理工作(PictureUtils.java)
Bitmap.recycle()
方法的调用需要一些解释。Android开发文档暗示不需要调用Bitmap.recycle()
方法,但实际上需要。因此,下面给出技术说明。
Bitmap.recycle()
方法释放了bitmap
占用的原始存储空间。这也是bitmap
对象最核心的部分。(取决于具体的Android系统版本,原始存储空间可大可小。Honeycomb以前,它存储了Java Bitmap
的所有数据。)
如果不主动调用recycle()
方法释放内存,占用的内存也会被清理。但是,它是在将来某个时点在finalizer中清理,而不是在bitmap
自身的垃圾回收时清理。这意味着很可能在finalizer调用之前,应用已经耗尽了内存资源。
finalizer的执行有时不太靠谱,且这类bug很难跟踪或重现。因此,如果应用使用的图片文件很大,最好主动调用recycle()
方法,以避免可能的内存耗尽问题。
在CrimeFragment
类中,添加onStop()
方法,并在其中调用cleanImageView(...)
方法清理内存,如代码清单20-17所示。
代码清单20-17 卸载图片(CrimeFragment.java)
在onStart()
方法中加载图片,然后在onStop()
方法中卸载图片是一种好习惯。这些方法标志着用户可以看到activity的时间点。如果改在onResume()
方法和onPause()
方法中加载和卸载图片,用户体验可能会很糟糕。
暂停的activity也可能部分可见,比如说,非全屏的activity视图显示在暂停的activity视图之上时。如果使用了onResume()
方法和onPause()
方法,那么图像消失后,因为没有被全部遮住,它又显示在了屏幕上。所以说,最佳实践就是,activity的视图一出现时就加载图片,然后等到activity再也不可见的情况下,再对它们进行卸载。
运行CriminalIntent应用。拍摄一张照片并确认它显示在Imageview
视图上。然后退出应用并重新启动它。确认进入同一Crime
明细界面时,Imageview
视图上的图片仍可正常显示。
按照CrimeCameraActivity
的初始显示方向,最好是以水平模式进行拍照。然而,如果不小心使用了竖直模式,拍照按钮上的图片可能无法按正确的方向显示。请通过本章第一个挑战练习修正该问题。
20.5 在 DialogFragment 中显示大图片
本章,CriminalIntent应用开发的最后环节是让用户查看Crime的大尺寸照片,如图20-10所示。
图20-10 显示较大图片的DialogFragment
以support.v4.DialogFragment为父类,创建一个名为ImageFragment的新类。
ImageFragment类需要知道Crime照片的文件路径。在ImageFragment.java中,新增一个newInstance(String)方法,该方法接受照片文件路径并放置到argument bundle中,如代码清单20-18所示。
代码清单20-18 创建ImageFragment(ImageFragment.java)
通过设置fragment的样式为DialogFragment.STYLE_NO_TITLE,获得一个如图20-10所示的 简洁用户界面。
ImageFragment不需要显示AlertDialog视图自带的标题和按钮。如果fragment不需要显示 标题和按钮,要实现显示大图片的对话框,采用覆盖onCreateView(...)方法并使用简单视图 的方式,要比覆盖onCreateDialog(...)方法并使用Dialog更简单、快捷且灵活。
在ImageFragment.java中,覆盖onCreateView(...)方法创建ImageView并从argument获取文 件路径。然后获取缩小版的图片并设置给ImageView。最后,只要图片不再需要,就主动覆盖onDestroyView()方法以释放内存,如代码清单20-19所示。
代码清单20-19 创建ImageFragment(ImageFragment.java)
最后,我们需要从CrimeFragment弹出显示图片的对话框。在CrimeFragment.java中,添加一个监听器方法给mPhotoView。在实现方法里,创建一个ImageFragment实例,然后通过调用ImageFragment的show(...)方法,将它添加给CrimePagerActivity的FragmentManager。另外,还需要一个字符串常量,用来唯一定位FragmentManager中的ImageFragment,如代码清单20-20所示。
代码清单20-20 显示ImageFragment界面(CrimeFragment.java)
运行CriminalIntent应用。拍摄一张照片,确认可以清楚地看到那些令人震惊的案发现场照。