Android开发技巧——大图裁剪
本篇内容是接上篇《Android开发技巧——定制仿微信图片裁剪控件》 的,先简单介绍对上篇所封装的裁剪控件的使用,再详细说明如何使用它进行大图裁剪,包括对旋转图片的裁剪。
裁剪控件的简单使用
XML代码
使用如普通控件一样,首先在布局文件里包含该控件:
<com.githang.clipimage.ClipImageView
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/clip_image_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@+id/bottom"
app:civClipPadding="@dimen/padding_common"
app:civHeight="2"
app:civMaskColor="@color/viewfinder_mask"
app:civWidth="3"/>
支持的属性如下:
- civHeight 高度比例,默认为1
- civWidth 宽度比例,默认为1
- civTipText 裁剪的提示文字
- civTipTextSize 裁剪的提示文字的大小
- civMaskColor 遮罩层颜色
- civClipPadding 裁剪框边距
Java代码
如果裁剪的图片不大,可以直接设置,就像使用ImageView一样,通过如下四种方法设置图片:
mClipImageView.setImageURI(Uri.fromFile(new File(mInput)));
mClipImageView.setImageBitmap(bitmap);
mClipImageView.setImageResource(R.drawable.xxxx);
mClipImageView.setImageDrawable(drawable);
裁剪的时候调用mClipImageView.clip();
就可以返回裁剪之后的Bitmap
对象。
大图裁剪
这里会把大图裁剪及图片文件可能旋转的情况一起处理。
注意:由于裁剪图片最终还是需要把裁剪结果以Bitmap对象加载到内存中,所以裁剪之后的图片也是会有大小限制的,否则会有OOM的情况。所以,下面会设一个裁剪后的最大宽度的值。
读取图片旋转角度
在第一篇《 Android开发技巧——Camera拍照功能 》的时候,有提到过像三星的手机,竖屏拍出来的照片还是横的,但是有Exif信息记录了它的旋转方向。考虑到我们进行裁剪的时候,也会遇到类似这样的照片,所以对于这种照片需要旋转的情况,我选择了在裁剪的时候才进行处理。所以首先,我们需要读到图片的旋转角度:
/**
* 读取图片属性:旋转的角度
*
* @param path 图片绝对路径
* @return degree旋转的角度
*/
public static int readPictureDegree(String path) {
int degree = 0;
try {
ExifInterface exifInterface = new ExifInterface(path);
int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
switch (orientation) {
case ExifInterface.ORIENTATION_ROTATE_90:
degree = 90;
break;
case ExifInterface.ORIENTATION_ROTATE_180:
degree = 180;
break;
case ExifInterface.ORIENTATION_ROTATE_270:
degree = 270;
break;
}
} catch (IOException e) {
e.printStackTrace();
}
return degree;
}
如果你能确保要裁剪的图片不大不会导致OOM的情况发生的话,是可以直接通过这个角度,创建一个Matrix
对象,进行postRotate
,然后由原图创建一个新的Bitmap
来得到一个正确朝向的图片的。但是这里考虑到我们要裁剪的图片是从手机里读取的,有可能有大图,而我们的裁剪控件本身只实现了简单的手势缩放和裁剪功能,并没有实现大图加载的功能,所以需要在设置图片进行之前进行一些预处理。
采样缩放
由于图片较大,而我们又需要把整张图都加载进来而不是只加载局部,所以就需要在加载的时候进行采样,来加载缩小之后的图片,这样加载到的图片较小,就能有效避免OOM了。
以前文提到的裁剪证件照为例,这里仍以宽度为参考值来计算采样值,具体是用宽还是高或者是综合宽高(这种情况较多,考虑到可能会有很长的图)来计算采样值,还得看你具体情况。在计算采样的时候,我们还需要用到上面读到的旋转值,在图片被旋转90度或180度时,进行宽和高的置换。所以,除了相关的控件,我们需要定义如下相关的变量:
private String mOutput;
private String mInput;
private int mMaxWidth;
// 图片被旋转的角度
private int mDegree;
// 大图被设置之前的采样比例
private int mSampleSize;
private int mSourceWidth;
private int mSourceHeight;
计算采样代码如下:
mClipImageView.post(new Runnable() {
@Override
public void run() {
mClipImageView.setMaxOutputWidth(mMaxWidth);
mDegree = readPictureDegree(mInput);
final boolean isRotate = (mDegree == 90 || mDegree == 270);
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(mInput, options);
mSourceWidth = options.outWidth;
mSourceHeight = options.outHeight;
// 如果图片被旋转,则宽高度置换
int w = isRotate ? options.outHeight : options.outWidth;
// 裁剪是宽高比例3:2,只考虑宽度情况,这里按border宽度的两倍来计算缩放。
mSampleSize = findBestSample(w, mClipImageView.getClipBorder().width());
//代码未完,将下面的[缩放及设置]里分段讲到。
}
});
由于我们是需要裁剪控件的裁剪框来计算采样,所以需要获取裁剪框,因此我们把上面的代码通过控件的post方法来调用。
inJustDecodeBounds
在许多讲大图缩放的博客都有讲到,相信很多朋友都清楚,本文就不赘述了。
注意:采样的值是2的幂次方的,如果你传的值不是2的幂次方,它在计算的时候最终会往下找到最近的2的幂次方的值。所以,如果你后面还需要用这个值来进行计算,就不要使用网上的一些直接用两个值相除进行计算sampleSize的方法。精确的计算方式应该是直接计算时这个2的幂次方的值,例如下面代码:
/**
* 计算最好的采样大小。
* @param origin 当前宽度
* @param target 限定宽度
* @return sampleSize
*/
private static int findBestSample(int origin, int target) {
int sample = 1;
for (int out = origin / 2; out > target; out /= 2) {
sample *= 2;
}
return sample;
}
缩放及设置
接下来就是设置inJustDecodeBounds
,inSampleSize
,以及把inPreferredConfig
设置为RGB_565
,然后把图片给加载进来,如下:
options.inJustDecodeBounds = false;
options.inSampleSize = mSampleSize;
options.inPreferredConfig = Bitmap.Config.RGB_565;
final Bitmap source = BitmapFactory.decodeFile(mInput, options);
这里加载的图片还是没有旋转到正确朝向的,所以我们要根据上面所计算的角度,对图片进行旋转。我们竖屏拍的图,在一些手机上是横着保存的,但是它会记录一个旋转90度的值在Exif中。如下图中,左边是保存的图,它依然是横着的,右边是我们显示时的图。所以我们读取到这个值后,需要对它进行顺时针的旋转。
代码如下:
// 解决图片被旋转的问题
Bitmap target;
if (mDegree == 0) {
target = source;
} else {
final Matrix matrix = new Matrix();
matrix.postRotate(mDegree);
target = Bitmap.createBitmap(source, 0, 0, source.getWidth(), source.getHeight(), matrix, false);
if (target != source && !source.isRecycled()) {
source.recycle();
}
}
mClipImageView.setImageBitmap(target);
这里需要补充的一个注意点是:Bitmap.createBitmap(source, 0, 0, source.getWidth(), source.getHeight(), matrix, false);
这个方法返回的Bitmap
不一定是重新创建的,如果matrix
相同并且宽高相同,而且你没有对Bitmap
进行其他设置的话,它可能会返回原来的对象。所以在创建新的Bitmap
之后,回收原来的Bitmap
时要判断是否可以回收,否则可能导致创建出来的target
对象被回收而使ImageView
的图片无法显示出来。
如上,就是完整的设置大图时的处理过程的代码。
裁剪
裁剪时需要创建一个裁剪之后的Bitmap
,再把它保存下来。下面介绍一下这个创建过程。完整代码如下:
private Bitmap createClippedBitmap() {
if (mSampleSize <= 1) {
return mClipImageView.clip();
}
// 获取缩放位移后的矩阵值
final float[] matrixValues = mClipImageView.getClipMatrixValues();
final float scale = matrixValues[Matrix.MSCALE_X];
final float transX = matrixValues[Matrix.MTRANS_X];
final float transY = matrixValues[Matrix.MTRANS_Y];
// 获取在显示的图片中裁剪的位置
final Rect border = mClipImageView.getClipBorder();
final float cropX = ((-transX + border.left) / scale) * mSampleSize;
final float cropY = ((-transY + border.top) / scale) * mSampleSize;
final float cropWidth = (border.width() / scale) * mSampleSize;
final float cropHeight = (border.height() / scale) * mSampleSize;
// 获取在旋转之前的裁剪位置
final RectF srcRect = new RectF(cropX, cropY, cropX + cropWidth, cropY + cropHeight);
final Rect clipRect = getRealRect(srcRect);
final BitmapFactory.Options ops = new BitmapFactory.Options();
final Matrix outputMatrix = new Matrix();
outputMatrix.setRotate(mDegree);
// 如果裁剪之后的图片宽高仍然太大,则进行缩小
if (mMaxWidth > 0 && cropWidth > mMaxWidth) {
ops.inSampleSize = findBestSample((int) cropWidth, mMaxWidth);
final float outputScale = mMaxWidth / (cropWidth / ops.inSampleSize);
outputMatrix.postScale(outputScale, outputScale);
}
// 裁剪
BitmapRegionDecoder decoder = null;
try {
decoder = BitmapRegionDecoder.newInstance(mInput, false);
final Bitmap source = decoder.decodeRegion(clipRect, ops);
recycleImageViewBitmap();
return Bitmap.createBitmap(source, 0, 0, source.getWidth(), source.getHeight(), outputMatrix, false);
} catch (Exception e) {
return mClipImageView.clip();
} finally {
if (decoder != null && !decoder.isRecycled()) {
decoder.recycle();
}
}
}
下面分段介绍。
计算在采样缩小前的裁剪框
首先,如果采样值不大于1,也就是我们没有进行图片缩小的时候,就不需要进行下面的计算了,直接调用我们的裁剪控件返回裁剪后的图片即可。否则,就是我们对图片进行缩放的情况了,所以会需要综合我们的采样值mSampleSize
,计算我们的裁剪框实际上在原图上的位置。所以会看到相对于上篇所讲的裁剪控件对裁剪框的计算,这里多乘了一个mSampleSize
的值,如下:
// 获取在显示的图片中裁剪的位置
final Rect border = mClipImageView.getClipBorder();
final float cropX = ((-transX + border.left) / scale) * mSampleSize;
final float cropY = ((-transY + border.top) / scale) * mSampleSize;
final float cropWidth = (border.width() / scale) * mSampleSize;
final float cropHeight = (border.height() / scale) * mSampleSize;
然后我们创建这个在原图大小时的裁剪框:
final RectF srcRect = new RectF(cropX, cropY, cropX + cropWidth, cropY + cropHeight);
计算在图片旋转前的裁剪框
对于大图的裁剪,我们可以使用BitmapRegionDecoder
类,来只加载图片的一部分,也就是用它来加载我们所需要裁剪的那一部分,但是它是从旋转之前的原图进行裁剪的,所以还需要对这个裁剪框进行反向的旋转,来计算它在原图上的位置。
如下图所示,ABCD是旋转90度之后的图片,EFGH是我们的裁剪框。
但是在原图中,它们的对应位置如下图所示:
也就是B点成了A点,A点成了D点,等等。
所以我们获取EFGH在ABCD中的位置,也不能像裁剪控件那样,而需要进行反转之后的计算。以旋转90度为例,现在我们的左上角变成了F点,那么它的left就是原来的top,它的top就是图片的高度减去原来的right,它的right就是原来的bottom,它的bottom就是图片的高度减去原来的left,完整代码如下:
private Rect getRealRect(RectF srcRect) {
switch (mDegree) {
case 90:
return new Rect((int) srcRect.top, (int) (mSourceHeight - srcRect.right),
(int) srcRect.bottom, (int) (mSourceHeight - srcRect.left));
case 180:
return new Rect((int) (mSourceWidth - srcRect.right), (int) (mSourceHeight - srcRect.bottom),
(int) (mSourceWidth - srcRect.left), (int) (mSourceHeight - srcRect.top));
case 270:
return new Rect((int) (mSourceWidth - srcRect.bottom), (int) srcRect.left,
(int) (mSourceWidth - srcRect.top), (int) srcRect.right);
default:
return new Rect((int) srcRect.left, (int) srcRect.top, (int) srcRect.right, (int) srcRect.bottom);
}
}
所以在原图上的真正的裁剪框位置是:
final Rect clipRect = getRealRect(srcRect);
局部加载所裁剪的图片部分
大图裁剪,我们使用BitmapRegionDecoder
类,它可以只加载指定的某一部分的图片内容,通过它的public Bitmap decodeRegion(Rect rect, BitmapFactory.Options options)
方法,我们可以把所裁剪的内容加载出来,得到一个Bitmap,这个Bitmap就是我们要裁剪的内容了。但是,我们加载的这部分内容,同样可能太宽,所以还可能需要进行采样缩小。如下:
final BitmapFactory.Options ops = new BitmapFactory.Options();
final Matrix outputMatrix = new Matrix();//用于最图图片的精确缩放
outputMatrix.setRotate(mDegree);
// 如果裁剪之后的图片宽高仍然太大,则进行缩小
if (mMaxWidth > 0 && cropWidth > mMaxWidth) {
ops.inSampleSize = findBestSample((int) cropWidth, mMaxWidth);
final float outputScale = mMaxWidth / (cropWidth / ops.inSampleSize);
outputMatrix.postScale(outputScale, outputScale);
}
计算出采样值sampleSize之后,再使用它及我们计算的裁剪框,加载所裁剪的内容:
// 裁剪
BitmapRegionDecoder decoder = null;
try {
decoder = BitmapRegionDecoder.newInstance(mInput, false);
final Bitmap source = decoder.decodeRegion(clipRect, ops);
recycleImageViewBitmap();
return Bitmap.createBitmap(source, 0, 0, source.getWidth(), source.getHeight(), outputMatrix, false);
} catch (Exception e) {
return mClipImageView.clip();
} finally {
if (decoder != null && !decoder.isRecycled()) {
decoder.recycle();
}
}
总结
完整代码见github上我的clip-image项目的示例ClipImageActivity.java。
上面例子中,我所用的图片并不大,下面我打包了一个大图的apk,它使用了维基百科上的一张世界地图,下载地址如下:http://download.csdn.net/detail/maosidiaoxian/9464200
上面的例子截图:
可以看出,在这个例子中,虽然在裁剪过程当中图片被缩放过所以不太清晰,但是我们真正的裁剪是对原图进行裁剪再进行适当的缩放的,所以裁剪之后的图片更清晰。
本文原创,转载请注明CSDN博客出处。
http://blog.csdn.net/maosidiaoxian/article/details/50912577
Android开发技巧——大图裁剪的更多相关文章
- 50个android开发技巧
50个android开发技巧 http://blog.csdn.net/column/details/androidhacks.html
- Android开发技巧——使用PopupWindow实现弹出菜单
在本文当中,我将会与大家分享一个封装了PopupWindow实现弹出菜单的类,并说明它的实现与使用. 因对界面的需求,android原生的弹出菜单已不能满足我们的需求,自定义菜单成了我们的唯一选择,在 ...
- Android开发技巧——实现可复用的ActionSheet菜单
在上一篇<Android开发技巧--使用Dialog实现仿QQ的ActionSheet菜单>中,讲了这种菜单的实现过程,接下来将把它改成一个可复用的控件库. 本文原创,转载请注明出处: h ...
- Android开发技巧——高亮的用户操作指南
Android开发技巧--高亮的用户操作指南 2015-12-15补记: 发现使用PopupWindow进行遮罩层的显示,在华为P7上会有问题.具体表现为:画出来的高亮部分会偏下.原因为:通过view ...
- Android开发技巧——自定义控件之增加状态
Android开发技巧--自定义控件之增加状态 题外话 这篇本该是上周四或上周五写的,无奈太久没写博客,前几段把我的兴头都用完了,就一拖再拖,直到今天.不想把这篇拖到下个月,所以还是先硬着头皮写了. ...
- Android开发技巧——自定义控件之使用style
Android开发技巧--自定义控件之使用style 回顾 在上一篇<Android开发技巧--自定义控件之自定义属性>中,我讲到了如何定义属性以及在自定义控件中获取这些属性的值,也提到了 ...
- Android开发技巧——自定义控件之自定义属性
Android开发技巧--自定义控件之自定义属性 掌握自定义控件是很重要的,因为通过自定义控件,能够:解决UI问题,优化布局性能,简化布局代码. 上一篇讲了如何通过xml把几个控件组织起来,并继承某个 ...
- Android开发技巧——自定义控件之组合控件
Android开发技巧--自定义控件之组合控件 我准备在接下来一段时间,写一系列有关Android自定义控件的博客,包括如何进行各种自定义,并分享一下我所知道的其中的技巧,注意点等. 还是那句老话,尽 ...
- Android开发技巧——写一个StepView
在我们的应用开发中,有些业务流程会涉及到多个步骤,或者是多个状态的转化,因此,会需要有相关的设计来展示该业务流程.比如<停车王>应用里的添加车牌的步骤. 通常,我们会把这类控件称为&quo ...
随机推荐
- sublime高亮代码导出
何在word/博客中使用SublimeText风格的代码高亮样式 原文链接:http://www.cnblogs.com/Wayou/p/highlight_code_with_sublimetext ...
- C# GetValue 正则获取开始结束代码
/// <summary> /// 获得字符串中开始和结束字符串中间得值 /// </summary> /// <param name="str"&g ...
- Linux 在添加一个新账号后却没有权限怎么办
当添加一个新账号后,我们可能会发现新账号sudo 时会报告不在sudoers中,使用su -s时输入密码后也会认证失败 上网搜索大部分都要求修改/etc/sudoers中的内容,但修改这个文件必须需要 ...
- 在iview的Table中添加Select(render)
首先对Render进行分析,在iview官方的文档中,找到了table插入Button的例子: { title: 'Action', key: 'action', width: 150, align: ...
- [HNOI 2014]江南乐
Description 题库链接 给你指定一个数 \(f\) ,并给你 \(T\) 组游戏,每组有 \(n\) 堆石子, \(A,B\) 两人轮流对石子进行操作,每次你可以选择其中任意一堆数量不小于 ...
- [NOI 2016]区间
Description 在数轴上有 $n$ 个闭区间 $[l_1,r_1],[l_2,r_2],...,[l_n,r_n]$.现在要从中选出 $m$ 个区间,使得这 $m$ 个区间共同包含至少一个位置 ...
- [HNOI2015]接水果
题目描述 风见幽香非常喜欢玩一个叫做 osu!的游戏,其中她最喜欢玩的模式就是接水果.由于她已经DT FC 了The big black, 她觉得这个游戏太简单了,于是发明了一个更加难的版本. 首先有 ...
- hdu 5266 pog loves szh III(lca + 线段树)
I - pog loves szh III Time Limit:6000MS Memory Limit:131072KB 64bit IO Format:%I64d & %I ...
- bzoj 2440 (莫比乌斯函数)
bzoj 2440 完全平方数 题意:找出第k个不是完全平方数的正整数倍的数. 例如 4 9 16 25 36什么的 通过容斥原理,我们减去所有完全数 4有n/4个,但是36这种会被重复减去, ...
- Codeforces Round #398 (div.2)简要题解
这场cf时间特别好,周六下午,于是就打了打(谁叫我永远1800上不去div1) 比以前div2的题目更均衡了,没有太简单和太难的...好像B题难度高了很多,然后卡了很多人. 然后我最后做了四题,E题感 ...