All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.bumptech.glide.load.resource.bitmap.Downsampler Maven / Gradle / Ivy

Go to download

A fast and efficient image loading library for Android focused on smooth scrolling.

There is a newer version: 5.0.0-rc01
Show newest version
package com.bumptech.glide.load.resource.bitmap;

import android.annotation.TargetApi;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
import android.os.Build;
import android.support.annotation.Nullable;
import android.util.DisplayMetrics;
import android.util.Log;
import com.bumptech.glide.load.DecodeFormat;
import com.bumptech.glide.load.ImageHeaderParser;
import com.bumptech.glide.load.ImageHeaderParserUtils;
import com.bumptech.glide.load.Option;
import com.bumptech.glide.load.Options;
import com.bumptech.glide.load.engine.Resource;
import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool;
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy.SampleSizeRounding;
import com.bumptech.glide.request.RequestOptions;
import com.bumptech.glide.request.target.Target;
import com.bumptech.glide.util.Preconditions;
import com.bumptech.glide.util.Util;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Queue;
import java.util.Set;

/**
 * Downsamples, decodes, and rotates images according to their exif orientation.
 */
public final class Downsampler {
  static final String TAG = "Downsampler";
  /**
   * Indicates the {@link com.bumptech.glide.load.DecodeFormat} that will be used in conjunction
   * with the image format to determine the {@link android.graphics.Bitmap.Config} to provide to
   * {@link android.graphics.BitmapFactory.Options#inPreferredConfig} when decoding the image.
   */
  public static final Option DECODE_FORMAT = Option.memory(
      "com.bumptech.glide.load.resource.bitmap.Downsampler.DecodeFormat", DecodeFormat.DEFAULT);
  /**
   * Indicates the {@link com.bumptech.glide.load.resource.bitmap.DownsampleStrategy} option that
   * will be used to calculate the sample size to use to downsample an image given the original
   * and target dimensions of the image.
   */
  public static final Option DOWNSAMPLE_STRATEGY =
      Option.memory("com.bumptech.glide.load.resource.bitmap.Downsampler.DownsampleStrategy",
          DownsampleStrategy.AT_LEAST);
  /**
   * Ensure that the size of the bitmap is fixed to the requested width and height of the
   * resource from the caller.  The final resource dimensions may differ from the requested
   * width and height, and thus setting this to true may result in the bitmap size differing
   * from the resource dimensions.
   *
   * This can be used as a performance optimization for KitKat and above by fixing the size of the
   * bitmap for a collection of requested resources so that the bitmap pool will not need to
   * allocate new bitmaps for images of different sizes.
   */
  public static final Option FIX_BITMAP_SIZE_TO_REQUESTED_DIMENSIONS =
      Option.memory("com.bumptech.glide.load.resource.bitmap.Downsampler.FixBitmapSize", false);

