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