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 17 package android.support.rastermill; 18 19 import android.graphics.Bitmap; 20 import android.graphics.BitmapShader; 21 import android.graphics.Canvas; 22 import android.graphics.ColorFilter; 23 import android.graphics.Paint; 24 import android.graphics.PixelFormat; 25 import android.graphics.Rect; 26 import android.graphics.RectF; 27 import android.graphics.Shader; 28 import android.graphics.drawable.Animatable; 29 import android.graphics.drawable.Drawable; 30 import android.os.Handler; 31 import android.os.HandlerThread; 32 import android.os.Process; 33 import android.os.SystemClock; 34 import android.util.Log; 35 36 public 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 mFinishedCallbackRunnable = new Runnable() { 235 @Override 236 public void run() { 237 synchronized (mLock) { 238 mNextFrameToDecode = -1; 239 mState = 0; 240 } 241 if (mOnFinishedListener != null) { 242 mOnFinishedListener.onFinished(FrameSequenceDrawable.this); 243 } 244 } 245 }; 246 247 private static Bitmap acquireAndValidateBitmap(BitmapProvider bitmapProvider, 248 int minWidth, int minHeight) { 249 Bitmap bitmap = bitmapProvider.acquireBitmap(minWidth, minHeight); 250 251 if (bitmap.getWidth() < minWidth 252 || bitmap.getHeight() < minHeight 253 || bitmap.getConfig() != Bitmap.Config.ARGB_8888) { 254 throw new IllegalArgumentException("Invalid bitmap provided"); 255 } 256 257 return bitmap; 258 } 259 260 public FrameSequenceDrawable(FrameSequence frameSequence) { 261 this(frameSequence, sAllocatingBitmapProvider); 262 } 263 264 public FrameSequenceDrawable(FrameSequence frameSequence, BitmapProvider bitmapProvider) { 265 if (frameSequence == null || bitmapProvider == null) throw new IllegalArgumentException(); 266 267 mFrameSequence = frameSequence; 268 mFrameSequenceState = frameSequence.createState(); 269 final int width = frameSequence.getWidth(); 270 final int height = frameSequence.getHeight(); 271 272 mBitmapProvider = bitmapProvider; 273 mFrontBitmap = acquireAndValidateBitmap(bitmapProvider, width, height); 274 mBackBitmap = acquireAndValidateBitmap(bitmapProvider, width, height); 275 mSrcRect = new Rect(0, 0, width, height); 276 mPaint = new Paint(); 277 mPaint.setFilterBitmap(true); 278 279 mFrontBitmapShader 280 = new BitmapShader(mFrontBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); 281 mBackBitmapShader 282 = new BitmapShader(mBackBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); 283 284 mLastSwap = 0; 285 286 mNextFrameToDecode = -1; 287 mFrameSequenceState.getFrame(0, mFrontBitmap, -1); 288 initializeDecodingThread(); 289 } 290 291 /** 292 * Pass true to mask the shape of the animated drawing content to a circle. 293 * 294 * <p> The masking circle will be the largest circle contained in the Drawable's bounds. 295 * Masking is done with BitmapShader, incurring minimal additional draw cost. 296 */ 297 public final void setCircleMaskEnabled(boolean circleMaskEnabled) { 298 if (mCircleMaskEnabled != circleMaskEnabled) { 299 mCircleMaskEnabled = circleMaskEnabled; 300 // Anti alias only necessary when using circular mask 301 mPaint.setAntiAlias(circleMaskEnabled); 302 invalidateSelf(); 303 } 304 } 305 306 public final boolean getCircleMaskEnabled() { 307 return mCircleMaskEnabled; 308 } 309 310 private void checkDestroyedLocked() { 311 if (mDestroyed) { 312 throw new IllegalStateException("Cannot perform operation on recycled drawable"); 313 } 314 } 315 316 public boolean isDestroyed() { 317 synchronized (mLock) { 318 return mDestroyed; 319 } 320 } 321 322 /** 323 * Marks the drawable as permanently recycled (and thus unusable), and releases any owned 324 * Bitmaps drawable to its BitmapProvider, if attached. 325 * 326 * If no BitmapProvider is attached to the drawable, recycle() is called on the Bitmaps. 327 */ 328 public void destroy() { 329 if (mBitmapProvider == null) { 330 throw new IllegalStateException("BitmapProvider must be non-null"); 331 } 332 333 Bitmap bitmapToReleaseA; 334 Bitmap bitmapToReleaseB = null; 335 synchronized (mLock) { 336 checkDestroyedLocked(); 337 338 bitmapToReleaseA = mFrontBitmap; 339 mFrontBitmap = null; 340 341 if (mState != STATE_DECODING) { 342 bitmapToReleaseB = mBackBitmap; 343 mBackBitmap = null; 344 } 345 346 mDestroyed = true; 347 } 348 349 // For simplicity and safety, we don't destroy the state object here 350 mBitmapProvider.releaseBitmap(bitmapToReleaseA); 351 if (bitmapToReleaseB != null) { 352 mBitmapProvider.releaseBitmap(bitmapToReleaseB); 353 } 354 } 355 356 @Override 357 protected void finalize() throws Throwable { 358 try { 359 mFrameSequenceState.destroy(); 360 } finally { 361 super.finalize(); 362 } 363 } 364 365 @Override 366 public void draw(Canvas canvas) { 367 synchronized (mLock) { 368 checkDestroyedLocked(); 369 if (mState == STATE_WAITING_TO_SWAP) { 370 // may have failed to schedule mark ready runnable, 371 // so go ahead and swap if swapping is due 372 if (mNextSwap - SystemClock.uptimeMillis() <= 0) { 373 mState = STATE_READY_TO_SWAP; 374 } 375 } 376 377 if (isRunning() && mState == STATE_READY_TO_SWAP) { 378 // Because draw has occurred, the view system is guaranteed to no longer hold a 379 // reference to the old mFrontBitmap, so we now use it to produce the next frame 380 Bitmap tmp = mBackBitmap; 381 mBackBitmap = mFrontBitmap; 382 mFrontBitmap = tmp; 383 384 BitmapShader tmpShader = mBackBitmapShader; 385 mBackBitmapShader = mFrontBitmapShader; 386 mFrontBitmapShader = tmpShader; 387 388 mLastSwap = SystemClock.uptimeMillis(); 389 390 boolean continueLooping = true; 391 if (mNextFrameToDecode == mFrameSequence.getFrameCount() - 1) { 392 mCurrentLoop++; 393 if ((mLoopBehavior == LOOP_FINITE && mCurrentLoop == mLoopCount) || 394 (mLoopBehavior == LOOP_DEFAULT && mCurrentLoop == mFrameSequence.getDefaultLoopCount())) { 395 continueLooping = false; 396 } 397 } 398 399 if (continueLooping) { 400 scheduleDecodeLocked(); 401 } else { 402 scheduleSelf(mFinishedCallbackRunnable, 0); 403 } 404 } 405 } 406 407 if (mCircleMaskEnabled) { 408 final Rect bounds = getBounds(); 409 final int bitmapWidth = getIntrinsicWidth(); 410 final int bitmapHeight = getIntrinsicHeight(); 411 final float scaleX = 1.0f * bounds.width() / bitmapWidth; 412 final float scaleY = 1.0f * bounds.height() / bitmapHeight; 413 414 canvas.save(); 415 // scale and translate to account for bounds, so we can operate in intrinsic 416 // width/height (so it's valid to use an unscaled bitmap shader) 417 canvas.translate(bounds.left, bounds.top); 418 canvas.scale(scaleX, scaleY); 419 420 final float unscaledCircleDiameter = Math.min(bounds.width(), bounds.height()); 421 final float scaledDiameterX = unscaledCircleDiameter / scaleX; 422 final float scaledDiameterY = unscaledCircleDiameter / scaleY; 423 424 // Want to draw a circle, but we have to compensate for canvas scale 425 mTempRectF.set( 426 (bitmapWidth - scaledDiameterX) / 2.0f, 427 (bitmapHeight - scaledDiameterY) / 2.0f, 428 (bitmapWidth + scaledDiameterX) / 2.0f, 429 (bitmapHeight + scaledDiameterY) / 2.0f); 430 mPaint.setShader(mFrontBitmapShader); 431 canvas.drawOval(mTempRectF, mPaint); 432 canvas.restore(); 433 } else { 434 mPaint.setShader(null); 435 canvas.drawBitmap(mFrontBitmap, mSrcRect, getBounds(), mPaint); 436 } 437 } 438 439 private void scheduleDecodeLocked() { 440 mState = STATE_SCHEDULED; 441 mNextFrameToDecode = (mNextFrameToDecode + 1) % mFrameSequence.getFrameCount(); 442 sDecodingThreadHandler.post(mDecodeRunnable); 443 } 444 445 @Override 446 public void run() { 447 // set ready to swap as necessary 448 boolean invalidate = false; 449 synchronized (mLock) { 450 if (mNextFrameToDecode >= 0 && mState == STATE_WAITING_TO_SWAP) { 451 mState = STATE_READY_TO_SWAP; 452 invalidate = true; 453 } 454 } 455 if (invalidate) { 456 invalidateSelf(); 457 } 458 } 459 460 @Override 461 public void start() { 462 if (!isRunning()) { 463 synchronized (mLock) { 464 checkDestroyedLocked(); 465 if (mState == STATE_SCHEDULED) return; // already scheduled 466 mCurrentLoop = 0; 467 scheduleDecodeLocked(); 468 } 469 } 470 } 471 472 @Override 473 public void stop() { 474 if (isRunning()) { 475 unscheduleSelf(this); 476 } 477 } 478 479 @Override 480 public boolean isRunning() { 481 synchronized (mLock) { 482 return mNextFrameToDecode > -1 && !mDestroyed; 483 } 484 } 485 486 @Override 487 public void unscheduleSelf(Runnable what) { 488 synchronized (mLock) { 489 mNextFrameToDecode = -1; 490 mState = 0; 491 } 492 super.unscheduleSelf(what); 493 } 494 495 @Override 496 public boolean setVisible(boolean visible, boolean restart) { 497 boolean changed = super.setVisible(visible, restart); 498 499 if (!visible) { 500 stop(); 501 } else if (restart || changed) { 502 stop(); 503 start(); 504 } 505 506 return changed; 507 } 508 509 // drawing properties 510 511 @Override 512 public void setFilterBitmap(boolean filter) { 513 mPaint.setFilterBitmap(filter); 514 } 515 516 @Override 517 public void setAlpha(int alpha) { 518 mPaint.setAlpha(alpha); 519 } 520 521 @Override 522 public void setColorFilter(ColorFilter colorFilter) { 523 mPaint.setColorFilter(colorFilter); 524 } 525 526 @Override 527 public int getIntrinsicWidth() { 528 return mFrameSequence.getWidth(); 529 } 530 531 @Override 532 public int getIntrinsicHeight() { 533 return mFrameSequence.getHeight(); 534 } 535 536 @Override 537 public int getOpacity() { 538 return mFrameSequence.isOpaque() ? PixelFormat.OPAQUE : PixelFormat.TRANSPARENT; 539 } 540 } 541