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.Canvas; 21 import android.graphics.ColorFilter; 22 import android.graphics.Paint; 23 import android.graphics.PixelFormat; 24 import android.graphics.Rect; 25 import android.graphics.drawable.Animatable; 26 import android.graphics.drawable.Drawable; 27 import android.os.Handler; 28 import android.os.HandlerThread; 29 import android.os.Process; 30 import android.os.SystemClock; 31 32 public 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; initializeDecodingThread()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 */ onFinished(FrameSequenceDrawable drawable)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 */ acquireBitmap(int minWidth, int minHeight)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 */ releaseBitmap(Bitmap bitmap)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 */ setOnFinishedListener(OnFinishedListener onFinishedListener)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 */ setLoopBehavior(int loopBehavior)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 acquireAndValidateBitmap(BitmapProvider bitmapProvider, int minWidth, int minHeight)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 FrameSequenceDrawable(FrameSequence frameSequence)198 public FrameSequenceDrawable(FrameSequence frameSequence) { 199 this(frameSequence, sAllocatingBitmapProvider); 200 } 201 FrameSequenceDrawable(FrameSequence frameSequence, BitmapProvider bitmapProvider)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 checkDestroyedLocked()224 private void checkDestroyedLocked() { 225 if (mDestroyed) { 226 throw new IllegalStateException("Cannot perform operation on recycled drawable"); 227 } 228 } 229 isDestroyed()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 */ destroy()242 public void destroy() { 243 destroy(mBitmapProvider); 244 } 245 destroy(BitmapProvider bitmapProvider)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 finalize()270 protected void finalize() throws Throwable { 271 try { 272 mFrameSequenceState.destroy(); 273 if (!mDestroyed) { 274 destroy(); 275 } 276 } finally { 277 super.finalize(); 278 } 279 } 280 281 @Override draw(Canvas canvas)282 public void draw(Canvas canvas) { 283 synchronized (mLock) { 284 checkDestroyedLocked(); 285 if (mState == STATE_WAITING_TO_SWAP) { 286 // may have failed to schedule mark ready runnable, 287 // so go ahead and swap if swapping is due 288 if (mNextSwap - SystemClock.uptimeMillis() <= 0) { 289 mState = STATE_READY_TO_SWAP; 290 } 291 } 292 293 if (isRunning() && mState == STATE_READY_TO_SWAP) { 294 // Because draw has occurred, the view system is guaranteed to no longer hold a 295 // reference to the old mFrontBitmap, so we now use it to produce the next frame 296 Bitmap tmp = mBackBitmap; 297 mBackBitmap = mFrontBitmap; 298 mFrontBitmap = tmp; 299 300 mLastSwap = SystemClock.uptimeMillis(); 301 302 boolean continueLooping = true; 303 if (mNextFrameToDecode == mFrameSequence.getFrameCount() - 1) { 304 mCurrentLoop++; 305 if ((mLoopBehavior == LOOP_ONCE && mCurrentLoop == 1) || 306 (mLoopBehavior == LOOP_DEFAULT && mCurrentLoop == mFrameSequence.getDefaultLoopCount())) { 307 continueLooping = false; 308 } 309 } 310 311 if (continueLooping) { 312 scheduleDecodeLocked(); 313 } else { 314 scheduleSelf(mCallbackRunnable, 0); 315 } 316 } 317 } 318 319 canvas.drawBitmap(mFrontBitmap, mSrcRect, getBounds(), mPaint); 320 } 321 scheduleDecodeLocked()322 private void scheduleDecodeLocked() { 323 mState = STATE_SCHEDULED; 324 mNextFrameToDecode = (mNextFrameToDecode + 1) % mFrameSequence.getFrameCount(); 325 sDecodingThreadHandler.post(mDecodeRunnable); 326 } 327 328 @Override run()329 public void run() { 330 // set ready to swap 331 synchronized (mLock) { 332 if (mState != STATE_WAITING_TO_SWAP || mNextFrameToDecode < 0) return; 333 mState = STATE_READY_TO_SWAP; 334 } 335 invalidateSelf(); 336 } 337 338 @Override start()339 public void start() { 340 if (!isRunning()) { 341 synchronized (mLock) { 342 checkDestroyedLocked(); 343 if (mState == STATE_SCHEDULED) return; // already scheduled 344 mCurrentLoop = 0; 345 scheduleDecodeLocked(); 346 } 347 } 348 } 349 350 @Override stop()351 public void stop() { 352 if (isRunning()) { 353 unscheduleSelf(this); 354 } 355 } 356 357 @Override isRunning()358 public boolean isRunning() { 359 synchronized (mLock) { 360 return mNextFrameToDecode > -1 && !mDestroyed; 361 } 362 } 363 364 @Override unscheduleSelf(Runnable what)365 public void unscheduleSelf(Runnable what) { 366 synchronized (mLock) { 367 mNextFrameToDecode = -1; 368 } 369 super.unscheduleSelf(what); 370 } 371 372 @Override setVisible(boolean visible, boolean restart)373 public boolean setVisible(boolean visible, boolean restart) { 374 boolean changed = super.setVisible(visible, restart); 375 376 if (!visible) { 377 stop(); 378 } else if (restart || changed) { 379 stop(); 380 start(); 381 } 382 383 return changed; 384 } 385 386 // drawing properties 387 388 @Override setFilterBitmap(boolean filter)389 public void setFilterBitmap(boolean filter) { 390 mPaint.setFilterBitmap(filter); 391 } 392 393 @Override setAlpha(int alpha)394 public void setAlpha(int alpha) { 395 mPaint.setAlpha(alpha); 396 } 397 398 @Override setColorFilter(ColorFilter colorFilter)399 public void setColorFilter(ColorFilter colorFilter) { 400 mPaint.setColorFilter(colorFilter); 401 } 402 403 @Override getIntrinsicWidth()404 public int getIntrinsicWidth() { 405 return mFrameSequence.getWidth(); 406 } 407 408 @Override getIntrinsicHeight()409 public int getIntrinsicHeight() { 410 return mFrameSequence.getHeight(); 411 } 412 413 @Override getOpacity()414 public int getOpacity() { 415 return mFrameSequence.isOpaque() ? PixelFormat.OPAQUE : PixelFormat.TRANSPARENT; 416 } 417 } 418