本篇内容是接上篇《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;
}

缩放及设置

接下来就是设置inJustDecodeBoundsinSampleSize,以及把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开发技巧——大图裁剪的更多相关文章

  1. 50个android开发技巧

    50个android开发技巧 http://blog.csdn.net/column/details/androidhacks.html

  2. Android开发技巧——使用PopupWindow实现弹出菜单

    在本文当中,我将会与大家分享一个封装了PopupWindow实现弹出菜单的类,并说明它的实现与使用. 因对界面的需求,android原生的弹出菜单已不能满足我们的需求,自定义菜单成了我们的唯一选择,在 ...

  3. Android开发技巧——实现可复用的ActionSheet菜单

    在上一篇<Android开发技巧--使用Dialog实现仿QQ的ActionSheet菜单>中,讲了这种菜单的实现过程,接下来将把它改成一个可复用的控件库. 本文原创,转载请注明出处: h ...

  4. Android开发技巧——高亮的用户操作指南

    Android开发技巧--高亮的用户操作指南 2015-12-15补记: 发现使用PopupWindow进行遮罩层的显示,在华为P7上会有问题.具体表现为:画出来的高亮部分会偏下.原因为:通过view ...

  5. Android开发技巧——自定义控件之增加状态

    Android开发技巧--自定义控件之增加状态 题外话 这篇本该是上周四或上周五写的,无奈太久没写博客,前几段把我的兴头都用完了,就一拖再拖,直到今天.不想把这篇拖到下个月,所以还是先硬着头皮写了. ...

  6. Android开发技巧——自定义控件之使用style

    Android开发技巧--自定义控件之使用style 回顾 在上一篇<Android开发技巧--自定义控件之自定义属性>中,我讲到了如何定义属性以及在自定义控件中获取这些属性的值,也提到了 ...

  7. Android开发技巧——自定义控件之自定义属性

    Android开发技巧--自定义控件之自定义属性 掌握自定义控件是很重要的,因为通过自定义控件,能够:解决UI问题,优化布局性能,简化布局代码. 上一篇讲了如何通过xml把几个控件组织起来,并继承某个 ...

  8. Android开发技巧——自定义控件之组合控件

    Android开发技巧--自定义控件之组合控件 我准备在接下来一段时间,写一系列有关Android自定义控件的博客,包括如何进行各种自定义,并分享一下我所知道的其中的技巧,注意点等. 还是那句老话,尽 ...

  9. Android开发技巧——写一个StepView

    在我们的应用开发中,有些业务流程会涉及到多个步骤,或者是多个状态的转化,因此,会需要有相关的设计来展示该业务流程.比如<停车王>应用里的添加车牌的步骤. 通常,我们会把这类控件称为&quo ...

随机推荐

  1. CSS3 3D transform变换

    .实际应用-图片的旋转木马效果 您可以狠狠地点击这里:图片的旋转木马效果demo 建议在足够新版本的FireFox浏览器或Safari浏览器下观看,Chrome可能需要居中定位查看,下图为效果缩略图: ...

  2. JavaScript 递归

    递归是一种解决问题的方法,它解决问题的各个小部分,直到解决最初的大问题.通常涉及 函数调用自身. 能够像下面这样直接调用自身的方法或函数,是递归函数: var recursiveFunction = ...

  3. 实验吧_密码忘记了(vim编辑器+代码审计)&天网管理系统(php弱比较+反序列化)

    密码忘记了 一开始尝试了各种注入发现都无效,在网页源码中找到了admin 的地址,输入地址栏发现并没有什么有用的信息,随便输个邮箱,网页返回了一个地址 ./step2.php?email=youmai ...

  4. jdk1.7中的常量池

    在探究jdk1.7中的常量池,我们可以先看看以下的这段代码 public static void main(String[] args) throws Throwable { List<Stri ...

  5. Linux(四)使用Xshell5远程登录

    Xshell5和Xftp5的安装包 链接:https://pan.baidu.com/s/1q3-ch75TW3lvC3KX25klNQ 密码:m31n 说明: 公司开发的时候,具体情况是这样的: 1 ...

  6. [SHOI 2011]双倍回文

    Description 题库链接 记一个字符串为 \(X\) ,它的倒置为 \(X^R\) .现在给你一个长度为 \(n\) 的字符串 \(S\) ,询问其最长的形同 \(XX^RXX^R\) 的子串 ...

  7. Evensgn 捡树枝

    问题 A: Evensgn 剪树枝 时间限制: 1 Sec  内存限制: 128 MB 题目描述 繁华中学有一棵苹果树.苹果树有 n 个节点(也就是苹果),n − 1 条边(也就 是树枝).调皮的 E ...

  8. [HEOI2016]排序

    题目描述 在2016年,佳媛姐姐喜欢上了数字序列.因而他经常研究关于序列的一些奇奇怪怪的问题,现在他在研究一个难题,需要你来帮助他.这个难题是这样子 的:给出一个1到n的全排列,现在对这个全排列序列进 ...

  9. 【HDU 2669】Romantic

    Problem Description The Sky is Sprite.The Birds is Fly in the Sky.The Wind is Wonderful.Blew Throw t ...

  10. C语言程序设计第一次作业(2017.10.10完成)

    一:程序框图以及正确运行结果: (1)给出圆半径,得出圆面积: ①程序框图如下: ②测试图如下: 经过测试 ,输入半径2能得出正确结果.多次测试,输入不同值,均得出正确结果,证明稳定性. ③实验分析: ...