/*
* Copyright (C) 2016 Google Inc. All Rights Reserved.
*
* 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 com.google.android.apps.santatracker.doodles.shared;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.os.Build.VERSION;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.BounceInterpolator;
import android.view.animation.OvershootInterpolator;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.TextView;
import com.google.android.apps.santatracker.doodles.R;
import com.google.android.apps.santatracker.doodles.shared.PineappleLogEvent.Builder;
import com.google.android.apps.santatracker.util.FontHelper;
import static com.google.android.apps.santatracker.doodles.shared.PineappleLogEvent.DEFAULT_DOODLE_NAME;
import static com.google.android.apps.santatracker.doodles.shared.PineappleLogEvent.GAME_OVER;
import static com.google.android.apps.santatracker.doodles.shared.PineappleLogEvent.HOME_CLICKED;
import static com.google.android.apps.santatracker.doodles.shared.PineappleLogEvent.REPLAY_CLICKED;
import static com.google.android.apps.santatracker.doodles.shared.PineappleLogEvent.SHARE_CLICKED;
/**
* Displays the score during the game and shows the end screen when the game is over.
*/
public class ScoreView extends FrameLayout {
private static final String TAG = ScoreView.class.getSimpleName();
public interface OnShareClickedListener {
void onShareClicked();
}
// Durations of various animations.
private static final int BUMP_MS = 300; // Bump when a point is scored.
private static final int ZOOM_UP_MS = 900; // Zooming score to middle of screen.
private static final int ZOOM_DOWN_MS = 400; // Zooming score to its end position.
private static final int SHARE_FADE_IN_MS = 500; // Fading in the share image.
private static final int SHARE_DROP_MS = 400; // Dropping the share image into place.
private static final int SHARE_Y_OFFSET_PX = -200; // Offset of the share image before it drops.
private static final int BG_FADE_IN_MS = 400; // Fading in the end screen background.
private static final int RESET_FADE_IN_MS = 300; // Fading in the score view when game is reset.
private static final int STAR_BOUNCE_IN_MS = 600; // Bouncing the stars into the end screen.
private static final int STAR_FADE_IN_MS = 500; // Fading the stars into the end screen.
private static final float STAR_BIG_SCALE = 2.0f;
// The score in the upper-left corner during the game. This also gets zoomed up to the
// middle of the screen at the end of the game.
private TextView currentScore;
// An invisible placeholder. Lets us use the android layout engine for figuring out where
// the score is positioned on the final screen. At the end of the game, currentScore animates
// from its original position/size to the position/size of finalScorePlaceholder.
private TextView finalScorePlaceholder;
// An invisible placeholder which is positioned at the center of the screen and is used as the
// intermediate position/size before the score drops into its final position.
private TextView centeredScorePlaceholder;
// Text that says "Game Over"
private TextView gameOverText;
// Widgets on the end screen.
private TextView bestScore;
private ImageView shareImage;
private LinearLayout menuItems;
private GameOverlayButton shareButton;
// A semi-opaque background which darkens the game during the end screen.
private View background;
// Initial state for the views involved in the end-screen animation, stored so it can be
// restored if "replay" is tapped.
private float currentScoreX = Float.NaN;
private float currentScoreY = Float.NaN;
private int currentScoreMarginStart;
private int currentScoreMarginTop;
private float currentScoreTextSizePx;
private float currentScoreAlpha;
private float finalScoreMaxWidth;
private float finalScoreMaxHeight;
private float centeredScoreMaxWidth;
private float centeredScoreMaxHeight;
private float backgroundAlpha;
private int mCurrentScoreValue;
private LinearLayout currentStars;
private RelativeLayout finalStars;
private int filledStarCount;
private DoodleConfig doodleConfig;
protected PineappleLogger logger;
private boolean canReplay;
private OnShareClickedListener shareClickedListener;
/**
* A listener for events which occur from the level finished screen.
*/
public interface LevelFinishedListener {
void onReplay();
String gameType();
float score();
int shareImageId();
}
protected LevelFinishedListener listener;
public ScoreView(Context context, OnShareClickedListener shareClickedListener) {
this(context, (AttributeSet) null);
this.shareClickedListener = shareClickedListener;
}
public ScoreView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ScoreView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
loadLayout(context);
resetToStartState();
}
public void setDoodleConfig(DoodleConfig doodleConfig) {
this.doodleConfig = doodleConfig;
}
public void setLogger(PineappleLogger logger) {
this.logger = logger;
}
protected void loadLayout(final Context context) {
setVisibility(View.INVISIBLE);
LayoutInflater inflater = LayoutInflater.from(context);
View view = inflater.inflate(R.layout.score_view, this);
gameOverText = (TextView) view.findViewById(R.id.text_game_over);
gameOverText.setVisibility(INVISIBLE);
FontHelper.makeLobster(gameOverText, false /* italic */);
currentScore = (TextView) view.findViewById(R.id.current_score);
// Store these for later so we can put currentScore back where it started after animating it.
currentScoreTextSizePx = currentScore.getTextSize();
currentScoreAlpha = currentScore.getAlpha();
currentScore.post(new Runnable() {
@Override
public void run() {
currentScoreX = currentScore.getX();
currentScoreY = currentScore.getY();
}
});
RelativeLayout.LayoutParams params =
(RelativeLayout.LayoutParams) currentScore.getLayoutParams();
if (VERSION.SDK_INT >= 17) {
currentScoreMarginStart = params.getMarginStart();
} else {
currentScoreMarginStart = params.leftMargin;
}
currentScoreMarginTop = params.topMargin;
finalScorePlaceholder = (TextView) view.findViewById(R.id.final_score_placeholder);
finalScorePlaceholder.setVisibility(INVISIBLE);
finalScoreMaxWidth = getResources().getDimension(R.dimen.final_score_max_width);
finalScoreMaxHeight = getResources().getDimension(R.dimen.final_score_max_height);
finalScorePlaceholder.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void afterTextChanged(Editable editable) {
UIUtil.fitToBounds(finalScorePlaceholder, finalScoreMaxWidth, finalScoreMaxHeight);
}
});
centeredScorePlaceholder = (TextView) view.findViewById(R.id.centered_score_placeholder);
centeredScorePlaceholder.setVisibility(INVISIBLE);
centeredScoreMaxWidth = getResources().getDimension(R.dimen.centered_score_max_width);
centeredScoreMaxHeight = getResources().getDimension(R.dimen.centered_score_max_height);
centeredScorePlaceholder.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void afterTextChanged(Editable editable) {
UIUtil.fitToBounds(centeredScorePlaceholder, centeredScoreMaxWidth, centeredScoreMaxHeight);
}
});
currentStars = (LinearLayout)
view.findViewById(R.id.current_stars);
currentStars.removeAllViews(); // Remove the stickers that are in the XML for testing layout.
finalStars = (RelativeLayout)
view.findViewById(R.id.final_stars);
bestScore = (TextView) view.findViewById(R.id.best_score);
shareImage = (ImageView) view.findViewById(R.id.share_image);
shareImage.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
replay();
}
});
menuItems = (LinearLayout) view.findViewById(R.id.menu_items);
View replayButton = view.findViewById(R.id.replay_button);
replayButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
replay();
}
});
shareButton = (GameOverlayButton) view.findViewById(R.id.share_button);
shareButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
EventBus.getInstance().sendEvent(EventBus.PLAY_SOUND, R.raw.menu_item_click);
logger.logEvent(new Builder(DEFAULT_DOODLE_NAME, SHARE_CLICKED)
.withEventSubType(listener.gameType())
.withEventValue1(listener.shareImageId())
.build());
if (shareClickedListener != null) {
shareClickedListener.onShareClicked();
}
}
});
View moreGamesButton = view.findViewById(R.id.menu_button);
moreGamesButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
goToMoreGames(context);
}
});
background = view.findViewById(R.id.score_view_background);
backgroundAlpha = background.getAlpha(); // Store for later use.
}
protected void goToMoreGames(Context context) {
EventBus.getInstance().sendEvent(EventBus.PLAY_SOUND, R.raw.menu_item_click);
logger.logEvent(new Builder(DEFAULT_DOODLE_NAME, HOME_CLICKED)
.withEventSubType(listener.gameType())
.build());
LaunchDecisionMaker.finishActivity(context);
}
private void replay() {
if (canReplay && listener != null) {
EventBus.getInstance().sendEvent(EventBus.PLAY_SOUND, R.raw.menu_item_click);
logger.logEvent(new Builder(DEFAULT_DOODLE_NAME, REPLAY_CLICKED)
.withEventSubType(listener.gameType())
.build());
listener.onReplay();
AndroidUtils.forceScreenToStayOn(getContext());
}
}
public void setListener(LevelFinishedListener listener) {
this.listener = listener;
}
/**
* Updates the best score field on the end screen to display the given best score.
*
* @param newScore The score to be displayed as the best score.
*/
public void updateBestScore(CharSequence newScore) {
bestScore.setText(
AndroidUtils.getText(getResources(), R.string.end_screen_best_score, newScore));
}
/**
* Sets the end screen header to the given text.
*
* <p>This header will be shown in place of the best score.</p>
*
* @param text The text to be put into the header.
*/
public void setHeaderText(CharSequence text) {
bestScore.setText(text);
}
public void updateCurrentScore(CharSequence newScore, boolean shouldBump) {
currentScore.setText(newScore);
finalScorePlaceholder.setText(newScore);
centeredScorePlaceholder.setText(newScore);
if (shouldBump) {
animateBump(currentScore);
}
}
public void setShareDrawable(Drawable drawable) {
shareImage.setImageDrawable(drawable);
}
public void clearAllStars() {
currentStars.removeAllViews();
filledStarCount = 0;
for (int i = 0; i < finalStars.getChildCount(); i++) {
FrameLayout star = (FrameLayout) finalStars.getChildAt(i);
star.findViewById(R.id.fill).setVisibility(INVISIBLE);
}
}
public void addStar() {
if (filledStarCount < 3) {
filledStarCount++;
int currentStarDimens = (int) AndroidUtils.dipToPixels(40);
addStarToLayout(currentStars, currentStarDimens, LinearLayout.LayoutParams.MATCH_PARENT);
EventBus.getInstance().sendEvent(EventBus.PLAY_SOUND, R.raw.ui_positive_sound);
}
}
public int getStarCount() {
return filledStarCount;
}
// Width & height are in pixels.
private void addStarToLayout(LinearLayout layout, int width, int height) {
ImageView image = new ImageView(getContext());
image.setImageResource(R.drawable.pineapple_star_filled);
animateBump(image);
layout.addView(image, new LinearLayout.LayoutParams(width, height));
}
public void resetToStartState() {
Log.i(TAG, "Reset to start state");
currentStars.setVisibility(VISIBLE);
bestScore.setVisibility(INVISIBLE);
shareImage.setVisibility(INVISIBLE);
menuItems.setVisibility(INVISIBLE);
shareButton.setVisibility(INVISIBLE);
background.setVisibility(INVISIBLE);
finalStars.setVisibility(INVISIBLE);
gameOverText.setVisibility(INVISIBLE);
if (!Float.isNaN(currentScoreX)) {
currentScore.setX(currentScoreX);
}
if (!Float.isNaN(currentScoreY)) {
currentScore.setY(currentScoreY);
}
currentScore.setTextSize(TypedValue.COMPLEX_UNIT_PX, currentScoreTextSizePx);
RelativeLayout.LayoutParams params =
(RelativeLayout.LayoutParams) currentScore.getLayoutParams();
if (VERSION.SDK_INT >= 17) {
params.setMarginStart(currentScoreMarginStart);
} else {
params.leftMargin = currentScoreMarginStart;
}
params.topMargin = currentScoreMarginTop;
updateCurrentScore(Integer.toString(0), false);
clearAllStars();
currentScore.setAlpha(0);
ValueAnimator fadeInCurrentScore = UIUtil.animator(RESET_FADE_IN_MS,
new AccelerateDecelerateInterpolator(),
new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
currentScore.setAlpha((float) valueAnimator.getAnimatedValue("alpha"));
}
},
UIUtil.floatValue("alpha", 0, currentScoreAlpha)
);
fadeInCurrentScore.start();
}
public void animateToEndState() {
AndroidUtils.allowScreenToTurnOff(getContext());
Log.i(TAG, "Animate to end state");
canReplay = false;
setVisibility(View.VISIBLE);
logger.logEvent(new Builder(DEFAULT_DOODLE_NAME, GAME_OVER)
.withEventSubType(listener.gameType())
.withLatencyMs(PineappleLogTimer.getInstance().timeElapsedMs())
.withEventValue1(listener.score())
.withEventValue2(getStarCount())
.build());
// TODO: Fade this out instead of making it invisible (will have to remove
// the layout:alignComponents that attach it current score, else it will move along with the
// score.)
currentStars.setVisibility(INVISIBLE);
// Initial state: controls & background are visible but alpha = 0
bestScore.setAlpha(0);
shareImage.setAlpha(0.0f);
background.setAlpha(0);
finalStars.setAlpha(0);
bestScore.setVisibility(VISIBLE);
shareImage.setVisibility(VISIBLE);
background.setVisibility(VISIBLE);
finalStars.setVisibility(VISIBLE);
gameOverText.setVisibility(VISIBLE);
// Offset the share image and stars so that they can bounce in.
final float shareImageY = shareImage.getY();
final float finalStarsY = finalStars.getY();
shareImage.setY(shareImageY + SHARE_Y_OFFSET_PX);
// Zoom the score to center of screen.
// I tried several other ways of doing this animation, none of which worked:
// 1. Using TranslateAnimation & ScaleAnimation instead of .animate(): Positions didn't work
// right when scaling, maybe because these animate how a view is displayed but not the actual
// view properties.
// 2. Using TranslateAnimation & ObjectAnimator: couldn't add ObjectAnimator to the same
// Animation set as TranslateAnimation.
// 3. Using .animate() to get a PropertyAnimator. Couldn't tween textSize without using
// .setUpdateListener, which requires API 19.
// 4. Small textSize, scaling up from 1: Text is blurry at scales > 1.
// 5. Large textSize, scaling up to 1: Final position was wrong, I couldn't figure out why.
// 6. Medium textSize, scaling up to 2.5: Error: font size too large to fit in cache. Tried
// turning off HW accel which fixed the cache errors but positioning was still wrong.
ValueAnimator zoomUp = UIUtil.animator(ZOOM_UP_MS, new ElasticOutInterpolator(0.35f),
new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
currentScore.setX((float) valueAnimator.getAnimatedValue("x"));
currentScore.setY((float) valueAnimator.getAnimatedValue("y"));
currentScore.setTextSize(TypedValue.COMPLEX_UNIT_PX,
(float) valueAnimator.getAnimatedValue("textSize"));
RelativeLayout.LayoutParams params =
(RelativeLayout.LayoutParams) currentScore.getLayoutParams();
if (VERSION.SDK_INT >= 17) {
params.setMarginStart((int) (float) valueAnimator.getAnimatedValue("marginStart"));
} else {
params.leftMargin = (int) (float) valueAnimator.getAnimatedValue("marginStart");
}
params.topMargin = (int) (float) valueAnimator.getAnimatedValue("topMargin");
currentScore.setAlpha((float) valueAnimator.getAnimatedValue("currentScoreAlpha"));
}
},
UIUtil.floatValue("x", currentScore.getX(), centeredScorePlaceholder.getX()),
UIUtil.floatValue("y", currentScore.getY(), centeredScorePlaceholder.getY()),
UIUtil.floatValue("textSize",
currentScoreTextSizePx, centeredScorePlaceholder.getTextSize()),
UIUtil.floatValue("marginStart", currentScoreMarginStart, 0),
UIUtil.floatValue("topMargin", currentScoreMarginTop, 0),
UIUtil.floatValue("currentScoreAlpha", currentScoreAlpha, 1)
);
// Zoom the score up to its final position.
ValueAnimator zoomBackDown = UIUtil.animator(ZOOM_DOWN_MS, new BounceInterpolator(),
new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
currentScore.setX((float) valueAnimator.getAnimatedValue("x"));
currentScore.setY((float) valueAnimator.getAnimatedValue("y"));
currentScore.setTextSize(TypedValue.COMPLEX_UNIT_PX,
(float) valueAnimator.getAnimatedValue("textSize"));
}
},
UIUtil.floatValue("x", centeredScorePlaceholder.getX(), finalScorePlaceholder.getX()),
UIUtil.floatValue("y", centeredScorePlaceholder.getY(), finalScorePlaceholder.getY()),
UIUtil.floatValue("textSize",
centeredScorePlaceholder.getTextSize(), finalScorePlaceholder.getTextSize())
);
ValueAnimator fadeInBackground = UIUtil.animator(BG_FADE_IN_MS,
new AccelerateDecelerateInterpolator(),
new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
background.setAlpha((float) valueAnimator.getAnimatedValue("bgAlpha"));
}
},
UIUtil.floatValue("bgAlpha", 0, backgroundAlpha)
);
ValueAnimator fadeInBestScore = UIUtil.animator(BG_FADE_IN_MS,
new AccelerateDecelerateInterpolator(),
new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
bestScore.setAlpha((float) valueAnimator.getAnimatedValue("bgAlpha"));
}
},
UIUtil.floatValue("bgAlpha", 0, backgroundAlpha)
);
ValueAnimator fadeInMenuItems = UIUtil.animator(BG_FADE_IN_MS,
new AccelerateDecelerateInterpolator(),
new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
float alpha = (float) valueAnimator.getAnimatedValue("alpha");
menuItems.setAlpha(alpha);
shareButton.setAlpha(alpha);
}
},
UIUtil.floatValue("alpha", 0, 1)
);
fadeInMenuItems.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
// Don't set menu items to be visible until the animation starts so that they aren't
// clickable until they start to appear.
menuItems.setVisibility(VISIBLE);
menuItems.setAlpha(0);
shareButton.setVisibility(VISIBLE);
shareButton.setAlpha(0);
canReplay = true;
}
});
ValueAnimator fadeInShareImageAndFinalStars = UIUtil.animator(SHARE_FADE_IN_MS,
new AccelerateDecelerateInterpolator(),
new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
float alpha = (float) valueAnimator.getAnimatedValue("alpha");
shareImage.setAlpha(alpha);
finalStars.setAlpha(alpha);
}
},
UIUtil.floatValue("alpha", 0, 1)
);
ValueAnimator dropShareImageAndFinalStars = UIUtil.animator(SHARE_DROP_MS,
new BounceInterpolator(),
new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
float yOffset = (float) valueAnimator.getAnimatedValue("yOffset");
shareImage.setY(shareImageY + yOffset);
}
},
UIUtil.floatValue("yOffset", SHARE_Y_OFFSET_PX, 0)
);
AnimatorSet animations = new AnimatorSet();
int numStars = finalStars.getChildCount();
ValueAnimator bounce = null;
long starStartDelay = ZOOM_UP_MS + SHARE_FADE_IN_MS + SHARE_DROP_MS / 2;
for (int i = 0; i < filledStarCount; i++) {
FrameLayout star = (FrameLayout) finalStars.getChildAt(numStars - i - 1);
ValueAnimator fade = getStarFadeIn((ImageView) star.findViewById(R.id.fill));
bounce = getStarBounceIn((ImageView) star.findViewById(R.id.fill));
animations.play(fade).after(starStartDelay + STAR_FADE_IN_MS * i);
animations.play(bounce).after(starStartDelay + STAR_FADE_IN_MS * i);
}
if (bounce != null) {
animations.play(fadeInMenuItems).after(bounce);
} else {
animations.play(fadeInMenuItems).after(starStartDelay + STAR_FADE_IN_MS);
}
animations.play(fadeInBackground).with(zoomUp);
animations.play(fadeInBestScore).after(fadeInBackground);
animations.play(zoomBackDown).after(zoomUp);
animations.play(fadeInShareImageAndFinalStars).after(zoomUp);
animations.play(dropShareImageAndFinalStars).after(fadeInShareImageAndFinalStars);
animations.start();
}
private ValueAnimator getStarFadeIn(final ImageView star) {
star.setAlpha(0.0f);
star.setVisibility(VISIBLE);
ValueAnimator fadeIn = UIUtil.animator(STAR_FADE_IN_MS,
new AccelerateDecelerateInterpolator(),
new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
float alpha = (float) valueAnimator.getAnimatedValue("alpha");
star.setAlpha(alpha);
}
},
UIUtil.floatValue("alpha", 0, 1)
);
fadeIn.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
EventBus.getInstance().sendEvent(EventBus.PLAY_SOUND, R.raw.ui_positive_sound);
}
});
return fadeIn;
}
private ValueAnimator getStarBounceIn(final ImageView star) {
return UIUtil.animator(STAR_BOUNCE_IN_MS,
new BounceInterpolator(),
new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
float scale = (float) valueAnimator.getAnimatedValue("scale");
star.setScaleX(scale);
star.setScaleY(scale);
}
},
UIUtil.floatValue("scale", STAR_BIG_SCALE, 1)
);
}
private void animateBump(final View view) {
ValueAnimator tween = UIUtil.animator(BUMP_MS, new OvershootInterpolator(),
new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
float scale = (float) valueAnimator.getAnimatedValue("scale");
view.setScaleX(scale);
view.setScaleY(scale);
}
},
UIUtil.floatValue("scale", 1.5f, 1));
tween.start();
}
}