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