| /* |
| * Copyright (C) 2013 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.android.bitmap.drawable; |
| |
| import android.content.res.Resources; |
| import android.graphics.Canvas; |
| import android.graphics.ColorFilter; |
| import android.graphics.Paint; |
| import android.graphics.PixelFormat; |
| import android.graphics.Rect; |
| import android.graphics.drawable.Drawable; |
| import android.util.DisplayMetrics; |
| import android.util.Log; |
| |
| import com.android.bitmap.BitmapCache; |
| import com.android.bitmap.DecodeTask; |
| import com.android.bitmap.DecodeTask.DecodeCallback; |
| import com.android.bitmap.DecodeTask.DecodeOptions; |
| import com.android.bitmap.NamedThreadFactory; |
| import com.android.bitmap.RequestKey; |
| import com.android.bitmap.RequestKey.Cancelable; |
| import com.android.bitmap.RequestKey.FileDescriptorFactory; |
| import com.android.bitmap.ReusableBitmap; |
| import com.android.bitmap.util.BitmapUtils; |
| import com.android.bitmap.util.RectUtils; |
| import com.android.bitmap.util.Trace; |
| |
| import java.util.concurrent.Executor; |
| import java.util.concurrent.LinkedBlockingQueue; |
| import java.util.concurrent.ThreadPoolExecutor; |
| import java.util.concurrent.TimeUnit; |
| |
| /** |
| * This class encapsulates the basic functionality needed to display a single image bitmap, |
| * including request creation/cancelling, and data unbinding and re-binding. |
| * <p> |
| * The actual bitmap decode work is handled by {@link DecodeTask}. |
| * <p> |
| * If being used with a long-lived cache (static cache, attached to the Application instead of the |
| * Activity, etc) then make sure to call {@link BasicBitmapDrawable#unbind()} at the appropriate |
| * times so the cache has accurate unref counts. The |
| * {@link com.android.bitmap.view.BitmapDrawableImageView} class has been created to do the |
| * appropriate unbind operation when the view is detached from the window. |
| */ |
| public class BasicBitmapDrawable extends Drawable implements DecodeCallback, |
| Drawable.Callback, RequestKey.Callback { |
| |
| protected RequestKey mCurrKey; |
| protected RequestKey mPrevKey; |
| protected int mDecodeWidth; |
| protected int mDecodeHeight; |
| |
| protected final Paint mPaint = new Paint(); |
| private final BitmapCache mCache; |
| private final Rect mRect = new Rect(); |
| |
| private final boolean mLimitDensity; |
| private final float mDensity; |
| private ReusableBitmap mBitmap; |
| private DecodeTask mTask; |
| private Cancelable mCreateFileDescriptorFactoryTask; |
| |
| private int mLayoutDirection; |
| |
| // based on framework CL:I015d77 |
| private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors(); |
| private static final int CORE_POOL_SIZE = CPU_COUNT + 1; |
| private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1; |
| |
| private static final Executor SMALL_POOL_EXECUTOR = new ThreadPoolExecutor( |
| CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, 1, TimeUnit.SECONDS, |
| new LinkedBlockingQueue<Runnable>(128), new NamedThreadFactory("decode")); |
| private static final Executor EXECUTOR = SMALL_POOL_EXECUTOR; |
| |
| private static final int MAX_BITMAP_DENSITY = DisplayMetrics.DENSITY_HIGH; |
| private static final float VERTICAL_CENTER = 1f / 2; |
| private static final float HORIZONTAL_CENTER = 1f / 2; |
| private static final float NO_MULTIPLIER = 1f; |
| |
| private static final String TAG = BasicBitmapDrawable.class.getSimpleName(); |
| private static final boolean DEBUG = DecodeTask.DEBUG; |
| |
| public BasicBitmapDrawable(final Resources res, final BitmapCache cache, |
| final boolean limitDensity) { |
| mDensity = res.getDisplayMetrics().density; |
| mCache = cache; |
| mLimitDensity = limitDensity; |
| mPaint.setFilterBitmap(true); |
| mPaint.setAntiAlias(true); |
| mPaint.setDither(true); |
| } |
| |
| public final RequestKey getKey() { |
| return mCurrKey; |
| } |
| |
| public final RequestKey getPreviousKey() { |
| return mPrevKey; |
| } |
| |
| protected ReusableBitmap getBitmap() { |
| return mBitmap; |
| } |
| |
| /** |
| * Set the dimensions to decode into. These dimensions should never change while the drawable is |
| * attached to the same cache, because caches can only contain bitmaps of one size for re-use. |
| * |
| * All UI operations should be called from the UI thread. |
| */ |
| public void setDecodeDimensions(int width, int height) { |
| if (mDecodeWidth == 0 || mDecodeHeight == 0) { |
| mDecodeWidth = width; |
| mDecodeHeight = height; |
| setImage(mCurrKey); |
| } |
| } |
| |
| /** |
| * Set layout direction. |
| * It ends with Local so as not conflict with hidden Drawable.setLayoutDirection. |
| * @param layoutDirection the resolved layout direction for the drawable, |
| * either {@link android.view.View#LAYOUT_DIRECTION_LTR} |
| * or {@link android.view.View#LAYOUT_DIRECTION_RTL} |
| */ |
| public void setLayoutDirectionLocal(int layoutDirection) { |
| if (mLayoutDirection != layoutDirection) { |
| mLayoutDirection = layoutDirection; |
| onLayoutDirectionChangeLocal(layoutDirection); |
| } |
| } |
| |
| /** |
| * Called when the drawable's resolved layout direction changes. |
| * It ends with Local so as not conflict with hidden Drawable.onLayoutDirectionChange. |
| * |
| * @param layoutDirection the new resolved layout direction |
| */ |
| public void onLayoutDirectionChangeLocal(int layoutDirection) {} |
| |
| /** |
| * Returns the resolved layout direction for this Drawable. |
| * It ends with Local so as not conflict with hidden Drawable.getLayoutDirection. |
| * |
| * @return One of {@link android.view.View#LAYOUT_DIRECTION_LTR}, |
| * {@link android.view.View#LAYOUT_DIRECTION_RTL} |
| * @see #setLayoutDirectionLocal(int) |
| */ |
| public int getLayoutDirectionLocal() { |
| return mLayoutDirection; |
| } |
| |
| /** |
| * Binds to the given key and start the decode process. This will first look in the cache, then |
| * decode from the request key if not found. |
| * |
| * The key being replaced will be kept in {@link #mPrevKey}. |
| * |
| * All UI operations should be called from the UI thread. |
| */ |
| public void bind(RequestKey key) { |
| Trace.beginSection("bind"); |
| if (mCurrKey != null && mCurrKey.equals(key)) { |
| Trace.endSection(); |
| return; |
| } |
| setImage(key); |
| Trace.endSection(); |
| } |
| |
| /** |
| * Unbinds the current key and bitmap from the drawable. This will cause the bitmap to decrement |
| * its ref count. |
| * |
| * This will assume that you do not want to keep the unbound key in {@link #mPrevKey}. |
| * |
| * All UI operations should be called from the UI thread. |
| */ |
| public void unbind() { |
| unbind(false); |
| } |
| |
| /** |
| * Unbinds the current key and bitmap from the drawable. This will cause the bitmap to decrement |
| * its ref count. |
| * |
| * If the temporary parameter is true, we will keep the unbound key in {@link #mPrevKey}. |
| * |
| * All UI operations should be called from the UI thread. |
| */ |
| public void unbind(boolean temporary) { |
| Trace.beginSection("unbind"); |
| setImage(null); |
| if (!temporary) { |
| mPrevKey = null; |
| } |
| Trace.endSection(); |
| } |
| |
| /** |
| * Should only be overriden, not called. |
| */ |
| protected void setImage(final RequestKey key) { |
| Trace.beginSection("set image"); |
| Trace.beginSection("release reference"); |
| if (mBitmap != null) { |
| mBitmap.releaseReference(); |
| mBitmap = null; |
| } |
| Trace.endSection(); |
| |
| mPrevKey = mCurrKey; |
| mCurrKey = key; |
| |
| if (mTask != null) { |
| mTask.cancel(); |
| mTask = null; |
| } |
| if (mCreateFileDescriptorFactoryTask != null) { |
| mCreateFileDescriptorFactoryTask.cancel(); |
| mCreateFileDescriptorFactoryTask = null; |
| } |
| |
| if (key == null) { |
| onDecodeFailed(); |
| Trace.endSection(); |
| return; |
| } |
| |
| // find cached entry here and skip decode if found. |
| final ReusableBitmap cached = mCache.get(key, true /* incrementRefCount */); |
| if (cached != null) { |
| setBitmap(cached); |
| if (DEBUG) { |
| Log.d(TAG, String.format("CACHE HIT key=%s", mCurrKey)); |
| } |
| } else { |
| loadFileDescriptorFactory(); |
| if (DEBUG) { |
| Log.d(TAG, String.format( |
| "CACHE MISS key=%s\ncache=%s", mCurrKey, mCache.toDebugString())); |
| } |
| } |
| Trace.endSection(); |
| } |
| |
| /** |
| * Should only be overriden, not called. |
| */ |
| protected void setBitmap(ReusableBitmap bmp) { |
| if (hasBitmap()) { |
| mBitmap.releaseReference(); |
| } |
| mBitmap = bmp; |
| invalidateSelf(); |
| } |
| |
| /** |
| * Should only be overriden, not called. |
| */ |
| protected void loadFileDescriptorFactory() { |
| if (mCurrKey == null || mDecodeWidth == 0 || mDecodeHeight == 0) { |
| onDecodeFailed(); |
| return; |
| } |
| |
| // Create file descriptor if request supports it. |
| mCreateFileDescriptorFactoryTask = mCurrKey |
| .createFileDescriptorFactoryAsync(mCurrKey, this); |
| if (mCreateFileDescriptorFactoryTask == null) { |
| // Use input stream if request does not. |
| decode(null); |
| } |
| } |
| |
| @Override |
| public void fileDescriptorFactoryCreated(final RequestKey key, |
| final FileDescriptorFactory factory) { |
| if (mCreateFileDescriptorFactoryTask == null) { |
| // Cancelled. |
| onDecodeFailed(); |
| return; |
| } |
| mCreateFileDescriptorFactoryTask = null; |
| |
| if (key.equals(mCurrKey)) { |
| decode(factory); |
| } |
| } |
| |
| /** |
| * Called when the decode process is cancelled at any time. |
| */ |
| protected void onDecodeFailed() { |
| invalidateSelf(); |
| } |
| |
| /** |
| * Should only be overriden, not called. |
| */ |
| protected void decode(final FileDescriptorFactory factory) { |
| Trace.beginSection("decode"); |
| final int bufferW; |
| final int bufferH; |
| if (mLimitDensity) { |
| final float scale = |
| Math.min(1f, (float) MAX_BITMAP_DENSITY / DisplayMetrics.DENSITY_DEFAULT |
| / mDensity); |
| bufferW = (int) (mDecodeWidth * scale); |
| bufferH = (int) (mDecodeHeight * scale); |
| } else { |
| bufferW = mDecodeWidth; |
| bufferH = mDecodeHeight; |
| } |
| |
| if (mTask != null) { |
| mTask.cancel(); |
| } |
| final DecodeOptions opts = new DecodeOptions(bufferW, bufferH, getDecodeHorizontalCenter(), |
| getDecodeVerticalCenter(), getDecodeStrategy()); |
| mTask = new DecodeTask(mCurrKey, opts, factory, this, mCache); |
| mTask.executeOnExecutor(getExecutor()); |
| Trace.endSection(); |
| } |
| |
| /** |
| * Return one of the STRATEGY constants in {@link DecodeOptions}. |
| */ |
| protected int getDecodeStrategy() { |
| return DecodeOptions.STRATEGY_ROUND_NEAREST; |
| } |
| |
| protected Executor getExecutor() { |
| return EXECUTOR; |
| } |
| |
| protected float getDrawVerticalCenter() { |
| return VERTICAL_CENTER; |
| } |
| |
| protected float getDrawVerticalOffsetMultiplier() { |
| return NO_MULTIPLIER; |
| } |
| |
| /** |
| * Clients can override this to specify which section of the source image to decode from. |
| * Possible applications include using face detection to always decode around facial features. |
| */ |
| protected float getDecodeHorizontalCenter() { |
| return HORIZONTAL_CENTER; |
| } |
| |
| /** |
| * Clients can override this to specify which section of the source image to decode from. |
| * Possible applications include using face detection to always decode around facial features. |
| */ |
| protected float getDecodeVerticalCenter() { |
| return VERTICAL_CENTER; |
| } |
| |
| @Override |
| public void draw(final Canvas canvas) { |
| final Rect bounds = getBounds(); |
| if (bounds.isEmpty()) { |
| return; |
| } |
| |
| if (hasBitmap()) { |
| BitmapUtils.calculateCroppedSrcRect( |
| mBitmap.getLogicalWidth(), mBitmap.getLogicalHeight(), |
| bounds.width(), bounds.height(), |
| bounds.height(), Integer.MAX_VALUE, getDecodeHorizontalCenter(), |
| getDrawVerticalCenter(), false /* absoluteFraction */, |
| getDrawVerticalOffsetMultiplier(), mRect); |
| |
| final int orientation = mBitmap.getOrientation(); |
| // calculateCroppedSrcRect() gave us the source rectangle "as if" the orientation has |
| // been corrected. We need to decode the uncorrected source rectangle. Calculate true |
| // coordinates. |
| RectUtils.rotateRectForOrientation(orientation, |
| new Rect(0, 0, mBitmap.getLogicalWidth(), mBitmap.getLogicalHeight()), |
| mRect); |
| |
| // We may need to rotate the canvas, so we also have to rotate the bounds. |
| final Rect rotatedBounds = new Rect(bounds); |
| RectUtils.rotateRect(orientation, bounds.centerX(), bounds.centerY(), rotatedBounds); |
| |
| // Rotate the canvas. |
| canvas.save(); |
| canvas.rotate(orientation, bounds.centerX(), bounds.centerY()); |
| onDrawBitmap(canvas, mRect, rotatedBounds); |
| canvas.restore(); |
| } |
| } |
| |
| protected boolean hasBitmap() { |
| return mBitmap != null && mBitmap.bmp != null; |
| } |
| |
| /** |
| * Override this method to customize how to draw the bitmap to the canvas for the given bounds. |
| * The bitmap to be drawn can be found at {@link #getBitmap()}. |
| */ |
| protected void onDrawBitmap(final Canvas canvas, final Rect src, final Rect dst) { |
| if (hasBitmap()) { |
| canvas.drawBitmap(mBitmap.bmp, src, dst, mPaint); |
| } |
| } |
| |
| @Override |
| public void setAlpha(int alpha) { |
| final int old = mPaint.getAlpha(); |
| mPaint.setAlpha(alpha); |
| if (alpha != old) { |
| invalidateSelf(); |
| } |
| } |
| |
| @Override |
| public void setColorFilter(ColorFilter cf) { |
| mPaint.setColorFilter(cf); |
| invalidateSelf(); |
| } |
| |
| @Override |
| public int getOpacity() { |
| return (hasBitmap() && (mBitmap.bmp.hasAlpha() || mPaint.getAlpha() < 255)) ? |
| PixelFormat.TRANSLUCENT : PixelFormat.OPAQUE; |
| } |
| |
| @Override |
| public void onDecodeBegin(final RequestKey key) { } |
| |
| @Override |
| public void onDecodeComplete(final RequestKey key, final ReusableBitmap result) { |
| if (key.equals(mCurrKey)) { |
| setBitmap(result); |
| } else { |
| // if the requests don't match (i.e. this request is stale), decrement the |
| // ref count to allow the bitmap to be pooled |
| if (result != null) { |
| result.releaseReference(); |
| } |
| } |
| } |
| |
| @Override |
| public void onDecodeCancel(final RequestKey key) { } |
| |
| @Override |
| public void invalidateDrawable(Drawable who) { |
| invalidateSelf(); |
| } |
| |
| @Override |
| public void scheduleDrawable(Drawable who, Runnable what, long when) { |
| scheduleSelf(what, when); |
| } |
| |
| @Override |
| public void unscheduleDrawable(Drawable who, Runnable what) { |
| unscheduleSelf(what); |
| } |
| } |