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 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 BasicBitmapDrawable(final Resources res, final BitmapCache cache, final boolean limitDensity)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 getKey()108 public final RequestKey getKey() { 109 return mCurrKey; 110 } 111 getPreviousKey()112 public final RequestKey getPreviousKey() { 113 return mPrevKey; 114 } 115 getBitmap()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 */ setDecodeDimensions(int width, int height)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 */ bind(RequestKey key)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 */ unbind()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 */ unbind(boolean temporary)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 */ setImage(final RequestKey key)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 */ setBitmap(ReusableBitmap bmp)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 */ loadFileDescriptorFactory()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 fileDescriptorFactoryCreated(final RequestKey key, final FileDescriptorFactory factory)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 */ decode(final FileDescriptorFactory factory)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 getExecutor()298 protected Executor getExecutor() { 299 return EXECUTOR; 300 } 301 getDrawVerticalCenter()302 protected float getDrawVerticalCenter() { 303 return VERTICAL_CENTER; 304 } 305 getDrawVerticalOffsetMultiplier()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 */ getDecodeVerticalCenter()314 protected float getDecodeVerticalCenter() { 315 return VERTICAL_CENTER; 316 } 317 318 @Override draw(final Canvas canvas)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 hasBitmap()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 */ onDrawBitmap(final Canvas canvas, final Rect src, final Rect dst)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 setAlpha(int alpha)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 setColorFilter(ColorFilter cf)377 public void setColorFilter(ColorFilter cf) { 378 mPaint.setColorFilter(cf); 379 invalidateSelf(); 380 } 381 382 @Override getOpacity()383 public int getOpacity() { 384 return (hasBitmap() && (mBitmap.bmp.hasAlpha() || mPaint.getAlpha() < 255)) ? 385 PixelFormat.TRANSLUCENT : PixelFormat.OPAQUE; 386 } 387 388 @Override onDecodeBegin(final RequestKey key)389 public void onDecodeBegin(final RequestKey key) { } 390 391 @Override onDecodeComplete(final RequestKey key, final ReusableBitmap result)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 onDecodeCancel(final RequestKey key)405 public void onDecodeCancel(final RequestKey key) { } 406 407 @Override invalidateDrawable(Drawable who)408 public void invalidateDrawable(Drawable who) { 409 invalidateSelf(); 410 } 411 412 @Override scheduleDrawable(Drawable who, Runnable what, long when)413 public void scheduleDrawable(Drawable who, Runnable what, long when) { 414 scheduleSelf(what, when); 415 } 416 417 @Override unscheduleDrawable(Drawable who, Runnable what)418 public void unscheduleDrawable(Drawable who, Runnable what) { 419 unscheduleSelf(what); 420 } 421 } 422