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