1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 * in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the License 10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 * or implied. See the License for the specific language governing permissions and limitations under 12 * the License. 13 */ 14 package android.support.v17.leanback.app; 15 16 import android.animation.Animator; 17 import android.animation.ValueAnimator; 18 import android.app.Activity; 19 import android.content.Context; 20 import android.content.res.Resources; 21 import android.content.res.TypedArray; 22 import android.graphics.Bitmap; 23 import android.graphics.Canvas; 24 import android.graphics.Color; 25 import android.graphics.ColorFilter; 26 import android.graphics.Matrix; 27 import android.graphics.Paint; 28 import android.graphics.PixelFormat; 29 import android.graphics.drawable.ColorDrawable; 30 import android.graphics.drawable.Drawable; 31 import android.graphics.drawable.LayerDrawable; 32 import android.os.Build; 33 import android.os.Handler; 34 import android.support.annotation.ColorInt; 35 import android.support.annotation.NonNull; 36 import android.support.v17.leanback.R; 37 import android.support.v17.leanback.widget.BackgroundHelper; 38 import android.support.v4.content.ContextCompat; 39 import android.support.v4.graphics.drawable.DrawableCompat; 40 import android.support.v4.view.animation.FastOutLinearInInterpolator; 41 import android.util.Log; 42 import android.view.View; 43 import android.view.Window; 44 import android.view.animation.AnimationUtils; 45 import android.view.animation.Interpolator; 46 47 import java.lang.ref.WeakReference; 48 49 /** 50 * Supports background image continuity between multiple Activities. 51 * 52 * <p>An Activity should instantiate a BackgroundManager and {@link #attach} 53 * to the Activity's window. When the Activity is started, the background is 54 * initialized to the current background values stored in a continuity service. 55 * The background continuity service is updated as the background is updated. 56 * 57 * <p>At some point, for example when it is stopped, the Activity may release 58 * its background state. 59 * 60 * <p>When an Activity is resumed, if the BackgroundManager has not been 61 * released, the continuity service is updated from the BackgroundManager state. 62 * If the BackgroundManager was released, the BackgroundManager inherits the 63 * current state from the continuity service. 64 * 65 * <p>When the last Activity is destroyed, the background state is reset. 66 * 67 * <p>Backgrounds consist of several layers, from back to front: 68 * <ul> 69 * <li>the background Drawable of the theme</li> 70 * <li>a solid color (set via {@link #setColor})</li> 71 * <li>two Drawables, previous and current (set via {@link #setBitmap} or 72 * {@link #setDrawable}), which may be in transition</li> 73 * </ul> 74 * 75 * <p>BackgroundManager holds references to potentially large bitmap Drawables. 76 * Call {@link #release} to release these references when the Activity is not 77 * visible. 78 */ 79 // TODO: support for multiple app processes requires a proper android service 80 // instead of the shared memory "service" implemented here. Such a service could 81 // support continuity between fragments of different applications if desired. 82 public final class BackgroundManager { 83 84 static final String TAG = "BackgroundManager"; 85 static final boolean DEBUG = false; 86 87 static final int FULL_ALPHA = 255; 88 private static final int CHANGE_BG_DELAY_MS = 500; 89 private static final int FADE_DURATION = 500; 90 91 private static final String FRAGMENT_TAG = BackgroundManager.class.getCanonicalName(); 92 93 Activity mContext; 94 Handler mHandler; 95 private View mBgView; 96 private BackgroundContinuityService mService; 97 private int mThemeDrawableResourceId; 98 private BackgroundFragment mFragmentState; 99 private boolean mAutoReleaseOnStop = true; 100 101 private int mHeightPx; 102 private int mWidthPx; 103 int mBackgroundColor; 104 Drawable mBackgroundDrawable; 105 private boolean mAttached; 106 private long mLastSetTime; 107 108 private final Interpolator mAccelerateInterpolator; 109 private final Interpolator mDecelerateInterpolator; 110 final ValueAnimator mAnimator; 111 112 static class BitmapDrawable extends Drawable { 113 114 static final class ConstantState extends Drawable.ConstantState { 115 final Bitmap mBitmap; 116 final Matrix mMatrix; 117 final Paint mPaint = new Paint(); 118 ConstantState(Bitmap bitmap, Matrix matrix)119 ConstantState(Bitmap bitmap, Matrix matrix) { 120 mBitmap = bitmap; 121 mMatrix = matrix != null ? matrix : new Matrix(); 122 mPaint.setFilterBitmap(true); 123 } 124 ConstantState(ConstantState copyFrom)125 ConstantState(ConstantState copyFrom) { 126 mBitmap = copyFrom.mBitmap; 127 mMatrix = copyFrom.mMatrix != null ? new Matrix(copyFrom.mMatrix) : new Matrix(); 128 if (copyFrom.mPaint.getAlpha() != FULL_ALPHA) { 129 mPaint.setAlpha(copyFrom.mPaint.getAlpha()); 130 } 131 if (copyFrom.mPaint.getColorFilter() != null) { 132 mPaint.setColorFilter(copyFrom.mPaint.getColorFilter()); 133 } 134 mPaint.setFilterBitmap(true); 135 } 136 137 @Override newDrawable()138 public Drawable newDrawable() { 139 return new BitmapDrawable(this); 140 } 141 142 @Override getChangingConfigurations()143 public int getChangingConfigurations() { 144 return 0; 145 } 146 } 147 148 ConstantState mState; 149 boolean mMutated; 150 BitmapDrawable(Resources resources, Bitmap bitmap)151 BitmapDrawable(Resources resources, Bitmap bitmap) { 152 this(resources, bitmap, null); 153 } 154 BitmapDrawable(Resources resources, Bitmap bitmap, Matrix matrix)155 BitmapDrawable(Resources resources, Bitmap bitmap, Matrix matrix) { 156 mState = new ConstantState(bitmap, matrix); 157 } 158 BitmapDrawable(ConstantState state)159 BitmapDrawable(ConstantState state) { 160 mState = state; 161 } 162 getBitmap()163 Bitmap getBitmap() { 164 return mState.mBitmap; 165 } 166 167 @Override draw(Canvas canvas)168 public void draw(Canvas canvas) { 169 if (mState.mBitmap == null) { 170 return; 171 } 172 if (mState.mPaint.getAlpha() < FULL_ALPHA && mState.mPaint.getColorFilter() != null) { 173 throw new IllegalStateException("Can't draw with translucent alpha and color filter"); 174 } 175 canvas.drawBitmap(mState.mBitmap, mState.mMatrix, mState.mPaint); 176 } 177 178 @Override getOpacity()179 public int getOpacity() { 180 return android.graphics.PixelFormat.TRANSLUCENT; 181 } 182 183 @Override setAlpha(int alpha)184 public void setAlpha(int alpha) { 185 mutate(); 186 if (mState.mPaint.getAlpha() != alpha) { 187 mState.mPaint.setAlpha(alpha); 188 invalidateSelf(); 189 } 190 } 191 192 /** 193 * Does not invalidateSelf to avoid recursion issues. 194 * Caller must ensure appropriate invalidation. 195 */ 196 @Override setColorFilter(ColorFilter cf)197 public void setColorFilter(ColorFilter cf) { 198 mutate(); 199 mState.mPaint.setColorFilter(cf); 200 invalidateSelf(); 201 } 202 203 @Override getColorFilter()204 public ColorFilter getColorFilter() { 205 return mState.mPaint.getColorFilter(); 206 } 207 208 @Override getConstantState()209 public ConstantState getConstantState() { 210 return mState; 211 } 212 213 @NonNull 214 @Override mutate()215 public Drawable mutate() { 216 if (!mMutated) { 217 mMutated = true; 218 mState = new ConstantState(mState); 219 } 220 return this; 221 } 222 } 223 224 static final class DrawableWrapper { 225 int mAlpha = FULL_ALPHA; 226 final Drawable mDrawable; 227 DrawableWrapper(Drawable drawable)228 public DrawableWrapper(Drawable drawable) { 229 mDrawable = drawable; 230 } DrawableWrapper(DrawableWrapper wrapper, Drawable drawable)231 public DrawableWrapper(DrawableWrapper wrapper, Drawable drawable) { 232 mDrawable = drawable; 233 mAlpha = wrapper.mAlpha; 234 } 235 getDrawable()236 public Drawable getDrawable() { 237 return mDrawable; 238 } 239 setColor(int color)240 public void setColor(int color) { 241 ((ColorDrawable) mDrawable).setColor(color); 242 } 243 } 244 245 static final class TranslucentLayerDrawable extends LayerDrawable { 246 DrawableWrapper[] mWrapper; 247 int mAlpha = FULL_ALPHA; 248 boolean mSuspendInvalidation; 249 WeakReference<BackgroundManager> mManagerWeakReference; 250 TranslucentLayerDrawable(BackgroundManager manager, Drawable[] drawables)251 TranslucentLayerDrawable(BackgroundManager manager, Drawable[] drawables) { 252 super(drawables); 253 mManagerWeakReference = new WeakReference(manager); 254 int count = drawables.length; 255 mWrapper = new DrawableWrapper[count]; 256 for (int i = 0; i < count; i++) { 257 mWrapper[i] = new DrawableWrapper(drawables[i]); 258 } 259 } 260 261 @Override setAlpha(int alpha)262 public void setAlpha(int alpha) { 263 if (mAlpha != alpha) { 264 mAlpha = alpha; 265 invalidateSelf(); 266 BackgroundManager manager = mManagerWeakReference.get(); 267 if (manager != null) { 268 manager.postChangeRunnable(); 269 } 270 } 271 } 272 setWrapperAlpha(int wrapperIndex, int alpha)273 void setWrapperAlpha(int wrapperIndex, int alpha) { 274 if (mWrapper[wrapperIndex] != null) { 275 mWrapper[wrapperIndex].mAlpha = alpha; 276 invalidateSelf(); 277 } 278 } 279 280 // Queried by system transitions 281 @Override getAlpha()282 public int getAlpha() { 283 return mAlpha; 284 } 285 286 @Override mutate()287 public Drawable mutate() { 288 Drawable drawable = super.mutate(); 289 int count = getNumberOfLayers(); 290 for (int i = 0; i < count; i++) { 291 if (mWrapper[i] != null) { 292 mWrapper[i] = new DrawableWrapper(mWrapper[i], getDrawable(i)); 293 } 294 } 295 return drawable; 296 } 297 298 @Override getOpacity()299 public int getOpacity() { 300 return PixelFormat.TRANSLUCENT; 301 } 302 303 @Override setDrawableByLayerId(int id, Drawable drawable)304 public boolean setDrawableByLayerId(int id, Drawable drawable) { 305 return updateDrawable(id, drawable) != null; 306 } 307 updateDrawable(int id, Drawable drawable)308 public DrawableWrapper updateDrawable(int id, Drawable drawable) { 309 super.setDrawableByLayerId(id, drawable); 310 for (int i = 0; i < getNumberOfLayers(); i++) { 311 if (getId(i) == id) { 312 mWrapper[i] = new DrawableWrapper(drawable); 313 // Must come after mWrapper was updated so it can be seen by updateColorFilter 314 invalidateSelf(); 315 return mWrapper[i]; 316 } 317 } 318 return null; 319 } 320 clearDrawable(int id, Context context)321 public void clearDrawable(int id, Context context) { 322 for (int i = 0; i < getNumberOfLayers(); i++) { 323 if (getId(i) == id) { 324 mWrapper[i] = null; 325 if (!(getDrawable(i) instanceof EmptyDrawable)) { 326 super.setDrawableByLayerId(id, createEmptyDrawable(context)); 327 } 328 break; 329 } 330 } 331 } 332 findWrapperIndexById(int id)333 public int findWrapperIndexById(int id) { 334 for (int i = 0; i < getNumberOfLayers(); i++) { 335 if (getId(i) == id) { 336 return i; 337 } 338 } 339 return -1; 340 } 341 342 @Override invalidateDrawable(Drawable who)343 public void invalidateDrawable(Drawable who) { 344 // Prevent invalidate when temporarily change child drawable's alpha in draw() 345 if (!mSuspendInvalidation) { 346 super.invalidateDrawable(who); 347 } 348 } 349 350 @Override draw(Canvas canvas)351 public void draw(Canvas canvas) { 352 for (int i = 0; i < mWrapper.length; i++) { 353 final Drawable d; 354 // For each child drawable, we multiple Wrapper's alpha and LayerDrawable's alpha 355 // temporarily using mSuspendInvalidation to suppress invalidate event. 356 if (mWrapper[i] != null && (d = mWrapper[i].getDrawable()) != null) { 357 int alpha = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT 358 ? DrawableCompat.getAlpha(d) : FULL_ALPHA; 359 final int savedAlpha = alpha; 360 int multiple = 0; 361 if (mAlpha < FULL_ALPHA) { 362 alpha = alpha * mAlpha; 363 multiple++; 364 } 365 if (mWrapper[i].mAlpha < FULL_ALPHA) { 366 alpha = alpha * mWrapper[i].mAlpha; 367 multiple++; 368 } 369 if (multiple == 0) { 370 d.draw(canvas); 371 } else { 372 if (multiple == 1) { 373 alpha = alpha / FULL_ALPHA; 374 } else if (multiple == 2) { 375 alpha = alpha / (FULL_ALPHA * FULL_ALPHA); 376 } 377 try { 378 mSuspendInvalidation = true; 379 d.setAlpha(alpha); 380 d.draw(canvas); 381 d.setAlpha(savedAlpha); 382 } finally { 383 mSuspendInvalidation = false; 384 } 385 } 386 } 387 } 388 } 389 } 390 createTranslucentLayerDrawable( LayerDrawable layerDrawable)391 TranslucentLayerDrawable createTranslucentLayerDrawable( 392 LayerDrawable layerDrawable) { 393 int numChildren = layerDrawable.getNumberOfLayers(); 394 Drawable[] drawables = new Drawable[numChildren]; 395 for (int i = 0; i < numChildren; i++) { 396 drawables[i] = layerDrawable.getDrawable(i); 397 } 398 TranslucentLayerDrawable result = new TranslucentLayerDrawable(this, drawables); 399 for (int i = 0; i < numChildren; i++) { 400 result.setId(i, layerDrawable.getId(i)); 401 } 402 return result; 403 } 404 405 TranslucentLayerDrawable mLayerDrawable; 406 int mImageInWrapperIndex; 407 int mImageOutWrapperIndex; 408 ChangeBackgroundRunnable mChangeRunnable; 409 private boolean mChangeRunnablePending; 410 411 private final Animator.AnimatorListener mAnimationListener = new Animator.AnimatorListener() { 412 final Runnable mRunnable = new Runnable() { 413 @Override 414 public void run() { 415 postChangeRunnable(); 416 } 417 }; 418 419 @Override 420 public void onAnimationStart(Animator animation) { 421 } 422 @Override 423 public void onAnimationRepeat(Animator animation) { 424 } 425 @Override 426 public void onAnimationEnd(Animator animation) { 427 if (mLayerDrawable != null) { 428 mLayerDrawable.clearDrawable(R.id.background_imageout, mContext); 429 } 430 mHandler.post(mRunnable); 431 } 432 @Override 433 public void onAnimationCancel(Animator animation) { 434 } 435 }; 436 437 private final ValueAnimator.AnimatorUpdateListener mAnimationUpdateListener = 438 new ValueAnimator.AnimatorUpdateListener() { 439 @Override 440 public void onAnimationUpdate(ValueAnimator animation) { 441 int fadeInAlpha = (Integer) animation.getAnimatedValue(); 442 if (mImageInWrapperIndex != -1) { 443 mLayerDrawable.setWrapperAlpha(mImageInWrapperIndex, fadeInAlpha); 444 } 445 } 446 }; 447 448 /** 449 * Shared memory continuity service. 450 */ 451 private static class BackgroundContinuityService { 452 private static final String TAG = "BackgroundContinuity"; 453 private static boolean DEBUG = BackgroundManager.DEBUG; 454 455 private static BackgroundContinuityService sService = new BackgroundContinuityService(); 456 457 private int mColor; 458 private Drawable mDrawable; 459 private int mCount; 460 461 /** Single cache of theme drawable */ 462 private int mLastThemeDrawableId; 463 private WeakReference<Drawable.ConstantState> mLastThemeDrawableState; 464 BackgroundContinuityService()465 private BackgroundContinuityService() { 466 reset(); 467 } 468 reset()469 private void reset() { 470 mColor = Color.TRANSPARENT; 471 mDrawable = null; 472 } 473 getInstance()474 public static BackgroundContinuityService getInstance() { 475 final int count = sService.mCount++; 476 if (DEBUG) Log.v(TAG, "Returning instance with new count " + count); 477 return sService; 478 } 479 unref()480 public void unref() { 481 if (mCount <= 0) throw new IllegalStateException("Can't unref, count " + mCount); 482 if (--mCount == 0) { 483 if (DEBUG) Log.v(TAG, "mCount is zero, resetting"); 484 reset(); 485 } 486 } getColor()487 public int getColor() { 488 return mColor; 489 } getDrawable()490 public Drawable getDrawable() { 491 return mDrawable; 492 } setColor(int color)493 public void setColor(int color) { 494 mColor = color; 495 mDrawable = null; 496 } setDrawable(Drawable drawable)497 public void setDrawable(Drawable drawable) { 498 mDrawable = drawable; 499 } getThemeDrawable(Context context, int themeDrawableId)500 public Drawable getThemeDrawable(Context context, int themeDrawableId) { 501 Drawable drawable = null; 502 if (mLastThemeDrawableState != null && mLastThemeDrawableId == themeDrawableId) { 503 Drawable.ConstantState drawableState = mLastThemeDrawableState.get(); 504 if (DEBUG) Log.v(TAG, "got cached theme drawable state " + drawableState); 505 if (drawableState != null) { 506 drawable = drawableState.newDrawable(); 507 } 508 } 509 if (drawable == null) { 510 drawable = ContextCompat.getDrawable(context, themeDrawableId); 511 if (DEBUG) Log.v(TAG, "loaded theme drawable " + drawable); 512 mLastThemeDrawableState = new WeakReference<Drawable.ConstantState>( 513 drawable.getConstantState()); 514 mLastThemeDrawableId = themeDrawableId; 515 } 516 // No mutate required because this drawable is never manipulated. 517 return drawable; 518 } 519 } 520 getDefaultDrawable()521 Drawable getDefaultDrawable() { 522 if (mBackgroundColor != Color.TRANSPARENT) { 523 return new ColorDrawable(mBackgroundColor); 524 } else { 525 return getThemeDrawable(); 526 } 527 } 528 getThemeDrawable()529 private Drawable getThemeDrawable() { 530 Drawable drawable = null; 531 if (mThemeDrawableResourceId != -1) { 532 drawable = mService.getThemeDrawable(mContext, mThemeDrawableResourceId); 533 } 534 if (drawable == null) { 535 drawable = createEmptyDrawable(mContext); 536 } 537 return drawable; 538 } 539 540 /** 541 * Returns the BackgroundManager associated with the given Activity. 542 * <p> 543 * The BackgroundManager will be created on-demand for each individual 544 * Activity. Subsequent calls will return the same BackgroundManager created 545 * for this Activity. 546 */ getInstance(Activity activity)547 public static BackgroundManager getInstance(Activity activity) { 548 BackgroundFragment fragment = (BackgroundFragment) activity.getFragmentManager() 549 .findFragmentByTag(FRAGMENT_TAG); 550 if (fragment != null) { 551 BackgroundManager manager = fragment.getBackgroundManager(); 552 if (manager != null) { 553 return manager; 554 } 555 // manager is null: this is a fragment restored by FragmentManager, 556 // fall through to create a BackgroundManager attach to it. 557 } 558 return new BackgroundManager(activity); 559 } 560 BackgroundManager(Activity activity)561 private BackgroundManager(Activity activity) { 562 mContext = activity; 563 mService = BackgroundContinuityService.getInstance(); 564 mHeightPx = mContext.getResources().getDisplayMetrics().heightPixels; 565 mWidthPx = mContext.getResources().getDisplayMetrics().widthPixels; 566 mHandler = new Handler(); 567 568 Interpolator defaultInterpolator = new FastOutLinearInInterpolator(); 569 mAccelerateInterpolator = AnimationUtils.loadInterpolator(mContext, 570 android.R.anim.accelerate_interpolator); 571 mDecelerateInterpolator = AnimationUtils.loadInterpolator(mContext, 572 android.R.anim.decelerate_interpolator); 573 574 mAnimator = ValueAnimator.ofInt(0, FULL_ALPHA); 575 mAnimator.addListener(mAnimationListener); 576 mAnimator.addUpdateListener(mAnimationUpdateListener); 577 mAnimator.setInterpolator(defaultInterpolator); 578 579 TypedArray ta = activity.getTheme().obtainStyledAttributes(new int[] { 580 android.R.attr.windowBackground }); 581 mThemeDrawableResourceId = ta.getResourceId(0, -1); 582 if (mThemeDrawableResourceId < 0) { 583 if (DEBUG) Log.v(TAG, "BackgroundManager no window background resource!"); 584 } 585 ta.recycle(); 586 587 createFragment(activity); 588 } 589 createFragment(Activity activity)590 private void createFragment(Activity activity) { 591 // Use a fragment to ensure the background manager gets detached properly. 592 BackgroundFragment fragment = (BackgroundFragment) activity.getFragmentManager() 593 .findFragmentByTag(FRAGMENT_TAG); 594 if (fragment == null) { 595 fragment = new BackgroundFragment(); 596 activity.getFragmentManager().beginTransaction().add(fragment, FRAGMENT_TAG).commit(); 597 } else { 598 if (fragment.getBackgroundManager() != null) { 599 throw new IllegalStateException("Created duplicated BackgroundManager for same " 600 + "activity, please use getInstance() instead"); 601 } 602 } 603 fragment.setBackgroundManager(this); 604 mFragmentState = fragment; 605 } 606 getImageInWrapper()607 DrawableWrapper getImageInWrapper() { 608 return mLayerDrawable == null 609 ? null : mLayerDrawable.mWrapper[mImageInWrapperIndex]; 610 } 611 getImageOutWrapper()612 DrawableWrapper getImageOutWrapper() { 613 return mLayerDrawable == null 614 ? null : mLayerDrawable.mWrapper[mImageOutWrapperIndex]; 615 } 616 617 /** 618 * Synchronizes state when the owning Activity is started. 619 * At that point the view becomes visible. 620 */ onActivityStart()621 void onActivityStart() { 622 updateImmediate(); 623 } 624 onStop()625 void onStop() { 626 if (isAutoReleaseOnStop()) { 627 release(); 628 } 629 } 630 onResume()631 void onResume() { 632 if (DEBUG) Log.v(TAG, "onResume " + this); 633 postChangeRunnable(); 634 } 635 syncWithService()636 private void syncWithService() { 637 int color = mService.getColor(); 638 Drawable drawable = mService.getDrawable(); 639 640 if (DEBUG) Log.v(TAG, "syncWithService color " + Integer.toHexString(color) 641 + " drawable " + drawable); 642 643 mBackgroundColor = color; 644 mBackgroundDrawable = drawable == null ? null : 645 drawable.getConstantState().newDrawable().mutate(); 646 647 updateImmediate(); 648 } 649 650 /** 651 * Makes the background visible on the given Window. The background manager must be attached 652 * when the background is set. 653 */ attach(Window window)654 public void attach(Window window) { 655 attachToViewInternal(window.getDecorView()); 656 } 657 658 /** 659 * Sets the resource id for the drawable to be shown when there is no background set. 660 * Overrides the window background drawable from the theme. This should 661 * be called before attaching. 662 */ setThemeDrawableResourceId(int resourceId)663 public void setThemeDrawableResourceId(int resourceId) { 664 mThemeDrawableResourceId = resourceId; 665 } 666 667 /** 668 * Adds the composite drawable to the given view. 669 */ attachToView(View sceneRoot)670 public void attachToView(View sceneRoot) { 671 attachToViewInternal(sceneRoot); 672 // clear background to reduce overdraw since the View will act as background. 673 // Activity transition below O has ghost effect for null window background where we 674 // need set a transparent background to force redraw the whole window. 675 mContext.getWindow().getDecorView().setBackground( 676 Build.VERSION.SDK_INT >= 26 ? null : new ColorDrawable(Color.TRANSPARENT)); 677 } 678 attachToViewInternal(View sceneRoot)679 void attachToViewInternal(View sceneRoot) { 680 if (mAttached) { 681 throw new IllegalStateException("Already attached to " + mBgView); 682 } 683 mBgView = sceneRoot; 684 mAttached = true; 685 syncWithService(); 686 } 687 688 /** 689 * Returns true if the background manager is currently attached; false otherwise. 690 */ isAttached()691 public boolean isAttached() { 692 return mAttached; 693 } 694 695 /** 696 * Release references to Drawables and put the BackgroundManager into the 697 * detached state. Called when the associated Activity is destroyed. 698 */ detach()699 void detach() { 700 if (DEBUG) Log.v(TAG, "detach " + this); 701 release(); 702 703 mBgView = null; 704 mAttached = false; 705 706 if (mService != null) { 707 mService.unref(); 708 mService = null; 709 } 710 } 711 712 /** 713 * Release references to Drawable/Bitmap. Typically called in Activity onStop() to reduce memory 714 * overhead when not visible. It's app's responsibility to restore the drawable/bitmap in 715 * Activity onStart(). The method is automatically called in onStop() when 716 * {@link #isAutoReleaseOnStop()} is true. 717 * @see #setAutoReleaseOnStop(boolean) 718 */ release()719 public void release() { 720 if (DEBUG) Log.v(TAG, "release " + this); 721 if (mChangeRunnable != null) { 722 mHandler.removeCallbacks(mChangeRunnable); 723 mChangeRunnable = null; 724 } 725 if (mAnimator.isStarted()) { 726 mAnimator.cancel(); 727 } 728 if (mLayerDrawable != null) { 729 mLayerDrawable.clearDrawable(R.id.background_imagein, mContext); 730 mLayerDrawable.clearDrawable(R.id.background_imageout, mContext); 731 mLayerDrawable = null; 732 } 733 mBackgroundDrawable = null; 734 } 735 736 /** 737 * Sets the drawable used as a dim layer. 738 * @deprecated No longer support dim layer. 739 */ 740 @Deprecated setDimLayer(Drawable drawable)741 public void setDimLayer(Drawable drawable) { 742 } 743 744 /** 745 * Returns the drawable used as a dim layer. 746 * @deprecated No longer support dim layer. 747 */ 748 @Deprecated getDimLayer()749 public Drawable getDimLayer() { 750 return null; 751 } 752 753 /** 754 * Returns the default drawable used as a dim layer. 755 * @deprecated No longer support dim layer. 756 */ 757 @Deprecated getDefaultDimLayer()758 public Drawable getDefaultDimLayer() { 759 return ContextCompat.getDrawable(mContext, R.color.lb_background_protection); 760 } 761 postChangeRunnable()762 void postChangeRunnable() { 763 if (mChangeRunnable == null || !mChangeRunnablePending) { 764 return; 765 } 766 767 // Postpone a pending change runnable until: no existing change animation in progress && 768 // activity is resumed (in the foreground) && layerdrawable fully opaque. 769 // If the layerdrawable is translucent then an activity transition is in progress 770 // and we want to use the optimized drawing path for performance reasons (see 771 // OptimizedTranslucentLayerDrawable). 772 if (mAnimator.isStarted()) { 773 if (DEBUG) Log.v(TAG, "animation in progress"); 774 } else if (!mFragmentState.isResumed()) { 775 if (DEBUG) Log.v(TAG, "not resumed"); 776 } else if (mLayerDrawable.getAlpha() < FULL_ALPHA) { 777 if (DEBUG) Log.v(TAG, "in transition, alpha " + mLayerDrawable.getAlpha()); 778 } else { 779 long delayMs = getRunnableDelay(); 780 if (DEBUG) Log.v(TAG, "posting runnable delayMs " + delayMs); 781 mLastSetTime = System.currentTimeMillis(); 782 mHandler.postDelayed(mChangeRunnable, delayMs); 783 mChangeRunnablePending = false; 784 } 785 } 786 lazyInit()787 private void lazyInit() { 788 if (mLayerDrawable != null) { 789 return; 790 } 791 792 LayerDrawable layerDrawable = (LayerDrawable) 793 ContextCompat.getDrawable(mContext, R.drawable.lb_background).mutate(); 794 mLayerDrawable = createTranslucentLayerDrawable(layerDrawable); 795 mImageInWrapperIndex = mLayerDrawable.findWrapperIndexById(R.id.background_imagein); 796 mImageOutWrapperIndex = mLayerDrawable.findWrapperIndexById(R.id.background_imageout); 797 BackgroundHelper.setBackgroundPreservingAlpha(mBgView, mLayerDrawable); 798 } 799 updateImmediate()800 private void updateImmediate() { 801 if (!mAttached) { 802 return; 803 } 804 lazyInit(); 805 806 if (mBackgroundDrawable == null) { 807 if (DEBUG) Log.v(TAG, "Use defefault background"); 808 mLayerDrawable.updateDrawable(R.id.background_imagein, getDefaultDrawable()); 809 } else { 810 if (DEBUG) Log.v(TAG, "Background drawable is available " + mBackgroundDrawable); 811 mLayerDrawable.updateDrawable(R.id.background_imagein, mBackgroundDrawable); 812 } 813 mLayerDrawable.clearDrawable(R.id.background_imageout, mContext); 814 } 815 816 /** 817 * Sets the background to the given color. The timing for when this becomes 818 * visible in the app is undefined and may take place after a small delay. 819 */ setColor(@olorInt int color)820 public void setColor(@ColorInt int color) { 821 if (DEBUG) Log.v(TAG, "setColor " + Integer.toHexString(color)); 822 823 mService.setColor(color); 824 mBackgroundColor = color; 825 mBackgroundDrawable = null; 826 if (mLayerDrawable == null) { 827 return; 828 } 829 setDrawableInternal(getDefaultDrawable()); 830 } 831 832 /** 833 * Sets the given drawable into the background. The provided Drawable will be 834 * used unmodified as the background, without any scaling or cropping 835 * applied to it. The timing for when this becomes visible in the app is 836 * undefined and may take place after a small delay. 837 */ setDrawable(Drawable drawable)838 public void setDrawable(Drawable drawable) { 839 if (DEBUG) Log.v(TAG, "setBackgroundDrawable " + drawable); 840 841 mService.setDrawable(drawable); 842 mBackgroundDrawable = drawable; 843 if (mLayerDrawable == null) { 844 return; 845 } 846 if (drawable == null) { 847 setDrawableInternal(getDefaultDrawable()); 848 } else { 849 setDrawableInternal(drawable); 850 } 851 } 852 853 /** 854 * Clears the Drawable set by {@link #setDrawable(Drawable)} or {@link #setBitmap(Bitmap)}. 855 * BackgroundManager will show a solid color set by {@link #setColor(int)} or theme drawable 856 * if color is not provided. 857 */ clearDrawable()858 public void clearDrawable() { 859 setDrawable(null); 860 } 861 setDrawableInternal(Drawable drawable)862 private void setDrawableInternal(Drawable drawable) { 863 if (!mAttached) { 864 throw new IllegalStateException("Must attach before setting background drawable"); 865 } 866 867 if (mChangeRunnable != null) { 868 if (sameDrawable(drawable, mChangeRunnable.mDrawable)) { 869 if (DEBUG) Log.v(TAG, "new drawable same as pending"); 870 return; 871 } 872 mHandler.removeCallbacks(mChangeRunnable); 873 mChangeRunnable = null; 874 } 875 876 mChangeRunnable = new ChangeBackgroundRunnable(drawable); 877 mChangeRunnablePending = true; 878 879 postChangeRunnable(); 880 } 881 getRunnableDelay()882 private long getRunnableDelay() { 883 return Math.max(0, mLastSetTime + CHANGE_BG_DELAY_MS - System.currentTimeMillis()); 884 } 885 886 /** 887 * Sets the given bitmap into the background. When using setCoverImageBitmap to set the 888 * background, the provided bitmap will be scaled and cropped to correctly 889 * fit within the dimensions of the view. The timing for when this becomes 890 * visible in the app is undefined and may take place after a small delay. 891 */ setBitmap(Bitmap bitmap)892 public void setBitmap(Bitmap bitmap) { 893 if (DEBUG) { 894 Log.v(TAG, "setCoverImageBitmap " + bitmap); 895 } 896 897 if (bitmap == null) { 898 setDrawable(null); 899 return; 900 } 901 902 if (bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) { 903 if (DEBUG) { 904 Log.v(TAG, "invalid bitmap width or height"); 905 } 906 return; 907 } 908 909 Matrix matrix = null; 910 911 if ((bitmap.getWidth() != mWidthPx || bitmap.getHeight() != mHeightPx)) { 912 int dwidth = bitmap.getWidth(); 913 int dheight = bitmap.getHeight(); 914 float scale; 915 916 // Scale proportionately to fit width and height. 917 if (dwidth * mHeightPx > mWidthPx * dheight) { 918 scale = (float) mHeightPx / (float) dheight; 919 } else { 920 scale = (float) mWidthPx / (float) dwidth; 921 } 922 923 int subX = Math.min((int) (mWidthPx / scale), dwidth); 924 int dx = Math.max(0, (dwidth - subX) / 2); 925 926 matrix = new Matrix(); 927 matrix.setScale(scale, scale); 928 matrix.preTranslate(-dx, 0); 929 930 if (DEBUG) { 931 Log.v(TAG, "original image size " + bitmap.getWidth() + "x" + bitmap.getHeight() 932 + " scale " + scale + " dx " + dx); 933 } 934 } 935 936 BitmapDrawable bitmapDrawable = new BitmapDrawable(mContext.getResources(), bitmap, matrix); 937 938 setDrawable(bitmapDrawable); 939 } 940 941 /** 942 * Enable or disable call release() in Activity onStop(). Default is true. 943 * @param autoReleaseOnStop True to call release() in Activity onStop(), false otherwise. 944 */ setAutoReleaseOnStop(boolean autoReleaseOnStop)945 public void setAutoReleaseOnStop(boolean autoReleaseOnStop) { 946 mAutoReleaseOnStop = autoReleaseOnStop; 947 } 948 949 /** 950 * @return True if release() in Activity.onStop(), false otherwise. 951 */ isAutoReleaseOnStop()952 public boolean isAutoReleaseOnStop() { 953 return mAutoReleaseOnStop; 954 } 955 956 /** 957 * Returns the current background color. 958 */ 959 @ColorInt getColor()960 public final int getColor() { 961 return mBackgroundColor; 962 } 963 964 /** 965 * Returns the current background {@link Drawable}. 966 */ getDrawable()967 public Drawable getDrawable() { 968 return mBackgroundDrawable; 969 } 970 sameDrawable(Drawable first, Drawable second)971 boolean sameDrawable(Drawable first, Drawable second) { 972 if (first == null || second == null) { 973 return false; 974 } 975 if (first == second) { 976 return true; 977 } 978 if (first instanceof BitmapDrawable && second instanceof BitmapDrawable) { 979 if (((BitmapDrawable) first).getBitmap().sameAs(((BitmapDrawable) second).getBitmap())) { 980 return true; 981 } 982 } 983 if (first instanceof ColorDrawable && second instanceof ColorDrawable) { 984 if (((ColorDrawable) first).getColor() == ((ColorDrawable) second).getColor()) { 985 return true; 986 } 987 } 988 return false; 989 } 990 991 /** 992 * Task which changes the background. 993 */ 994 final class ChangeBackgroundRunnable implements Runnable { 995 final Drawable mDrawable; 996 ChangeBackgroundRunnable(Drawable drawable)997 ChangeBackgroundRunnable(Drawable drawable) { 998 mDrawable = drawable; 999 } 1000 1001 @Override run()1002 public void run() { 1003 runTask(); 1004 mChangeRunnable = null; 1005 } 1006 runTask()1007 private void runTask() { 1008 if (mLayerDrawable == null) { 1009 if (DEBUG) Log.v(TAG, "runTask while released - should not happen"); 1010 return; 1011 } 1012 1013 DrawableWrapper imageInWrapper = getImageInWrapper(); 1014 if (imageInWrapper != null) { 1015 if (sameDrawable(mDrawable, imageInWrapper.getDrawable())) { 1016 if (DEBUG) Log.v(TAG, "new drawable same as current"); 1017 return; 1018 } 1019 1020 if (DEBUG) Log.v(TAG, "moving image in to image out"); 1021 // Order is important! Setting a drawable "removes" the 1022 // previous one from the view 1023 mLayerDrawable.clearDrawable(R.id.background_imagein, mContext); 1024 mLayerDrawable.updateDrawable(R.id.background_imageout, 1025 imageInWrapper.getDrawable()); 1026 } 1027 1028 applyBackgroundChanges(); 1029 } 1030 applyBackgroundChanges()1031 void applyBackgroundChanges() { 1032 if (!mAttached) { 1033 return; 1034 } 1035 1036 if (DEBUG) Log.v(TAG, "applyBackgroundChanges drawable " + mDrawable); 1037 1038 DrawableWrapper imageInWrapper = getImageInWrapper(); 1039 if (imageInWrapper == null && mDrawable != null) { 1040 if (DEBUG) Log.v(TAG, "creating new imagein drawable"); 1041 imageInWrapper = mLayerDrawable.updateDrawable( 1042 R.id.background_imagein, mDrawable); 1043 if (DEBUG) Log.v(TAG, "imageInWrapper animation starting"); 1044 mLayerDrawable.setWrapperAlpha(mImageInWrapperIndex, 0); 1045 } 1046 1047 mAnimator.setDuration(FADE_DURATION); 1048 mAnimator.start(); 1049 1050 } 1051 1052 } 1053 1054 static class EmptyDrawable extends BitmapDrawable { EmptyDrawable(Resources res)1055 EmptyDrawable(Resources res) { 1056 super(res, (Bitmap) null); 1057 } 1058 } 1059 createEmptyDrawable(Context context)1060 static Drawable createEmptyDrawable(Context context) { 1061 return new EmptyDrawable(context.getResources()); 1062 } 1063 1064 } 1065