com.bumptech.glide.load.resource.bitmap.TransformationUtils Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of glide Show documentation
Show all versions of glide Show documentation
A fast and efficient image loading library for Android focused on smooth scrolling.
package com.bumptech.glide.load.resource.bitmap;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.RectF;
import android.graphics.Shader;
import android.media.ExifInterface;
import android.os.Build;
import android.support.annotation.NonNull;
import android.util.Log;
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
import com.bumptech.glide.util.Preconditions;
import com.bumptech.glide.util.Synthetic;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* A class with methods to efficiently resize Bitmaps.
*/
public final class TransformationUtils {
private static final String TAG = "TransformationUtils";
public static final int PAINT_FLAGS = Paint.DITHER_FLAG | Paint.FILTER_BITMAP_FLAG;
private static final Paint DEFAULT_PAINT = new Paint(PAINT_FLAGS);
private static final int CIRCLE_CROP_PAINT_FLAGS = PAINT_FLAGS | Paint.ANTI_ALIAS_FLAG;
private static final Paint CIRCLE_CROP_SHAPE_PAINT = new Paint(CIRCLE_CROP_PAINT_FLAGS);
private static final Paint CIRCLE_CROP_BITMAP_PAINT;
// See #738.
private static final List MODELS_REQUIRING_BITMAP_LOCK =
Arrays.asList(
"XT1097",
"XT1085");
/**
* https://github.com/bumptech/glide/issues/738 On some devices (Moto X with android 5.1) bitmap
* drawing is not thread safe.
* This lock only locks for these specific devices. For other types of devices the lock is always
* available and therefore does not impact performance
*/
private static final Lock BITMAP_DRAWABLE_LOCK =
MODELS_REQUIRING_BITMAP_LOCK.contains(Build.MODEL)
&& Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP_MR1
? new ReentrantLock() : new NoLock();
static {
CIRCLE_CROP_BITMAP_PAINT = new Paint(CIRCLE_CROP_PAINT_FLAGS);
CIRCLE_CROP_BITMAP_PAINT.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
}
private TransformationUtils() {
// Utility class.
}
public static Lock getBitmapDrawableLock() {
return BITMAP_DRAWABLE_LOCK;
}
/**
* A potentially expensive operation to crop the given Bitmap so that it fills the given
* dimensions. This operation is significantly less expensive in terms of memory if a mutable
* Bitmap with the given dimensions is passed in as well.
*
* @param pool The BitmapPool to obtain a bitmap from.
* @param inBitmap The Bitmap to resize.
* @param width The width in pixels of the final Bitmap.
* @param height The height in pixels of the final Bitmap.
* @return The resized Bitmap (will be recycled if recycled is not null).
*/
public static Bitmap centerCrop(@NonNull BitmapPool pool, @NonNull Bitmap inBitmap, int width,
int height) {
if (inBitmap.getWidth() == width && inBitmap.getHeight() == height) {
return inBitmap;
}
// From ImageView/Bitmap.createScaledBitmap.
final float scale;
float dx = 0, dy = 0;
Matrix m = new Matrix();
if (inBitmap.getWidth() * height > width * inBitmap.getHeight()) {
scale = (float) height / (float) inBitmap.getHeight();
dx = (width - inBitmap.getWidth() * scale) * 0.5f;
} else {
scale = (float) width / (float) inBitmap.getWidth();
dy = (height - inBitmap.getHeight() * scale) * 0.5f;
}
m.setScale(scale, scale);
m.postTranslate((int) (dx + 0.5f), (int) (dy + 0.5f));
Bitmap result = pool.get(width, height, getSafeConfig(inBitmap));
// We don't add or remove alpha, so keep the alpha setting of the Bitmap we were given.
TransformationUtils.setAlpha(inBitmap, result);
applyMatrix(inBitmap, result, m);
return result;
}
/**
* An expensive operation to resize the given Bitmap down so that it fits within the given
* dimensions maintain the original proportions.
*
* @param pool The BitmapPool obtain a bitmap from.
* @param inBitmap The Bitmap to shrink.
* @param width The width in pixels the final image will fit within.
* @param height The height in pixels the final image will fit within.
* @return A new Bitmap shrunk to fit within the given dimensions, or toFit if toFit's width or
* height matches the given dimensions and toFit fits within the given dimensions
*/
public static Bitmap fitCenter(@NonNull BitmapPool pool, @NonNull Bitmap inBitmap, int width,
int height) {
if (inBitmap.getWidth() == width && inBitmap.getHeight() == height) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "requested target size matches input, returning input");
}
return inBitmap;
}
final float widthPercentage = width / (float) inBitmap.getWidth();
final float heightPercentage = height / (float) inBitmap.getHeight();
final float minPercentage = Math.min(widthPercentage, heightPercentage);
// take the floor of the target width/height, not round. If the matrix
// passed into drawBitmap rounds differently, we want to slightly
// overdraw, not underdraw, to avoid artifacts from bitmap reuse.
final int targetWidth = (int) (minPercentage * inBitmap.getWidth());
final int targetHeight = (int) (minPercentage * inBitmap.getHeight());
if (inBitmap.getWidth() == targetWidth && inBitmap.getHeight() == targetHeight) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "adjusted target size matches input, returning input");
}
return inBitmap;
}
Bitmap.Config config = getSafeConfig(inBitmap);
Bitmap toReuse = pool.get(targetWidth, targetHeight, config);
// We don't add or remove alpha, so keep the alpha setting of the Bitmap we were given.
TransformationUtils.setAlpha(inBitmap, toReuse);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "request: " + width + "x" + height);
Log.v(TAG, "toFit: " + inBitmap.getWidth() + "x" + inBitmap.getHeight());
Log.v(TAG, "toReuse: " + toReuse.getWidth() + "x" + toReuse.getHeight());
Log.v(TAG, "minPct: " + minPercentage);
}
Matrix matrix = new Matrix();
matrix.setScale(minPercentage, minPercentage);
applyMatrix(inBitmap, toReuse, matrix);
return toReuse;
}
/**
* If the Bitmap is smaller or equal to the Target it returns the original size, if not then
* {@link #fitCenter(BitmapPool, Bitmap, int, int)} is called instead.
*
* @param pool The BitmapPool obtain a bitmap from.
* @param inBitmap The Bitmap to center.
* @param width The width in pixels of the target.
* @param height The height in pixels of the target.
* @return returns input Bitmap if smaller or equal to target, or toFit if the Bitmap's width or
* height is larger than the given dimensions
*/
public static Bitmap centerInside(@NonNull BitmapPool pool, @NonNull Bitmap inBitmap, int width,
int height) {
if (inBitmap.getWidth() <= width && inBitmap.getHeight() <= height) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "requested target size larger or equal to input, returning input");
}
return inBitmap;
} else {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "requested target size too big for input, fit centering instead");
}
return fitCenter(pool, inBitmap, width, height);
}
}
/**
* Sets the alpha of the Bitmap we're going to re-use to the alpha of the Bitmap we're going to
* transform. This keeps {@link android.graphics.Bitmap#hasAlpha()}} consistent before and after
* the transformation for transformations that don't add or remove transparent pixels.
*
* @param inBitmap The {@link android.graphics.Bitmap} that will be transformed.
* @param outBitmap The {@link android.graphics.Bitmap} that will be returned from the
* transformation.
*/
public static void setAlpha(Bitmap inBitmap, Bitmap outBitmap) {
outBitmap.setHasAlpha(inBitmap.hasAlpha());
}
/**
* This is an expensive operation that copies the image in place with the pixels rotated. If
* possible rather use getOrientationMatrix, and put that as the imageMatrix on an ImageView.
*
* @param imageToOrient Image Bitmap to orient.
* @param degreesToRotate number of degrees to rotate the image by. If zero the original image is
* returned unmodified.
* @return The oriented bitmap. May be the imageToOrient without modification, or a new Bitmap.
*/
public static Bitmap rotateImage(@NonNull Bitmap imageToOrient, int degreesToRotate) {
Bitmap result = imageToOrient;
try {
if (degreesToRotate != 0) {
Matrix matrix = new Matrix();
matrix.setRotate(degreesToRotate);
result = Bitmap.createBitmap(imageToOrient, 0, 0, imageToOrient.getWidth(),
imageToOrient.getHeight(), matrix, true /*filter*/);
}
} catch (Exception e) {
if (Log.isLoggable(TAG, Log.ERROR)) {
Log.e(TAG, "Exception when trying to orient image", e);
}
}
return result;
}
/**
* Get the # of degrees an image must be rotated to match the given exif orientation.
*
* @param exifOrientation The exif orientation [1-8]
* @return the number of degrees to rotate
*/
public static int getExifOrientationDegrees(int exifOrientation) {
final int degreesToRotate;
switch (exifOrientation) {
case ExifInterface.ORIENTATION_TRANSPOSE:
case ExifInterface.ORIENTATION_ROTATE_90:
degreesToRotate = 90;
break;
case ExifInterface.ORIENTATION_ROTATE_180:
case ExifInterface.ORIENTATION_FLIP_VERTICAL:
degreesToRotate = 180;
break;
case ExifInterface.ORIENTATION_TRANSVERSE:
case ExifInterface.ORIENTATION_ROTATE_270:
degreesToRotate = 270;
break;
default:
degreesToRotate = 0;
break;
}
return degreesToRotate;
}
/**
* Rotate and/or flip the image to match the given exif orientation.
*
* @param pool A pool that may or may not contain an image of the necessary
* dimensions.
* @param inBitmap The bitmap to rotate/flip.
* @param exifOrientation the exif orientation [1-8].
* @return The rotated and/or flipped image or toOrient if no rotation or flip was necessary.
*/
public static Bitmap rotateImageExif(@NonNull BitmapPool pool, @NonNull Bitmap inBitmap,
int exifOrientation) {
final Matrix matrix = new Matrix();
initializeMatrixForRotation(exifOrientation, matrix);
if (matrix.isIdentity()) {
return inBitmap;
}
// From Bitmap.createBitmap.
final RectF newRect = new RectF(0, 0, inBitmap.getWidth(), inBitmap.getHeight());
matrix.mapRect(newRect);
final int newWidth = Math.round(newRect.width());
final int newHeight = Math.round(newRect.height());
Bitmap.Config config = getSafeConfig(inBitmap);
Bitmap result = pool.get(newWidth, newHeight, config);
matrix.postTranslate(-newRect.left, -newRect.top);
applyMatrix(inBitmap, result, matrix);
return result;
}
/**
* Crop the image to a circle and resize to the specified width/height. The circle crop will
* have the same width and height equal to the min-edge of the result image.
*
* @param pool The BitmapPool obtain a bitmap from.
* @param inBitmap The Bitmap to resize.
* @param destWidth The width in pixels of the final Bitmap.
* @param destHeight The height in pixels of the final Bitmap.
* @return The resized Bitmap (will be recycled if recycled is not null).
*/
public static Bitmap circleCrop(@NonNull BitmapPool pool, @NonNull Bitmap inBitmap,
int destWidth, int destHeight) {
int destMinEdge = Math.min(destWidth, destHeight);
float radius = destMinEdge / 2f;
int srcWidth = inBitmap.getWidth();
int srcHeight = inBitmap.getHeight();
float scaleX = destMinEdge / (float) srcWidth;
float scaleY = destMinEdge / (float) srcHeight;
float maxScale = Math.max(scaleX, scaleY);
float scaledWidth = maxScale * srcWidth;
float scaledHeight = maxScale * srcHeight;
float left = (destMinEdge - scaledWidth) / 2f;
float top = (destMinEdge - scaledHeight) / 2f;
RectF destRect = new RectF(left, top, left + scaledWidth, top + scaledHeight);
// Alpha is required for this transformation.
Bitmap toTransform = getAlphaSafeBitmap(pool, inBitmap);
Bitmap result = pool.get(destMinEdge, destMinEdge, Bitmap.Config.ARGB_8888);
result.setHasAlpha(true);
BITMAP_DRAWABLE_LOCK.lock();
try {
Canvas canvas = new Canvas(result);
// Draw a circle
canvas.drawCircle(radius, radius, radius, CIRCLE_CROP_SHAPE_PAINT);
// Draw the bitmap in the circle
canvas.drawBitmap(toTransform, null, destRect, CIRCLE_CROP_BITMAP_PAINT);
clear(canvas);
} finally {
BITMAP_DRAWABLE_LOCK.unlock();
}
if (!toTransform.equals(inBitmap)) {
pool.put(toTransform);
}
return result;
}
private static Bitmap getAlphaSafeBitmap(@NonNull BitmapPool pool,
@NonNull Bitmap maybeAlphaSafe) {
if (Bitmap.Config.ARGB_8888.equals(maybeAlphaSafe.getConfig())) {
return maybeAlphaSafe;
}
Bitmap argbBitmap = pool.get(maybeAlphaSafe.getWidth(), maybeAlphaSafe.getHeight(),
Bitmap.Config.ARGB_8888);
new Canvas(argbBitmap).drawBitmap(maybeAlphaSafe, 0 /*left*/, 0 /*top*/, null /*pain*/);
// We now own this Bitmap. It's our responsibility to replace it in the pool outside this method
// when we're finished with it.
return argbBitmap;
}
/**
* Creates a bitmap from a source bitmap and rounds the corners.
*
* @param inBitmap the source bitmap to use as a basis for the created bitmap.
* @param width the width of the generated bitmap.
* @param height the height of the generated bitmap.
* @param roundingRadius the corner radius to be applied (in device-specific pixels).
* @return a {@link Bitmap} similar to inBitmap but with rounded corners.
* @throws IllegalArgumentException if roundingRadius, width or height is 0 or less.
*/
public static Bitmap roundedCorners(@NonNull BitmapPool pool, @NonNull Bitmap inBitmap,
int width, int height, int roundingRadius) {
Preconditions.checkArgument(width > 0, "width must be greater than 0.");
Preconditions.checkArgument(height > 0, "height must be greater than 0.");
Preconditions.checkArgument(roundingRadius > 0, "roundingRadius must be greater than 0.");
// Alpha is required for this transformation.
Bitmap toTransform = getAlphaSafeBitmap(pool, inBitmap);
Bitmap result = pool.get(width, height, Bitmap.Config.ARGB_8888);
result.setHasAlpha(true);
BitmapShader shader = new BitmapShader(toTransform, Shader.TileMode.CLAMP,
Shader.TileMode.CLAMP);
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setShader(shader);
RectF rect = new RectF(0, 0, result.getWidth(), result.getHeight());
BITMAP_DRAWABLE_LOCK.lock();
try {
Canvas canvas = new Canvas(result);
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
canvas.drawRoundRect(rect, roundingRadius, roundingRadius, paint);
clear(canvas);
} finally {
BITMAP_DRAWABLE_LOCK.unlock();
}
if (!toTransform.equals(inBitmap)) {
pool.put(toTransform);
}
return result;
}
// Avoids warnings in M+.
private static void clear(Canvas canvas) {
canvas.setBitmap(null);
}
private static Bitmap.Config getSafeConfig(Bitmap bitmap) {
return bitmap.getConfig() != null ? bitmap.getConfig() : Bitmap.Config.ARGB_8888;
}
private static void applyMatrix(@NonNull Bitmap inBitmap, @NonNull Bitmap targetBitmap,
Matrix matrix) {
BITMAP_DRAWABLE_LOCK.lock();
try {
Canvas canvas = new Canvas(targetBitmap);
canvas.drawBitmap(inBitmap, matrix, DEFAULT_PAINT);
clear(canvas);
} finally {
BITMAP_DRAWABLE_LOCK.unlock();
}
}
// Visible for testing.
static void initializeMatrixForRotation(int exifOrientation, Matrix matrix) {
switch (exifOrientation) {
case ExifInterface.ORIENTATION_FLIP_HORIZONTAL:
matrix.setScale(-1, 1);
break;
case ExifInterface.ORIENTATION_ROTATE_180:
matrix.setRotate(180);
break;
case ExifInterface.ORIENTATION_FLIP_VERTICAL:
matrix.setRotate(180);
matrix.postScale(-1, 1);
break;
case ExifInterface.ORIENTATION_TRANSPOSE:
matrix.setRotate(90);
matrix.postScale(-1, 1);
break;
case ExifInterface.ORIENTATION_ROTATE_90:
matrix.setRotate(90);
break;
case ExifInterface.ORIENTATION_TRANSVERSE:
matrix.setRotate(-90);
matrix.postScale(-1, 1);
break;
case ExifInterface.ORIENTATION_ROTATE_270:
matrix.setRotate(-90);
break;
default:
// Do nothing.
}
}
private static final class NoLock implements Lock {
@Synthetic
NoLock() { }
@Override
public void lock() {
// do nothing
}
@Override
public void lockInterruptibly() throws InterruptedException {
// do nothing
}
@Override
public boolean tryLock() {
return true;
}
@Override
public boolean tryLock(long time, @NonNull TimeUnit unit) throws InterruptedException {
return true;
}
@Override
public void unlock() {
// do nothing
}
@NonNull
@Override
public Condition newCondition() {
throw new UnsupportedOperationException("Should not be called");
}
}
}