BackgroundManager.java revision 7f0ed3452a3378c851217cd300950ba2e03f2649
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 */ 14package android.support.v17.leanback.app; 15 16import android.support.v17.leanback.R; 17import android.animation.ObjectAnimator; 18import android.app.Activity; 19import android.content.Context; 20import android.content.res.TypedArray; 21import android.graphics.Bitmap; 22import android.graphics.Color; 23import android.graphics.Matrix; 24import android.graphics.drawable.BitmapDrawable; 25import android.graphics.drawable.ColorDrawable; 26import android.graphics.drawable.Drawable; 27import android.graphics.drawable.LayerDrawable; 28import android.os.Handler; 29import android.util.Log; 30import android.view.Gravity; 31import android.view.LayoutInflater; 32import android.view.View; 33import android.view.ViewGroup; 34import android.view.Window; 35import android.view.WindowManager; 36import android.view.animation.LinearInterpolator; 37 38/** 39 * Supports background continuity between multiple activities. 40 * 41 * An activity should instantiate a BackgroundManager and {@link #attach} 42 * to the activity's window. When the activity is started, the background is 43 * initialized to the current background values stored in a continuity service. 44 * The background continuity service is updated as the background is updated. 45 * 46 * At some point, for example when stopped, the activity may release its background 47 * state. 48 * 49 * When an activity is resumed, if the BM has not been released, the continuity service 50 * is updated from the BM state. If the BM was released, the BM inherits the current 51 * state from the continuity service. 52 * 53 * When the last activity is destroyed, the background state is reset. 54 * 55 * Backgrounds consist of several layers, from back to front: 56 * - the background drawable of the theme 57 * - a solid color (set via setColor) 58 * - two drawables, previous and current (set via setBitmap or setDrawable), 59 * which may be in transition 60 * 61 * BackgroundManager holds references to potentially large bitmap drawables. 62 * Call {@link #release} to release these references when the activity is not 63 * visible. 64 * 65 * TODO: support for multiple app processes requires a proper android service 66 * instead of the shared memory "service" implemented here. Such a service could 67 * support continuity between fragments of different applications if desired. 68 */ 69public final class BackgroundManager { 70 private static final String TAG = "BackgroundManager"; 71 private static final boolean DEBUG = false; 72 73 private static final int FULL_ALPHA = 255; 74 private static final int DIM_ALPHA_ON_SOLID = (int) (0.8f * FULL_ALPHA); 75 private static final int CHANGE_BG_DELAY_MS = 500; 76 private static final int FADE_DURATION_QUICK = 200; 77 private static final int FADE_DURATION_SLOW = 1000; 78 79 /** 80 * Using a separate window for backgrounds can improve graphics performance by 81 * leveraging hardware display layers. 82 * TODO: support a leanback configuration option. 83 */ 84 private static final boolean USE_SEPARATE_WINDOW = false; 85 86 /** 87 * If true, bitmaps will be scaled to the exact display size. 88 * Small bitmaps will be scaled up, using more memory but improving display quality. 89 * Large bitmaps will be scaled down to use less memory. 90 * Introduces an allocation overhead. 91 * TODO: support a leanback configuration option. 92 */ 93 private static final boolean SCALE_BITMAPS_TO_FIT = true; 94 95 private static final String WINDOW_NAME = "BackgroundManager"; 96 private static final String FRAGMENT_TAG = BackgroundManager.class.getCanonicalName(); 97 98 private Context mContext; 99 private Handler mHandler; 100 private Window mWindow; 101 private WindowManager mWindowManager; 102 private View mBgView; 103 private BackgroundContinuityService mService; 104 private int mThemeDrawableResourceId; 105 106 private int mHeightPx; 107 private int mWidthPx; 108 private Drawable mBackgroundDrawable; 109 private int mBackgroundColor; 110 private boolean mAttached; 111 112 private class DrawableWrapper { 113 protected int mAlpha; 114 protected Drawable mDrawable; 115 protected ObjectAnimator mAnimator; 116 protected boolean mAnimationPending; 117 118 public DrawableWrapper(Drawable drawable) { 119 mDrawable = drawable; 120 setAlpha(FULL_ALPHA); 121 } 122 123 public Drawable getDrawable() { 124 return mDrawable; 125 } 126 public void setAlpha(int alpha) { 127 mAlpha = alpha; 128 mDrawable.setAlpha(alpha); 129 } 130 public int getAlpha() { 131 return mAlpha; 132 } 133 public void setColor(int color) { 134 ((ColorDrawable) mDrawable).setColor(color); 135 } 136 public void fadeIn(int durationMs, int delayMs) { 137 fade(durationMs, delayMs, FULL_ALPHA); 138 } 139 public void fadeOut(int durationMs) { 140 fade(durationMs, 0, 0); 141 } 142 public void fade(int durationMs, int delayMs, int alpha) { 143 if (mAnimator != null && mAnimator.isStarted()) { 144 mAnimator.cancel(); 145 } 146 mAnimator = ObjectAnimator.ofInt(this, "alpha", alpha); 147 mAnimator.setInterpolator(new LinearInterpolator()); 148 mAnimator.setDuration(durationMs); 149 mAnimator.setStartDelay(delayMs); 150 mAnimationPending = true; 151 } 152 public boolean isAnimationPending() { 153 return mAnimationPending; 154 } 155 public boolean isAnimationStarted() { 156 return mAnimator != null && mAnimator.isStarted(); 157 } 158 public void startAnimation() { 159 mAnimator.start(); 160 mAnimationPending = false; 161 } 162 } 163 164 private LayerDrawable mLayerDrawable; 165 private DrawableWrapper mLayerWrapper; 166 private DrawableWrapper mImageInWrapper; 167 private DrawableWrapper mImageOutWrapper; 168 private DrawableWrapper mColorWrapper; 169 private DrawableWrapper mDimWrapper; 170 171 private Drawable mThemeDrawable; 172 private ChangeBackgroundRunnable mChangeRunnable; 173 174 /** 175 * Shared memory continuity service. 176 */ 177 private static class BackgroundContinuityService { 178 private static final String TAG = "BackgroundContinuityService"; 179 private static boolean DEBUG = BackgroundManager.DEBUG; 180 181 private static BackgroundContinuityService sService = new BackgroundContinuityService(); 182 183 private int mColor; 184 private Drawable mDrawable; 185 private int mCount; 186 187 private BackgroundContinuityService() { 188 reset(); 189 } 190 191 private void reset() { 192 mColor = Color.TRANSPARENT; 193 mDrawable = null; 194 } 195 196 public static BackgroundContinuityService getInstance() { 197 final int count = sService.mCount++; 198 if (DEBUG) Log.v(TAG, "Returning instance with new count " + count); 199 return sService; 200 } 201 202 public void unref() { 203 if (mCount <= 0) throw new IllegalStateException("Can't unref, count " + mCount); 204 if (--mCount == 0) { 205 if (DEBUG) Log.v(TAG, "mCount is zero, resetting"); 206 reset(); 207 } 208 } 209 public int getColor() { 210 return mColor; 211 } 212 public Drawable getDrawable() { 213 return mDrawable; 214 } 215 public void setColor(int color) { 216 mColor = color; 217 } 218 public void setDrawable(Drawable drawable) { 219 mDrawable = drawable; 220 } 221 } 222 223 private Drawable getThemeDrawable() { 224 Drawable drawable = null; 225 if (mThemeDrawableResourceId != -1) { 226 drawable = mContext.getResources().getDrawable(mThemeDrawableResourceId); 227 } 228 if (drawable == null) { 229 drawable = createEmptyDrawable(); 230 } 231 return drawable; 232 } 233 234 /** 235 * Get background manager associated with the activity. 236 * <p> 237 * The background manager will be created on-demand for each individual 238 * activity. Consequent calls will return the same background manager created 239 * for this activity. 240 * </p> 241 */ 242 public static BackgroundManager getInstance(Activity activity) { 243 BackgroundFragment fragment = (BackgroundFragment) activity.getFragmentManager() 244 .findFragmentByTag(FRAGMENT_TAG); 245 if (fragment != null) { 246 BackgroundManager manager = fragment.getBackgroundManager(); 247 if (manager != null) { 248 return manager; 249 } 250 // manager is null: this is a fragment restored by FragmentManager, 251 // fall through to create a BackgroundManager attach to it. 252 } 253 return new BackgroundManager(activity); 254 } 255 256 /** 257 * Construct a background manager instance. 258 * Initial background set from continuity service. 259 * @deprecated Use getInstance(Activity) 260 */ 261 public BackgroundManager(Activity activity) { 262 mContext = activity; 263 mService = BackgroundContinuityService.getInstance(); 264 mHeightPx = mContext.getResources().getDisplayMetrics().heightPixels; 265 mWidthPx = mContext.getResources().getDisplayMetrics().widthPixels; 266 mHandler = new Handler(); 267 268 TypedArray ta = activity.getTheme().obtainStyledAttributes(new int[] { 269 android.R.attr.windowBackground }); 270 mThemeDrawableResourceId = ta.getResourceId(0, -1); 271 if (mThemeDrawableResourceId < 0) { 272 if (DEBUG) Log.v(TAG, "BackgroundManager no window background resource!"); 273 } 274 ta.recycle(); 275 276 createFragment(activity); 277 } 278 279 private void createFragment(Activity activity) { 280 // Use a fragment to ensure the background manager gets detached properly. 281 BackgroundFragment fragment = (BackgroundFragment) activity.getFragmentManager() 282 .findFragmentByTag(FRAGMENT_TAG); 283 if (fragment == null) { 284 fragment = new BackgroundFragment(); 285 activity.getFragmentManager().beginTransaction().add(fragment, FRAGMENT_TAG).commit(); 286 } else { 287 if (fragment.getBackgroundManager() != null) { 288 throw new IllegalStateException("Created duplicated BackgroundManager for same " + 289 "activity, please use getInstance() instead"); 290 } 291 } 292 fragment.setBackgroundManager(this); 293 } 294 295 /** 296 * Synchronizes state when the owning activity is resumed. 297 */ 298 void onActivityResume() { 299 if (mService == null) { 300 return; 301 } 302 if (mLayerDrawable == null) { 303 if (DEBUG) Log.v(TAG, "onActivityResume: released state, syncing with service"); 304 syncWithService(); 305 } else { 306 if (DEBUG) Log.v(TAG, "onActivityResume: updating service color " 307 + mBackgroundColor + " drawable " + mBackgroundDrawable); 308 mService.setColor(mBackgroundColor); 309 mService.setDrawable(mBackgroundDrawable); 310 } 311 } 312 313 private void syncWithService() { 314 int color = mService.getColor(); 315 Drawable drawable = mService.getDrawable(); 316 317 if (DEBUG) Log.v(TAG, "syncWithService color " + Integer.toHexString(color) 318 + " drawable " + drawable); 319 320 if (drawable != null) { 321 drawable = drawable.getConstantState().newDrawable(mContext.getResources()).mutate(); 322 } 323 324 mBackgroundColor = color; 325 mBackgroundDrawable = drawable; 326 327 updateImmediate(); 328 } 329 330 private void lazyInit() { 331 if (mLayerDrawable != null) { 332 return; 333 } 334 335 mLayerDrawable = (LayerDrawable) mContext.getResources().getDrawable( 336 R.drawable.lb_background); 337 mBgView.setBackground(mLayerDrawable); 338 339 mLayerDrawable.setDrawableByLayerId(R.id.background_imageout, createEmptyDrawable()); 340 341 mDimWrapper = new DrawableWrapper( 342 mLayerDrawable.findDrawableByLayerId(R.id.background_dim)); 343 344 mLayerWrapper = new DrawableWrapper(mLayerDrawable); 345 346 mColorWrapper = new DrawableWrapper( 347 mLayerDrawable.findDrawableByLayerId(R.id.background_color)); 348 } 349 350 /** 351 * Make the background visible on the given window. 352 */ 353 public void attach(Window window) { 354 if (USE_SEPARATE_WINDOW) { 355 attachBehindWindow(window); 356 } else { 357 attachToView(window.getDecorView()); 358 } 359 } 360 361 private void attachBehindWindow(Window window) { 362 if (DEBUG) Log.v(TAG, "attachBehindWindow " + window); 363 mWindow = window; 364 mWindowManager = window.getWindowManager(); 365 366 WindowManager.LayoutParams params = new WindowManager.LayoutParams( 367 // Media window sits behind the main application window 368 WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA, 369 // Avoid default to software format RGBA 370 WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, 371 android.graphics.PixelFormat.TRANSLUCENT); 372 params.setTitle(WINDOW_NAME); 373 params.width = ViewGroup.LayoutParams.MATCH_PARENT; 374 params.height = ViewGroup.LayoutParams.MATCH_PARENT; 375 376 View backgroundView = LayoutInflater.from(mContext).inflate( 377 R.layout.lb_background_window, null); 378 mWindowManager.addView(backgroundView, params); 379 380 attachToView(backgroundView); 381 } 382 383 private void attachToView(View sceneRoot) { 384 mBgView = sceneRoot; 385 mAttached = true; 386 syncWithService(); 387 } 388 389 /** 390 * Releases references to drawables and puts the background manager into 391 * detached state. 392 * Called when the associated activity is destroyed. 393 * @hide 394 */ 395 void detach() { 396 if (DEBUG) Log.v(TAG, "detach"); 397 release(); 398 399 if (mWindowManager != null && mBgView != null) { 400 mWindowManager.removeViewImmediate(mBgView); 401 } 402 403 mWindowManager = null; 404 mWindow = null; 405 mBgView = null; 406 mAttached = false; 407 408 if (mService != null) { 409 mService.unref(); 410 mService = null; 411 } 412 } 413 414 /** 415 * Releases references to drawables. 416 * Typically called to reduce memory overhead when not visible. 417 * <p> 418 * When an activity is resumed, if the BM has not been released, the continuity service 419 * is updated from the BM state. If the BM was released, the BM inherits the current 420 * state from the continuity service. 421 * </p> 422 */ 423 public void release() { 424 if (DEBUG) Log.v(TAG, "release"); 425 if (mLayerDrawable != null) { 426 mLayerDrawable.setDrawableByLayerId(R.id.background_imagein, createEmptyDrawable()); 427 mLayerDrawable.setDrawableByLayerId(R.id.background_imageout, createEmptyDrawable()); 428 mLayerDrawable = null; 429 } 430 mLayerWrapper = null; 431 mImageInWrapper = null; 432 mImageOutWrapper = null; 433 mColorWrapper = null; 434 mDimWrapper = null; 435 mThemeDrawable = null; 436 if (mChangeRunnable != null) { 437 mChangeRunnable.cancel(); 438 mChangeRunnable = null; 439 } 440 releaseBackgroundBitmap(); 441 } 442 443 private void releaseBackgroundBitmap() { 444 mBackgroundDrawable = null; 445 } 446 447 private void updateImmediate() { 448 lazyInit(); 449 450 mColorWrapper.setColor(mBackgroundColor); 451 if (mDimWrapper != null) { 452 mDimWrapper.setAlpha(mBackgroundColor == Color.TRANSPARENT ? 0 : DIM_ALPHA_ON_SOLID); 453 } 454 showWallpaper(mBackgroundColor == Color.TRANSPARENT); 455 456 mThemeDrawable = getThemeDrawable(); 457 mLayerDrawable.setDrawableByLayerId(R.id.background_theme, mThemeDrawable); 458 459 if (mBackgroundDrawable == null) { 460 mLayerDrawable.setDrawableByLayerId(R.id.background_imagein, createEmptyDrawable()); 461 } else { 462 if (DEBUG) Log.v(TAG, "Background drawable is available"); 463 mImageInWrapper = new DrawableWrapper(mBackgroundDrawable); 464 mLayerDrawable.setDrawableByLayerId(R.id.background_imagein, mBackgroundDrawable); 465 if (mDimWrapper != null) { 466 mDimWrapper.setAlpha(FULL_ALPHA); 467 } 468 } 469 } 470 471 /** 472 * Sets the given color into the background. 473 * Timing is undefined. 474 */ 475 public void setColor(int color) { 476 if (DEBUG) Log.v(TAG, "setColor " + Integer.toHexString(color)); 477 478 mBackgroundColor = color; 479 mService.setColor(mBackgroundColor); 480 481 if (mColorWrapper != null) { 482 mColorWrapper.setColor(mBackgroundColor); 483 } 484 } 485 486 /** 487 * Set the given drawable into the background. 488 * Timing is undefined. 489 */ 490 public void setDrawable(Drawable drawable) { 491 if (DEBUG) Log.v(TAG, "setBackgroundDrawable " + drawable); 492 setDrawableInternal(drawable); 493 } 494 495 private void setDrawableInternal(Drawable drawable) { 496 if (!mAttached) throw new IllegalStateException("Must attach before setting background drawable"); 497 498 if (mChangeRunnable != null) { 499 mChangeRunnable.cancel(); 500 } 501 mChangeRunnable = new ChangeBackgroundRunnable(drawable); 502 503 mHandler.postDelayed(mChangeRunnable, CHANGE_BG_DELAY_MS); 504 } 505 506 /** 507 * Set the given bitmap into the background. 508 * Timing is undefined. 509 */ 510 public void setBitmap(Bitmap bitmap) { 511 if (DEBUG) Log.v(TAG, "setBitmap " + bitmap); 512 513 if (bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) { 514 if (DEBUG) Log.v(TAG, "invalid bitmap width or height"); 515 return; 516 } 517 518 if (mBackgroundDrawable instanceof BitmapDrawable && 519 ((BitmapDrawable) mBackgroundDrawable).getBitmap() == bitmap) { 520 if (DEBUG) Log.v(TAG, "same bitmap detected"); 521 mService.setDrawable(mBackgroundDrawable); 522 return; 523 } 524 525 if (SCALE_BITMAPS_TO_FIT && bitmap.getWidth() != mWidthPx) { 526 // Scale proportionately to fit width. 527 final float scale = (float) mWidthPx / (float) bitmap.getWidth(); 528 int height = (int) (mHeightPx / scale); 529 if (height > bitmap.getHeight()) { 530 height = bitmap.getHeight(); 531 } 532 533 Matrix matrix = new Matrix(); 534 matrix.postScale(scale, scale); 535 536 if (DEBUG) Log.v(TAG, "original image size " + bitmap.getWidth() + "x" + bitmap.getHeight() + 537 " extracting height " + height); 538 bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), height, matrix, true); 539 if (DEBUG) Log.v(TAG, "new image size " + bitmap.getWidth() + "x" + bitmap.getHeight()); 540 } 541 542 BitmapDrawable bitmapDrawable = new BitmapDrawable(mContext.getResources(), bitmap); 543 bitmapDrawable.setGravity(Gravity.CLIP_HORIZONTAL); 544 545 setDrawableInternal(bitmapDrawable); 546 } 547 548 private void applyBackgroundChanges() { 549 if (!mAttached || mLayerWrapper == null) { 550 return; 551 } 552 553 if (DEBUG) Log.v(TAG, "applyBackgroundChanges drawable " + mBackgroundDrawable); 554 555 int dimAlpha = 0; 556 557 if (mImageOutWrapper != null && mImageOutWrapper.isAnimationPending()) { 558 if (DEBUG) Log.v(TAG, "mImageOutWrapper animation starting"); 559 mImageOutWrapper.startAnimation(); 560 mImageOutWrapper = null; 561 dimAlpha = DIM_ALPHA_ON_SOLID; 562 } 563 564 if (mImageInWrapper == null && mBackgroundDrawable != null) { 565 if (DEBUG) Log.v(TAG, "creating new imagein drawable"); 566 mImageInWrapper = new DrawableWrapper(mBackgroundDrawable); 567 mLayerDrawable.setDrawableByLayerId(R.id.background_imagein, mBackgroundDrawable); 568 if (DEBUG) Log.v(TAG, "mImageInWrapper animation starting"); 569 mImageInWrapper.setAlpha(0); 570 mImageInWrapper.fadeIn(FADE_DURATION_SLOW, 0); 571 mImageInWrapper.startAnimation(); 572 dimAlpha = FULL_ALPHA; 573 } 574 575 if (mDimWrapper != null && dimAlpha != 0) { 576 if (DEBUG) Log.v(TAG, "dimwrapper animation starting to " + dimAlpha); 577 mDimWrapper.fade(FADE_DURATION_SLOW, 0, dimAlpha); 578 mDimWrapper.startAnimation(); 579 } 580 } 581 582 /** 583 * Returns the color currently in use by the background. 584 */ 585 public final int getColor() { 586 return mBackgroundColor; 587 } 588 589 /** 590 * Returns the {@link Drawable} currently in use by the background. 591 */ 592 public Drawable getDrawable() { 593 return mBackgroundDrawable; 594 } 595 596 /** 597 * Task which changes the background. 598 */ 599 class ChangeBackgroundRunnable implements Runnable { 600 private Drawable mDrawable; 601 private boolean mCancel; 602 603 ChangeBackgroundRunnable(Drawable drawable) { 604 mDrawable = drawable; 605 } 606 607 public void cancel() { 608 mCancel = true; 609 } 610 611 @Override 612 public void run() { 613 if (!mCancel) { 614 runTask(); 615 } 616 } 617 618 private void runTask() { 619 boolean newBackground = false; 620 lazyInit(); 621 622 if (mDrawable != mBackgroundDrawable) { 623 newBackground = true; 624 if (mDrawable instanceof BitmapDrawable && 625 mBackgroundDrawable instanceof BitmapDrawable) { 626 if (((BitmapDrawable) mDrawable).getBitmap() == 627 ((BitmapDrawable) mBackgroundDrawable).getBitmap()) { 628 if (DEBUG) Log.v(TAG, "same underlying bitmap detected"); 629 newBackground = false; 630 } 631 } 632 } 633 634 if (!newBackground) { 635 return; 636 } 637 638 releaseBackgroundBitmap(); 639 640 if (mImageInWrapper != null) { 641 mImageOutWrapper = new DrawableWrapper(mImageInWrapper.getDrawable()); 642 mImageOutWrapper.setAlpha(mImageInWrapper.getAlpha()); 643 mImageOutWrapper.fadeOut(FADE_DURATION_QUICK); 644 645 // Order is important! Setting a drawable "removes" the 646 // previous one from the view 647 mLayerDrawable.setDrawableByLayerId(R.id.background_imagein, createEmptyDrawable()); 648 mLayerDrawable.setDrawableByLayerId(R.id.background_imageout, 649 mImageOutWrapper.getDrawable()); 650 mImageInWrapper.setAlpha(0); 651 mImageInWrapper = null; 652 } 653 654 mBackgroundDrawable = mDrawable; 655 mService.setDrawable(mBackgroundDrawable); 656 657 applyBackgroundChanges(); 658 } 659 } 660 661 private Drawable createEmptyDrawable() { 662 Bitmap bitmap = null; 663 return new BitmapDrawable(mContext.getResources(), bitmap); 664 } 665 666 private void showWallpaper(boolean show) { 667 if (mWindow == null) { 668 return; 669 } 670 671 WindowManager.LayoutParams layoutParams = mWindow.getAttributes(); 672 if (show) { 673 if ((layoutParams.flags & WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER) != 0) { 674 return; 675 } 676 if (DEBUG) Log.v(TAG, "showing wallpaper"); 677 layoutParams.flags |= WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER; 678 } else { 679 if ((layoutParams.flags & WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER) == 0) { 680 return; 681 } 682 if (DEBUG) Log.v(TAG, "hiding wallpaper"); 683 layoutParams.flags &= ~WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER; 684 } 685 686 mWindow.setAttributes(layoutParams); 687 } 688} 689