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