/*
* 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.widget.quiz;
import android.animation.ArgbEvaluator;
import android.animation.ObjectAnimator;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.ColorInt;
import android.support.v4.content.ContextCompat;
import android.support.v4.view.MarginLayoutParamsCompat;
import android.support.v4.view.animation.LinearOutSlowInInterpolator;
import android.util.Property;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Interpolator;
import android.view.inputmethod.InputMethodManager;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.google.samples.apps.topeka.R;
import com.google.samples.apps.topeka.activity.QuizActivity;
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.quiz.Quiz;
import com.google.samples.apps.topeka.widget.fab.CheckableFab;
/**
* This is the base class for displaying a {@link com.google.samples.apps.topeka.model.quiz.Quiz}.
* <p>
* Subclasses need to implement {@link AbsQuizView#createQuizContentView()}
* in order to allow solution of a quiz.
* </p>
* <p>
* Also {@link AbsQuizView#allowAnswer(boolean)} needs to be called with
* <code>true</code> in order to mark the quiz solved.
* </p>
*
* @param <Q> The type of {@link com.google.samples.apps.topeka.model.quiz.Quiz} you want to
* display.
*/
public abstract class AbsQuizView<Q extends Quiz> extends FrameLayout {
private static final int ANSWER_HIDE_DELAY = 500;
private static final int FOREGROUND_COLOR_CHANGE_DELAY = 750;
private final int mSpacingDouble;
private final LayoutInflater mLayoutInflater;
private final Category mCategory;
private final Q mQuiz;
private final Interpolator mLinearOutSlowInInterpolator;
private final Handler mHandler;
private final InputMethodManager mInputMethodManager;
private boolean mAnswered;
private TextView mQuestionView;
private CheckableFab mSubmitAnswer;
private Runnable mHideFabRunnable;
private Runnable mMoveOffScreenRunnable;
/**
* Enables creation of views for quizzes.
*
* @param context The context for this view.
* @param category The {@link Category} this view is running in.
* @param quiz The actual {@link Quiz} that is going to be displayed.
*/
public AbsQuizView(Context context, Category category, Q quiz) {
super(context);
mQuiz = quiz;
mCategory = category;
mSpacingDouble = getResources().getDimensionPixelSize(R.dimen.spacing_double);
mLayoutInflater = LayoutInflater.from(context);
mSubmitAnswer = getSubmitButton();
mLinearOutSlowInInterpolator = new LinearOutSlowInInterpolator();
mHandler = new Handler();
mInputMethodManager = (InputMethodManager) context.getSystemService
(Context.INPUT_METHOD_SERVICE);
setId(quiz.getId());
setUpQuestionView();
LinearLayout container = createContainerLayout(context);
View quizContentView = getInitializedContentView();
addContentView(container, quizContentView);
addOnLayoutChangeListener(new OnLayoutChangeListener() {
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom,
int oldLeft,
int oldTop, int oldRight, int oldBottom) {
removeOnLayoutChangeListener(this);
addFloatingActionButton();
}
});
}
/**
* Sets the behaviour for all question views.
*/
private void setUpQuestionView() {
mQuestionView = (TextView) mLayoutInflater.inflate(R.layout.question, this, false);
mQuestionView.setBackgroundColor(ContextCompat.getColor(getContext(),
mCategory.getTheme().getPrimaryColor()));
mQuestionView.setText(getQuiz().getQuestion());
}
private LinearLayout createContainerLayout(Context context) {
LinearLayout container = new LinearLayout(context);
container.setId(R.id.absQuizViewContainer);
container.setOrientation(LinearLayout.VERTICAL);
return container;
}
private View getInitializedContentView() {
View quizContentView = createQuizContentView();
quizContentView.setId(R.id.quiz_content);
quizContentView.setSaveEnabled(true);
setDefaultPadding(quizContentView);
if (quizContentView instanceof ViewGroup) {
((ViewGroup) quizContentView).setClipToPadding(false);
}
setMinHeightInternal(quizContentView);
return quizContentView;
}
private void addContentView(LinearLayout container, View quizContentView) {
LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT,
LayoutParams.WRAP_CONTENT);
container.addView(mQuestionView, layoutParams);
container.addView(quizContentView, layoutParams);
addView(container, layoutParams);
}
private void addFloatingActionButton() {
final int fabSize = getResources().getDimensionPixelSize(R.dimen.size_fab);
int bottomOfQuestionView = findViewById(R.id.question_view).getBottom();
final LayoutParams fabLayoutParams = new LayoutParams(fabSize, fabSize,
Gravity.END | Gravity.TOP);
final int halfAFab = fabSize / 2;
fabLayoutParams.setMargins(0, // left
bottomOfQuestionView - halfAFab, //top
0, // right
mSpacingDouble); // bottom
MarginLayoutParamsCompat.setMarginEnd(fabLayoutParams, mSpacingDouble);
if (ApiLevelHelper.isLowerThan(Build.VERSION_CODES.LOLLIPOP)) {
// Account for the fab's emulated shadow.
fabLayoutParams.topMargin -= (mSubmitAnswer.getPaddingTop() / 2);
}
addView(mSubmitAnswer, fabLayoutParams);
}
private CheckableFab getSubmitButton() {
if (null == mSubmitAnswer) {
mSubmitAnswer = (CheckableFab) getLayoutInflater()
.inflate(R.layout.answer_submit, this, false);
mSubmitAnswer.hide();
mSubmitAnswer.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
submitAnswer(v);
if (mInputMethodManager.isAcceptingText()) {
mInputMethodManager.hideSoftInputFromWindow(v.getWindowToken(), 0);
}
mSubmitAnswer.setEnabled(false);
}
});
}
return mSubmitAnswer;
}
private void setDefaultPadding(View view) {
view.setPadding(mSpacingDouble, mSpacingDouble, mSpacingDouble, mSpacingDouble);
}
protected LayoutInflater getLayoutInflater() {
return mLayoutInflater;
}
/**
* Implementations should create the content view for the type of
* {@link com.google.samples.apps.topeka.model.quiz.Quiz} they want to display.
*
* @return the created view to solve the quiz.
*/
protected abstract View createQuizContentView();
/**
* Implementations must make sure that the answer provided is evaluated and correctly rated.
*
* @return <code>true</code> if the question has been correctly answered, else
* <code>false</code>.
*/
protected abstract boolean isAnswerCorrect();
/**
* Save the user input to a bundle for orientation changes.
*
* @return The bundle containing the user's input.
*/
public abstract Bundle getUserInput();
/**
* Restore the user's input.
*
* @param savedInput The input that the user made in a prior instance of this view.
*/
public abstract void setUserInput(Bundle savedInput);
public Q getQuiz() {
return mQuiz;
}
protected boolean isAnswered() {
return mAnswered;
}
/**
* Sets the quiz to answered or unanswered.
*
* @param answered <code>true</code> if an answer was selected, else <code>false</code>.
*/
protected void allowAnswer(final boolean answered) {
if (null != mSubmitAnswer) {
if (answered) {
mSubmitAnswer.show();
} else {
mSubmitAnswer.hide();
}
mAnswered = answered;
}
}
/**
* Sets the quiz to answered if it not already has been answered.
* Otherwise does nothing.
*/
protected void allowAnswer() {
if (!isAnswered()) {
allowAnswer(true);
}
}
/**
* Allows children to submit an answer via code.
*/
protected void submitAnswer() {
submitAnswer(findViewById(R.id.submitAnswer));
}
@SuppressWarnings("UnusedParameters")
private void submitAnswer(final View v) {
final boolean answerCorrect = isAnswerCorrect();
mQuiz.setSolved(true);
performScoreAnimation(answerCorrect);
}
/**
* Animates the view nicely when the answer has been submitted.
*
* @param answerCorrect <code>true</code> if the answer was correct, else <code>false</code>.
*/
private void performScoreAnimation(final boolean answerCorrect) {
((QuizActivity) getContext()).lockIdlingResource();
// Decide which background color to use.
final int backgroundColor = ContextCompat.getColor(getContext(),
answerCorrect ? R.color.green : R.color.red);
adjustFab(answerCorrect, backgroundColor);
resizeView();
moveViewOffScreen(answerCorrect);
// Animate the foreground color to match the background color.
// This overlays all content within the current view.
animateForegroundColor(backgroundColor);
}
@SuppressLint("NewApi")
private void adjustFab(boolean answerCorrect, int backgroundColor) {
mSubmitAnswer.setChecked(answerCorrect);
mSubmitAnswer.setBackgroundTintList(ColorStateList.valueOf(backgroundColor));
mHideFabRunnable = new Runnable() {
@Override
public void run() {
mSubmitAnswer.hide();
}
};
mHandler.postDelayed(mHideFabRunnable, ANSWER_HIDE_DELAY);
}
private void resizeView() {
final float widthHeightRatio = (float) getHeight() / (float) getWidth();
// Animate X and Y scaling separately to allow different start delays.
// object animators for x and y with different durations and then run them independently
resizeViewProperty(View.SCALE_X, .5f, 200);
resizeViewProperty(View.SCALE_Y, .5f / widthHeightRatio, 300);
}
private void resizeViewProperty(Property<View, Float> property,
float targetScale, int durationOffset) {
ObjectAnimator animator = ObjectAnimator.ofFloat(this, property,
1f, targetScale);
animator.setInterpolator(mLinearOutSlowInInterpolator);
animator.setStartDelay(FOREGROUND_COLOR_CHANGE_DELAY + durationOffset);
animator.start();
}
@Override
protected void onDetachedFromWindow() {
if (mHideFabRunnable != null) {
mHandler.removeCallbacks(mHideFabRunnable);
}
if (mMoveOffScreenRunnable != null) {
mHandler.removeCallbacks(mMoveOffScreenRunnable);
}
super.onDetachedFromWindow();
}
private void animateForegroundColor(@ColorInt final int targetColor) {
ObjectAnimator animator = ObjectAnimator.ofInt(this, ViewUtils.FOREGROUND_COLOR,
Color.TRANSPARENT, targetColor);
animator.setEvaluator(new ArgbEvaluator());
animator.setStartDelay(FOREGROUND_COLOR_CHANGE_DELAY);
animator.start();
}
private void moveViewOffScreen(final boolean answerCorrect) {
// Move the current view off the screen.
mMoveOffScreenRunnable = new Runnable() {
@Override
public void run() {
mCategory.setScore(getQuiz(), answerCorrect);
if (getContext() instanceof QuizActivity) {
((QuizActivity) getContext()).proceed();
}
}
};
mHandler.postDelayed(mMoveOffScreenRunnable,
FOREGROUND_COLOR_CHANGE_DELAY * 2);
}
private void setMinHeightInternal(View view) {
view.setMinimumHeight(getResources().getDimensionPixelSize(R.dimen.min_height_question));
}
}