  /**
   * Indicates that it's safe or unsafe to decode {@link Bitmap}s with
   * {@link Bitmap.Config#HARDWARE}.
   *
   * 

Callers should almost never set this value to {@code true} manually. Glide will already do * so when Glide believes it's safe to do (when no transformations are applied). Instead, callers * can set this value to {@code false} to prevent Glide from decoding hardware bitmaps if Glide * is unable to detect that hardware bitmaps are unsafe. For example, you should set this to * {@code false} if you plan to draw it to a software {@link android.graphics.Canvas} or if you * plan to inspect the {@link Bitmap}s pixels with {@link Bitmap#getPixel(int, int)} or * {@link Bitmap#getPixels(int[], int, int, int, int, int, int)}. * *

Callers can disable hardware {@link Bitmap}s for all loads using * {@link com.bumptech.glide.GlideBuilder#setDefaultRequestOptions(RequestOptions)}. * *

This option is ignored unless we're on Android O+. */ public static final Option ALLOW_HARDWARE_CONFIG = Option.memory( "com.bumtpech.glide.load.resource.bitmap.Downsampler.AllowHardwareDecode", null); private static final String WBMP_MIME_TYPE = "image/vnd.wap.wbmp"; private static final String ICO_MIME_TYPE = "image/x-ico"; private static final Set NO_DOWNSAMPLE_PRE_N_MIME_TYPES = Collections.unmodifiableSet( new HashSet<>( Arrays.asList( WBMP_MIME_TYPE, ICO_MIME_TYPE ) ) ); private static final DecodeCallbacks EMPTY_CALLBACKS = new DecodeCallbacks() { @Override public void onObtainBounds() { // Do nothing. } @Override public void onDecodeComplete(BitmapPool bitmapPool, Bitmap downsampled) throws IOException { // Do nothing. } }; private static final Set TYPES_THAT_USE_POOL_PRE_KITKAT = Collections.unmodifiableSet( EnumSet.of( ImageHeaderParser.ImageType.JPEG, ImageHeaderParser.ImageType.PNG_A, ImageHeaderParser.ImageType.PNG ) ); private static final Queue OPTIONS_QUEUE = Util.createQueue(0); // 5MB. This is the max image header size we can handle, we preallocate a much smaller buffer // but will resize up to this amount if necessary. private static final int MARK_POSITION = 5 * 1024 * 1024; private final BitmapPool bitmapPool; private final DisplayMetrics displayMetrics; private final ArrayPool byteArrayPool; private final List parsers; private final HardwareConfigState hardwareConfigState = HardwareConfigState.getInstance(); public Downsampler(List parsers, DisplayMetrics displayMetrics, BitmapPool bitmapPool, ArrayPool byteArrayPool) { this.parsers = parsers; this.displayMetrics = Preconditions.checkNotNull(displayMetrics); this.bitmapPool = Preconditions.checkNotNull(bitmapPool); this.byteArrayPool = Preconditions.checkNotNull(byteArrayPool); } public boolean handles(InputStream is) { // We expect Downsampler to handle any available type Android supports. return true; } public boolean handles(ByteBuffer byteBuffer) { // We expect downsampler to handle any available type Android supports. return true; } /** * Returns a Bitmap decoded from the given {@link InputStream} that is rotated to match any EXIF * data present in the stream and that is downsampled according to the given dimensions and any * provided {@link com.bumptech.glide.load.resource.bitmap.DownsampleStrategy} option. * * @see #decode(InputStream, int, int, Options, DecodeCallbacks) */ public Resource decode(InputStream is, int outWidth, int outHeight, Options options) throws IOException { return decode(is, outWidth, outHeight, options, EMPTY_CALLBACKS); } /** * Returns a Bitmap decoded from the given {@link InputStream} that is rotated to match any EXIF * data present in the stream and that is downsampled according to the given dimensions and any * provided {@link com.bumptech.glide.load.resource.bitmap.DownsampleStrategy} option. * *

If a Bitmap is present in the * {@link com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool} whose dimensions exactly match * those of the image for the given InputStream is available, the operation is much less expensive * in terms of memory.

* *

The provided {@link java.io.InputStream} must return true from * {@link java.io.InputStream#markSupported()} and is expected to support a reasonably large * mark limit to accommodate reading large image headers (~5MB).

* * @param is An {@link InputStream} to the data for the image. * @param requestedWidth The width the final image should be close to. * @param requestedHeight The height the final image should be close to. * @param options A set of options that may contain one or more supported options that influence * how a Bitmap will be decoded from the given stream. * @param callbacks A set of callbacks allowing callers to optionally respond to various * significant events during the decode process. * @return A new bitmap containing the image from the given InputStream, or recycle if recycle is * not null. */ @SuppressWarnings("resource") public Resource decode(InputStream is, int requestedWidth, int requestedHeight, Options options, DecodeCallbacks callbacks) throws IOException { Preconditions.checkArgument(is.markSupported(), "You must provide an InputStream that supports" + " mark()"); byte[] bytesForOptions = byteArrayPool.get(ArrayPool.STANDARD_BUFFER_SIZE_BYTES, byte[].class); BitmapFactory.Options bitmapFactoryOptions = getDefaultOptions(); bitmapFactoryOptions.inTempStorage = bytesForOptions; DecodeFormat decodeFormat = options.get(DECODE_FORMAT); DownsampleStrategy downsampleStrategy = options.get(DOWNSAMPLE_STRATEGY); boolean fixBitmapToRequestedDimensions = options.get(FIX_BITMAP_SIZE_TO_REQUESTED_DIMENSIONS); boolean isHardwareConfigAllowed = options.get(ALLOW_HARDWARE_CONFIG) != null && options.get(ALLOW_HARDWARE_CONFIG); if (decodeFormat == DecodeFormat.PREFER_ARGB_8888_DISALLOW_HARDWARE) { isHardwareConfigAllowed = false; } try { Bitmap result = decodeFromWrappedStreams(is, bitmapFactoryOptions, downsampleStrategy, decodeFormat, isHardwareConfigAllowed, requestedWidth, requestedHeight, fixBitmapToRequestedDimensions, callbacks); return BitmapResource.obtain(result, bitmapPool); } finally { releaseOptions(bitmapFactoryOptions); byteArrayPool.put(bytesForOptions, byte[].class); } } private Bitmap decodeFromWrappedStreams(InputStream is, BitmapFactory.Options options, DownsampleStrategy downsampleStrategy, DecodeFormat decodeFormat, boolean isHardwareConfigAllowed, int requestedWidth, int requestedHeight, boolean fixBitmapToRequestedDimensions, DecodeCallbacks callbacks) throws IOException { int[] sourceDimensions = getDimensions(is, options, callbacks, bitmapPool); int sourceWidth = sourceDimensions[0]; int sourceHeight = sourceDimensions[1]; String sourceMimeType = options.outMimeType; int orientation = ImageHeaderParserUtils.getOrientation(parsers, is, byteArrayPool); int degreesToRotate = TransformationUtils.getExifOrientationDegrees(orientation); boolean isExifOrientationRequired = TransformationUtils.isExifOrientationRequired(orientation); int targetWidth = requestedWidth == Target.SIZE_ORIGINAL ? sourceWidth : requestedWidth; int targetHeight = requestedHeight == Target.SIZE_ORIGINAL ? sourceHeight : requestedHeight; calculateScaling(downsampleStrategy, degreesToRotate, sourceWidth, sourceHeight, targetWidth, targetHeight, options); calculateConfig( is, decodeFormat, isHardwareConfigAllowed, isExifOrientationRequired, options, targetWidth, targetHeight); boolean isKitKatOrGreater = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; // Prior to KitKat, the inBitmap size must exactly match the size of the bitmap we're decoding. if ((options.inSampleSize == 1 || isKitKatOrGreater) && shouldUsePool(is)) { int expectedWidth; int expectedHeight; if (fixBitmapToRequestedDimensions && isKitKatOrGreater) { expectedWidth = targetWidth; expectedHeight = targetHeight; } else { float densityMultiplier = isScaling(options) ? (float) options.inTargetDensity / options.inDensity : 1f; int sampleSize = options.inSampleSize; int downsampledWidth = (int) Math.ceil(sourceWidth / (float) sampleSize); int downsampledHeight = (int) Math.ceil(sourceHeight / (float) sampleSize); expectedWidth = Math.round(downsampledWidth * densityMultiplier); expectedHeight = Math.round(downsampledHeight * densityMultiplier); if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "Calculated target [" + expectedWidth + "x" + expectedHeight + "] for source" + " [" + sourceWidth + "x" + sourceHeight + "]" + ", sampleSize: " + sampleSize + ", targetDensity: " + options.inTargetDensity + ", density: " + options.inDensity + ", density multiplier: " + densityMultiplier); } } // If this isn't an image, or BitmapFactory was unable to parse the size, width and height // will be -1 here. if (expectedWidth > 0 && expectedHeight > 0) { setInBitmap(options, bitmapPool, expectedWidth, expectedHeight); } } Bitmap downsampled = decodeStream(is, options, callbacks, bitmapPool); callbacks.onDecodeComplete(bitmapPool, downsampled); if (Log.isLoggable(TAG, Log.VERBOSE)) { logDecode(sourceWidth, sourceHeight, sourceMimeType, options, downsampled, requestedWidth, requestedHeight); } Bitmap rotated = null; if (downsampled != null) { // If we scaled, the Bitmap density will be our inTargetDensity. Here we correct it back to // the expected density dpi. downsampled.setDensity(displayMetrics.densityDpi); rotated = TransformationUtils.rotateImageExif(bitmapPool, downsampled, orientation); if (!downsampled.equals(rotated)) { bitmapPool.put(downsampled); } } return rotated; } // Visible for testing. static void calculateScaling(DownsampleStrategy downsampleStrategy, int degreesToRotate, int sourceWidth, int sourceHeight, int targetWidth, int targetHeight, BitmapFactory.Options options) { // We can't downsample source content if we can't determine its dimensions. if (sourceWidth <= 0 || sourceHeight <= 0) { return; } final float exactScaleFactor; if (degreesToRotate == 90 || degreesToRotate == 270) { // If we're rotating the image +-90 degrees, we need to downsample accordingly so the image // width is decreased to near our target's height and the image height is decreased to near // our target width. //noinspection SuspiciousNameCombination exactScaleFactor = downsampleStrategy.getScaleFactor(sourceHeight, sourceWidth, targetWidth, targetHeight); } else { exactScaleFactor = downsampleStrategy.getScaleFactor(sourceWidth, sourceHeight, targetWidth, targetHeight); } if (exactScaleFactor <= 0f) { throw new IllegalArgumentException("Cannot scale with factor: " + exactScaleFactor + " from: " + downsampleStrategy); } SampleSizeRounding rounding = downsampleStrategy.getSampleSizeRounding(sourceWidth, sourceHeight, targetWidth, targetHeight); if (rounding == null) { throw new IllegalArgumentException("Cannot round with null rounding"); } int outWidth = (int) (exactScaleFactor * sourceWidth + 0.5f); int outHeight = (int) (exactScaleFactor * sourceHeight + 0.5f); int widthScaleFactor = sourceWidth / outWidth; int heightScaleFactor = sourceHeight / outHeight; int scaleFactor = rounding == SampleSizeRounding.MEMORY ? Math.max(widthScaleFactor, heightScaleFactor) : Math.min(widthScaleFactor, heightScaleFactor); int powerOfTwoSampleSize; // BitmapFactory does not support downsampling wbmp files on platforms <= M. See b/27305903. if (Build.VERSION.SDK_INT <= 23 && NO_DOWNSAMPLE_PRE_N_MIME_TYPES.contains(options.outMimeType)) { powerOfTwoSampleSize = 1; } else { powerOfTwoSampleSize = Math.max(1, Integer.highestOneBit(scaleFactor)); if (rounding == SampleSizeRounding.MEMORY && powerOfTwoSampleSize < (1.f / exactScaleFactor)) { powerOfTwoSampleSize = powerOfTwoSampleSize << 1; } } float adjustedScaleFactor = powerOfTwoSampleSize * exactScaleFactor; options.inSampleSize = powerOfTwoSampleSize; // Density scaling is only supported if inBitmap is null prior to KitKat. Avoid setting // densities here so we calculate the final Bitmap size correctly. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { options.inTargetDensity = (int) (1000 * adjustedScaleFactor + 0.5f); options.inDensity = 1000; } if (isScaling(options)) { options.inScaled = true; } else { options.inDensity = options.inTargetDensity = 0; } if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "Calculate scaling" + ", source: [" + sourceWidth + "x" + sourceHeight + "]" + ", target: [" + targetWidth + "x" + targetHeight + "]" + ", exact scale factor: " + exactScaleFactor + ", power of 2 sample size: " + powerOfTwoSampleSize + ", adjusted scale factor: " + adjustedScaleFactor + ", target density: " + options.inTargetDensity + ", density: " + options.inDensity); } } private boolean shouldUsePool(InputStream is) throws IOException { // On KitKat+, any bitmap (of a given config) can be used to decode any other bitmap // (with the same config). if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { return true; } try { ImageHeaderParser.ImageType type = ImageHeaderParserUtils.getType(parsers, is, byteArrayPool); // We cannot reuse bitmaps when decoding images that are not PNG or JPG prior to KitKat. // See: https://groups.google.com/forum/#!msg/android-developers/Mp0MFVFi1Fo/e8ZQ9FGdWdEJ return TYPES_THAT_USE_POOL_PRE_KITKAT.contains(type); } catch (IOException e) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Cannot determine the image type from header", e); } } return false; } private void calculateConfig( InputStream is, DecodeFormat format, boolean isHardwareConfigAllowed, boolean isExifOrientationRequired, BitmapFactory.Options optionsWithScaling, int targetWidth, int targetHeight) throws IOException { if (hardwareConfigState.setHardwareConfigIfAllowed( targetWidth, targetHeight, optionsWithScaling, format, isHardwareConfigAllowed, isExifOrientationRequired)) { return; } // Changing configs can cause skewing on 4.1, see issue #128. if (format == DecodeFormat.PREFER_ARGB_8888 || format == DecodeFormat.PREFER_ARGB_8888_DISALLOW_HARDWARE || Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN) { optionsWithScaling.inPreferredConfig = Bitmap.Config.ARGB_8888; return; } boolean hasAlpha = false; try { hasAlpha = ImageHeaderParserUtils.getType(parsers, is, byteArrayPool).hasAlpha(); } catch (IOException e) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Cannot determine whether the image has alpha or not from header" + ", format " + format, e); } } optionsWithScaling.inPreferredConfig = hasAlpha ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565; if (optionsWithScaling.inPreferredConfig == Config.RGB_565 || optionsWithScaling.inPreferredConfig == Config.ARGB_4444 || optionsWithScaling.inPreferredConfig == Config.ALPHA_8) { optionsWithScaling.inDither = true; } } /** * A method for getting the dimensions of an image from the given InputStream. * * @param is The InputStream representing the image. * @param options The options to pass to {@link BitmapFactory#decodeStream(java.io.InputStream, * android.graphics.Rect, android.graphics.BitmapFactory.Options)}. * @return an array containing the dimensions of the image in the form {width, height}. */ private static int[] getDimensions(InputStream is, BitmapFactory.Options options, DecodeCallbacks decodeCallbacks, BitmapPool bitmapPool) throws IOException { options.inJustDecodeBounds = true; decodeStream(is, options, decodeCallbacks, bitmapPool); options.inJustDecodeBounds = false; return new int[] { options.outWidth, options.outHeight }; } private static Bitmap decodeStream(InputStream is, BitmapFactory.Options options, DecodeCallbacks callbacks, BitmapPool bitmapPool) throws IOException { if (options.inJustDecodeBounds) { is.mark(MARK_POSITION); } else { // Once we've read the image header, we no longer need to allow the buffer to expand in // size. To avoid unnecessary allocations reading image data, we fix the mark limit so that it // is no larger than our current buffer size here. We need to do so immediately before // decoding the full image to avoid having our mark limit overridden by other calls to // markand reset. See issue #225. callbacks.onObtainBounds(); } // BitmapFactory.Options out* variables are reset by most calls to decodeStream, successful or // otherwise, so capture here in case we log below. int sourceWidth = options.outWidth; int sourceHeight = options.outHeight; String outMimeType = options.outMimeType; final Bitmap result; TransformationUtils.getBitmapDrawableLock().lock(); try { result = BitmapFactory.decodeStream(is, null, options); } catch (IllegalArgumentException e) { IOException bitmapAssertionException = newIoExceptionForInBitmapAssertion(e, sourceWidth, sourceHeight, outMimeType, options); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Failed to decode with inBitmap, trying again without Bitmap re-use", bitmapAssertionException); } if (options.inBitmap != null) { try { is.reset(); bitmapPool.put(options.inBitmap); options.inBitmap = null; return decodeStream(is, options, callbacks, bitmapPool); } catch (IOException resetException) { throw bitmapAssertionException; } } throw bitmapAssertionException; } finally { TransformationUtils.getBitmapDrawableLock().unlock(); } if (options.inJustDecodeBounds) { is.reset(); } return result; } private static boolean isScaling(BitmapFactory.Options options) { return options.inTargetDensity > 0 && options.inDensity > 0 && options.inTargetDensity != options.inDensity; } private static void logDecode(int sourceWidth, int sourceHeight, String outMimeType, BitmapFactory.Options options, Bitmap result, int requestedWidth, int requestedHeight) { Log.v(TAG, "Decoded " + getBitmapString(result) + " from [" + sourceWidth + "x" + sourceHeight + "] " + outMimeType + " with inBitmap " + getInBitmapString(options) + " for [" + requestedWidth + "x" + requestedHeight + "]" + ", sample size: " + options.inSampleSize + ", density: " + options.inDensity + ", target density: " + options.inTargetDensity + ", thread: " + Thread.currentThread().getName()); } private static String getInBitmapString(BitmapFactory.Options options) { return getBitmapString(options.inBitmap); } @Nullable @TargetApi(Build.VERSION_CODES.KITKAT) private static String getBitmapString(Bitmap bitmap) { if (bitmap == null) { return null; } String sizeString = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT ? " (" + bitmap.getAllocationByteCount() + ")" : ""; return "[" + bitmap.getWidth() + "x" + bitmap.getHeight() + "] " + bitmap.getConfig() + sizeString; } // BitmapFactory throws an IllegalArgumentException if any error occurs attempting to decode a // file when inBitmap is non-null, including those caused by partial or corrupt data. We still log // the error because the IllegalArgumentException is supposed to catch errors reusing Bitmaps, so // want some useful log output. In most cases this can be safely treated as a normal IOException. private static IOException newIoExceptionForInBitmapAssertion(IllegalArgumentException e, int outWidth, int outHeight, String outMimeType, BitmapFactory.Options options) { return new IOException("Exception decoding bitmap" + ", outWidth: " + outWidth + ", outHeight: " + outHeight + ", outMimeType: " + outMimeType + ", inBitmap: " + getInBitmapString(options), e); } @SuppressWarnings("PMD.CollapsibleIfStatements") @TargetApi(Build.VERSION_CODES.O) private static void setInBitmap(BitmapFactory.Options options, BitmapPool bitmapPool, int width, int height) { // Avoid short circuiting, it appears to break on some devices. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (options.inPreferredConfig == Config.HARDWARE) { return; } } // BitmapFactory will clear out the Bitmap before writing to it, so getDirty is safe. options.inBitmap = bitmapPool.getDirty(width, height, options.inPreferredConfig); } private static synchronized BitmapFactory.Options getDefaultOptions() { BitmapFactory.Options decodeBitmapOptions; synchronized (OPTIONS_QUEUE) { decodeBitmapOptions = OPTIONS_QUEUE.poll(); } if (decodeBitmapOptions == null) { decodeBitmapOptions = new BitmapFactory.Options(); resetOptions(decodeBitmapOptions); } return decodeBitmapOptions; } private static void releaseOptions(BitmapFactory.Options decodeBitmapOptions) { resetOptions(decodeBitmapOptions); synchronized (OPTIONS_QUEUE) { OPTIONS_QUEUE.offer(decodeBitmapOptions); } } private static void resetOptions(BitmapFactory.Options decodeBitmapOptions) { decodeBitmapOptions.inTempStorage = null; decodeBitmapOptions.inDither = false; decodeBitmapOptions.inScaled = false; decodeBitmapOptions.inSampleSize = 1; decodeBitmapOptions.inPreferredConfig = null; decodeBitmapOptions.inJustDecodeBounds = false; decodeBitmapOptions.inDensity = 0; decodeBitmapOptions.inTargetDensity = 0; decodeBitmapOptions.outWidth = 0; decodeBitmapOptions.outHeight = 0; decodeBitmapOptions.outMimeType = null; decodeBitmapOptions.inBitmap = null; decodeBitmapOptions.inMutable = true; } /** * Callbacks for key points during decodes. */ public interface DecodeCallbacks { void onObtainBounds(); void onDecodeComplete(BitmapPool bitmapPool, Bitmap downsampled) throws IOException; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy