FrameSequenceDrawable.java revision 769d2c1df936a328f324a953f3cdf5901dd99078
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 only once. 107 */ 108 public static final int LOOP_ONCE = 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 * Define looping behavior of frame sequence. 122 * 123 * Must be one of LOOP_ONCE, LOOP_INF, or LOOP_DEFAULT 124 */ 125 public void setLoopBehavior(int loopBehavior) { 126 mLoopBehavior = loopBehavior; 127 } 128 129 private final FrameSequence mFrameSequence; 130 private final FrameSequence.State mFrameSequenceState; 131 132 private final Paint mPaint; 133 private BitmapShader mFrontBitmapShader; 134 private BitmapShader mBackBitmapShader; 135 private final Rect mSrcRect; 136 private boolean mCircleMaskEnabled; 137 138 //Protects the fields below 139 private final Object mLock = new Object(); 140 141 private final BitmapProvider mBitmapProvider; 142 private boolean mDestroyed = false; 143 private Bitmap mFrontBitmap; 144 private Bitmap mBackBitmap; 145 146 private static final int STATE_SCHEDULED = 1; 147 private static final int STATE_DECODING = 2; 148 private static final int STATE_WAITING_TO_SWAP = 3; 149 private static final int STATE_READY_TO_SWAP = 4; 150 151 private int mState; 152 private int mCurrentLoop; 153 private int mLoopBehavior = LOOP_DEFAULT; 154 155 private long mLastSwap; 156 private long mNextSwap; 157 private int mNextFrameToDecode; 158 private OnFinishedListener mOnFinishedListener; 159 160 /** 161 * Runs on decoding thread, only modifies mBackBitmap's pixels 162 */ 163 private Runnable mDecodeRunnable = new Runnable() { 164 @Override 165 public void run() { 166 int nextFrame; 167 Bitmap bitmap; 168 synchronized (mLock) { 169 if (mDestroyed) return; 170 171 nextFrame = mNextFrameToDecode; 172 if (nextFrame < 0) { 173 return; 174 } 175 bitmap = mBackBitmap; 176 mState = STATE_DECODING; 177 } 178 int lastFrame = nextFrame - 2; 179 boolean exceptionDuringDecode = false; 180 long invalidateTimeMs = 0; 181 try { 182 invalidateTimeMs = mFrameSequenceState.getFrame(nextFrame, bitmap, lastFrame); 183 } catch(Exception e) { 184 // Exception during decode: continue, but delay next frame indefinitely. 185 Log.e(TAG, "exception during decode: " + e); 186 exceptionDuringDecode = true; 187 } 188 189 if (invalidateTimeMs < MIN_DELAY_MS) { 190 invalidateTimeMs = DEFAULT_DELAY_MS; 191 } 192 193 boolean schedule = false; 194 Bitmap bitmapToRelease = null; 195 synchronized (mLock) { 196 if (mDestroyed) { 197 bitmapToRelease = mBackBitmap; 198 mBackBitmap = null; 199 } else if (mNextFrameToDecode >= 0 && mState == STATE_DECODING) { 200 schedule = true; 201 mNextSwap = exceptionDuringDecode ? Long.MAX_VALUE : invalidateTimeMs + mLastSwap; 202 mState = STATE_WAITING_TO_SWAP; 203 } 204 } 205 if (schedule) { 206 scheduleSelf(FrameSequenceDrawable.this, mNextSwap); 207 } 208 if (bitmapToRelease != null) { 209 // destroy the bitmap here, since there's no safe way to get back to 210 // drawable thread - drawable is likely detached, so schedule is noop. 211 mBitmapProvider.releaseBitmap(bitmapToRelease); 212 } 213 } 214 }; 215 216 private Runnable mCallbackRunnable = new Runnable() { 217 @Override 218 public void run() { 219 if (mOnFinishedListener != null) { 220 mOnFinishedListener.onFinished(FrameSequenceDrawable.this); 221 } 222 } 223 }; 224 225 private static Bitmap acquireAndValidateBitmap(BitmapProvider bitmapProvider, 226 int minWidth, int minHeight) { 227 Bitmap bitmap = bitmapProvider.acquireBitmap(minWidth, minHeight); 228 229 if (bitmap.getWidth() < minWidth 230 || bitmap.getHeight() < minHeight 231 || bitmap.getConfig() != Bitmap.Config.ARGB_8888) { 232 throw new IllegalArgumentException("Invalid bitmap provided"); 233 } 234 235 return bitmap; 236 } 237 238 public FrameSequenceDrawable(FrameSequence frameSequence) { 239 this(frameSequence, sAllocatingBitmapProvider); 240 } 241 242 public FrameSequenceDrawable(FrameSequence frameSequence, BitmapProvider bitmapProvider) { 243 if (frameSequence == null || bitmapProvider == null) throw new IllegalArgumentException(); 244 245 mFrameSequence = frameSequence; 246 mFrameSequenceState = frameSequence.createState(); 247 final int width = frameSequence.getWidth(); 248 final int height = frameSequence.getHeight(); 249 250 mBitmapProvider = bitmapProvider; 251 mFrontBitmap = acquireAndValidateBitmap(bitmapProvider, width, height); 252 mBackBitmap = acquireAndValidateBitmap(bitmapProvider, width, height); 253 mSrcRect = new Rect(0, 0, width, height); 254 mPaint = new Paint(); 255 mPaint.setFilterBitmap(true); 256 257 mFrontBitmapShader 258 = new BitmapShader(mFrontBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); 259 mBackBitmapShader 260 = new BitmapShader(mBackBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); 261 262 mLastSwap = 0; 263 264 mNextFrameToDecode = -1; 265 mFrameSequenceState.getFrame(0, mFrontBitmap, -1); 266 initializeDecodingThread(); 267 } 268 269 /** 270 * Pass true to mask the shape of the animated drawing content to a circle. 271 * 272 * <p> The masking circle will be the largest circle contained in the Drawable's bounds. 273 * Masking is done with BitmapShader, incurring minimal additional draw cost. 274 */ 275 public final void setCircleMaskEnabled(boolean circleMaskEnabled) { 276 mCircleMaskEnabled = circleMaskEnabled; 277 // Anti alias only necessary when using circular mask 278 mPaint.setAntiAlias(circleMaskEnabled); 279 } 280 281 private void checkDestroyedLocked() { 282 if (mDestroyed) { 283 throw new IllegalStateException("Cannot perform operation on recycled drawable"); 284 } 285 } 286 287 public boolean isDestroyed() { 288 synchronized (mLock) { 289 return mDestroyed; 290 } 291 } 292 293 /** 294 * Marks the drawable as permanently recycled (and thus unusable), and releases any owned 295 * Bitmaps drawable to its BitmapProvider, if attached. 296 * 297 * If no BitmapProvider is attached to the drawable, recycle() is called on the Bitmaps. 298 */ 299 public void destroy() { 300 if (mBitmapProvider == null) { 301 throw new IllegalStateException("BitmapProvider must be non-null"); 302 } 303 304 Bitmap bitmapToReleaseA; 305 Bitmap bitmapToReleaseB = null; 306 synchronized (mLock) { 307 checkDestroyedLocked(); 308 309 bitmapToReleaseA = mFrontBitmap; 310 mFrontBitmap = null; 311 312 if (mState != STATE_DECODING) { 313 bitmapToReleaseB = mBackBitmap; 314 mBackBitmap = null; 315 } 316 317 mDestroyed = true; 318 } 319 320 // For simplicity and safety, we don't destroy the state object here 321 mBitmapProvider.releaseBitmap(bitmapToReleaseA); 322 if (bitmapToReleaseB != null) { 323 mBitmapProvider.releaseBitmap(bitmapToReleaseB); 324 } 325 } 326 327 @Override 328 protected void finalize() throws Throwable { 329 try { 330 mFrameSequenceState.destroy(); 331 } finally { 332 super.finalize(); 333 } 334 } 335 336 @Override 337 public void draw(Canvas canvas) { 338 synchronized (mLock) { 339 checkDestroyedLocked(); 340 if (mState == STATE_WAITING_TO_SWAP) { 341 // may have failed to schedule mark ready runnable, 342 // so go ahead and swap if swapping is due 343 if (mNextSwap - SystemClock.uptimeMillis() <= 0) { 344 mState = STATE_READY_TO_SWAP; 345 } 346 } 347 348 if (isRunning() && mState == STATE_READY_TO_SWAP) { 349 // Because draw has occurred, the view system is guaranteed to no longer hold a 350 // reference to the old mFrontBitmap, so we now use it to produce the next frame 351 Bitmap tmp = mBackBitmap; 352 mBackBitmap = mFrontBitmap; 353 mFrontBitmap = tmp; 354 355 BitmapShader tmpShader = mBackBitmapShader; 356 mBackBitmapShader = mFrontBitmapShader; 357 mFrontBitmapShader = tmpShader; 358 359 mLastSwap = SystemClock.uptimeMillis(); 360 361 boolean continueLooping = true; 362 if (mNextFrameToDecode == mFrameSequence.getFrameCount() - 1) { 363 mCurrentLoop++; 364 if ((mLoopBehavior == LOOP_ONCE && mCurrentLoop == 1) || 365 (mLoopBehavior == LOOP_DEFAULT && mCurrentLoop == mFrameSequence.getDefaultLoopCount())) { 366 continueLooping = false; 367 } 368 } 369 370 if (continueLooping) { 371 scheduleDecodeLocked(); 372 } else { 373 scheduleSelf(mCallbackRunnable, 0); 374 } 375 } 376 } 377 378 if (mCircleMaskEnabled) { 379 Rect bounds = getBounds(); 380 mPaint.setShader(mFrontBitmapShader); 381 float width = bounds.width(); 382 float height = bounds.height(); 383 float circleRadius = (Math.min(width, height)) / 2f; 384 canvas.drawCircle(width / 2f, height / 2f, circleRadius, mPaint); 385 } else { 386 mPaint.setShader(null); 387 canvas.drawBitmap(mFrontBitmap, mSrcRect, getBounds(), mPaint); 388 } 389 } 390 391 private void scheduleDecodeLocked() { 392 mState = STATE_SCHEDULED; 393 mNextFrameToDecode = (mNextFrameToDecode + 1) % mFrameSequence.getFrameCount(); 394 sDecodingThreadHandler.post(mDecodeRunnable); 395 } 396 397 @Override 398 public void run() { 399 // set ready to swap as necessary 400 boolean invalidate = false; 401 synchronized (mLock) { 402 if (mNextFrameToDecode >= 0 && mState == STATE_WAITING_TO_SWAP) { 403 mState = STATE_READY_TO_SWAP; 404 invalidate = true; 405 } 406 } 407 if (invalidate) { 408 invalidateSelf(); 409 } 410 } 411 412 @Override 413 public void start() { 414 if (!isRunning()) { 415 synchronized (mLock) { 416 checkDestroyedLocked(); 417 if (mState == STATE_SCHEDULED) return; // already scheduled 418 mCurrentLoop = 0; 419 scheduleDecodeLocked(); 420 } 421 } 422 } 423 424 @Override 425 public void stop() { 426 if (isRunning()) { 427 unscheduleSelf(this); 428 } 429 } 430 431 @Override 432 public boolean isRunning() { 433 synchronized (mLock) { 434 return mNextFrameToDecode > -1 && !mDestroyed; 435 } 436 } 437 438 @Override 439 public void unscheduleSelf(Runnable what) { 440 synchronized (mLock) { 441 mNextFrameToDecode = -1; 442 mState = 0; 443 } 444 super.unscheduleSelf(what); 445 } 446 447 @Override 448 public boolean setVisible(boolean visible, boolean restart) { 449 boolean changed = super.setVisible(visible, restart); 450 451 if (!visible) { 452 stop(); 453 } else if (restart || changed) { 454 stop(); 455 start(); 456 } 457 458 return changed; 459 } 460 461 // drawing properties 462 463 @Override 464 public void setFilterBitmap(boolean filter) { 465 mPaint.setFilterBitmap(filter); 466 } 467 468 @Override 469 public void setAlpha(int alpha) { 470 mPaint.setAlpha(alpha); 471 } 472 473 @Override 474 public void setColorFilter(ColorFilter colorFilter) { 475 mPaint.setColorFilter(colorFilter); 476 } 477 478 @Override 479 public int getIntrinsicWidth() { 480 return mFrameSequence.getWidth(); 481 } 482 483 @Override 484 public int getIntrinsicHeight() { 485 return mFrameSequence.getHeight(); 486 } 487 488 @Override 489 public int getOpacity() { 490 return mFrameSequence.isOpaque() ? PixelFormat.OPAQUE : PixelFormat.TRANSPARENT; 491 } 492} 493