/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package android.support.v17.leanback.app; import android.animation.Animator; import android.animation.ValueAnimator; import android.app.Activity; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.ColorFilter; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.PixelFormat; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; import android.os.Build; import android.os.Handler; import android.support.annotation.ColorInt; import android.support.annotation.NonNull; import android.support.v17.leanback.R; import android.support.v17.leanback.widget.BackgroundHelper; import android.support.v4.content.ContextCompat; import android.support.v4.graphics.drawable.DrawableCompat; import android.support.v4.os.BuildCompat; import android.support.v4.view.animation.FastOutLinearInInterpolator; import android.util.Log; import android.view.View; import android.view.Window; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; import java.lang.ref.WeakReference; /** * Supports background image continuity between multiple Activities. * *

An Activity should instantiate a BackgroundManager and {@link #attach} * to the Activity's window. When the Activity is started, the background is * initialized to the current background values stored in a continuity service. * The background continuity service is updated as the background is updated. * *

At some point, for example when it is stopped, the Activity may release * its background state. * *

When an Activity is resumed, if the BackgroundManager has not been * released, the continuity service is updated from the BackgroundManager state. * If the BackgroundManager was released, the BackgroundManager inherits the * current state from the continuity service. * *

When the last Activity is destroyed, the background state is reset. * *

Backgrounds consist of several layers, from back to front: *

* *

BackgroundManager holds references to potentially large bitmap Drawables. * Call {@link #release} to release these references when the Activity is not * visible. */ // TODO: support for multiple app processes requires a proper android service // instead of the shared memory "service" implemented here. Such a service could // support continuity between fragments of different applications if desired. public final class BackgroundManager { static final String TAG = "BackgroundManager"; static final boolean DEBUG = false; static final int FULL_ALPHA = 255; private static final int CHANGE_BG_DELAY_MS = 500; private static final int FADE_DURATION = 500; private static final String FRAGMENT_TAG = BackgroundManager.class.getCanonicalName(); Activity mContext; Handler mHandler; private View mBgView; private BackgroundContinuityService mService; private int mThemeDrawableResourceId; private BackgroundFragment mFragmentState; private boolean mAutoReleaseOnStop = true; private int mHeightPx; private int mWidthPx; int mBackgroundColor; Drawable mBackgroundDrawable; private boolean mAttached; private long mLastSetTime; private final Interpolator mAccelerateInterpolator; private final Interpolator mDecelerateInterpolator; final ValueAnimator mAnimator; static class BitmapDrawable extends Drawable { static final class ConstantState extends Drawable.ConstantState { final Bitmap mBitmap; final Matrix mMatrix; final Paint mPaint = new Paint(); ConstantState(Bitmap bitmap, Matrix matrix) { mBitmap = bitmap; mMatrix = matrix != null ? matrix : new Matrix(); mPaint.setFilterBitmap(true); } ConstantState(ConstantState copyFrom) { mBitmap = copyFrom.mBitmap; mMatrix = copyFrom.mMatrix != null ? new Matrix(copyFrom.mMatrix) : new Matrix(); if (copyFrom.mPaint.getAlpha() != FULL_ALPHA) { mPaint.setAlpha(copyFrom.mPaint.getAlpha()); } if (copyFrom.mPaint.getColorFilter() != null) { mPaint.setColorFilter(copyFrom.mPaint.getColorFilter()); } mPaint.setFilterBitmap(true); } @Override public Drawable newDrawable() { return new BitmapDrawable(this); } @Override public int getChangingConfigurations() { return 0; } } ConstantState mState; boolean mMutated; BitmapDrawable(Resources resources, Bitmap bitmap) { this(resources, bitmap, null); } BitmapDrawable(Resources resources, Bitmap bitmap, Matrix matrix) { mState = new ConstantState(bitmap, matrix); } BitmapDrawable(ConstantState state) { mState = state; } Bitmap getBitmap() { return mState.mBitmap; } @Override public void draw(Canvas canvas) { if (mState.mBitmap == null) { return; } if (mState.mPaint.getAlpha() < FULL_ALPHA && mState.mPaint.getColorFilter() != null) { throw new IllegalStateException("Can't draw with translucent alpha and color filter"); } canvas.drawBitmap(mState.mBitmap, mState.mMatrix, mState.mPaint); } @Override public int getOpacity() { return android.graphics.PixelFormat.TRANSLUCENT; } @Override public void setAlpha(int alpha) { mutate(); if (mState.mPaint.getAlpha() != alpha) { mState.mPaint.setAlpha(alpha); invalidateSelf(); } } /** * Does not invalidateSelf to avoid recursion issues. * Caller must ensure appropriate invalidation. */ @Override public void setColorFilter(ColorFilter cf) { mutate(); mState.mPaint.setColorFilter(cf); invalidateSelf(); } @Override public ColorFilter getColorFilter() { return mState.mPaint.getColorFilter(); } @Override public ConstantState getConstantState() { return mState; } @NonNull @Override public Drawable mutate() { if (!mMutated) { mMutated = true; mState = new ConstantState(mState); } return this; } } static final class DrawableWrapper { int mAlpha = FULL_ALPHA; final Drawable mDrawable; public DrawableWrapper(Drawable drawable) { mDrawable = drawable; } public DrawableWrapper(DrawableWrapper wrapper, Drawable drawable) { mDrawable = drawable; mAlpha = wrapper.mAlpha; } public Drawable getDrawable() { return mDrawable; } public void setColor(int color) { ((ColorDrawable) mDrawable).setColor(color); } } static final class TranslucentLayerDrawable extends LayerDrawable { DrawableWrapper[] mWrapper; int mAlpha = FULL_ALPHA; boolean mSuspendInvalidation; WeakReference mManagerWeakReference; TranslucentLayerDrawable(BackgroundManager manager, Drawable[] drawables) { super(drawables); mManagerWeakReference = new WeakReference(manager); int count = drawables.length; mWrapper = new DrawableWrapper[count]; for (int i = 0; i < count; i++) { mWrapper[i] = new DrawableWrapper(drawables[i]); } } @Override public void setAlpha(int alpha) { if (mAlpha != alpha) { mAlpha = alpha; invalidateSelf(); BackgroundManager manager = mManagerWeakReference.get(); if (manager != null) { manager.postChangeRunnable(); } } } void setWrapperAlpha(int wrapperIndex, int alpha) { if (mWrapper[wrapperIndex] != null) { mWrapper[wrapperIndex].mAlpha = alpha; invalidateSelf(); } } // Queried by system transitions @Override public int getAlpha() { return mAlpha; } @Override public Drawable mutate() { Drawable drawable = super.mutate(); int count = getNumberOfLayers(); for (int i = 0; i < count; i++) { if (mWrapper[i] != null) { mWrapper[i] = new DrawableWrapper(mWrapper[i], getDrawable(i)); } } return drawable; } @Override public int getOpacity() { return PixelFormat.TRANSLUCENT; } @Override public boolean setDrawableByLayerId(int id, Drawable drawable) { return updateDrawable(id, drawable) != null; } public DrawableWrapper updateDrawable(int id, Drawable drawable) { super.setDrawableByLayerId(id, drawable); for (int i = 0; i < getNumberOfLayers(); i++) { if (getId(i) == id) { mWrapper[i] = new DrawableWrapper(drawable); // Must come after mWrapper was updated so it can be seen by updateColorFilter invalidateSelf(); return mWrapper[i]; } } return null; } public void clearDrawable(int id, Context context) { for (int i = 0; i < getNumberOfLayers(); i++) { if (getId(i) == id) { mWrapper[i] = null; if (!(getDrawable(i) instanceof EmptyDrawable)) { super.setDrawableByLayerId(id, createEmptyDrawable(context)); } break; } } } public int findWrapperIndexById(int id) { for (int i = 0; i < getNumberOfLayers(); i++) { if (getId(i) == id) { return i; } } return -1; } @Override public void invalidateDrawable(Drawable who) { // Prevent invalidate when temporarily change child drawable's alpha in draw() if (!mSuspendInvalidation) { super.invalidateDrawable(who); } } @Override public void draw(Canvas canvas) { for (int i = 0; i < mWrapper.length; i++) { final Drawable d; // For each child drawable, we multiple Wrapper's alpha and LayerDrawable's alpha // temporarily using mSuspendInvalidation to suppress invalidate event. if (mWrapper[i] != null && (d = mWrapper[i].getDrawable()) != null) { int alpha = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT ? DrawableCompat.getAlpha(d) : FULL_ALPHA; final int savedAlpha = alpha; int multiple = 0; if (mAlpha < FULL_ALPHA) { alpha = alpha * mAlpha; multiple++; } if (mWrapper[i].mAlpha < FULL_ALPHA) { alpha = alpha * mWrapper[i].mAlpha; multiple++; } if (multiple == 0) { d.draw(canvas); } else { if (multiple == 1) { alpha = alpha / FULL_ALPHA; } else if (multiple == 2) { alpha = alpha / (FULL_ALPHA * FULL_ALPHA); } try { mSuspendInvalidation = true; d.setAlpha(alpha); d.draw(canvas); d.setAlpha(savedAlpha); } finally { mSuspendInvalidation = false; } } } } } } TranslucentLayerDrawable createTranslucentLayerDrawable( LayerDrawable layerDrawable) { int numChildren = layerDrawable.getNumberOfLayers(); Drawable[] drawables = new Drawable[numChildren]; for (int i = 0; i < numChildren; i++) { drawables[i] = layerDrawable.getDrawable(i); } TranslucentLayerDrawable result = new TranslucentLayerDrawable(this, drawables); for (int i = 0; i < numChildren; i++) { result.setId(i, layerDrawable.getId(i)); } return result; } TranslucentLayerDrawable mLayerDrawable; int mImageInWrapperIndex; int mImageOutWrapperIndex; ChangeBackgroundRunnable mChangeRunnable; private boolean mChangeRunnablePending; private final Animator.AnimatorListener mAnimationListener = new Animator.AnimatorListener() { final Runnable mRunnable = new Runnable() { @Override public void run() { postChangeRunnable(); } }; @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { } @Override public void onAnimationEnd(Animator animation) { if (mLayerDrawable != null) { mLayerDrawable.clearDrawable(R.id.background_imageout, mContext); } mHandler.post(mRunnable); } @Override public void onAnimationCancel(Animator animation) { } }; private final ValueAnimator.AnimatorUpdateListener mAnimationUpdateListener = new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { int fadeInAlpha = (Integer) animation.getAnimatedValue(); if (mImageInWrapperIndex != -1) { mLayerDrawable.setWrapperAlpha(mImageInWrapperIndex, fadeInAlpha); } } }; /** * Shared memory continuity service. */ private static class BackgroundContinuityService { private static final String TAG = "BackgroundContinuity"; private static boolean DEBUG = BackgroundManager.DEBUG; private static BackgroundContinuityService sService = new BackgroundContinuityService(); private int mColor; private Drawable mDrawable; private int mCount; /** Single cache of theme drawable */ private int mLastThemeDrawableId; private WeakReference mLastThemeDrawableState; private BackgroundContinuityService() { reset(); } private void reset() { mColor = Color.TRANSPARENT; mDrawable = null; } public static BackgroundContinuityService getInstance() { final int count = sService.mCount++; if (DEBUG) Log.v(TAG, "Returning instance with new count " + count); return sService; } public void unref() { if (mCount <= 0) throw new IllegalStateException("Can't unref, count " + mCount); if (--mCount == 0) { if (DEBUG) Log.v(TAG, "mCount is zero, resetting"); reset(); } } public int getColor() { return mColor; } public Drawable getDrawable() { return mDrawable; } public void setColor(int color) { mColor = color; mDrawable = null; } public void setDrawable(Drawable drawable) { mDrawable = drawable; } public Drawable getThemeDrawable(Context context, int themeDrawableId) { Drawable drawable = null; if (mLastThemeDrawableState != null && mLastThemeDrawableId == themeDrawableId) { Drawable.ConstantState drawableState = mLastThemeDrawableState.get(); if (DEBUG) Log.v(TAG, "got cached theme drawable state " + drawableState); if (drawableState != null) { drawable = drawableState.newDrawable(); } } if (drawable == null) { drawable = ContextCompat.getDrawable(context, themeDrawableId); if (DEBUG) Log.v(TAG, "loaded theme drawable " + drawable); mLastThemeDrawableState = new WeakReference( drawable.getConstantState()); mLastThemeDrawableId = themeDrawableId; } // No mutate required because this drawable is never manipulated. return drawable; } } Drawable getDefaultDrawable() { if (mBackgroundColor != Color.TRANSPARENT) { return new ColorDrawable(mBackgroundColor); } else { return getThemeDrawable(); } } private Drawable getThemeDrawable() { Drawable drawable = null; if (mThemeDrawableResourceId != -1) { drawable = mService.getThemeDrawable(mContext, mThemeDrawableResourceId); } if (drawable == null) { drawable = createEmptyDrawable(mContext); } return drawable; } /** * Returns the BackgroundManager associated with the given Activity. *

