/* * Copyright 2015 Google Inc. * * 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.samples.apps.topeka.activity; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ArgbEvaluator; import android.animation.ObjectAnimator; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.Context; import android.content.Intent; import android.graphics.Color; import android.os.Build; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.VisibleForTesting; import android.support.design.widget.FloatingActionButton; import android.support.test.espresso.contrib.CountingIdlingResource; import android.support.v4.app.ActivityCompat; import android.support.v4.content.ContextCompat; import android.support.v4.view.ViewCompat; import android.support.v4.view.ViewPropertyAnimatorListenerAdapter; import android.support.v4.view.animation.FastOutLinearInInterpolator; import android.support.v4.view.animation.FastOutSlowInInterpolator; import android.support.v7.app.AppCompatActivity; import android.util.Log; import android.view.View; import android.view.ViewAnimationUtils; import android.view.Window; import android.view.animation.Interpolator; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.TextView; import com.google.samples.apps.topeka.R; import com.google.samples.apps.topeka.fragment.QuizFragment; import com.google.samples.apps.topeka.helper.ApiLevelHelper; import com.google.samples.apps.topeka.helper.ViewUtils; import com.google.samples.apps.topeka.model.Category; import com.google.samples.apps.topeka.model.JsonAttributes; import com.google.samples.apps.topeka.persistence.TopekaDatabaseHelper; import com.google.samples.apps.topeka.widget.TextSharedElementCallback; import java.util.List; import static com.google.samples.apps.topeka.adapter.CategoryAdapter.DRAWABLE; public class QuizActivity extends AppCompatActivity { private static final String TAG = "QuizActivity"; private static final String IMAGE_CATEGORY = "image_category_"; private static final String STATE_IS_PLAYING = "isPlaying"; private static final String FRAGMENT_TAG = "Quiz"; private Interpolator mInterpolator; private Category mCategory; private QuizFragment mQuizFragment; private FloatingActionButton mQuizFab; private boolean mSavedStateIsPlaying; private ImageView mIcon; private Animator mCircularReveal; private ObjectAnimator mColorChange; private CountingIdlingResource mCountingIdlingResource; private View mToolbarBack; private final View.OnClickListener mOnClickListener = new View.OnClickListener() { @Override public void onClick(final View v) { switch (v.getId()) { case R.id.fab_quiz: startQuizFromClickOn(v); break; case R.id.submitAnswer: submitAnswer(); break; case R.id.quiz_done: ActivityCompat.finishAfterTransition(QuizActivity.this); break; case R.id.back: onBackPressed(); break; default: throw new UnsupportedOperationException( "OnClick has not been implemented for " + getResources(). getResourceName(v.getId())); } } }; public static Intent getStartIntent(Context context, Category category) { Intent starter = new Intent(context, QuizActivity.class); starter.putExtra(Category.TAG, category.getId()); return starter; } @Override protected void onCreate(Bundle savedInstanceState) { mCountingIdlingResource = new CountingIdlingResource("Quiz"); String categoryId = getIntent().getStringExtra(Category.TAG); mInterpolator = new FastOutSlowInInterpolator(); if (null != savedInstanceState) { mSavedStateIsPlaying = savedInstanceState.getBoolean(STATE_IS_PLAYING); } super.onCreate(savedInstanceState); populate(categoryId); int categoryNameTextSize = getResources() .getDimensionPixelSize(R.dimen.category_item_text_size); int paddingStart = getResources().getDimensionPixelSize(R.dimen.spacing_double); final int startDelay = getResources().getInteger(R.integer.toolbar_transition_duration); ActivityCompat.setEnterSharedElementCallback(this, new TextSharedElementCallback(categoryNameTextSize, paddingStart) { @Override public void onSharedElementStart(List<String> sharedElementNames, List<View> sharedElements, List<View> sharedElementSnapshots) { super.onSharedElementStart(sharedElementNames, sharedElements, sharedElementSnapshots); mToolbarBack.setScaleX(0f); mToolbarBack.setScaleY(0f); } @Override public void onSharedElementEnd(List<String> sharedElementNames, List<View> sharedElements, List<View> sharedElementSnapshots) { super.onSharedElementEnd(sharedElementNames, sharedElements, sharedElementSnapshots); // Make sure to perform this animation after the transition has ended. ViewCompat.animate(mToolbarBack) .setStartDelay(startDelay) .scaleX(1f) .scaleY(1f) .alpha(1f); } }); } @Override protected void onResume() { if (mSavedStateIsPlaying) { mQuizFragment = (QuizFragment) getSupportFragmentManager().findFragmentByTag( FRAGMENT_TAG); if (!mQuizFragment.hasSolvedStateListener()) { mQuizFragment.setSolvedStateListener(getSolvedStateListener()); } findViewById(R.id.quiz_fragment_container).setVisibility(View.VISIBLE); mQuizFab.hide(); mIcon.setVisibility(View.GONE); } else { initQuizFragment(); } super.onResume(); } @Override public void onSaveInstanceState(@NonNull Bundle outState) { outState.putBoolean(STATE_IS_PLAYING, mQuizFab.getVisibility() == View.GONE); super.onSaveInstanceState(outState); } @Override public void onBackPressed() { if (mIcon == null || mQuizFab == null) { // Skip the animation if icon or fab are not initialized. super.onBackPressed(); return; } ViewCompat.animate(mToolbarBack) .scaleX(0f) .scaleY(0f) .alpha(0f) .setDuration(100) .start(); // Scale the icon and fab to 0 size before calling onBackPressed if it exists. ViewCompat.animate(mIcon) .scaleX(.7f) .scaleY(.7f) .alpha(0f) .setInterpolator(mInterpolator) .start(); ViewCompat.animate(mQuizFab) .scaleX(0f) .scaleY(0f) .setInterpolator(mInterpolator) .setStartDelay(100) .setListener(new ViewPropertyAnimatorListenerAdapter() { @SuppressLint("NewApi") @Override public void onAnimationEnd(View view) { if (isFinishing() || (ApiLevelHelper.isAtLeast(Build.VERSION_CODES.JELLY_BEAN_MR1) && isDestroyed())) { return; } QuizActivity.super.onBackPressed(); } }) .start(); } private void startQuizFromClickOn(final View clickedView) { initQuizFragment(); getSupportFragmentManager() .beginTransaction() .replace(R.id.quiz_fragment_container, mQuizFragment, FRAGMENT_TAG) .commit(); final FrameLayout container = (FrameLayout) findViewById(R.id.quiz_fragment_container); container.setBackgroundColor(ContextCompat. getColor(this, mCategory.getTheme().getWindowBackgroundColor())); revealFragmentContainer(clickedView, container); // the toolbar should not have more elevation than the content while playing setToolbarElevation(false); } private void revealFragmentContainer(final View clickedView, final FrameLayout fragmentContainer) { if (ApiLevelHelper.isAtLeast(Build.VERSION_CODES.LOLLIPOP)) { revealFragmentContainerLollipop(clickedView, fragmentContainer); } else { fragmentContainer.setVisibility(View.VISIBLE); clickedView.setVisibility(View.GONE); mIcon.setVisibility(View.GONE); } } @TargetApi(Build.VERSION_CODES.LOLLIPOP) private void revealFragmentContainerLollipop(final View clickedView, final FrameLayout fragmentContainer) { prepareCircularReveal(clickedView, fragmentContainer); ViewCompat.animate(clickedView) .scaleX(0) .scaleY(0) .alpha(0) .setInterpolator(mInterpolator) .setListener(new ViewPropertyAnimatorListenerAdapter() { @Override public void onAnimationEnd(View view) { fragmentContainer.setVisibility(View.VISIBLE); clickedView.setVisibility(View.GONE); } }) .start(); fragmentContainer.setVisibility(View.VISIBLE); AnimatorSet animatorSet = new AnimatorSet(); animatorSet.play(mCircularReveal).with(mColorChange); animatorSet.start(); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) private void prepareCircularReveal(View startView, FrameLayout targetView) { int centerX = (startView.getLeft() + startView.getRight()) / 2; // Subtract the start view's height to adjust for relative coordinates on screen. int centerY = (startView.getTop() + startView.getBottom()) / 2 - startView.getHeight(); float endRadius = (float) Math.hypot(centerX, centerY); mCircularReveal = ViewAnimationUtils.createCircularReveal( targetView, centerX, centerY, startView.getWidth(), endRadius); mCircularReveal.setInterpolator(new FastOutLinearInInterpolator()); mCircularReveal.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mIcon.setVisibility(View.GONE); mCircularReveal.removeListener(this); } }); // Adding a color animation from the FAB's color to transparent creates a dissolve like // effect to the circular reveal. int accentColor = ContextCompat.getColor(this, mCategory.getTheme().getAccentColor()); mColorChange = ObjectAnimator.ofInt(targetView, ViewUtils.FOREGROUND_COLOR, accentColor, Color.TRANSPARENT); mColorChange.setEvaluator(new ArgbEvaluator()); mColorChange.setInterpolator(mInterpolator); } @SuppressLint("NewApi") public void setToolbarElevation(boolean shouldElevate) { if (ApiLevelHelper.isAtLeast(Build.VERSION_CODES.LOLLIPOP)) { mToolbarBack.setElevation(shouldElevate ? getResources().getDimension(R.dimen.elevation_header) : 0); } } private void initQuizFragment() { if (mQuizFragment != null) { return; } mQuizFragment = QuizFragment.newInstance(mCategory.getId(), getSolvedStateListener()); // the toolbar should not have more elevation than the content while playing setToolbarElevation(false); } @NonNull private QuizFragment.SolvedStateListener getSolvedStateListener() { return new QuizFragment.SolvedStateListener() { @Override public void onCategorySolved() { setResultSolved(); setToolbarElevation(true); displayDoneFab(); } private void displayDoneFab() { /* We're re-using the already existing fab and give it some * new values. This has to run delayed due to the queued animation * to hide the fab initially. */ if (null != mCircularReveal && mCircularReveal.isRunning()) { mCircularReveal.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { showQuizFabWithDoneIcon(); mCircularReveal.removeListener(this); } }); } else { showQuizFabWithDoneIcon(); } } private void showQuizFabWithDoneIcon() { mQuizFab.setImageResource(R.drawable.ic_tick); mQuizFab.setId(R.id.quiz_done); mQuizFab.setVisibility(View.VISIBLE); mQuizFab.setScaleX(0f); mQuizFab.setScaleY(0f); ViewCompat.animate(mQuizFab) .scaleX(1) .scaleY(1) .setInterpolator(mInterpolator) .setListener(null) .start(); } }; } private void setResultSolved() { Intent categoryIntent = new Intent(); categoryIntent.putExtra(JsonAttributes.ID, mCategory.getId()); setResult(R.id.solved, categoryIntent); } /** * Proceeds the quiz to it's next state. */ public void proceed() { submitAnswer(); } /** * Solely exists for testing purposes and making sure Espresso does not get confused. */ public void lockIdlingResource() { mCountingIdlingResource.increment(); } private void submitAnswer() { mCountingIdlingResource.decrement(); if (!mQuizFragment.showNextPage()) { mQuizFragment.showSummary(); setResultSolved(); return; } setToolbarElevation(false); } @SuppressLint("NewApi") private void populate(String categoryId) { if (null == categoryId) { Log.w(TAG, "Didn't find a category. Finishing"); finish(); } mCategory = TopekaDatabaseHelper.getCategoryWith(this, categoryId); setTheme(mCategory.getTheme().getStyleId()); if (ApiLevelHelper.isAtLeast(Build.VERSION_CODES.LOLLIPOP)) { Window window = getWindow(); window.setStatusBarColor(ContextCompat.getColor(this, mCategory.getTheme().getPrimaryDarkColor())); } initLayout(mCategory.getId()); initToolbar(mCategory); } private void initLayout(String categoryId) { setContentView(R.layout.activity_quiz); //noinspection PrivateResource mIcon = (ImageView) findViewById(R.id.icon); int resId = getResources().getIdentifier(IMAGE_CATEGORY + categoryId, DRAWABLE, getApplicationContext().getPackageName()); mIcon.setImageResource(resId); mIcon.setImageResource(resId); ViewCompat.animate(mIcon) .scaleX(1) .scaleY(1) .alpha(1) .setInterpolator(mInterpolator) .setStartDelay(300) .start(); mQuizFab = (FloatingActionButton) findViewById(R.id.fab_quiz); mQuizFab.setImageResource(R.drawable.ic_play); if (mSavedStateIsPlaying) { mQuizFab.hide(); } else { mQuizFab.show(); } mQuizFab.setOnClickListener(mOnClickListener); } private void initToolbar(Category category) { mToolbarBack = findViewById(R.id.back); mToolbarBack.setOnClickListener(mOnClickListener); TextView titleView = (TextView) findViewById(R.id.category_title); titleView.setText(category.getName()); titleView.setTextColor(ContextCompat.getColor(this, category.getTheme().getTextPrimaryColor())); if (mSavedStateIsPlaying) { // the toolbar should not have more elevation than the content while playing setToolbarElevation(false); } } @SuppressWarnings("unused") @VisibleForTesting public CountingIdlingResource getCountingIdlingResource() { return mCountingIdlingResource; } }