BasicBitmapDrawable.java revision 9c6ac19d4a3d39b7c2992060957920118ff56a65
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    protected static Rect sRect;
61
62    protected RequestKey mCurrKey;
63    protected final Paint mPaint = new Paint();
64
65    private final BitmapCache mCache;
66    private final boolean mLimitDensity;
67    private final float mDensity;
68    private ReusableBitmap mBitmap;
69    private DecodeTask mTask;
70    private Cancelable mCreateFileDescriptorFactoryTask;
71    private int mDecodeWidth;
72    private int mDecodeHeight;
73
74    // based on framework CL:I015d77
75    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
76    private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
77    private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
78
79    private static final Executor SMALL_POOL_EXECUTOR = new ThreadPoolExecutor(
80            CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, 1, TimeUnit.SECONDS,
81            new LinkedBlockingQueue<Runnable>(128), new NamedThreadFactory("decode"));
82    private static final Executor EXECUTOR = SMALL_POOL_EXECUTOR;
83
84    private static final int MAX_BITMAP_DENSITY = DisplayMetrics.DENSITY_HIGH;
85    private static final float VERTICAL_CENTER = 1f / 2;
86
87    private static final String TAG = BasicBitmapDrawable.class.getSimpleName();
88    private static final boolean DEBUG = DecodeTask.DEBUG;
89
90    public BasicBitmapDrawable(final Resources res, final BitmapCache cache,
91            final boolean limitDensity) {
92        mDensity = res.getDisplayMetrics().density;
93        mCache = cache;
94        mLimitDensity = limitDensity;
95        mPaint.setFilterBitmap(true);
96        mPaint.setAntiAlias(true);
97        mPaint.setDither(true);
98
99        if (sRect == null) {
100            sRect = new Rect();
101        }
102    }
103
104    public RequestKey getKey() {
105        return mCurrKey;
106    }
107
108    /**
109     * Set the dimensions to decode into.
110     */
111    public void setDecodeDimensions(int w, int h) {
112        mDecodeWidth = w;
113        mDecodeHeight = h;
114        loadFileDescriptorFactory();
115    }
116
117    public void unbind() {
118        setImage(null);
119    }
120
121    public void bind(RequestKey key) {
122        setImage(key);
123    }
124
125    private void setImage(final RequestKey key) {
126        if (mCurrKey != null && mCurrKey.equals(key)) {
127            return;
128        }
129
130        Trace.beginSection("set image");
131        Trace.beginSection("release reference");
132        if (mBitmap != null) {
133            mBitmap.releaseReference();
134            mBitmap = null;
135        }
136        Trace.endSection();
137        mCurrKey = key;
138
139        if (mTask != null) {
140            mTask.cancel();
141            mTask = null;
142        }
143        if (mCreateFileDescriptorFactoryTask != null) {
144            mCreateFileDescriptorFactoryTask.cancel();
145            mCreateFileDescriptorFactoryTask = null;
146        }
147
148        if (key == null) {
149            invalidateSelf();
150            Trace.endSection();
151            return;
152        }
153
154        // find cached entry here and skip decode if found.
155        final ReusableBitmap cached = mCache.get(key, true /* incrementRefCount */);
156        if (cached != null) {
157            setBitmap(cached);
158            if (DEBUG) {
159                Log.d(TAG, String.format("CACHE HIT key=%s", mCurrKey));
160            }
161        } else {
162            loadFileDescriptorFactory();
163            if (DEBUG) {
164                Log.d(TAG, String.format(
165                        "CACHE MISS key=%s\ncache=%s", mCurrKey, mCache.toDebugString()));
166            }
167        }
168        Trace.endSection();
169    }
170
171    @Override
172    public void draw(final Canvas canvas) {
173        final Rect bounds = getBounds();
174        if (bounds.isEmpty()) {
175            return;
176        }
177
178        if (mBitmap != null && mBitmap.bmp != null) {
179            BitmapUtils.calculateCroppedSrcRect(
180                    mBitmap.getLogicalWidth(), mBitmap.getLogicalHeight(),
181                    bounds.width(), bounds.height(),
182                    bounds.height(), Integer.MAX_VALUE,
183                    VERTICAL_CENTER, false /* absoluteFraction */,
184                    1, sRect);
185
186            final int orientation = mBitmap.getOrientation();
187            // calculateCroppedSrcRect() gave us the source rectangle "as if" the orientation has
188            // been corrected. We need to decode the uncorrected source rectangle. Calculate true
189            // coordinates.
190            RectUtils.rotateRectForOrientation(orientation,
191                    new Rect(0, 0, mBitmap.getLogicalWidth(), mBitmap.getLogicalHeight()),
192                    sRect);
193
194            // We may need to rotate the canvas, so we also have to rotate the bounds.
195            final Rect rotatedBounds = new Rect(bounds);
196            RectUtils.rotateRect(orientation, bounds.centerX(), bounds.centerY(), rotatedBounds);
197
198            // Rotate the canvas.
199            canvas.save();
200            canvas.rotate(orientation, bounds.centerX(), bounds.centerY());
201            canvas.drawBitmap(mBitmap.bmp, sRect, rotatedBounds, mPaint);
202            canvas.restore();
203        }
204    }
205
206    @Override
207    public void setAlpha(int alpha) {
208        final int old = mPaint.getAlpha();
209        mPaint.setAlpha(alpha);
210        if (alpha != old) {
211            invalidateSelf();
212        }
213    }
214
215    @Override
216    public void setColorFilter(ColorFilter cf) {
217        mPaint.setColorFilter(cf);
218        invalidateSelf();
219    }
220
221    @Override
222    public int getOpacity() {
223        return (mBitmap != null && (mBitmap.bmp.hasAlpha() || mPaint.getAlpha() < 255)) ?
224                PixelFormat.TRANSLUCENT : PixelFormat.OPAQUE;
225    }
226
227    @Override
228    public void onDecodeBegin(final RequestKey key) { }
229
230    @Override
231    public void onDecodeComplete(final RequestKey key, final ReusableBitmap result) {
232        if (key.equals(mCurrKey)) {
233            setBitmap(result);
234        } else {
235            // if the requests don't match (i.e. this request is stale), decrement the
236            // ref count to allow the bitmap to be pooled
237            if (result != null) {
238                result.releaseReference();
239            }
240        }
241    }
242
243    @Override
244    public void onDecodeCancel(final RequestKey key) { }
245
246    protected ReusableBitmap getBitmap() {
247        return mBitmap;
248    }
249
250    private void setBitmap(ReusableBitmap bmp) {
251        if (mBitmap != null && mBitmap != bmp) {
252            mBitmap.releaseReference();
253        }
254        mBitmap = bmp;
255        invalidateSelf();
256    }
257
258    private void loadFileDescriptorFactory() {
259        if (mCurrKey == null) {
260            return;
261        }
262        if (mDecodeWidth == 0 || mDecodeHeight == 0) {
263            return;
264        }
265
266        // Create file descriptor if request supports it.
267        mCreateFileDescriptorFactoryTask = mCurrKey
268                .createFileDescriptorFactoryAsync(mCurrKey, this);
269        if (mCreateFileDescriptorFactoryTask == null) {
270            // Use input stream if request does not.
271            decode(null);
272        }
273    }
274
275    @Override
276    public void fileDescriptorFactoryCreated(final RequestKey key,
277            final FileDescriptorFactory factory) {
278        if (mCreateFileDescriptorFactoryTask == null) {
279            // Cancelled.
280            return;
281        }
282        mCreateFileDescriptorFactoryTask = null;
283
284        if (key.equals(mCurrKey)) {
285            decode(factory);
286        }
287    }
288
289    private 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 (bufferW == 0 || bufferH == 0) {
305            Trace.endSection();
306            return;
307        }
308        if (mTask != null) {
309            mTask.cancel();
310        }
311        final DecodeOptions opts = new DecodeOptions(bufferW, bufferH, VERTICAL_CENTER,
312                DecodeOptions.STRATEGY_ROUND_NEAREST);
313        mTask = new DecodeTask(mCurrKey, opts, factory, this, mCache);
314        mTask.executeOnExecutor(EXECUTOR);
315        Trace.endSection();
316    }
317
318    @Override
319    public void invalidateDrawable(Drawable who) {
320        invalidateSelf();
321    }
322
323    @Override
324    public void scheduleDrawable(Drawable who, Runnable what, long when) {
325        scheduleSelf(what, when);
326    }
327
328    @Override
329    public void unscheduleDrawable(Drawable who, Runnable what) {
330        unscheduleSelf(what);
331    }
332}
333