* The BackgroundManager will be created on-demand for each individual * Activity. Subsequent calls will return the same BackgroundManager created * for this Activity. */ public static BackgroundManager getInstance(Activity activity) { BackgroundFragment fragment = (BackgroundFragment) activity.getFragmentManager() .findFragmentByTag(FRAGMENT_TAG); if (fragment != null) { BackgroundManager manager = fragment.getBackgroundManager(); if (manager != null) { return manager; } // manager is null: this is a fragment restored by FragmentManager, // fall through to create a BackgroundManager attach to it. } return new BackgroundManager(activity); } private BackgroundManager(Activity activity) { mContext = activity; mService = BackgroundContinuityService.getInstance(); mHeightPx = mContext.getResources().getDisplayMetrics().heightPixels; mWidthPx = mContext.getResources().getDisplayMetrics().widthPixels; mHandler = new Handler(); Interpolator defaultInterpolator = new FastOutLinearInInterpolator(); mAccelerateInterpolator = AnimationUtils.loadInterpolator(mContext, android.R.anim.accelerate_interpolator); mDecelerateInterpolator = AnimationUtils.loadInterpolator(mContext, android.R.anim.decelerate_interpolator); mAnimator = ValueAnimator.ofInt(0, FULL_ALPHA); mAnimator.addListener(mAnimationListener); mAnimator.addUpdateListener(mAnimationUpdateListener); mAnimator.setInterpolator(defaultInterpolator); TypedArray ta = activity.getTheme().obtainStyledAttributes(new int[] { android.R.attr.windowBackground }); mThemeDrawableResourceId = ta.getResourceId(0, -1); if (mThemeDrawableResourceId < 0) { if (DEBUG) Log.v(TAG, "BackgroundManager no window background resource!"); } ta.recycle(); createFragment(activity); } private void createFragment(Activity activity) { // Use a fragment to ensure the background manager gets detached properly. BackgroundFragment fragment = (BackgroundFragment) activity.getFragmentManager() .findFragmentByTag(FRAGMENT_TAG); if (fragment == null) { fragment = new BackgroundFragment(); activity.getFragmentManager().beginTransaction().add(fragment, FRAGMENT_TAG).commit(); } else { if (fragment.getBackgroundManager() != null) { throw new IllegalStateException("Created duplicated BackgroundManager for same " + "activity, please use getInstance() instead"); } } fragment.setBackgroundManager(this); mFragmentState = fragment; } DrawableWrapper getImageInWrapper() { return mLayerDrawable == null ? null : mLayerDrawable.mWrapper[mImageInWrapperIndex]; } DrawableWrapper getImageOutWrapper() { return mLayerDrawable == null ? null : mLayerDrawable.mWrapper[mImageOutWrapperIndex]; } /** * Synchronizes state when the owning Activity is started. * At that point the view becomes visible. */ void onActivityStart() { updateImmediate(); } void onStop() { if (isAutoReleaseOnStop()) { release(); } } void onResume() { if (DEBUG) Log.v(TAG, "onResume " + this); postChangeRunnable(); } private void syncWithService() { int color = mService.getColor(); Drawable drawable = mService.getDrawable(); if (DEBUG) Log.v(TAG, "syncWithService color " + Integer.toHexString(color) + " drawable " + drawable); mBackgroundColor = color; mBackgroundDrawable = drawable == null ? null : drawable.getConstantState().newDrawable().mutate(); updateImmediate(); } /** * Makes the background visible on the given Window. The background manager must be attached * when the background is set. */ public void attach(Window window) { attachToViewInternal(window.getDecorView()); } /** * Sets the resource id for the drawable to be shown when there is no background set. * Overrides the window background drawable from the theme. This should * be called before attaching. */ public void setThemeDrawableResourceId(int resourceId) { mThemeDrawableResourceId = resourceId; } /** * Adds the composite drawable to the given view. */ public void attachToView(View sceneRoot) { attachToViewInternal(sceneRoot); // clear background to reduce overdraw since the View will act as background. // Activity transition below O has ghost effect for null window background where we // need set a transparent background to force redraw the whole window. mContext.getWindow().getDecorView().setBackground( BuildCompat.isAtLeastO() ? null : new ColorDrawable(Color.TRANSPARENT)); } void attachToViewInternal(View sceneRoot) { if (mAttached) { throw new IllegalStateException("Already attached to " + mBgView); } mBgView = sceneRoot; mAttached = true; syncWithService(); } /** * Returns true if the background manager is currently attached; false otherwise. */ public boolean isAttached() { return mAttached; } /** * Release references to Drawables and put the BackgroundManager into the * detached state. Called when the associated Activity is destroyed. */ void detach() { if (DEBUG) Log.v(TAG, "detach " + this); release(); mBgView = null; mAttached = false; if (mService != null) { mService.unref(); mService = null; } } /** * Release references to Drawable/Bitmap. Typically called in Activity onStop() to reduce memory * overhead when not visible. It's app's responsibility to restore the drawable/bitmap in * Activity onStart(). The method is automatically called in onStop() when * {@link #isAutoReleaseOnStop()} is true. * @see #setAutoReleaseOnStop(boolean) */ public void release() { if (DEBUG) Log.v(TAG, "release " + this); if (mChangeRunnable != null) { mHandler.removeCallbacks(mChangeRunnable); mChangeRunnable = null; } if (mAnimator.isStarted()) { mAnimator.cancel(); } if (mLayerDrawable != null) { mLayerDrawable.clearDrawable(R.id.background_imagein, mContext); mLayerDrawable.clearDrawable(R.id.background_imageout, mContext); mLayerDrawable = null; } mBackgroundDrawable = null; } /** * Sets the drawable used as a dim layer. * @deprecated No longer support dim layer. */ @Deprecated public void setDimLayer(Drawable drawable) { } /** * Returns the drawable used as a dim layer. * @deprecated No longer support dim layer. */ @Deprecated public Drawable getDimLayer() { return null; } /** * Returns the default drawable used as a dim layer. * @deprecated No longer support dim layer. */ @Deprecated public Drawable getDefaultDimLayer() { return ContextCompat.getDrawable(mContext, R.color.lb_background_protection); } void postChangeRunnable() { if (mChangeRunnable == null || !mChangeRunnablePending) { return; } // Postpone a pending change runnable until: no existing change animation in progress && // activity is resumed (in the foreground) && layerdrawable fully opaque. // If the layerdrawable is translucent then an activity transition is in progress // and we want to use the optimized drawing path for performance reasons (see // OptimizedTranslucentLayerDrawable). if (mAnimator.isStarted()) { if (DEBUG) Log.v(TAG, "animation in progress"); } else if (!mFragmentState.isResumed()) { if (DEBUG) Log.v(TAG, "not resumed"); } else if (mLayerDrawable.getAlpha() < FULL_ALPHA) { if (DEBUG) Log.v(TAG, "in transition, alpha " + mLayerDrawable.getAlpha()); } else { long delayMs = getRunnableDelay(); if (DEBUG) Log.v(TAG, "posting runnable delayMs " + delayMs); mLastSetTime = System.currentTimeMillis(); mHandler.postDelayed(mChangeRunnable, delayMs); mChangeRunnablePending = false; } } private void lazyInit() { if (mLayerDrawable != null) { return; } LayerDrawable layerDrawable = (LayerDrawable) ContextCompat.getDrawable(mContext, R.drawable.lb_background).mutate(); mLayerDrawable = createTranslucentLayerDrawable(layerDrawable); mImageInWrapperIndex = mLayerDrawable.findWrapperIndexById(R.id.background_imagein); mImageOutWrapperIndex = mLayerDrawable.findWrapperIndexById(R.id.background_imageout); BackgroundHelper.setBackgroundPreservingAlpha(mBgView, mLayerDrawable); } private void updateImmediate() { if (!mAttached) { return; } lazyInit(); if (mBackgroundDrawable == null) { if (DEBUG) Log.v(TAG, "Use defefault background"); mLayerDrawable.updateDrawable(R.id.background_imagein, getDefaultDrawable()); } else { if (DEBUG) Log.v(TAG, "Background drawable is available " + mBackgroundDrawable); mLayerDrawable.updateDrawable(R.id.background_imagein, mBackgroundDrawable); } mLayerDrawable.clearDrawable(R.id.background_imageout, mContext); } /** * Sets the background to the given color. The timing for when this becomes * visible in the app is undefined and may take place after a small delay. */ public void setColor(@ColorInt int color) { if (DEBUG) Log.v(TAG, "setColor " + Integer.toHexString(color)); mService.setColor(color); mBackgroundColor = color; mBackgroundDrawable = null; if (mLayerDrawable == null) { return; } setDrawableInternal(getDefaultDrawable()); } /** * Sets the given drawable into the background. The provided Drawable will be * used unmodified as the background, without any scaling or cropping * applied to it. The timing for when this becomes visible in the app is * undefined and may take place after a small delay. */ public void setDrawable(Drawable drawable) { if (DEBUG) Log.v(TAG, "setBackgroundDrawable " + drawable); mService.setDrawable(drawable); mBackgroundDrawable = drawable; if (mLayerDrawable == null) { return; } if (drawable == null) { setDrawableInternal(getDefaultDrawable()); } else { setDrawableInternal(drawable); } } /** * Clears the Drawable set by {@link #setDrawable(Drawable)} or {@link #setBitmap(Bitmap)}. * BackgroundManager will show a solid color set by {@link #setColor(int)} or theme drawable * if color is not provided. */ public void clearDrawable() { setDrawable(null); } private void setDrawableInternal(Drawable drawable) { if (!mAttached) { throw new IllegalStateException("Must attach before setting background drawable"); } if (mChangeRunnable != null) { if (sameDrawable(drawable, mChangeRunnable.mDrawable)) { if (DEBUG) Log.v(TAG, "new drawable same as pending"); return; } mHandler.removeCallbacks(mChangeRunnable); mChangeRunnable = null; } mChangeRunnable = new ChangeBackgroundRunnable(drawable); mChangeRunnablePending = true; postChangeRunnable(); } private long getRunnableDelay() { return Math.max(0, mLastSetTime + CHANGE_BG_DELAY_MS - System.currentTimeMillis()); } /** * Sets the given bitmap into the background. When using setCoverImageBitmap to set the * background, the provided bitmap will be scaled and cropped to correctly * fit within the dimensions of the view. The timing for when this becomes * visible in the app is undefined and may take place after a small delay. */ public void setBitmap(Bitmap bitmap) { if (DEBUG) { Log.v(TAG, "setCoverImageBitmap " + bitmap); } if (bitmap == null) { setDrawable(null); return; } if (bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) { if (DEBUG) { Log.v(TAG, "invalid bitmap width or height"); } return; } Matrix matrix = null; if ((bitmap.getWidth() != mWidthPx || bitmap.getHeight() != mHeightPx)) { int dwidth = bitmap.getWidth(); int dheight = bitmap.getHeight(); float scale; // Scale proportionately to fit width and height. if (dwidth * mHeightPx > mWidthPx * dheight) { scale = (float) mHeightPx / (float) dheight; } else { scale = (float) mWidthPx / (float) dwidth; } int subX = Math.min((int) (mWidthPx / scale), dwidth); int dx = Math.max(0, (dwidth - subX) / 2); matrix = new Matrix(); matrix.setScale(scale, scale); matrix.preTranslate(-dx, 0); if (DEBUG) { Log.v(TAG, "original image size " + bitmap.getWidth() + "x" + bitmap.getHeight() + " scale " + scale + " dx " + dx); } } BitmapDrawable bitmapDrawable = new BitmapDrawable(mContext.getResources(), bitmap, matrix); setDrawable(bitmapDrawable); } /** * Enable or disable call release() in Activity onStop(). Default is true. * @param autoReleaseOnStop True to call release() in Activity onStop(), false otherwise. */ public void setAutoReleaseOnStop(boolean autoReleaseOnStop) { mAutoReleaseOnStop = autoReleaseOnStop; } /** * @return True if release() in Activity.onStop(), false otherwise. */ public boolean isAutoReleaseOnStop() { return mAutoReleaseOnStop; } /** * Returns the current background color. */ @ColorInt public final int getColor() { return mBackgroundColor; } /** * Returns the current background {@link Drawable}. */ public Drawable getDrawable() { return mBackgroundDrawable; } boolean sameDrawable(Drawable first, Drawable second) { if (first == null || second == null) { return false; } if (first == second) { return true; } if (first instanceof BitmapDrawable && second instanceof BitmapDrawable) { if (((BitmapDrawable) first).getBitmap().sameAs(((BitmapDrawable) second).getBitmap())) { return true; } } if (first instanceof ColorDrawable && second instanceof ColorDrawable) { if (((ColorDrawable) first).getColor() == ((ColorDrawable) second).getColor()) { return true; } } return false; } /** * Task which changes the background. */ final class ChangeBackgroundRunnable implements Runnable { final Drawable mDrawable; ChangeBackgroundRunnable(Drawable drawable) { mDrawable = drawable; } @Override public void run() { runTask(); mChangeRunnable = null; } private void runTask() { if (mLayerDrawable == null) { if (DEBUG) Log.v(TAG, "runTask while released - should not happen"); return; } DrawableWrapper imageInWrapper = getImageInWrapper(); if (imageInWrapper != null) { if (sameDrawable(mDrawable, imageInWrapper.getDrawable())) { if (DEBUG) Log.v(TAG, "new drawable same as current"); return; } if (DEBUG) Log.v(TAG, "moving image in to image out"); // Order is important! Setting a drawable "removes" the // previous one from the view mLayerDrawable.clearDrawable(R.id.background_imagein, mContext); mLayerDrawable.updateDrawable(R.id.background_imageout, imageInWrapper.getDrawable()); } applyBackgroundChanges(); } void applyBackgroundChanges() { if (!mAttached) { return; } if (DEBUG) Log.v(TAG, "applyBackgroundChanges drawable " + mDrawable); DrawableWrapper imageInWrapper = getImageInWrapper(); if (imageInWrapper == null && mDrawable != null) { if (DEBUG) Log.v(TAG, "creating new imagein drawable"); imageInWrapper = mLayerDrawable.updateDrawable( R.id.background_imagein, mDrawable); if (DEBUG) Log.v(TAG, "imageInWrapper animation starting"); mLayerDrawable.setWrapperAlpha(mImageInWrapperIndex, 0); } mAnimator.setDuration(FADE_DURATION); mAnimator.start(); } } static class EmptyDrawable extends BitmapDrawable { EmptyDrawable(Resources res) { super(res, (Bitmap) null); } } static Drawable createEmptyDrawable(Context context) { return new EmptyDrawable(context.getResources()); } }