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