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