http://blog.csdn.net/floodingfire/article/details/8144587
译者:Ryan Hoo
来源:http://www.androidworks.com/crop_large_photos_with_android
译者按:在外企工作的半年多中花了不少时间在国外的网站上搜寻资料,其中有一些相当有含金量的文章,我会陆陆续续翻译成中文,与大家共享之。初次翻译,“信达雅”三境界恐怕只到信的层次,望大家见谅!
这篇文章相当经典而实用,想当初我做手机拍照截图的时候,大多都是在网上抄来抄去的内容,从来没有人考虑过实际项目中的需求。实际上,拍照传大图片,如果用普通方式会耗用极大的内存,Android一个App原则上的16M内存限制可以一下子被耗光。Android在拍照上有一个隐藏的设计,如果拍照图片过大,只返回一张缩略图。具体到不同手机,都是不一样的。
-------------------------------------------------------------------------------------
译文:
概述
我写这篇文章是为了发表我对MediaStore裁剪图片功能的一些简要研究。基本上,如果你要写一个应用程序,使用已有的Media Gallery并允许用户在你的应用里选取TA的图片的一部分(可选功能:人脸识别)。 可以使用一个Intent做到这个,但是也存在着相应的问题,总的来说也缺少这方面的文档告诉我们怎么实现它。
另外,这篇文章基于2.1并且在Nexus One上做了测试。
Nexus One上的实现似乎被这群人写在了这里:
Media Gellery for Nexus One
。
反馈
这篇文章需要使用基于我的研究所写的程序。如果你对我推荐的实现方案有所改进,请让我知道。我会相应的更新这篇文章。
Intent细节
首先,让我们探讨下Intent以及它的特点。在看了一些代码示例以后,我发现我可以很轻松的使用如下的Intent调用裁剪功能:
1 |
Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null ); |
2 |
intent.setType(“image/*”); |
3 |
intent.putExtra(“crop”, “ true ”); |
然而,这是在我缺少附加的文档,不知道这些选项的具体含义等等情况之下的选择。所以,我将我的yanj整理成一个表格 ,并写了一个演示程序,力图演示控制此功能的所有可供选项。
你可以在你的程序中使用使用我的代码,并且扩展它。我会将之附加在这篇文章上。
Exta Options Table for image/* crop:
附加选项 |
数据类型 |
描述 |
crop |
String |
发送裁剪信号 |
aspectX |
int |
X方向上的比例 |
aspectY |
int |
Y方向上的比例 |
outputX |
int |
裁剪区的宽 |
outputY |
int |
裁剪区的高 |
scale |
boolean |
是否保留比例 |
return-data |
boolean |
是否将数据保留在Bitmap中返回 |
data |
Parcelable |
相应的Bitmap数据 |
circleCrop |
String |
圆形裁剪区域? |
MediaStore.EXTRA_OUTPUT ("output") |
URI |
将URI指向相应的file:///...,详见代码示例 |
现在,最令人困惑的是MediaStore.EXTRA_OUTPUT以及return-data选项。
你主要有两种方式从这个Intent中取得返回的bitmap:获取内部数据或者提供一个Uri以便程序可以将数据写入。
方法1:如果你将return-data设置为“true”,你将会获得一个与内部数据关联的Action,并且bitmap以此方式返回:(Bitmap)extras.getParcelable("data")。注意:如果你最终要获取的图片非常大,那么此方法会给你带来麻烦,所以你要控制outputX和outputY保持在较小的尺寸。鉴于此原因,在我的代码中没有使用此方法((Bitmap)extras.getParcelable("data"))。
下面是CropImage.java的源码片段:
2 |
Bundle myExtras = getIntent().getExtras(); |
3 |
if (myExtras != null && (myExtras.getParcelable( "data" ) != null || myExtras.getBoolean( "return-data" ))) |
5 |
Bundle extras = new Bundle(); |
6 |
extras.putParcelable( "data" , croppedImage); |
7 |
setResult(RESULT_OK,( new Intent()).setAction( "inline-data" ).putExtras(extras)); |
方法2:
如果你将return-data设置为“false”,那么在onActivityResult的Intent数据中你将不会接收到任何Bitmap,相反,你需要将MediaStore.EXTRA_OUTPUT关联到一个Uri,此Uri是用来存放Bitmap的。
但是还有一些条件,首先你需要有一个短暂的与此Uri相关联的文件地址,当然这不是个大问题(除非是那些没有sdcard的设备)。
下面是CropImage.java关于操作Uri的源码片段:
01 |
if (mSaveUri != null ) { |
02 |
OutputStream outputStream = null ; |
04 |
outputStream = mContentResolver.openOutputStream(mSaveUri); |
05 |
if (outputStream != null ) { |
06 |
croppedImage.compress(mOutputFormat, 75 , outputStream); |
08 |
} catch (IOException ex) { |
10 |
Log.e(TAG, "Cannot open file: " + mSaveUri, ex); |
12 |
Util.closeSilently(outputStream); |
14 |
Bundle extras = new Bundle(); |
15 |
setResult(RESULT_OK, new Intent(mSaveUri.toString()).putExtras(extras)); |
代码示例:
我已经附上了一些代码示例,应该可以让你测试多种配置。请让我知道它对你是否有用。
代码下载:
MediaStoreTest
01 |
/** Called when the activity is first created. */ |
03 |
public void onCreate(Bundle savedInstanceState) { |
04 |
super .onCreate(savedInstanceState); |
06 |
setContentView(R.layout.main); |
07 |
mBtn = (Button) findViewById(R.id.btnLaunch); |
08 |
photo = (ImageView) findViewById(R.id.imgPhoto); |
09 |
mBtn.setOnClickListener( new OnClickListener() { |
11 |
public void onClick(View v) { |
14 |
Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null ); |
15 |
intent.setType( "image/*" ); |
16 |
intent.putExtra( "crop" , "true" ); |
17 |
intent.putExtra( "aspectX" , aspectX); |
18 |
intent.putExtra( "aspectY" , aspectY); |
19 |
intent.putExtra( "outputX" , outputX); |
20 |
intent.putExtra( "outputY" , outputY); |
21 |
intent.putExtra( "scale" , scale); |
22 |
intent.putExtra( "return-data" , return_data); |
23 |
intent.putExtra(MediaStore.EXTRA_OUTPUT, getTempUri()); |
24 |
intent.putExtra( "outputFormat" , |
25 |
Bitmap.CompressFormat.JPEG.toString()); <span style= "color:#48465A;font-family:monospace;font-size:11px;line-height:normal;background-color:#EFEFEF;" > |
27 |
intent.putExtra( "circleCrop" , true ); |
30 |
startActivityForResult(intent, PHOTO_PICKED); |
31 |
} catch (ActivityNotFoundException e) { |
32 |
Toast.makeText(thiz, R.string.photoPickerNotFoundText, |
33 |
Toast.LENGTH_LONG).show(); |
40 |
private Uri getTempUri() { |
41 |
return Uri.fromFile(getTempFile()); |
44 |
private File getTempFile() { |
45 |
if (isSDCARDMounted()) { |
47 |
File f = new File(Environment.getExternalStorageDirectory(), |
51 |
} catch (IOException e) { |
53 |
Toast.makeText(thiz, R.string.fileIOIssue, Toast.LENGTH_LONG) |
62 |
private boolean isSDCARDMounted() { |
63 |
String status = Environment.getExternalStorageState(); |
65 |
if (status.equals(Environment.MEDIA_MOUNTED)) |
71 |
protected void onActivityResult( int requestCode, int resultCode, Intent data) { |
72 |
super .onActivityResult(requestCode, resultCode, data); |
74 |
switch (requestCode) { |
76 |
if (resultCode == RESULT_OK) { |
78 |
Log.w(TAG, "Null data, but RESULT_OK, from image picker!" ); |
79 |
Toast t = Toast.makeText( this , R.string.no_photo_picked, |
85 |
final Bundle extras = data.getExtras(); |
87 |
File tempFile = getTempFile(); |
89 |
if (data.getAction() != null ) { |
90 |
processPhotoUpdate(tempFile); |
附录:
My comments
Thank you so much! The tutorial is great! Actually the secret of cropping photos on Android is using Uri if the photo is in large size and using Bitmap if you want but make sure that the bitmap is not too big.(You can use it for cropping avatar or other requirements with a limited size of the photo. Different phones have different limits. Normally if you want to use a bitmap, the size shouldn't be bigger than 300. Otherwise the Uri is suggested.)
约几个月前,我正为公司的APP在Android手机上实现拍照截图而烦恼不已。
上网搜索,确实有不少的例子,大多都是抄来抄去,而且水平多半处于demo的样子,可以用来讲解知识点,但是一碰到实际项目,就漏洞百出。
当时我用大众化的解决方案,暂时性的做了一个拍照截图的功能,似乎看起来很不错。问题随之而来,我用的是小米手机,在别的手机上都运行正常,小米这里却总是碰钉子。虽然我是个理性的米粉,但是也暗地里把小米的工程师问候了个遍。真是惭愧!
翻文档也找不出个答案来,我一直对com.android.camera.action.CROP持有大大的疑问,它是从哪里来,它能干什么,它接收处理什么类型的数据?Google对此却讳莫如深,在官方文档中只有Intent中有只言片语言及,却不甚详尽。
随着项目的驱动,我不能抱着不了解原理就不往前走的心态,唯一要做的,是解决问题。最后在德问上找到一条解决方案,说是哪怕是大米也没问题。当时乐呵呵将代码改了改,确实在所有的手机上跑起来了,一时如释重负,对这个的疑问也抛诸脑后了。
直到月前,BOSS要求将拍照上传到服务器的图片分辨率加倍。OK,加倍简单,增加outputX以及outputY不就得了?
1 |
intent.putExtra( "outputX" , outputX); |
2 |
intent.putExtra( "outputY" , outputY); |
这一增加,吓了我一跳。BOSS的手机拍到的照片几乎就是个缩略图,但是被我问候了全体工程师的小米在这个时候就体现出国产神机的范儿了,小米上的尺寸一切正常。这个为什么呢?我大致了解原因,却不知道如何解决。
在Android中,Intent触发Camera程序,拍好照片后,将会返回数据,但是考虑到内存问题,Camera不会将全尺寸的图像返回给调用的Activity,一般情况下,有可能返回的是缩略图,比如120*160px。
这是为什么呢?这不是一个Bug,而是经过精心设计的,却对开发者不透明。
以我的小米手机为例,摄像头800W像素,根据我目前设置拍出来的图片尺寸为3200*2400px。有人说,那就返回呗,大不了耗1-2M的内存,不错,这个尺寸的图片确实只有1.8M左右的大小。但是你想不到的是,这个尺寸对应的Bitmap会耗光你应用程序的所有内存。Android出于安全性考虑,只会给你一个寒碜的缩略图。
在Android2.3中,默认的Bitmap为32位,类型是ARGB_8888,也就意味着一个像素点占用4个字节的内存。我们来做一个简单的计算题:3200*2400*4 bytes = 30M。
如此惊人的数字!哪怕你愿意为一张生命周期超不过10s的位图愿意耗费这么巨大的内存,Android也不会答应的。
1 |
Mobile devices typically have constrained system resources. |
2 |
Android devices can have as little as 16MB of memory available to a single application. |
这是Android Doc的原文,虽然不同手机系统的厂商可能围绕16M这个数字有微微的上调,但是这30M,一般的手机还真挥霍不起。也只有小米这种牛机,内存堪比个人PC,本着土财主般挥金如土的霸气才能做到。
OK,说了这么多,无非是吐吐苦水,爆爆个人经历而已,实际的解决方案在哪里呢?
我也是Google到的,话说一般百度不了的问题,那就Google或者直接StackOverFlow,只不过得看英文罢了。
最后翻来覆去,我在国外的一个Android团队的博客中找到了相应的方案,印证了我的猜想同时也给出了实际的代码。
我将这篇文章翻译成了中文,作为本博客的基础,建议详细看看。
【译】如何使用Android MediaStore裁剪大图片
这篇博客了不起的地方在于解决了Android对返回图片的大小限制,并且详细解释了裁剪图片的Intent附加数据的具体含义。OK,我只是站在巨人的肩膀上,改善方案,适应更广泛需求而已。
拿图说事儿:
Intent("com.android.camera.action.CROP")对应的所有可选数据都一目了然。在了解上面个个选项的含义之后,我们将目光着眼于三个极为重要的选项:
data、MediaStore.EXTRA_OUTPUT以及return-data。
data和MediaStore.EXTRA_OUTPUT都是可选的传入数据选项,你可以选择设置data为Bitmap,或者将相应的数据与URI关联起来,你也可以选择是否返回数据(return-data: true)。
为什么还有不用返回数据的选项?如果对URI足够了解的话,应该知道URI与File相似,你所有的操作如裁剪将数据都保存在了URI中,你已经持有了相应的URI,也就无需多此一举,再返回Bitmap了。
前面已经说到,可以设置data为Bitmap,但是这种操作的限制在于,你的Bitmap不能太大。因此,我们前进的思路似乎明确了:截大图用URI,小图用Bitmap。
我将这个思路整理成一张图片:
这篇主要让大家了解需求的来源,以及如何去思考分析并解决问题。下一篇博客将介绍具体的操作。
在这篇博客中,我将向大家展示如何从相册截图。
上一篇博客中,我就拍照截图这一需求进行了详细的分析,试图让大家了解Android本身的限制,以及我们应当采取的实现方案。
根据我们的分析与总结,图片的来源有拍照和相册,而可采取的操作有
前面我们了解到,使用Bitmap有可能会导致图片过大,而不能返回实际大小的图片,我将采用大图Uri,小图Bitmap的数据存储方式。
我们将要使用到URI来保存拍照后的图片:
1 |
private static final String IMAGE_FILE_LOCATION = "file:///sdcard/temp.jpg" ;//temp file |
2 |
Uri imageUri = Uri.parse(IMAGE_FILE_LOCATION); |
不难知道,我们从相册选取图片的Action为Intent.ACTION_GET_CONTENT。
根据我们上一篇博客的分析,我准备好了两个实例的Intent。
一、从相册截大图:
01 |
Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null ); |
02 |
intent.setType( "image/*" ); |
03 |
intent.putExtra( "crop" , "true" ); |
04 |
intent.putExtra( "aspectX" , 2 ); |
05 |
intent.putExtra( "aspectY" , 1 ); |
06 |
intent.putExtra( "outputX" , 600 ); |
07 |
intent.putExtra( "outputY" , 300 ); |
08 |
intent.putExtra( "scale" , true ); |
09 |
intent.putExtra( "return-data" , false ); |
10 |
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri); |
11 |
intent.putExtra( "outputFormat" , Bitmap.CompressFormat.JPEG.toString()); |
12 |
intent.putExtra( "noFaceDetection" , true ); |
13 |
startActivityForResult(intent, CHOOSE_BIG_PICTURE); |
二、从相册截小图
01 |
Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null ); |
02 |
intent.setType( "image/*" ); |
03 |
intent.putExtra( "crop" , "true" ); |
04 |
intent.putExtra( "aspectX" , 2 ); |
05 |
intent.putExtra( "aspectY" , 1 ); |
06 |
intent.putExtra( "outputX" , 200 ); |
07 |
intent.putExtra( "outputY" , 100 ); |
08 |
intent.putExtra( "scale" , true ); |
09 |
intent.putExtra( "return-data" , true ); |
10 |
intent.putExtra( "outputFormat" , Bitmap.CompressFormat.JPEG.toString()); |
11 |
intent.putExtra( "noFaceDetection" , true ); |
12 |
startActivityForResult(intent, CHOOSE_SMALL_PICTURE); |
三、对应的onActivityResult可以这样处理返回的数据
01 |
switch (requestCode) { |
02 |
case CHOOSE_BIG_PICTURE: |
03 |
Log.d(TAG, "CHOOSE_BIG_PICTURE: data = " + data); |
05 |
Bitmap bitmap = decodeUriAsBitmap(imageUri); |
06 |
imageView.setImageBitmap(bitmap); |
09 |
case CHOOSE_SMALL_PICTURE: |
11 |
Bitmap bitmap = data.getParcelableExtra( "data" ); |
12 |
imageView.setImageBitmap(bitmap); |
14 |
Log.e(TAG, "CHOOSE_SMALL_PICTURE: data = " + data); |
01 |
private Bitmap decodeUriAsBitmap(Uri uri){ |
04 |
bitmap = BitmapFactory.decodeStream(getContentResolver().openInputStream(uri)); |
05 |
} catch (FileNotFoundException e) { |