FrameSequenceDrawable.java revision 958761ceb97982b510d3059d31ba8c45700a1654
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 17package android.support.rastermill; 18 19import android.graphics.Bitmap; 20import android.graphics.BitmapShader; 21import android.graphics.Canvas; 22import android.graphics.ColorFilter; 23import android.graphics.Paint; 24import android.graphics.PixelFormat; 25import android.graphics.Rect; 26import android.graphics.RectF; 27import android.graphics.Shader; 28import android.graphics.drawable.Animatable; 29import android.graphics.drawable.Drawable; 30import android.os.Handler; 31import android.os.HandlerThread; 32import android.os.Process; 33import android.os.SystemClock; 34import android.util.Log; 35 36public class FrameSequenceDrawable extends Drawable implements Animatable, Runnable { 37 private static final String TAG = "FrameSequence"; 38 /** 39 * These constants are chosen to imitate common browser behavior for WebP/GIF. 40 * If other decoders are added, this behavior should be moved into the WebP/GIF decoders. 41 * 42 * Note that 0 delay is undefined behavior in the GIF standard. 43 */ 44 private static final long MIN_DELAY_MS = 20; 45 private static final long DEFAULT_DELAY_MS = 100; 46 47 private static final Object sLock = new Object(); 48 private static HandlerThread sDecodingThread; 49 private static Handler sDecodingThreadHandler; 50 private static void initializeDecodingThread() { 51 synchronized (sLock) { 52 if (sDecodingThread != null) return; 53 54 sDecodingThread = new HandlerThread("FrameSequence decoding thread", 55 Process.THREAD_PRIORITY_BACKGROUND); 56 sDecodingThread.start(); 57 sDecodingThreadHandler = new Handler(sDecodingThread.getLooper()); 58 } 59 } 60 61 public static interface OnFinishedListener { 62 /** 63 * Called when a FrameSequenceDrawable has finished looping. 64 * 65 * Note that this is will not be called if the drawable is explicitly 66 * stopped, or marked invisible. 67 */ 68 public abstract void onFinished(FrameSequenceDrawable drawable); 69 } 70 71 public static interface BitmapProvider { 72 /** 73 * Called by FrameSequenceDrawable to aquire an 8888 Bitmap with minimum dimensions. 74 */ 75 public abstract Bitmap acquireBitmap(int minWidth, int minHeight); 76 77 /** 78 * Called by FrameSequenceDrawable to release a Bitmap it no longer needs. The Bitmap 79 * will no longer be used at all by the drawable, so it is safe to reuse elsewhere. 80 * 81 * This method may be called by FrameSequenceDrawable on any thread. 82 */ 83 public abstract void releaseBitmap(Bitmap bitmap); 84 } 85 86 private static BitmapProvider sAllocatingBitmapProvider = new BitmapProvider() { 87 @Override 88 public Bitmap acquireBitmap(int minWidth, int minHeight) { 89 return Bitmap.createBitmap(minWidth, minHeight, Bitmap.Config.ARGB_8888); 90 } 91 92 @Override 93 public void releaseBitmap(Bitmap bitmap) {} 94 }; 95 96 /** 97 * Register a callback to be invoked when a FrameSequenceDrawable finishes looping. 98 * 99 * @see #setLoopBehavior(int) 100 */ 101 public void setOnFinishedListener(OnFinishedListener onFinishedListener) { 102 mOnFinishedListener = onFinishedListener; 103 } 104 105 /** 106 * Loop a finite number of times, which can be set using setLoopCount. Default to loop once. 107 */ 108 public static final int LOOP_FINITE = 1; 109 110 /** 111 * Loop continuously. The OnFinishedListener will never be called. 112 */ 113 public static final int LOOP_INF = 2; 114 115 /** 116 * Use loop count stored in source data, or LOOP_ONCE if not present. 117 */ 118 public static final int LOOP_DEFAULT = 3; 119 120 /** 121 * Loop only once. 122 * 123 * @deprecated Use LOOP_FINITE instead. 124 */ 125 @Deprecated 126 public static final int LOOP_ONCE = LOOP_FINITE; 127 128 /** 129 * Define looping behavior of frame sequence. 130 * 131 * Must be one of LOOP_ONCE, LOOP_INF, LOOP_DEFAULT, or LOOP_FINITE. 132 */ 133 public void setLoopBehavior(int loopBehavior) { 134 mLoopBehavior = loopBehavior; 135 } 136 137 /** 138 * Set the number of loops in LOOP_FINITE mode. The number must be a postive integer. 139 */ 140 public void setLoopCount(int loopCount) { 141 mLoopCount = loopCount; 142 } 143 144 private final FrameSequence mFrameSequence; 145 private final FrameSequence.State mFrameSequenceState; 146 147 private final Paint mPaint; 148 private BitmapShader mFrontBitmapShader; 149 private BitmapShader mBackBitmapShader; 150 private final Rect mSrcRect; 151 private boolean mCircleMaskEnabled; 152 153 //Protects the fields below 154 private final Object mLock = new Object(); 155 156 private final BitmapProvider mBitmapProvider; 157 private boolean mDestroyed = false; 158 private Bitmap mFrontBitmap; 159 private Bitmap mBackBitmap; 160 161 private static final int STATE_SCHEDULED = 1; 162 private static final int STATE_DECODING = 2; 163 private static final int STATE_WAITING_TO_SWAP = 3; 164 private static final int STATE_READY_TO_SWAP = 4; 165 166 private int mState; 167 private int mCurrentLoop; 168 private int mLoopBehavior = LOOP_DEFAULT; 169 private int mLoopCount = 1; 170 171 private long mLastSwap; 172 private long mNextSwap; 173 private int mNextFrameToDecode; 174 private OnFinishedListener mOnFinishedListener; 175 176 private RectF mTempRectF = new RectF(); 177 178 /** 179 * Runs on decoding thread, only modifies mBackBitmap's pixels 180 */ 181 private Runnable mDecodeRunnable = new Runnable() { 182 @Override 183 public void run() { 184 int nextFrame; 185 Bitmap bitmap; 186 synchronized (mLock) { 187 if (mDestroyed) return; 188 189 nextFrame = mNextFrameToDecode; 190 if (nextFrame < 0) { 191 return; 192 } 193 bitmap = mBackBitmap; 194 mState = STATE_DECODING; 195 } 196 int lastFrame = nextFrame - 2; 197 boolean exceptionDuringDecode = false; 198 long invalidateTimeMs = 0; 199 try { 200 invalidateTimeMs = mFrameSequenceState.getFrame(nextFrame, bitmap, lastFrame); 201 } catch(Exception e) { 202 // Exception during decode: continue, but delay next frame indefinitely. 203 Log.e(TAG, "exception during decode: " + e); 204 exceptionDuringDecode = true; 205 } 206 207 if (invalidateTimeMs < MIN_DELAY_MS) { 208 invalidateTimeMs = DEFAULT_DELAY_MS; 209 } 210 211 boolean schedule = false; 212 Bitmap bitmapToRelease = null; 213 synchronized (mLock) { 214 if (mDestroyed) { 215 bitmapToRelease = mBackBitmap; 216 mBackBitmap = null; 217 } else if (mNextFrameToDecode >= 0 && mState == STATE_DECODING) { 218 schedule = true; 219 mNextSwap = exceptionDuringDecode ? Long.MAX_VALUE : invalidateTimeMs + mLastSwap; 220 mState = STATE_WAITING_TO_SWAP; 221 } 222 } 223 if (schedule) { 224 scheduleSelf(FrameSequenceDrawable.this, mNextSwap); 225 } 226 if (bitmapToRelease != null) { 227 // destroy the bitmap here, since there's no safe way to get back to 228 // drawable thread - drawable is likely detached, so schedule is noop. 229 mBitmapProvider.releaseBitmap(bitmapToRelease); 230 } 231 } 232 }; 233 234 private Runnable mCallbackRunnable = new Runnable() { 235 @Override 236 public void run() { 237 if (mOnFinishedListener != null) { 238 mOnFinishedListener.onFinished(FrameSequenceDrawable.this); 239 } 240 } 241 }; 242 243 private static Bitmap acquireAndValidateBitmap(BitmapProvider bitmapProvider, 244 int minWidth, int minHeight) { 245 Bitmap bitmap = bitmapProvider.acquireBitmap(minWidth, minHeight); 246 247 if (bitmap.getWidth() < minWidth 248 || bitmap.getHeight() < minHeight 249 || bitmap.getConfig() != Bitmap.Config.ARGB_8888) { 250 throw new IllegalArgumentException("Invalid bitmap provided"); 251 } 252 253 return bitmap; 254 } 255 256 public FrameSequenceDrawable(FrameSequence frameSequence) { 257 this(frameSequence, sAllocatingBitmapProvider); 258 } 259 260 public FrameSequenceDrawable(FrameSequence frameSequence, BitmapProvider bitmapProvider) { 261 if (frameSequence == null || bitmapProvider == null) throw new IllegalArgumentException(); 262 263 mFrameSequence = frameSequence; 264 mFrameSequenceState = frameSequence.createState(); 265 final int width = frameSequence.getWidth(); 266 final int height = frameSequence.getHeight(); 267 268 mBitmapProvider = bitmapProvider; 269 mFrontBitmap = acquireAndValidateBitmap(bitmapProvider, width, height); 270 mBackBitmap = acquireAndValidateBitmap(bitmapProvider, width, height); 271 mSrcRect = new Rect(0, 0, width, height); 272 mPaint = new Paint(); 273 mPaint.setFilterBitmap(true); 274 275 mFrontBitmapShader 276 = new BitmapShader(mFrontBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); 277 mBackBitmapShader 278 = new BitmapShader(mBackBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); 279 280 mLastSwap = 0; 281 282 mNextFrameToDecode = -1; 283 mFrameSequenceState.getFrame(0, mFrontBitmap, -1); 284 initializeDecodingThread(); 285 } 286 287 /** 288 * Pass true to mask the shape of the animated drawing content to a circle. 289 * 290 * <p> The masking circle will be the largest circle contained in the Drawable's bounds. 291 * Masking is done with BitmapShader, incurring minimal additional draw cost. 292 */ 293 public final void setCircleMaskEnabled(boolean circleMaskEnabled) { 294 if (mCircleMaskEnabled != circleMaskEnabled) { 295 mCircleMaskEnabled = circleMaskEnabled; 296 // Anti alias only necessary when using circular mask 297 mPaint.setAntiAlias(circleMaskEnabled); 298 invalidateSelf(); 299 } 300 } 301 302 public final boolean getCircleMaskEnabled() { 303 return mCircleMaskEnabled; 304 } 305 306 private void checkDestroyedLocked() { 307 if (mDestroyed) { 308 throw new IllegalStateException("Cannot perform operation on recycled drawable"); 309 } 310 } 311 312 public boolean isDestroyed() { 313 synchronized (mLock) { 314 return mDestroyed; 315 } 316 } 317 318 /** 319 * Marks the drawable as permanently recycled (and thus unusable), and releases any owned 320 * Bitmaps drawable to its BitmapProvider, if attached. 321 * 322 * If no BitmapProvider is attached to the drawable, recycle() is called on the Bitmaps. 323 */ 324 public void destroy() { 325 if (mBitmapProvider == null) { 326 throw new IllegalStateException("BitmapProvider must be non-null"); 327 } 328 329 Bitmap bitmapToReleaseA; 330 Bitmap bitmapToReleaseB = null; 331 synchronized (mLock) { 332 checkDestroyedLocked(); 333 334 bitmapToReleaseA = mFrontBitmap; 335 mFrontBitmap = null; 336 337 if (mState != STATE_DECODING) { 338 bitmapToReleaseB = mBackBitmap; 339 mBackBitmap = null; 340 } 341 342 mDestroyed = true; 343 } 344 345 // For simplicity and safety, we don't destroy the state object here 346 mBitmapProvider.releaseBitmap(bitmapToReleaseA); 347 if (bitmapToReleaseB != null) { 348 mBitmapProvider.releaseBitmap(bitmapToReleaseB); 349 } 350 } 351 352 @Override 353 protected void finalize() throws Throwable { 354 try { 355 mFrameSequenceState.destroy(); 356 } finally { 357 super.finalize(); 358 } 359 } 360 361 @Override 362 public void draw(Canvas canvas) { 363 synchronized (mLock) { 364 checkDestroyedLocked(); 365 if (mState == STATE_WAITING_TO_SWAP) { 366 // may have failed to schedule mark ready runnable, 367 // so go ahead and swap if swapping is due 368 if (mNextSwap - SystemClock.uptimeMillis() <= 0) { 369 mState = STATE_READY_TO_SWAP; 370 } 371 } 372 373 if (isRunning() && mState == STATE_READY_TO_SWAP) { 374 // Because draw has occurred, the view system is guaranteed to no longer hold a 375 // reference to the old mFrontBitmap, so we now use it to produce the next frame 376 Bitmap tmp = mBackBitmap; 377 mBackBitmap = mFrontBitmap; 378 mFrontBitmap = tmp; 379 380 BitmapShader tmpShader = mBackBitmapShader; 381 mBackBitmapShader = mFrontBitmapShader; 382 mFrontBitmapShader = tmpShader; 383 384 mLastSwap = SystemClock.uptimeMillis(); 385 386 boolean continueLooping = true; 387 if (mNextFrameToDecode == mFrameSequence.getFrameCount() - 1) { 388 mCurrentLoop++; 389 if ((mLoopBehavior == LOOP_ONCE && mCurrentLoop == mLoopCount) || 390 (mLoopBehavior == LOOP_DEFAULT && mCurrentLoop == mFrameSequence.getDefaultLoopCount())) { 391 continueLooping = false; 392 } 393 } 394 395 if (continueLooping) { 396 scheduleDecodeLocked(); 397 } else { 398 scheduleSelf(mCallbackRunnable, 0); 399 } 400 } 401 } 402 403 if (mCircleMaskEnabled) { 404 final Rect bounds = getBounds(); 405 final int bitmapWidth = getIntrinsicWidth(); 406 final int bitmapHeight = getIntrinsicHeight(); 407 final float scaleX = 1.0f * bounds.width() / bitmapWidth; 408 final float scaleY = 1.0f * bounds.height() / bitmapHeight; 409 410 canvas.save(); 411 // scale and translate to account for bounds, so we can operate in intrinsic 412 // width/height (so it's valid to use an unscaled bitmap shader) 413 canvas.translate(bounds.left, bounds.top); 414 canvas.scale(scaleX, scaleY); 415 416 final float unscaledCircleDiameter = Math.min(bounds.width(), bounds.height()); 417 final float scaledDiameterX = unscaledCircleDiameter / scaleX; 418 final float scaledDiameterY = unscaledCircleDiameter / scaleY; 419 420 // Want to draw a circle, but we have to compensate for canvas scale 421 mTempRectF.set( 422 (bitmapWidth - scaledDiameterX) / 2.0f, 423 (bitmapHeight - scaledDiameterY) / 2.0f, 424 (bitmapWidth + scaledDiameterX) / 2.0f, 425 (bitmapHeight + scaledDiameterY) / 2.0f); 426 mPaint.setShader(mFrontBitmapShader); 427 canvas.drawOval(mTempRectF, mPaint); 428 canvas.restore(); 429 } else { 430 mPaint.setShader(null); 431 canvas.drawBitmap(mFrontBitmap, mSrcRect, getBounds(), mPaint); 432 } 433 } 434 435 private void scheduleDecodeLocked() { 436 mState = STATE_SCHEDULED; 437 mNextFrameToDecode = (mNextFrameToDecode + 1) % mFrameSequence.getFrameCount(); 438 sDecodingThreadHandler.post(mDecodeRunnable); 439 } 440 441 @Override 442 public void run() { 443 // set ready to swap as necessary 444 boolean invalidate = false; 445 synchronized (mLock) { 446 if (mNextFrameToDecode >= 0 && mState == STATE_WAITING_TO_SWAP) { 447 mState = STATE_READY_TO_SWAP; 448 invalidate = true; 449 } 450 } 451 if (invalidate) { 452 invalidateSelf(); 453 } 454 } 455 456 @Override 457 public void start() { 458 if (!isRunning()) { 459 synchronized (mLock) { 460 checkDestroyedLocked(); 461 if (mState == STATE_SCHEDULED) return; // already scheduled 462 mCurrentLoop = 0; 463 scheduleDecodeLocked(); 464 } 465 } 466 } 467 468 @Override 469 public void stop() { 470 if (isRunning()) { 471 unscheduleSelf(this); 472 } 473 } 474 475 @Override 476 public boolean isRunning() { 477 synchronized (mLock) { 478 return mNextFrameToDecode > -1 && !mDestroyed; 479 } 480 } 481 482 @Override 483 public void unscheduleSelf(Runnable what) { 484 synchronized (mLock) { 485 mNextFrameToDecode = -1; 486 mState = 0; 487 } 488 super.unscheduleSelf(what); 489 } 490 491 @Override 492 public boolean setVisible(boolean visible, boolean restart) { 493 boolean changed = super.setVisible(visible, restart); 494 495 if (!visible) { 496 stop(); 497 } else if (restart || changed) { 498 stop(); 499 start(); 500 } 501 502 return changed; 503 } 504 505 // drawing properties 506 507 @Override 508 public void setFilterBitmap(boolean filter) { 509 mPaint.setFilterBitmap(filter); 510 } 511 512 @Override 513 public void setAlpha(int alpha) { 514 mPaint.setAlpha(alpha); 515 } 516 517 @Override 518 public void setColorFilter(ColorFilter colorFilter) { 519 mPaint.setColorFilter(colorFilter); 520 } 521 522 @Override 523 public int getIntrinsicWidth() { 524 return mFrameSequence.getWidth(); 525 } 526 527 @Override 528 public int getIntrinsicHeight() { 529 return mFrameSequence.getHeight(); 530 } 531 532 @Override 533 public int getOpacity() { 534 return mFrameSequence.isOpaque() ? PixelFormat.OPAQUE : PixelFormat.TRANSPARENT; 535 } 536} 537