本篇内容是接上篇《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. java 连接mysql

    目前还沉浸在java自动化测试中不能自拔! 自动化过程中免不了要从数据库取值与期望值比较,目前我项目刚开始就需要用到了. 下面我把操作过程写下来: 我的项目框架是java+maven+testNG,所 ...

  2. 华为防火墙USG5500-企业双ISP出口

    需求:(1)技术部IP地址自动获取,网段为192.168.10.0/24,该部门访问Internet的报文正常情况下流入链路ISP1. 总经办IP地址自动获取,网段为192.168.20.0/24,该 ...

  3. Eclipse代码块折叠插件,安装使用

    在代码编写中经常会遇到一些很长的set(xxx)的代码,非常影响体验. 而Eclipse的folding插件可以自定义的将代码块进行折叠. 效果如下图所示: 可以根据代码块的功能来进行折叠,从而保证代 ...

  4. further occurrences of HTTP header parsing errors will be logged at DEBUG level.错误

    今天进行项目测试的时候出现了further occurrences of HTTP header parsing errors will be logged at DEBUG level.错误,查了半 ...

  5. python读取excel时,数字自动转化为float

    xlrd模块去读excel时会将数字类型的自动转化为浮点数,这是一个小坑.在网上查了一下,该模块的作者也说过Excel treats all numbers as floats. In general ...

  6. .NET Core2.0+MVC 用session,cookie实现的sso单点登录

    博主刚接触.NET Core2.0,想做一个单点登录的demo,所以参考了一些资料,这里给上链接: 1.http://www.cnblogs.com/baibaomen/p/sso-sequence- ...

  7. openfire彻底卸载的方法

    最近百度找openfire彻底卸载的方法,很多都是三句命令行的答案.但是那三句真的无法完全卸载 终于从openfire官网找到了卸载的命令 终端执行下面的命令 sudo rm -rf /usr/loc ...

  8. 对于IO流的个人理解

    Samuel 2018-04-21 在这之前,我给你们构造这样一个生活用水的场景: 人们日常生活需要生活用水,那么,水从哪里来呢? 大家都学过初中的物理常识,水在地表,通过蒸发,变成水蒸气去到空中,在 ...

  9. 使用vba做一个正则表达式提取文本工具

    测试中经常会遇到对数据的处理,比如我要删除某些特定数据,数据源是从网页请求中抓取,这时候可能复制下来一大堆内容,其中我们只需要特定的某些部分,笔者通常做法是拷贝到notepad++中处理,结合RegT ...

  10. [HAOI 2011]Problem c

    Description 给n个人安排座位,先给每个人一个1~n的编号,设第i个人的编号为ai(不同人的编号可以相同),接着从第一个人开始,大家依次入座,第i个人来了以后尝试坐到ai,如果ai被占据了, ...