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