Home | History | Annotate | Download | only in drawable
      1 /*
      2  * Copyright (C) 2013 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.android.bitmap.drawable;
     17 
     18 import android.content.res.Resources;
     19 import android.graphics.Canvas;
     20 import android.graphics.ColorFilter;
     21 import android.graphics.Paint;
     22 import android.graphics.PixelFormat;
     23 import android.graphics.Rect;
     24 import android.graphics.drawable.Drawable;
     25 import android.util.DisplayMetrics;
     26 import android.util.Log;
     27 
     28 import com.android.bitmap.BitmapCache;
     29 import com.android.bitmap.DecodeTask;
     30 import com.android.bitmap.DecodeTask.DecodeCallback;
     31 import com.android.bitmap.DecodeTask.DecodeOptions;
     32 import com.android.bitmap.NamedThreadFactory;
     33 import com.android.bitmap.RequestKey;
     34 import com.android.bitmap.RequestKey.Cancelable;
     35 import com.android.bitmap.RequestKey.FileDescriptorFactory;
     36 import com.android.bitmap.ReusableBitmap;
     37 import com.android.bitmap.util.BitmapUtils;
     38 import com.android.bitmap.util.RectUtils;
     39 import com.android.bitmap.util.Trace;
     40 
     41 import java.util.concurrent.Executor;
     42 import java.util.concurrent.LinkedBlockingQueue;
     43 import java.util.concurrent.ThreadPoolExecutor;
     44 import java.util.concurrent.TimeUnit;
     45 
     46 /**
     47  * This class encapsulates the basic functionality needed to display a single image bitmap,
     48  * including request creation/cancelling, and data unbinding and re-binding.
     49  * <p>
     50  * The actual bitmap decode work is handled by {@link DecodeTask}.
     51  * <p>
     52  * If being used with a long-lived cache (static cache, attached to the Application instead of the
     53  * Activity, etc) then make sure to call {@link BasicBitmapDrawable#unbind()} at the appropriate
     54  * times so the cache has accurate unref counts. The
     55  * {@link com.android.bitmap.view.BitmapDrawableImageView} class has been created to do the
     56  * appropriate unbind operation when the view is detached from the window.
     57  */
     58 public class BasicBitmapDrawable extends Drawable implements DecodeCallback,
     59         Drawable.Callback, RequestKey.Callback {
     60 
     61     protected RequestKey mCurrKey;
     62     protected RequestKey mPrevKey;
     63     protected int mDecodeWidth;
     64     protected int mDecodeHeight;
     65 
     66     protected final Paint mPaint = new Paint();
     67     private final BitmapCache mCache;
     68     private final Rect mRect = new Rect();
     69 
     70     private final boolean mLimitDensity;
     71     private final float mDensity;
     72     private ReusableBitmap mBitmap;
     73     private DecodeTask mTask;
     74     private Cancelable mCreateFileDescriptorFactoryTask;
     75 
     76     private int mLayoutDirection;
     77 
     78     // based on framework CL:I015d77
     79     private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
     80     private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
     81     private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
     82 
     83     private static final Executor SMALL_POOL_EXECUTOR = new ThreadPoolExecutor(
     84             CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, 1, TimeUnit.SECONDS,
     85             new LinkedBlockingQueue<Runnable>(128), new NamedThreadFactory("decode"));
     86     private static final Executor EXECUTOR = SMALL_POOL_EXECUTOR;
     87 
     88     private static final int MAX_BITMAP_DENSITY = DisplayMetrics.DENSITY_HIGH;
     89     private static final float VERTICAL_CENTER = 1f / 2;
     90     private static final float HORIZONTAL_CENTER = 1f / 2;
     91     private static final float NO_MULTIPLIER = 1f;
     92 
     93     private static final String TAG = BasicBitmapDrawable.class.getSimpleName();
     94     private static final boolean DEBUG = DecodeTask.DEBUG;
     95 
     96     public BasicBitmapDrawable(final Resources res, final BitmapCache cache,
     97             final boolean limitDensity) {
     98         mDensity = res.getDisplayMetrics().density;
     99         mCache = cache;
    100         mLimitDensity = limitDensity;
    101         mPaint.setFilterBitmap(true);
    102         mPaint.setAntiAlias(true);
    103         mPaint.setDither(true);
    104     }
    105 
    106     public final RequestKey getKey() {
    107         return mCurrKey;
    108     }
    109 
    110     public final RequestKey getPreviousKey() {
    111         return mPrevKey;
    112     }
    113 
    114     protected ReusableBitmap getBitmap() {
    115         return mBitmap;
    116     }
    117 
    118     /**
    119      * Set the dimensions to decode into. These dimensions should never change while the drawable is
    120      * attached to the same cache, because caches can only contain bitmaps of one size for re-use.
    121      *
    122      * All UI operations should be called from the UI thread.
    123      */
    124     public void setDecodeDimensions(int width, int height) {
    125         if (mDecodeWidth == 0 || mDecodeHeight == 0) {
    126             mDecodeWidth = width;
    127             mDecodeHeight = height;
    128             setImage(mCurrKey);
    129         }
    130     }
    131 
    132     /**
    133      * Set layout direction.
    134      * It ends with Local so as not conflict with hidden Drawable.setLayoutDirection.
    135      * @param layoutDirection the resolved layout direction for the drawable,
    136      *                        either {@link android.view.View#LAYOUT_DIRECTION_LTR}
    137      *                        or {@link android.view.View#LAYOUT_DIRECTION_RTL}
    138      */
    139     public void setLayoutDirectionLocal(int layoutDirection) {
    140         if (mLayoutDirection != layoutDirection) {
    141             mLayoutDirection = layoutDirection;
    142             onLayoutDirectionChangeLocal(layoutDirection);
    143         }
    144     }
    145 
    146     /**
    147      * Called when the drawable's resolved layout direction changes.
    148      * It ends with Local so as not conflict with hidden Drawable.onLayoutDirectionChange.
    149      *
    150      * @param layoutDirection the new resolved layout direction
    151      */
    152     public void onLayoutDirectionChangeLocal(int layoutDirection) {}
    153 
    154     /**
    155      * Returns the resolved layout direction for this Drawable.
    156      * It ends with Local so as not conflict with hidden Drawable.getLayoutDirection.
    157      *
    158      * @return One of {@link android.view.View#LAYOUT_DIRECTION_LTR},
    159      *         {@link android.view.View#LAYOUT_DIRECTION_RTL}
    160      * @see #setLayoutDirectionLocal(int)
    161      */
    162     public int getLayoutDirectionLocal() {
    163         return mLayoutDirection;
    164     }
    165 
    166     /**
    167      * Binds to the given key and start the decode process. This will first look in the cache, then
    168      * decode from the request key if not found.
    169      *
    170      * The key being replaced will be kept in {@link #mPrevKey}.
    171      *
    172      * All UI operations should be called from the UI thread.
    173      */
    174     public void bind(RequestKey key) {
    175         Trace.beginSection("bind");
    176         if (mCurrKey != null && mCurrKey.equals(key)) {
    177             Trace.endSection();
    178             return;
    179         }
    180         setImage(key);
    181         Trace.endSection();
    182     }
    183 
    184     /**
    185      * Unbinds the current key and bitmap from the drawable. This will cause the bitmap to decrement
    186      * its ref count.
    187      *
    188      * This will assume that you do not want to keep the unbound key in {@link #mPrevKey}.
    189      *
    190      * All UI operations should be called from the UI thread.
    191      */
    192     public void unbind() {
    193         unbind(false);
    194     }
    195 
    196     /**
    197      * Unbinds the current key and bitmap from the drawable. This will cause the bitmap to decrement
    198      * its ref count.
    199      *
    200      * If the temporary parameter is true, we will keep the unbound key in {@link #mPrevKey}.
    201      *
    202      * All UI operations should be called from the UI thread.
    203      */
    204     public void unbind(boolean temporary) {
    205         Trace.beginSection("unbind");
    206         setImage(null);
    207         if (!temporary) {
    208             mPrevKey = null;
    209         }
    210         Trace.endSection();
    211     }
    212 
    213     /**
    214      * Should only be overriden, not called.
    215      */
    216     protected void setImage(final RequestKey key) {
    217         Trace.beginSection("set image");
    218         Trace.beginSection("release reference");
    219         if (mBitmap != null) {
    220             mBitmap.releaseReference();
    221             mBitmap = null;
    222         }
    223         Trace.endSection();
    224 
    225         mPrevKey = mCurrKey;
    226         mCurrKey = key;
    227 
    228         if (mTask != null) {
    229             mTask.cancel();
    230             mTask = null;
    231         }
    232         if (mCreateFileDescriptorFactoryTask != null) {
    233             mCreateFileDescriptorFactoryTask.cancel();
    234             mCreateFileDescriptorFactoryTask = null;
    235         }
    236 
    237         if (key == null) {
    238             onDecodeFailed();
    239             Trace.endSection();
    240             return;
    241         }
    242 
    243         // find cached entry here and skip decode if found.
    244         final ReusableBitmap cached = mCache.get(key, true /* incrementRefCount */);
    245         if (cached != null) {
    246             setBitmap(cached);
    247             if (DEBUG) {
    248                 Log.d(TAG, String.format("CACHE HIT key=%s", mCurrKey));
    249             }
    250         } else {
    251             loadFileDescriptorFactory();
    252             if (DEBUG) {
    253                 Log.d(TAG, String.format(
    254                         "CACHE MISS key=%s\ncache=%s", mCurrKey, mCache.toDebugString()));
    255             }
    256         }
    257         Trace.endSection();
    258     }
    259 
    260     /**
    261      * Should only be overriden, not called.
    262      */
    263     protected void setBitmap(ReusableBitmap bmp) {
    264         if (hasBitmap()) {
    265             mBitmap.releaseReference();
    266         }
    267         mBitmap = bmp;
    268         invalidateSelf();
    269     }
    270 
    271     /**
    272      * Should only be overriden, not called.
    273      */
    274     protected void loadFileDescriptorFactory() {
    275         if (mCurrKey == null || mDecodeWidth == 0 || mDecodeHeight == 0) {
    276             onDecodeFailed();
    277             return;
    278         }
    279 
    280         // Create file descriptor if request supports it.
    281         mCreateFileDescriptorFactoryTask = mCurrKey
    282                 .createFileDescriptorFactoryAsync(mCurrKey, this);
    283         if (mCreateFileDescriptorFactoryTask == null) {
    284             // Use input stream if request does not.
    285             decode(null);
    286         }
    287     }
    288 
    289     @Override
    290     public void fileDescriptorFactoryCreated(final RequestKey key,
    291             final FileDescriptorFactory factory) {
    292         if (mCreateFileDescriptorFactoryTask == null) {
    293             // Cancelled.
    294             onDecodeFailed();
    295             return;
    296         }
    297         mCreateFileDescriptorFactoryTask = null;
    298 
    299         if (key.equals(mCurrKey)) {
    300             decode(factory);
    301         }
    302     }
    303 
    304     /**
    305      * Called when the decode process is cancelled at any time.
    306      */
    307     protected void onDecodeFailed() {
    308         invalidateSelf();
    309     }
    310 
    311     /**
    312      * Should only be overriden, not called.
    313      */
    314     protected void decode(final FileDescriptorFactory factory) {
    315         Trace.beginSection("decode");
    316         final int bufferW;
    317         final int bufferH;
    318         if (mLimitDensity) {
    319             final float scale =
    320                     Math.min(1f, (float) MAX_BITMAP_DENSITY / DisplayMetrics.DENSITY_DEFAULT
    321                             / mDensity);
    322             bufferW = (int) (mDecodeWidth * scale);
    323             bufferH = (int) (mDecodeHeight * scale);
    324         } else {
    325             bufferW = mDecodeWidth;
    326             bufferH = mDecodeHeight;
    327         }
    328 
    329         if (mTask != null) {
    330             mTask.cancel();
    331         }
    332         final DecodeOptions opts = new DecodeOptions(bufferW, bufferH, getDecodeHorizontalCenter(),
    333                 getDecodeVerticalCenter(), getDecodeStrategy());
    334         mTask = new DecodeTask(mCurrKey, opts, factory, this, mCache);
    335         mTask.executeOnExecutor(getExecutor());
    336         Trace.endSection();
    337     }
    338 
    339     /**
    340      * Return one of the STRATEGY constants in {@link DecodeOptions}.
    341      */
    342     protected int getDecodeStrategy() {
    343         return DecodeOptions.STRATEGY_ROUND_NEAREST;
    344     }
    345 
    346     protected Executor getExecutor() {
    347         return EXECUTOR;
    348     }
    349 
    350     protected float getDrawVerticalCenter() {
    351         return VERTICAL_CENTER;
    352     }
    353 
    354     protected float getDrawVerticalOffsetMultiplier() {
    355         return NO_MULTIPLIER;
    356     }
    357 
    358     /**
    359      * Clients can override this to specify which section of the source image to decode from.
    360      * Possible applications include using face detection to always decode around facial features.
    361      */
    362     protected float getDecodeHorizontalCenter() {
    363         return HORIZONTAL_CENTER;
    364     }
    365 
    366     /**
    367      * Clients can override this to specify which section of the source image to decode from.
    368      * Possible applications include using face detection to always decode around facial features.
    369      */
    370     protected float getDecodeVerticalCenter() {
    371         return VERTICAL_CENTER;
    372     }
    373 
    374     @Override
    375     public void draw(final Canvas canvas) {
    376         final Rect bounds = getBounds();
    377         if (bounds.isEmpty()) {
    378             return;
    379         }
    380 
    381         if (hasBitmap()) {
    382             BitmapUtils.calculateCroppedSrcRect(
    383                     mBitmap.getLogicalWidth(), mBitmap.getLogicalHeight(),
    384                     bounds.width(), bounds.height(),
    385                     bounds.height(), Integer.MAX_VALUE, getDecodeHorizontalCenter(),
    386                     getDrawVerticalCenter(), false /* absoluteFraction */,
    387                     getDrawVerticalOffsetMultiplier(), mRect);
    388 
    389             final int orientation = mBitmap.getOrientation();
    390             // calculateCroppedSrcRect() gave us the source rectangle "as if" the orientation has
    391             // been corrected. We need to decode the uncorrected source rectangle. Calculate true
    392             // coordinates.
    393             RectUtils.rotateRectForOrientation(orientation,
    394                     new Rect(0, 0, mBitmap.getLogicalWidth(), mBitmap.getLogicalHeight()),
    395                     mRect);
    396 
    397             // We may need to rotate the canvas, so we also have to rotate the bounds.
    398             final Rect rotatedBounds = new Rect(bounds);
    399             RectUtils.rotateRect(orientation, bounds.centerX(), bounds.centerY(), rotatedBounds);
    400 
    401             // Rotate the canvas.
    402             canvas.save();
    403             canvas.rotate(orientation, bounds.centerX(), bounds.centerY());
    404             onDrawBitmap(canvas, mRect, rotatedBounds);
    405             canvas.restore();
    406         }
    407     }
    408 
    409     protected boolean hasBitmap() {
    410         return mBitmap != null && mBitmap.bmp != null;
    411     }
    412 
    413     /**
    414      * Override this method to customize how to draw the bitmap to the canvas for the given bounds.
    415      * The bitmap to be drawn can be found at {@link #getBitmap()}.
    416      */
    417     protected void onDrawBitmap(final Canvas canvas, final Rect src, final Rect dst) {
    418         if (hasBitmap()) {
    419             canvas.drawBitmap(mBitmap.bmp, src, dst, mPaint);
    420         }
    421     }
    422 
    423     @Override
    424     public void setAlpha(int alpha) {
    425         final int old = mPaint.getAlpha();
    426         mPaint.setAlpha(alpha);
    427         if (alpha != old) {
    428             invalidateSelf();
    429         }
    430     }
    431 
    432     @Override
    433     public void setColorFilter(ColorFilter cf) {
    434         mPaint.setColorFilter(cf);
    435         invalidateSelf();
    436     }
    437 
    438     @Override
    439     public int getOpacity() {
    440         return (hasBitmap() && (mBitmap.bmp.hasAlpha() || mPaint.getAlpha() < 255)) ?
    441                 PixelFormat.TRANSLUCENT : PixelFormat.OPAQUE;
    442     }
    443 
    444     @Override
    445     public void onDecodeBegin(final RequestKey key) { }
    446 
    447     @Override
    448     public void onDecodeComplete(final RequestKey key, final ReusableBitmap result) {
    449         if (key.equals(mCurrKey)) {
    450             setBitmap(result);
    451         } else {
    452             // if the requests don't match (i.e. this request is stale), decrement the
    453             // ref count to allow the bitmap to be pooled
    454             if (result != null) {
    455                 result.releaseReference();
    456             }
    457         }
    458     }
    459 
    460     @Override
    461     public void onDecodeCancel(final RequestKey key) { }
    462 
    463     @Override
    464     public void invalidateDrawable(Drawable who) {
    465         invalidateSelf();
    466     }
    467 
    468     @Override
    469     public void scheduleDrawable(Drawable who, Runnable what, long when) {
    470         scheduleSelf(what, when);
    471     }
    472 
    473     @Override
    474     public void unscheduleDrawable(Drawable who, Runnable what) {
    475         unscheduleSelf(what);
    476     }
    477 }
    478