相机拍摄的照片比较大,保存在SQLite数据库中肯定不行。显然,它们需要在设备文件系统的某个地方保存。
设备上就有这么一个地方:私有存储空间。使用类似 Context.getFileStreamPath(String) 和 Context.getFilesDir() 这样的方法,像照片这样的文件也可以这么保存。结果就是照片文件保存在 databases 子目录相邻的某个子目录中。
Context类提供的基本文件和目录处理方法如下:
如果存储的文件仅供应用内部使用,使用上述方法就够了。
如果其他应用要读写你的文件,事情就没那么简单了。比如外部相机应用需要在你的应用里保存拍摄的照片。虽然有个 Context.MODE_WORLD_READABLE 可以传入 openFileOutput(String,int) 方法,但这个flag已经废弃了。即使强制使用,在新系统设备上也不那么可靠。以前,还有个方法是通过公共外部存储转存,但出于安全考虑,这条路在新设备版本上也被堵住了。
如果想共享文件给其他应用,或是接收其他应用文件(如相机应用拍摄的照片),可以通过 ContentProvider 把要共享的文件暴露出来。ContentProvider 允许你暴露内容URI给其他应用。
如果只想从其他应用接收一个文件,自己实现ContentProvider太浪费。Google提供了一个名叫 FileProvider 的便利类。
首先,声明 FileProvider 为 ContentProvider,并给予一个指定权限。在AndroidManifest.xml中添加一个FileProvider声明:
这里的权限是指一个位置:文件保存地。把FileProvider和你指定的位置关联起来,就相当于你给发出请求的其他应用一个目标地。添加 exported = "false" 属性就意味着,除了你自己以及你给予授权的人,其他任何人都不允许使用你的FileProvider。而 grantUriPermissions 属性用来给其他应用授权,允许它们向你指定位置的 URI 写入文件。
既然已让Android知道FileProvider在哪儿,还需要配置 FileProvider,让它知道该暴露哪些文件。这个配置用另外一个XML资源文件处理。在项目工具窗口,右键点击app/res目录,然后选择 New--->Android resource file菜单项,资源类型选XML。
这是个描述性XML文件,其表达的意思是把私有存储空间的根路径映射为 crime_photos。
最后,在清单文件中,添加一个meta-data标签,让FileProvider能找到files.xml文件。
我们需要的intent操作是定义在 MediaStore 类中的 ACTION_IMAGE_CAPTURE。MediaStore 类定义了一些公共接口,可用于处理图像、视频以及音乐这些常见的多媒体任务。
ACTION_IMAGE_CAPTURE 打开相机应用,默认只能拍摄缩略图这样的低分辨率照片,而且照片会保存在 onActivityResult(......) 返回的 Intent 对象里。
要想获得全尺寸照片,就要让它使用文件系统存储照片。这可以通过传入保存在 MediaStore.EXTRA_OUTPUT 中的指向存储路径的 Uri 来完成,这个Uri会指向 FileProvider 提供的位置。
final Intent captureImage = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
boolean canTakePhoto = mPhotoFile != null && captureImage.resolveActivity(packageManager)
!= null;
mPhotoButton.setEnabled(canTakePhoto);
mPhotoButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Uri uri = FileProvider.getUriForFile(getActivity(),
"com.guoxuebing.demo.criminalintent.fileprovider",mPhotoFile);
captureImage.putExtra(MediaStore.EXTRA_OUTPUT,uri);
List cameraActivities = getActivity().getPackageManager()
.queryIntentActivities(captureImage,PackageManager.MATCH_DEFAULT_ONLY);
for (ResolveInfo activity : cameraActivities){
getActivity().grantUriPermission(activity.activityInfo.packageName,uri,
Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
startActivityForResult(captureImage,REQUEST_PHOTO);
}
}
});
调用 FileProvider.getUriForFile(......) 会把本地文件路径转换为相机能看见的Uri形式。要实际写入文件,还需要给相机应用权限。为了授权,我们授予 FLAG_GRANT_WRITE_URI_PERMISSION 给所有 cameraImage intent 的目标Activity,以此允许它们在Uri指定的位置写文件。当然,还有个前提条件:在声明 FileProvider 的时候添加过 android:grantUriPermissions 属性。
有了照片,接下来就是找到并加载它,然后展示给用户。使用 Bitmap对象展示。Bitmap 是个简单对象,它只存储实际像素数据,也就是说,即使原始照片已经压缩过,但存入 Bitmap对象时,文件并不会同样压缩,因此,一张1600万像素24位的相机照片,存为JPG格式大约5MB,一旦载入Bitmap对象,就会立即膨胀至48MB!
这个问题需要手动缩放位图照片。具体做法就是,首先确定文件到底有多大,然后考虑按照给定区域大小合理缩放文件,最后重新读取缩放后的文件,创建Bitmap对象。
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) {
float heightScale = srcHeight / destHeight;
float widthScale = srcWidth / destWidth;
inSampleSize = Math.round(heightScale > widthScale ? heightScale : widthScale);
}
options = new BitmapFactory.Options();
options.inSampleSize = inSampleSize;
// Read in and create final bitmap
return BitmapFactory.decodeFile(path, options);
}
上述方法中,inSampleSize 值很关键。它决定着缩略图像素的大小。假设这个值是1的话,就表明缩略图和原始照片的水平像素大小一样;如果是2的话,它们的水平像素比就是1:2,也就是缩略图像素数是原始文件的四分之一。
解决图片缩放问题之后,还有新问题:页面刚启动时,没有人知道 photoView 有多大,onResume() 方法启动后,才会有首个实例化布局出现,此时显示在屏幕上的视图才会有大小尺寸。
解决方案有两个:要么等布局实例化完成并显示,要么干脆使用保守估算值。特定条件下,虽然估算比较主观,但是是唯一可行的方法。添加一个静态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);
}
这个方法先确定屏幕的尺寸,然后按此缩放图像。这样就能保证载入的ImageView永远不会过大。这是一个比较保守的估算,但是能解决问题。
告诉Android系统,该应用需要使用相机功能。在AndroidManifest.xml 文件里加入
默认情况下,声明使用某个设备功能后,应用就无法支持那些无此功能的设备了。不过,这里设置 required 属性为false,Android系统因此就知道,即使不带相机的设备会导致该应用功能缺失,但应用仍然可以正常安装使用。
API 21 中将原来的 camera API 弃用,转而推荐新增的 camera2 API。具体架构区别这里不叙述,有想要了解的可以自行百度查看。