Java tutorial
/* * 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.view.quiz; 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.design.widget.FloatingActionButton; 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.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.helper.ApiLevelHelper; import com.google.samples.apps.topeka.helper.ViewUtils; import com.google.samples.apps.topeka.model.Avatar; import com.google.samples.apps.topeka.model.AvatarEntity; import com.google.samples.apps.topeka.model.Category; import com.google.samples.apps.topeka.model.CategoryEntity; import com.google.samples.apps.topeka.model.JsonAttributes; import com.google.samples.apps.topeka.model.Theme; import com.google.samples.apps.topeka.model.ThemeEntity; import com.google.samples.apps.topeka.model.mapper.CategoryDataMapper; import com.google.samples.apps.topeka.model.quiz.Quiz; import com.google.samples.apps.topeka.persistence.CategoryRepositoryImpl; import com.google.samples.apps.topeka.persistence.PlayerRepositoryImpl; import com.google.samples.apps.topeka.presenter.quiz.QuizContract; import com.google.samples.apps.topeka.presenter.quiz.QuizPresenter; import com.google.samples.apps.topeka.repository.CategoryRepository; import com.google.samples.apps.topeka.repository.PlayerRepository; 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 implements QuizContract.View, QuizFragment.FragmentViewCreatedListener { 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 FloatingActionButton mQuizFab; private ImageView mIcon; private Animator mCircularReveal; private ObjectAnimator mColorChange; private View mToolbarBack; private boolean mSavedStateIsPlaying; private QuizContract.Presenter presenter; private QuizFragment mQuizFragment; private final View.OnClickListener mOnClickListener = new View.OnClickListener() { @Override public void onClick(final View v) { switch (v.getId()) { case R.id.fab_quiz: presenter.actionStartQuizClicked(); 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) { mInterpolator = new FastOutSlowInInterpolator(); super.onCreate(savedInstanceState); 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); } }); if (null != savedInstanceState) { mSavedStateIsPlaying = savedInstanceState.getBoolean(STATE_IS_PLAYING); } this.presenter = initPresenter(); } private QuizContract.Presenter initPresenter() { String categoryId = getIntent().getStringExtra(Category.TAG); CategoryRepository categoryRepository = new CategoryRepositoryImpl(getApplication()); PlayerRepository playerRepository = new PlayerRepositoryImpl(getApplication()); return new QuizPresenter(categoryId, categoryRepository, playerRepository); } @Override protected void onResume() { this.presenter.setView(this); this.presenter.resume(); if (mSavedStateIsPlaying) { this.presenter.actionQuizPlayShoudBeRestored(); } super.onResume(); } @Override protected void onPause() { this.presenter.pause(); super.onPause(); } private void initQuizFragment(Theme theme) { if (mQuizFragment != null) { return; } mQuizFragment = QuizFragment.newInstance(theme, this); // the toolbar should not have more elevation than the content while playing setToolbarElevation(false); } @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 revealFragmentContainer(final View clickedView, final FrameLayout fragmentContainer, int themeAccentColor) { if (ApiLevelHelper.isAtLeast(Build.VERSION_CODES.LOLLIPOP)) { revealFragmentContainerLollipop(clickedView, fragmentContainer, themeAccentColor); } 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, int themeAccentColor) { prepareCircularReveal(clickedView, fragmentContainer, themeAccentColor); 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 themeAccentColor) { 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. mColorChange = ObjectAnimator.ofInt(targetView, ViewUtils.FOREGROUND_COLOR, themeAccentColor, 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); } } @Override public 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(); } @Override public void setResultSolved(String categoryId) { Intent categoryIntent = new Intent(); categoryIntent.putExtra(JsonAttributes.ID, categoryId); setResult(R.id.solved, categoryIntent); } @Override public void initLayout(ThemeEntity theme) { final Theme androidTheme = Theme.values()[theme.ordinal()]; setTheme(androidTheme.getStyleId()); setContentView(R.layout.activity_quiz); setTheme(androidTheme.getStyleId()); if (ApiLevelHelper.isAtLeast(Build.VERSION_CODES.LOLLIPOP)) { Window window = getWindow(); window.setStatusBarColor(ContextCompat.getColor(this, androidTheme.getPrimaryDarkColor())); } } @Override public void renderQuizCategoryIcon(String categoryId) { 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(); } @Override public void refreshQuizCategoryPlayFab(boolean visible) { mQuizFab = (FloatingActionButton) findViewById(R.id.fab_quiz); mQuizFab.setImageResource(R.drawable.ic_play); if (mSavedStateIsPlaying) { mQuizFab.hide(); } else { mQuizFab.show(); } mQuizFab.setOnClickListener(mOnClickListener); } @Override public void initToolbar(String categoryName, ThemeEntity theme) { mToolbarBack = findViewById(R.id.back); mToolbarBack.setOnClickListener(mOnClickListener); final Theme androidTheme = Theme.values()[theme.ordinal()]; TextView titleView = (TextView) findViewById(R.id.category_title); titleView.setText(categoryName); titleView.setTextColor(ContextCompat.getColor(this, androidTheme.getTextPrimaryColor())); if (mSavedStateIsPlaying) { // the toolbar should not have more elevation than the content while playing setToolbarElevation(false); } } @Override public void renderQuizView(ThemeEntity theme, boolean withFabRevealAnimation) { final Theme androidTheme = Theme.values()[theme.ordinal()]; initQuizFragment(androidTheme); 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, androidTheme.getWindowBackgroundColor())); revealFragmentContainer(findViewById(R.id.fab_quiz), container, androidTheme.getAccentColor()); // the toolbar should not have more elevation than the content while playing setToolbarElevation(false); } @Override public void displayProgress(int currentQuizPosition, int quizPositionsCount) { mQuizFragment.displayProgress(currentQuizPosition, quizPositionsCount); } @Override public void displayAvatar(AvatarEntity avatar) { mQuizFragment.displayAvatar(Avatar.values()[avatar.ordinal()]); } @Override public void loadQuizDetails(CategoryEntity category, int quizPosition) { mQuizFragment.loadQuizView(CategoryDataMapper.from(category), quizPosition); } @Override public void showNextQuizPage() { mQuizFragment.showNextQuizPage(); } @Override public void displaySummary(CategoryEntity category) { mQuizFragment.showSummary(CategoryDataMapper.from(category)); setToolbarElevation(false); displayDoneFab(); } // QuizFragment.FragmentViewCreatedListener @Override public void onQuizViewRendered() { this.presenter.quizViewRendered(); } // todo // called from AbsQuizView instance that casts its context to QuizActivity // should be done more elegant. passing listener in quiz adapter doesn't work because // AbsQuizView instance is recreated somewhere and loses listener instance. public <Q extends Quiz> void notifyAnswerSubmitted(Q mQuiz, boolean answerCorrect) { this.presenter.actionQuizAnswerSubmitted(mQuiz, answerCorrect); } }