调用系统相机拍照有两种方式:
ONE: 使用Intent传递Bitmap
button.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { Intent intent=new Intent("android.media.action.IMAGE_CAPTURE"); startActivityForResult(intent,1); }
为一个button 设置点击事件,隐式intent的action为"android.media.action.IMAGE_CAPTURE"
同样应该为其设置回调方法。。
protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode,resultCode,data); if(resultCode==RESULT_OK){ if(requestCode==1){ Bundle bundler=data.getExtras(); Bitmap bitmap=(Bitmap) bundler.get("data"); iamgeview.setImageBitmap(bitmap); //自定义一个ImageView接受Bitmap } } }
相机的Activity会将Bitmap对象序列化后存放Bundler里 。大概代码:
Bundle bundle=new Bundle(); bundle.putParcelable("data", object); //object即是序列化后的Bitmap对象 intent.putExtras(bundle);
这里不需要权限,因为我们只是通过intent打开相机的activity而打开相机的activity并没有权限。 这种方法得到的Bitmap只是缩略图,画质非常的差,android做了相关处理防止内存泄露。 所以一般我们用的是第二种。
TWO: 使用Intent传递URI:
二者不同的是前者直接在两个Activity之间传递Bitmap,而后者是发送一个URI给相机的Acitivity后让其把Bitmap数据直接保存到URI对应的文件里,
然后我们的app再直接访问这个文件,这样就不存在内存泄露了。
button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { File file=new File(getExternalCacheDir().getPath(),"Camera"); if (file.exists()){ file.delete(); } try { file.createNewFile(); } catch (IOException e) { e.printStackTrace(); } Uri image_uri=Uri.fromFile(file); Intent intent=new Intent("android.media.action.IMAGE_CAPTURE"); intent.putExtra(MediaStore.EXTRA_OUTPUT,image_uri); startActivityForResult(intent,1); } });
getExternalCacheDir() // 得到系统给app分配的外置存储空间文件夹:0/Android/data/包名/cache
我们在这里创建了一个名为Camera的文件。然后通过Uri.fromFile(file)得到文件的uri,然后将其存放在一个"key"为MediaStore.EXTRA_OUTPUT 这个必须是固定格式不然相机Activity得不到正确的数据。 这个key的本身的值为"output"。继续修改回调方法。
protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode,resultCode,data); if(resultCode==RESULT_OK){ if(requestCode==1){ File file=new File(getExternalCacheDir(),"Camera"); if(file.exists()){ try { imageview.setImageBitmap(BitmapFactory.decodeStream(new FileInputStream(file))); } catch (FileNotFoundException e) { } } } } }
将这个file文件读取后放到imageview中,然后就ok了。
为什么这里任然要执行onActivityResult 应为我们发起的intent目标Activity本身就是一个最终会执行setResult(),然后finish()。
不同上述的是 前者无intent.putExtra(MediaStore.EXTRA_OUTPUT,image_uri);所以猜想源码肯定是会读取这个值,如果读不到则把Bitmap数据包装成intent
发回来,能读到则把Bitmap数据存放到uri对应的File文件,而intent的返回值为null。
注意: intent传输uri方法在sdk版本低于24也就是android7.0时使用,而对于7.0及以上我们则需要用到内容提供器(FileProvider),
详细阅读这里:http://blog.csdn.net/djy1992/article/details/72533310
为什么出现这种问题,其实就是Uri.fromFile(file)方法不让用了,我们不能从File转换Uri了,所以需要FileProvider。
从继承关系看
public class FileProvider extends ContentProviderFileProvider是继承ContentProvider所以也需要在Mainfest中定义
<provider android:name="android.support.v4.content.FileProvider" android:authorities="com.android.shuai" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file"></meta-data> </provider>
name:为定值"android.support.v4.content.FileProvider"。相当自定义的ContentProvider,FileProvider为重写的类。
authorities:自定义值,
android:exported="false": 不可更改否则报错 android:grantUriPermissions="true"> :不可更改否则报错<meta-data>标签内的name:为定值,
resource:新建XML目录新建XML文件
<?xml version="1.0" encoding="utf-8"?> <paths xmlns:android="http://schemas.android.com/apk/res/android"> <external-path name="LinLangTianXia" path="" /> </paths>
<external-path> : 首地址,通俗讲就是地址从这里开始。
path="" : 首地址后的详细地址:
臂如标签就是上述的<external-path> ,path=""; 那么我们的内容提供器可以提供的转换File地址为:
"/Storage/"。只要要转换的File文件被这个目录包括就可以。如果path="A" 那就是"/Storage/A/"。
同理<external-path>标签还有很多;像<files-path><cache-path>这种,如果要转换的File文件不在上述内就会报错。
name则是上述地址的映射,比如上述"/Storage/A/" name="LinLangTianXia" 最后为/LinLangTianXia/, 也就是说将我们的真实地址隐藏了。
举个例子 :7.0以下得到的Uri为:file:///storage/emulated/0/Android/data/listen.myapplication/cache/Camera:
7.0以上得到的Uri为: content://com.android.shuai/LinLangTianXia/Android/data/listen.myapplication/cache/Camera:
格式为:content://authorities/name/+未映射的地址。 authorities告诉程序是那个内容提供者, name映射真实地址。
当然这个转换过程不用我们来操作看代码:
if(Build.VERSION.SDK_INT>=24){ image_uri=FileProvider.getUriForFile(Main.this,"com.android.shuai",file); }
调用一下FileProvider.getUriForFile(Context,authorities,file)就ok了, 这个方法会自动帮我们封装Uri,这个file就是上述我们说的file
必须在其内部,否则报错。 其他不变同7.0以下下, 通俗讲FileProvider只是帮我们修改了Uri使其更安全而已。 当然阅读到这会有个疑问它们具体的过程到底是怎样的,我们看源码:
public static Uri getUriForFile(Context context, String authority, File file) { final PathStrategy strategy = getPathStrategy(context, authority); return strategy.getUriForFile(file); //看到strategy对象可以帮我们转换File为Uri }
进入PathStrategy发现是个接口实现它的是类SimplePathStrategy,继续进:
static class SimplePathStrategy implements PathStrategy { private final String mAuthority; private final HashMap<String, File> mRoots = new HashMap<String, File>(); public SimplePathStrategy(String authority) { mAuthority = authority; //传入的authority。 } @Override public Uri getUriForFile(File file) { String path; try { path = file.getCanonicalPath(); //相当file.getPath(); } catch (IOException e) { throw new IllegalArgumentException("Failed to resolve canonical path for " + file); } // Find the most-specific root path Map.Entry<String, File> mostSpecific = null; for (Map.Entry<String, File> root : mRoots.entrySet()) { final String rootPath = root.getValue().getPath(); if (path.startsWith(rootPath) && (mostSpecific == null || rootPath.length() > mostSpecific.getValue().getPath().length())) { mostSpecific = root; } } if (mostSpecific == null) { throw new IllegalArgumentException( "Failed to find configured root that contains " + path); } // Start at first char of path under root final String rootPath = mostSpecific.getValue().getPath(); if (rootPath.endsWith("/")) { path = path.substring(rootPath.length()); } else { path = path.substring(rootPath.length() + 1); } // Encode the tag and path separately path = Uri.encode(mostSpecific.getKey()) + '/' + Uri.encode(path, "/"); return new Uri.Builder().scheme("content") .authority(mAuthority).encodedPath(path).build(); } } //截取部分
看这行: return new Uri.Builder().scheme("content") .authority(mAuthority).encodedPath(path).build(); 是不是就是我们上述的结果。
看到相信你肯定会有疑惑,既然Uri被这种方式修改那在接收端会不会存在另一种方式给它修改回去,没错,接着看剩下源码:
class SimplePathStrategy {
private final
String
mAuthority
;
private final
HashMap<String, File>
mRoots
=
new
HashMap<String, File>();
public
SimplePathStrategy(String authority) {
mAuthority
= authority;
}
/**
* Add a mapping from a name to a filesystem root. The provider only offers
* access to files that live under configured roots.
*/
public void
addRoot(String name, File root) {
if
(TextUtils.
isEmpty
(name)) {
throw new
IllegalArgumentException(
"Name must not be empty"
);
}
try
{
// Resolve to canonical path to keep path checking fast
root = root.getCanonicalFile();
}
catch
(IOException e) {
throw new
IllegalArgumentException(
"Failed to resolve canonical path for "
+ root, e);
}
mRoots
.put(name, root);
}
public
File getFileForUri(Uri uri) {
String path = uri.getEncodedPath();
final int
splitIndex = path.indexOf(
'/'
,
1
);
final
String tag = Uri.
decode
(path.substring(
1
, splitIndex));
path = Uri.
decode
(path.substring(splitIndex +
1
));
final
File root =
mRoots
.get(tag);
if
(root ==
null
) {
throw new
IllegalArgumentException(
"Unable to find configured root for "
+ uri);
}
File file =
new
File(root, path);
try
{
file = file.getCanonicalFile();
}
catch
(IOException e) {
throw new
IllegalArgumentException(
"Failed to resolve canonical path for "
+ file);
}
if
(!file.getPath().startsWith(root.getPath())) {
throw new
SecurityException(
"Resolved path jumped beyond configured root"
);
}
return
file}
}
看到这些代码是不是有点头疼,别急,慢慢来: 笔者将其提取然后直接调用getFileForUri报错提示root为空,所以上述的addRoot要先调用,
那这个addroot方法又在那里调用呢?
public SimplePathStrategy parsePathStrategy(Context context, String authority) throws IOException, XmlPullParserException { final SimplePathStrategy strat = new SimplePathStrategy(authority); //这里创建了SimplePathStrategy对象 final ProviderInfo info = context.getPackageManager() .resolveContentProvider(authority, PackageManager.GET_META_DATA); final XmlResourceParser in = info.loadXmlMetaData( context.getPackageManager(), META_DATA_FILE_PROVIDER_PATHS); if (in == null) { throw new IllegalArgumentException( "Missing " + META_DATA_FILE_PROVIDER_PATHS + " meta-data"); } int type; while ((type = in.next()) != END_DOCUMENT) { if (type == START_TAG) { final String tag = in.getName(); final String name = in.getAttributeValue(null, ATTR_NAME); String path = in.getAttributeValue(null, ATTR_PATH); File target = null; if (TAG_ROOT_PATH.equals(tag)) { target = DEVICE_ROOT; } else if (TAG_FILES_PATH.equals(tag)) { target = context.getFilesDir(); } else if (TAG_CACHE_PATH.equals(tag)) { target = context.getCacheDir(); } else if (TAG_EXTERNAL.equals(tag)) { target = Environment.getExternalStorageDirectory(); } else if (TAG_EXTERNAL_FILES.equals(tag)) { File[] externalFilesDirs = ContextCompat.getExternalFilesDirs(context, null); if (externalFilesDirs.length > 0) { target = externalFilesDirs[0]; } } else if (TAG_EXTERNAL_CACHE.equals(tag)) { File[] externalCacheDirs = ContextCompat.getExternalCacheDirs(context); if (externalCacheDirs.length > 0) { target = externalCacheDirs[0]; } } if (target != null) { strat.addRoot(name, buildPath(target, path)); //这里调用了addRoot方法(); } } } return strat; }
ok 我们的逻辑也算梳理顺了, 在相机源码中肯定先调用了parsePathStrategy()方法得到了SimplePathStrategy
对象,然后直接调用了它的getFileForUri()方法于是就将Uri给改了回来。 笔者熬夜到两点了, 水平有限源码分析的很浅
,但是看在笔者努力的份上 给点支持,谢谢!!!