本章主要讲了如何使用 intent 拍照,存储照片和展示照片
GitHub 地址:
完成16章,未完成挑战
完成16章挑战1
完成16章挑战2
1. 外部存储
相机照片动辄几 MB 大小,直接保存在数据库中肯定不现实。很自然,大家会想到直接使用设备的文件系统。
一般来讲,应用都应该使用私有存储空间保存各类文件。还记得吗?在前面章节中,我们在私有存储空间保存过 SQLite 数据文件。使用类似 Context.getFileStreamPath(String)和 Context.getFilesDir()这样的方法,我们也可以实现这样的存储目标,下表所示:
Context 类提供的方法 | 使用目的 |
---|---|
File getFilesDir() |
获取/data/data/ |
FileInputStream openFileInput(String name) |
打开现有文件进行读取 |
FileOutputStream openFileOutput(String name, int mode) |
打开文件进行写入,如不存在就创建它 |
File getDir(String name, int mode) |
获取/data/data/ |
String[] fileList() |
获取/data/data/ |
File getCacheDir() |
获取/data/data/ |
如果想存储的文件仅供应用内部使用,使用上表中的各类方法就可以了。而如果想共享文件给其他应用或是接收其他应用的文件(如相机应用拍摄的照片)时,路只有一条:使用外部存储保存文件。
外部存储有两类:主外部存储和其他各类存储介质。所有的 Android 设备至少应有一个主外部存储地。使用Environment.getExternalStorageDirectory()
可以返回这个外部存储目录。 以前,这个存储地通常是指 SD 卡,但现在都已基本整合至了设备内部。即使现在还有设备使用扩展外部存储,也应算作其他各类存储介质这一类了。
Context 也提供了一些访问外部存储空间要用到的方法,如下表所示。
方法 | 使用目的 |
---|---|
File getExternalCacheDir() |
获取主外部存储上的缓存文件目录。用法类似 getCacheDir()方法,但要注意,Android 一般不会自动清理该目录 |
File[] getExternalCacheDirs() |
获取多个外部存储上的缓存文件目录 |
File getExternalFilesDir(String) |
获取主外部存储上存放常规文件的文件目录。通过 String 参数,可访问特定内容类型的子目录。内容类型常量以 DIRECTORY_为前缀,定义在 Environment 中 。 例如 , 用于 图像 文件 的 Environment.DIRECTORY_ PICTURES |
File[] getExternalFilesDirs(String) |
类似 getExternalFilesDir(String)方法,但该方法可获取指定类型的所有文件目录 |
File[] getExternalMediaDirs() |
获取 Android 存储图片、视频和音乐文件的所有外部文件目录。和 getExternalFilesDir(Environment.DIRECTORY_PICTURES) 方法 区别 在于,调用该方法,多媒体扫描器会自动扫描目标目录,并将存放的多媒体文件暴露给能够播放音乐、浏览视频和图片的应用。也就是说, getExternalMediaDirs()方法返回目录中存放的任何文件都会自动出现在多媒体应用中 |
1.1 指定照片存放位置
首先,一张照片的文件名我们用一个 Crime 的 ID 来标识,所以在 Crime.java 中加入了获取文件名的方法:
public String getPhotoFileName() {
return "IMG_" + getId().toString() + ".jpg";
}
然后在 CrimeLab.java 中加入获取路径文件的函数:
public File getPhotoFile(Crime crime) {
File externalFilesDir = mContext
.getExternalFilesDir(Environment.DIRECTORY_PICTURES);
if (externalFilesDir == null) {
return null;
}
return new File(externalFilesDir, crime.getPhotoFileName());
}
1.2 外部存储使用权限
读写外部存储需要获得权限,一般在AndroidManifest.xml
中使用
标签来使用。而对于 API 19(Android 4.4)及以后的新版系统来说,应用不需要再申请 Context.getExternalFilesDir(String)
所需要的权限了,所以这个权限申请是这么写的:
2. 使用相机 intent
实现拍照功能只需要使用一个隐式 intent,分为下面几步:
- 获取保存图片的文件存储位置
- 处理拍照按钮,实现触发拍照,其实就是发送一个带有
MediaStore.ACTION_IMAGE_CAPTURE
的 intent 即可。
对于 intent 的操作,我们需要定义在 MediaStore 类中的ACTION_CAPTURE_IMAGE
。MediaStore 类定义了一些公共接口,可用于处理图像、视频以及音乐这些常见的多媒体任务。当然,这也包括触发相机应用的拍照 intent。
如果只用ACTION_IMAGE_CAPTURE
打开相机应用,默认只能拍摄缩略图这样的低分辨率照片,而且照片会保存在 onActivityResult(...)返回的 Intent 对象里。要想获得全尺寸照片,就要让它使用文件系统存储照片。这可以通过传入保存在 MediaStore.EXTRA_OUTPUT
中的指向存储路径的 Uri 来完成。
编写用于拍照的隐式 intent,拍摄的照片应该保存在 mPhotoFile 指定的地方。同时,别忘了检查设备上是否安装有相机应用,以及是否有地方存储照片。
mPhotoButton = (ImageButton) v.findViewById(R.id.crime_camera);
// 首先创建一个用于拍照的 Intent 对象
final Intent captureImage = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
// 检查是否有可拍照的应用
boolean canTakePhoto = mPhotoFile != null &&
captureImage.resolveActivity(packageManager) != null;
mPhotoButton.setEnabled(canTakePhoto);
if (canTakePhoto) {
// 建立访问照片目录的 Uri
Uri uri = Uri.fromFile(mPhotoFile);
// 将该 Uri 放入 intent 对象中
captureImage.putExtra(MediaStore.EXTRA_OUTPUT, uri);
}
mPhotoButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// 使用 startActivityForResult 是为了拍完照后刷新视图
startActivityForResult(captureImage, REQUEST_PHOTO);
}
});
3. 缩放和显示位图
有了照片,接下来就是找到并加载它,然后展示给用户看。在技术实现上,这需要加载照片到大小合适的 Bitmap 对象中。而要从文件生成 Bitmap 对象,我们需要 BitmapFactory 类:
Bitmap bitmap = BitmapFactory.decodeFile(mPhotoFile.getPath());
Bitmap 是个简单对象,它只存储实际像素数据。也就是说,即使原始照片已压缩过,但存入 Bitmap 对象时,文件并不会同样压缩。因此,如果有一个16万像素24位已压缩为5Mb 大小的 JPG 照片文件,一旦载入 Bitmap 对象,就会立即膨胀至48Mb 大小!
这个问题可以设法解决,但需要手工缩放位图照片。具体做法就是,首先确认文件到底有多大,然后考虑按照给定区域大小合理缩放文件。最后,重新读取缩放后的文件,创建 Bitmap 对象。
既然需要处理图像文件,我们建立一个通用的工具类,名为 PictureUtils.java。在其中添加 getScaledBitmap(String, int, int)缩放方法,
public class PictureUtils {
public static Bitmap getScaledBitmap(String path, int destWidth, int destHeight) {
// Read in the dimensions of the image on disk
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(path, options);
float srcWidth = options.outWidth;
float srcHeight = options.outHeight;
// Figure out how much to scale down by
int inSampleSize = 1;
if (srcHeight > destHeight || srcWidth > destWidth) {
if (srcWidth > srcHeight) {
inSampleSize = Math.round(srcHeight / destHeight);
} else {
inSampleSize = Math.round(srcWidth / destWidth);
}
}
options = new BitmapFactory.Options();
options.inSampleSize = inSampleSize;
// Read in and create final bitmap
return BitmapFactory.decodeFile(path, options);
}
}
上述方法中,inSampleSize 值很关键。它决定着缩略图像素的大小。假设这个值是1的话,就表明缩略图和原始照片的水平像素大小一样。如果是2的话,它们的水平像素比就是1∶2。因此,inSampleSize 值为2时,缩略图的像素数就是原始文件的四分之一。
问题总是接踵而来。解决了缩放问题,又冒出了新问题:fragment 刚启动时,PhotoView 究竟有多大无人知道。onCreate(...)、onStart()和 onResume()方法启动后,才会有首个实例化布局出现。也就在此时,显示在屏幕上的视图才会有大小尺寸。这也是出现新问题的原因。
解决方案有两个:要么等布局实例化完成并显示,要么干脆使用保守估算值。特定条件下, 尽管估算比较主观,但确实是一个切实可行的办法。再添加一个 getScaledBitmap(String, Activity)静态 Bitmap 估算方法。
public static Bitmap getScaledBitmap(String path, Activity activity) {
Point size = new Point();
activity.getWindowManager().getDefaultDisplay()
.getSize(size);
return getScaledBitmap(path, size.x, size.y);
}
4. 功能声明
应用的拍照功能用起来不错,但还有件事情要做:告诉目标用户应用具有拍照功能。
假如应用要用到诸如相机、NFC,或者任何其他的随设备走的功能时,都应该要让 Android 系统知道。否则,假如设备缺少这样的功能,类似 Google Play 商店的安装程序就会拒绝安装应用。
为声明需要使用相机,在 AndroidManifest.xml 中加入
标签:
5. 布局文件中的
标签
如果有重复的布局可以使用,那么可以采用 include 标签,直接在不同的 layout 中引用。
然而,经验表明,布局文件的优点是可靠又好用。例如,直接查看布局文件内容,就可以快速准确地知道应用视图是如何构建的。然而,一旦用了 include 标签,一切就不好说了。还想明白视图构成的话,就得仔细翻看布局主文件以及所有 include 的布局文件。这种非直观的感觉,极易让人失去耐心。
用户界面是应用改动相对频繁的部分。既然这样,不顾一切地追求复用原则很可能会适得其反。因此,在视图层开发时,我们一定要多多考量,尽量做到审慎、合理地使用 include 标签。
6. 挑战练习
6.1 优化照片显示
新建一个 GlancePictureFragment,继承自 DialogFragment,代码如下:
public class GlancePictureFragment extends DialogFragment {
private static final String ARG_PATH = "path";
private ImageView mImage;
// 由于文件比较大,所以将文件路径传入即可
public static GlancePictureFragment newInstance(String path) {
Bundle args = new Bundle();
args.putString(ARG_PATH, path);
GlancePictureFragment fragment = new GlancePictureFragment();
fragment.setArguments(args);
return fragment;
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
// 使用 getArguments() 方法取出照片文件路径
String path = getArguments().getString(ARG_PATH);
// 这个新的 style 其实就做了一件事,那就是使窗口全屏
// 注意如果继承了 @android:Theme.Dialog 的话,窗口
// 大小就限定了,所以我没有继承
final Dialog dialog = new Dialog(getActivity(), R.style.CustomDialogTheme);
// 这个 layout 中只有一个 ImageView
dialog.setContentView(R.layout.dialog_image_glance);
mImage = (ImageView) dialog.findViewById(R.id.glance_image);
// 仍然使用 PictureUtils 类的工具来获得缩放的 Bitmap
mImage.setImageBitmap(
PictureUtils.getScaledBitmap(path, getActivity()));
mImage.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 点击图片则退出该 dialog
dialog.dismiss();
}
});
return dialog;
}
}
然后在图片的点击事件中声明即可
6.2 优化缩略图加载
首先修改更新视图的函数,接受高宽的指定像素:
private void updatePhotoView(int width, int height) {
if (mPhotoFile == null || !mPhotoFile.exists()) {
mPhotoView.setImageDrawable(null);
} else {
Bitmap bitmap = PictureUtils.getScaledBitmap(
mPhotoFile.getPath(), width, height);
mPhotoView.setImageBitmap(bitmap);
}
}
之后,先获取 mPhotoView 的 ViewTreeObserver,然后设置 OnGlobalLayoutListener 监听器,在监听器中即可获取视图的高度和宽度,然后进行图片显示。
mPhotoObserver = mPhotoView.getViewTreeObserver();
mPhotoObserver.addOnGlobalLayoutListener(
new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
updatePhotoView(
mPhotoView.getWidth(),
mPhotoView.getHeight());
Log.i("CrimeFragment", "onGlobalLayout: Observed");
}
});
GitHub Page: kniost.github.io
:http://www.jianshu.com/u/723da691aa42