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