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;
    }

缩放及设置

接下来就是设置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中。如下图中,左边是保存的图,它依然是横着的,右边是我们显示时的图。所以我们读取到这个值后,需要对它进行顺时针的旋转。
Android开发技巧——大图裁剪_第1张图片
代码如下:

        // 解决图片被旋转的问题
        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是我们的裁剪框。
Android开发技巧——大图裁剪_第2张图片
但是在原图中,它们的对应位置如下图所示:
Android开发技巧——大图裁剪_第3张图片
也就是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) (mSourceHeight - srcRect.right), (int) (mSourceWidth - srcRect.bottom),
                        (int) (mSourceHeight - srcRect.left), (int) (mSourceWidth - 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

上面的例子截图:
Android开发技巧——大图裁剪_第4张图片
可以看出,在这个例子中,虽然在裁剪过程当中图片被缩放过所以不太清晰,但是我们真正的裁剪是对原图进行裁剪再进行适当的缩放的,所以裁剪之后的图片更清晰。

本文原创,转载请注明CSDN博客出处。
http://blog.csdn.net/maosidiaoxian/article/details/50912577

你可能感兴趣的:(android,图片裁剪,局部加载,大图裁剪)