// Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package org.chromium.chrome.browser.ntp; import android.animation.Animator; import android.animation.ObjectAnimator; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Bitmap.Config; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.drawable.Drawable; import android.text.TextUtils; import android.util.AttributeSet; import android.util.Property; import android.view.Gravity; import android.view.View; import android.view.View.OnClickListener; import android.widget.FrameLayout; import org.chromium.chrome.R; import org.chromium.chrome.browser.ntp.LogoBridge.Logo; import org.chromium.chrome.browser.ntp.NewTabPageView.NewTabPageManager; import org.chromium.chrome.browser.widget.LoadingView; import java.lang.ref.WeakReference; import jp.tomorrowkey.android.gifplayer.BaseGifDrawable; import jp.tomorrowkey.android.gifplayer.BaseGifImage; /** * This view shows the default search provider's logo and fades in a new logo if one becomes * available. It also maintains a {@link BaseGifDrawable} that will be played when the user clicks * this view and we have an animated GIF logo ready. */ public class LogoView extends FrameLayout implements OnClickListener { // Number of milliseconds for a new logo to fade in. private static final int LOGO_TRANSITION_TIME_MS = 400; // The default logo is shared across all NTPs. private static WeakReference<Bitmap> sDefaultLogo; // mLogo and mNewLogo are remembered for cross fading animation. private Bitmap mLogo; private Bitmap mNewLogo; private BaseGifDrawable mAnimatedLogoDrawable; private ObjectAnimator mFadeAnimation; private Paint mPaint; private Matrix mLogoMatrix; private Matrix mNewLogoMatrix; private Matrix mAnimatedLogoMatrix; private boolean mLogoIsDefault; private boolean mNewLogoIsDefault; private LoadingView mLoadingView; /** * A measure from 0 to 1 of how much the new logo has faded in. 0 shows the old logo, 1 shows * the new logo, and intermediate values show the new logo cross-fading in over the old logo. * Set to 0 when not transitioning. */ private float mTransitionAmount; private NewTabPageManager mManager; private final Property<LogoView, Float> mTransitionProperty = new Property<LogoView, Float>(Float.class, "") { @Override public Float get(LogoView logoView) { return logoView.mTransitionAmount; } @Override public void set(LogoView logoView, Float amount) { assert amount >= 0f; assert amount <= 1f; if (logoView.mTransitionAmount != amount) { logoView.mTransitionAmount = amount; invalidate(); } } }; /** * Constructor used to inflate a LogoView from XML. */ public LogoView(Context context, AttributeSet attrs) { super(context, attrs); mLogo = getDefaultLogo(); mLogoMatrix = new Matrix(); mLogoIsDefault = true; mPaint = new Paint(); mPaint.setFilterBitmap(true); // Mark this view as non-clickable so that accessibility will ignore it. When a non-default // logo is shown, this view will be marked clickable again. setOnClickListener(this); setClickable(false); setWillNotDraw(false); mLoadingView = new LoadingView(getContext()); LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); lp.gravity = Gravity.CENTER; mLoadingView.setLayoutParams(lp); mLoadingView.setVisibility(View.GONE); addView(mLoadingView); } /** * Sets the NewTabPageManager to notify when the logo is pressed. */ public void setMananger(NewTabPageManager manager) { mManager = manager; } /** * Jumps to the end of the logo cross-fading animation, if any. */ public void endFadeAnimation() { if (mFadeAnimation != null) { mFadeAnimation.end(); mFadeAnimation = null; } } /** * @return True after we receive an animated logo from the server. */ private boolean isAnimatedLogoShowing() { return mAnimatedLogoDrawable != null; } /** * Starts playing the given animated GIF logo. */ public void playAnimatedLogo(BaseGifImage gifImage) { mLoadingView.hideLoadingUI(); mAnimatedLogoDrawable = new BaseGifDrawable(gifImage, Config.ARGB_8888); mAnimatedLogoMatrix = new Matrix(); setMatrix(mAnimatedLogoDrawable.getIntrinsicWidth(), mAnimatedLogoDrawable.getIntrinsicHeight(), mAnimatedLogoMatrix, false); // Set callback here to ensure #invalidateDrawable() is called. mAnimatedLogoDrawable.setCallback(this); mAnimatedLogoDrawable.start(); } /** * Lets logo view show a spinning progressbar. */ public void showLoadingView() { mLoadingView.showLoadingUI(); } /** * Fades in a new logo over the current logo. * * @param logo The new logo to fade in. May be null to reset to the default logo. */ public void updateLogo(Logo logo) { if (logo == null) { updateLogo(getDefaultLogo(), null, true); } else { String contentDescription = TextUtils.isEmpty(logo.altText) ? null : getResources().getString(R.string.accessibility_google_doodle, logo.altText); updateLogo(logo.image, contentDescription, false); } } private void updateLogo(Bitmap logo, final String contentDescription, boolean isDefaultLogo) { if (mFadeAnimation != null) mFadeAnimation.end(); mNewLogo = logo; mNewLogoMatrix = new Matrix(); mNewLogoIsDefault = isDefaultLogo; setMatrix(mNewLogo.getWidth(), mNewLogo.getHeight(), mNewLogoMatrix, mNewLogoIsDefault); mFadeAnimation = ObjectAnimator.ofFloat(this, mTransitionProperty, 0f, 1f); mFadeAnimation.setDuration(LOGO_TRANSITION_TIME_MS); mFadeAnimation.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { } @Override public void onAnimationEnd(Animator animation) { mLogo = mNewLogo; mLogoMatrix = mNewLogoMatrix; mLogoIsDefault = mNewLogoIsDefault; mNewLogo = null; mNewLogoMatrix = null; mTransitionAmount = 0f; mFadeAnimation = null; setContentDescription(contentDescription); setClickable(!mNewLogoIsDefault); } @Override public void onAnimationCancel(Animator animation) { onAnimationEnd(animation); invalidate(); } }); mFadeAnimation.start(); } /** * @return Whether a new logo is currently fading in over the old logo. */ private boolean isTransitioning() { return mTransitionAmount != 0f; } /** * Sets the matrix to scale and translate the image so that it will be centered in the LogoView * and scaled to fit within the LogoView. * * @param preventUpscaling Whether the image should not be scaled up. If true, the image might * not fill the entire view but will still be centered. */ private void setMatrix(int imageWidth, int imageHeight, Matrix matrix, boolean preventUpscaling) { int width = getWidth(); int height = getHeight(); float scale = Math.min((float) width / imageWidth, (float) height / imageHeight); if (preventUpscaling) scale = Math.min(1.0f, scale); int imageOffsetX = Math.round((width - imageWidth * scale) * 0.5f); int imageOffsetY = Math.round((height - imageHeight * scale) * 0.5f); matrix.setScale(scale, scale); matrix.postTranslate(imageOffsetX, imageOffsetY); } /** * @return The default logo. */ private Bitmap getDefaultLogo() { Bitmap defaultLogo = sDefaultLogo == null ? null : sDefaultLogo.get(); if (defaultLogo == null) { defaultLogo = BitmapFactory.decodeResource(getResources(), R.drawable.google_logo); sDefaultLogo = new WeakReference<Bitmap>(defaultLogo); } return defaultLogo; } @Override protected boolean verifyDrawable(Drawable who) { return (who == mAnimatedLogoDrawable) || super.verifyDrawable(who); } @Override public void invalidateDrawable(Drawable drawable) { // mAnimatedLogoDrawable doesn't actually know its bounds, so super.invalidateDrawable() // doesn't invalidate the right area. Instead invalidate the entire view; the drawable takes // up most of the view anyway so this is just as efficient. // @see ImageView#invalidateDrawable(). if (drawable == mAnimatedLogoDrawable) invalidate(); else super.invalidateDrawable(drawable); } @Override protected void onDraw(Canvas canvas) { if (isAnimatedLogoShowing()) { if (mFadeAnimation != null) mFadeAnimation.cancel(); // Free the old bitmaps to allow them to be GC'd. mLogo = null; mNewLogo = null; canvas.save(); canvas.concat(mAnimatedLogoMatrix); mAnimatedLogoDrawable.draw(canvas); canvas.restore(); } else { if (mLogo != null && mTransitionAmount < 0.5f) { mPaint.setAlpha((int) (255 * 2 * (0.5f - mTransitionAmount))); canvas.save(); canvas.concat(mLogoMatrix); canvas.drawBitmap(mLogo, 0, 0, mPaint); canvas.restore(); } if (mNewLogo != null && mTransitionAmount > 0.5f) { mPaint.setAlpha((int) (255 * 2 * (mTransitionAmount - 0.5f))); canvas.save(); canvas.concat(mNewLogoMatrix); canvas.drawBitmap(mNewLogo, 0, 0, mPaint); canvas.restore(); } } } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { if (w != oldw || h != oldh) { if (mAnimatedLogoDrawable != null) { setMatrix(mAnimatedLogoDrawable.getIntrinsicWidth(), mAnimatedLogoDrawable.getIntrinsicHeight(), mAnimatedLogoMatrix, false); } if (mLogo != null) { setMatrix(mLogo.getWidth(), mLogo.getHeight(), mLogoMatrix, mLogoIsDefault); } if (mNewLogo != null) { setMatrix(mNewLogo.getWidth(), mNewLogo.getHeight(), mNewLogoMatrix, mNewLogoIsDefault); } } } @Override public void onClick(View view) { if (view == this && mManager != null && !isTransitioning()) { mManager.onLogoClicked(isAnimatedLogoShowing()); } } }