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 BasicBitmapDrawable(final Resources res, final BitmapCache cache, final boolean limitDensity)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 getKey()106 public final RequestKey getKey() { 107 return mCurrKey; 108 } 109 getPreviousKey()110 public final RequestKey getPreviousKey() { 111 return mPrevKey; 112 } 113 getBitmap()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 */ setDecodeDimensions(int width, int height)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 */ setLayoutDirectionLocal(int layoutDirection)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 */ onLayoutDirectionChangeLocal(int layoutDirection)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 */ getLayoutDirectionLocal()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 */ bind(RequestKey key)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 */ unbind()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 */ unbind(boolean temporary)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 */ setImage(final RequestKey key)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 */ setBitmap(ReusableBitmap bmp)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 */ loadFileDescriptorFactory()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 fileDescriptorFactoryCreated(final RequestKey key, final FileDescriptorFactory factory)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 */ onDecodeFailed()307 protected void onDecodeFailed() { 308 invalidateSelf(); 309 } 310 311 /** 312 * Should only be overriden, not called. 313 */ decode(final FileDescriptorFactory factory)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 */ getDecodeStrategy()342 protected int getDecodeStrategy() { 343 return DecodeOptions.STRATEGY_ROUND_NEAREST; 344 } 345 getExecutor()346 protected Executor getExecutor() { 347 return EXECUTOR; 348 } 349 getDrawVerticalCenter()350 protected float getDrawVerticalCenter() { 351 return VERTICAL_CENTER; 352 } 353 getDrawVerticalOffsetMultiplier()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 */ getDecodeHorizontalCenter()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 */ getDecodeVerticalCenter()370 protected float getDecodeVerticalCenter() { 371 return VERTICAL_CENTER; 372 } 373 374 @Override draw(final Canvas canvas)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 hasBitmap()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 */ onDrawBitmap(final Canvas canvas, final Rect src, final Rect dst)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 setAlpha(int alpha)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 setColorFilter(ColorFilter cf)433 public void setColorFilter(ColorFilter cf) { 434 mPaint.setColorFilter(cf); 435 invalidateSelf(); 436 } 437 438 @Override getOpacity()439 public int getOpacity() { 440 return (hasBitmap() && (mBitmap.bmp.hasAlpha() || mPaint.getAlpha() < 255)) ? 441 PixelFormat.TRANSLUCENT : PixelFormat.OPAQUE; 442 } 443 444 @Override onDecodeBegin(final RequestKey key)445 public void onDecodeBegin(final RequestKey key) { } 446 447 @Override onDecodeComplete(final RequestKey key, final ReusableBitmap result)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 onDecodeCancel(final RequestKey key)461 public void onDecodeCancel(final RequestKey key) { } 462 463 @Override invalidateDrawable(Drawable who)464 public void invalidateDrawable(Drawable who) { 465 invalidateSelf(); 466 } 467 468 @Override scheduleDrawable(Drawable who, Runnable what, long when)469 public void scheduleDrawable(Drawable who, Runnable what, long when) { 470 scheduleSelf(what, when); 471 } 472 473 @Override unscheduleDrawable(Drawable who, Runnable what)474 public void unscheduleDrawable(Drawable who, Runnable what) { 475 unscheduleSelf(what); 476 } 477 } 478