diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle b/app/build.gradle index d611944d..d4ddb68c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -32,7 +32,6 @@ android { buildTypes { release { minifyEnabled true - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } @@ -52,6 +51,11 @@ dependencies { androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.1') { exclude module: 'espresso-idling-resource' } + androidTestCompile('com.android.support.test.espresso:espresso-contrib:2.2.1') { + exclude module: 'espresso-core' + exclude module: 'recyclerview-v7' + exclude module: 'support-v4' + } androidTestCompile 'com.android.support.test:rules:0.4' androidTestCompile 'com.android.support.test:runner:0.4' androidTestCompile 'org.hamcrest:hamcrest-core:1.3' diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro deleted file mode 100644 index bb65c6fe..00000000 --- a/app/proguard-rules.pro +++ /dev/null @@ -1,17 +0,0 @@ -# Add project specific ProGuard rules here. -# By default, the flags in this file are appended to flags specified -# in /Applications/Android Studio.app/sdk/tools/proguard/proguard-android.txt -# You can edit the include path and order by changing the proguardFiles -# directive in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# Add any project specific keep options here: - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} diff --git a/app/src/androidTest/java/com/google/samples/apps/topeka/activity/CategorySelectionActivityTest.java b/app/src/androidTest/java/com/google/samples/apps/topeka/activity/CategorySelectionActivityTest.java index e7395f04..248d914d 100644 --- a/app/src/androidTest/java/com/google/samples/apps/topeka/activity/CategorySelectionActivityTest.java +++ b/app/src/androidTest/java/com/google/samples/apps/topeka/activity/CategorySelectionActivityTest.java @@ -19,6 +19,7 @@ import android.content.Context; import android.content.Intent; import android.support.test.InstrumentationRegistry; +import android.support.test.espresso.contrib.RecyclerViewActions; import android.support.test.rule.ActivityTestRule; import android.support.test.runner.AndroidJUnit4; import android.test.suitebuilder.annotation.LargeTest; @@ -37,18 +38,15 @@ import java.util.List; -import static android.support.test.espresso.Espresso.onData; import static android.support.test.espresso.Espresso.onView; import static android.support.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu; import static android.support.test.espresso.action.ViewActions.click; +import static android.support.test.espresso.action.ViewActions.scrollTo; import static android.support.test.espresso.assertion.ViewAssertions.matches; import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; import static android.support.test.espresso.matcher.ViewMatchers.withId; import static android.support.test.espresso.matcher.ViewMatchers.withText; import static junit.framework.Assert.assertFalse; -import static org.hamcrest.Matchers.allOf; -import static org.hamcrest.Matchers.instanceOf; -import static org.hamcrest.Matchers.is; @RunWith(AndroidJUnit4.class) @LargeTest @@ -80,11 +78,14 @@ public void loadCategories() { } @Test - public void allCategories_areDisplayed() { - for (Category category : mCategories) { - onData(allOf(is(instanceOf(Category.class)), is(category))) - .inAdapterView(withId(R.id.categories)) - .check(matches(isDisplayed())); + public void allCategories_areDisplayed() throws InterruptedException { + String categoryName; + for (int i = 0; i < mCategories.size(); i++) { + categoryName = mCategories.get(i).getName(); + onView(withId(R.id.categories)) + .perform(RecyclerViewActions.actionOnItemAtPosition(i, scrollTo())); + onView(withText(categoryName)).check(matches(isDisplayed())); + } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4225553e..1c5ce141 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -25,6 +25,7 @@ android:theme="@style/Topeka"> diff --git a/app/src/main/java/android/util/IntProperty.java b/app/src/main/java/android/util/IntProperty.java deleted file mode 100644 index 064d58ed..00000000 --- a/app/src/main/java/android/util/IntProperty.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * 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 android.util; - -/** - * An implementation of {@link android.util.Property} to be used specifically with fields of type - * int. This type-specific subclass enables performance benefit by allowing - * calls to a {@link #set(Object, Integer) set()} function that takes the primitive - * int type and avoids autoboxing and other overhead associated with the - * Integer class. - * - * @param The class on which the Property is declared. - */ -public abstract class IntProperty extends Property { - - public IntProperty(String name) { - super(Integer.class, name); - } - - /** - * A type-specific override of the {@link #set(Object, Integer)} that is faster when dealing - * with fields of type int. - */ - public abstract void setValue(T object, int value); - - @Override - final public void set(T object, Integer value) { - //noinspection UnnecessaryUnboxing - setValue(object, value.intValue()); - } - -} \ No newline at end of file diff --git a/app/src/main/java/com/google/samples/apps/topeka/activity/CategorySelectionActivity.java b/app/src/main/java/com/google/samples/apps/topeka/activity/CategorySelectionActivity.java index c680c4cf..38b73a15 100644 --- a/app/src/main/java/com/google/samples/apps/topeka/activity/CategorySelectionActivity.java +++ b/app/src/main/java/com/google/samples/apps/topeka/activity/CategorySelectionActivity.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 Google Inc. + * Copyright 2015 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. @@ -19,12 +19,16 @@ import android.app.Activity; import android.content.Context; import android.content.Intent; +import android.os.Build; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v4.app.ActivityCompat; import android.support.v4.app.ActivityOptionsCompat; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; +import android.transition.TransitionInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; @@ -32,6 +36,7 @@ import com.google.samples.apps.topeka.R; import com.google.samples.apps.topeka.fragment.CategorySelectionFragment; +import com.google.samples.apps.topeka.helper.ApiLevelHelper; import com.google.samples.apps.topeka.helper.PreferencesHelper; import com.google.samples.apps.topeka.model.Player; import com.google.samples.apps.topeka.persistence.TopekaDatabaseHelper; @@ -73,6 +78,7 @@ protected void onCreate(Bundle savedInstanceState) { } else { setProgressBarVisibility(View.GONE); } + supportPostponeEnterTransition(); } @Override @@ -100,6 +106,14 @@ public boolean onCreateOptionsMenu(Menu menu) { return true; } + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.category_container); + if (fragment != null) { + fragment.onActivityResult(requestCode, resultCode, data); + } + } + @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { @@ -114,7 +128,11 @@ public boolean onOptionsItemSelected(MenuItem item) { private void signOut() { PreferencesHelper.signOut(this); TopekaDatabaseHelper.reset(this); - SignInActivity.start(this, false, null); + if (ApiLevelHelper.isAtLeast(Build.VERSION_CODES.LOLLIPOP)) { + getWindow().setExitTransition(TransitionInflater.from(this) + .inflateTransition(R.transition.category_enter)); + } + SignInActivity.start(this, false); ActivityCompat.finishAfterTransition(this); } @@ -124,8 +142,13 @@ private String getDisplayName(Player player) { } private void attachCategoryGridFragment() { - getSupportFragmentManager().beginTransaction() - .replace(R.id.quiz_container, CategorySelectionFragment.newInstance()) + FragmentManager supportFragmentManager = getSupportFragmentManager(); + Fragment fragment = supportFragmentManager.findFragmentById(R.id.category_container); + if (!(fragment instanceof CategorySelectionFragment)) { + fragment = CategorySelectionFragment.newInstance(); + } + supportFragmentManager.beginTransaction() + .replace(R.id.category_container, fragment) .commit(); setProgressBarVisibility(View.GONE); } diff --git a/app/src/main/java/com/google/samples/apps/topeka/activity/QuizActivity.java b/app/src/main/java/com/google/samples/apps/topeka/activity/QuizActivity.java index f1f09310..6d961af6 100644 --- a/app/src/main/java/com/google/samples/apps/topeka/activity/QuizActivity.java +++ b/app/src/main/java/com/google/samples/apps/topeka/activity/QuizActivity.java @@ -18,10 +18,14 @@ 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; @@ -32,21 +36,28 @@ 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.support.v7.widget.Toolbar; 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; @@ -55,20 +66,21 @@ 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 int UNDEFINED = -1; private static final String FRAGMENT_TAG = "Quiz"; private Interpolator mInterpolator; - private String mCategoryId; + private Category mCategory; private QuizFragment mQuizFragment; - private Toolbar mToolbar; private FloatingActionButton mQuizFab; private boolean mSavedStateIsPlaying; private ImageView mIcon; private Animator mCircularReveal; + private ObjectAnimator mColorChange; private CountingIdlingResource mCountingIdlingResource; + private View mToolbarBack; + - private View.OnClickListener mOnClickListener = new View.OnClickListener() { + View.OnClickListener mOnClickListener = new View.OnClickListener() { @Override public void onClick(final View v) { switch (v.getId()) { @@ -81,13 +93,9 @@ public void onClick(final View v) { case R.id.quiz_done: ActivityCompat.finishAfterTransition(QuizActivity.this); break; - case UNDEFINED: - final CharSequence contentDescription = v.getContentDescription(); - if (contentDescription != null && contentDescription - .equals(getString(R.string.up))) { - onBackPressed(); - break; - } + case R.id.back: + onBackPressed(); + break; default: throw new UnsupportedOperationException( "OnClick has not been implemented for " + getResources(). @@ -105,13 +113,45 @@ public static Intent getStartIntent(Context context, Category category) { @Override protected void onCreate(Bundle savedInstanceState) { mCountingIdlingResource = new CountingIdlingResource("Quiz"); - mCategoryId = getIntent().getStringExtra(Category.TAG); + String categoryId = getIntent().getStringExtra(Category.TAG); mInterpolator = new FastOutSlowInInterpolator(); if (null != savedInstanceState) { mSavedStateIsPlaying = savedInstanceState.getBoolean(STATE_IS_PLAYING); } super.onCreate(savedInstanceState); - populate(mCategoryId); + 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 sharedElementNames, + List sharedElements, + List sharedElementSnapshots) { + super.onSharedElementStart(sharedElementNames, + sharedElements, + sharedElementSnapshots); + mToolbarBack.setScaleX(0f); + mToolbarBack.setScaleY(0f); + } + + @Override + public void onSharedElementEnd(List sharedElementNames, + List sharedElements, + List 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 @@ -119,7 +159,13 @@ 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(); + } else { + initQuizFragment(); } super.onResume(); } @@ -138,6 +184,13 @@ public void 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) @@ -168,15 +221,18 @@ && isDestroyed())) { private void startQuizFromClickOn(final View clickedView) { initQuizFragment(); - getSupportFragmentManager().beginTransaction() - .replace(R.id.quiz_fragment_container, mQuizFragment, FRAGMENT_TAG).commit(); - final View fragmentContainer = findViewById(R.id.quiz_fragment_container); - revealFragmentContainer(clickedView, fragmentContainer); + getSupportFragmentManager() + .beginTransaction() + .replace(R.id.quiz_fragment_container, mQuizFragment, FRAGMENT_TAG) + .commit(); + final FrameLayout container = (FrameLayout) findViewById(R.id.quiz_fragment_container); + revealFragmentContainer(clickedView, container); // the toolbar should not have more elevation than the content while playing setToolbarElevation(false); } - private void revealFragmentContainer(final View clickedView, final View fragmentContainer) { + private void revealFragmentContainer(final View clickedView, + final FrameLayout fragmentContainer) { if (ApiLevelHelper.isAtLeast(Build.VERSION_CODES.LOLLIPOP)) { revealFragmentContainerLollipop(clickedView, fragmentContainer); } else { @@ -188,30 +244,38 @@ private void revealFragmentContainer(final View clickedView, final View fragment @TargetApi(Build.VERSION_CODES.LOLLIPOP) private void revealFragmentContainerLollipop(final View clickedView, - final View fragmentContainer) { + 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); - mCircularReveal.start(); 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, View targetView) { + private void prepareCircularReveal(View startView, FrameLayout targetView) { int centerX = (startView.getLeft() + startView.getRight()) / 2; - int centerY = (startView.getTop() + startView.getBottom()) / 2; - float finalRadius = (float) Math.hypot((double) centerX, (double) centerY); + // 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((double) centerX, (double) centerY); mCircularReveal = ViewAnimationUtils.createCircularReveal( - targetView, centerX, centerY, 0, finalRadius); + targetView, centerX, centerY, startView.getWidth(), endRadius); + mCircularReveal.setInterpolator(new FastOutLinearInInterpolator()); mCircularReveal.addListener(new AnimatorListenerAdapter() { @Override @@ -220,60 +284,79 @@ public void onAnimationEnd(Animator animation) { 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); } public void setToolbarElevation(boolean shouldElevate) { if (ApiLevelHelper.isAtLeast(Build.VERSION_CODES.LOLLIPOP)) { - mToolbar.setElevation(shouldElevate ? + mToolbarBack.setElevation(shouldElevate ? getResources().getDimension(R.dimen.elevation_header) : 0); } } private void initQuizFragment() { - mQuizFragment = QuizFragment.newInstance(mCategoryId, - new QuizFragment.SolvedStateListener() { - @Override - public void onCategorySolved() { - setToolbarElevation(true); - displayDoneFab(); - } + if (mQuizFragment != null) { + return; + } + mQuizFragment = QuizFragment.newInstance(mCategory.getId(), getSolvedStateListener()); + // the toolbar should not have more elevation than the content while playing + setToolbarElevation(false); + } - 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 { + @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(); - } - }); - if (ApiLevelHelper.isAtLeast(Build.VERSION_CODES.LOLLIPOP)) { - // the toolbar should not have more elevation than the content while playing - setToolbarElevation(false); - } + 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); } /** @@ -294,6 +377,7 @@ private void submitAnswer() { mCountingIdlingResource.decrement(); if (!mQuizFragment.showNextPage()) { mQuizFragment.showSummary(); + setResultSolved(); return; } setToolbarElevation(false); @@ -304,15 +388,15 @@ private void populate(String categoryId) { Log.w(TAG, "Didn't find a category. Finishing"); finish(); } - Category category = TopekaDatabaseHelper.getCategoryWith(this, categoryId); - setTheme(category.getTheme().getStyleId()); + mCategory = TopekaDatabaseHelper.getCategoryWith(this, categoryId); + setTheme(mCategory.getTheme().getStyleId()); if (ApiLevelHelper.isAtLeast(Build.VERSION_CODES.LOLLIPOP)) { Window window = getWindow(); window.setStatusBarColor(ContextCompat.getColor(this, - category.getTheme().getPrimaryDarkColor())); + mCategory.getTheme().getPrimaryDarkColor())); } - initLayout(category.getId()); - initToolbar(category); + initLayout(mCategory.getId()); + initToolbar(mCategory); } private void initLayout(String categoryId) { @@ -341,11 +425,12 @@ private void initLayout(String categoryId) { } private void initToolbar(Category category) { - mToolbar = (Toolbar) findViewById(R.id.toolbar_activity_quiz); - mToolbar.setBackgroundColor( - ContextCompat.getColor(this, category.getTheme().getPrimaryColor())); - mToolbar.setTitle(category.getName()); - mToolbar.setNavigationOnClickListener(mOnClickListener); + 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); diff --git a/app/src/main/java/com/google/samples/apps/topeka/activity/SignInActivity.java b/app/src/main/java/com/google/samples/apps/topeka/activity/SignInActivity.java index f40eb885..e1203cd5 100644 --- a/app/src/main/java/com/google/samples/apps/topeka/activity/SignInActivity.java +++ b/app/src/main/java/com/google/samples/apps/topeka/activity/SignInActivity.java @@ -17,9 +17,10 @@ package com.google.samples.apps.topeka.activity; import android.app.Activity; -import android.app.ActivityOptions; import android.content.Intent; import android.os.Bundle; +import android.support.v4.app.ActivityCompat; +import android.support.v4.app.ActivityOptionsCompat; import android.support.v7.app.AppCompatActivity; import com.google.samples.apps.topeka.R; @@ -30,16 +31,12 @@ public class SignInActivity extends AppCompatActivity { private static final String EXTRA_EDIT = "EDIT"; - public static void start(Activity activity, Boolean edit, ActivityOptions options) { + public static void start(Activity activity, Boolean edit) { Intent starter = new Intent(activity, SignInActivity.class); starter.putExtra(EXTRA_EDIT, edit); - if (options == null) { - activity.startActivity(starter); - activity.overridePendingTransition(android.R.anim.slide_in_left, - android.R.anim.slide_out_right); - } else { - activity.startActivity(starter, options.toBundle()); - } + ActivityCompat.startActivity(activity, + starter, + ActivityOptionsCompat.makeSceneTransitionAnimation(activity).toBundle()); } @Override diff --git a/app/src/main/java/com/google/samples/apps/topeka/adapter/CategoryAdapter.java b/app/src/main/java/com/google/samples/apps/topeka/adapter/CategoryAdapter.java index 68c56af1..e27831dc 100644 --- a/app/src/main/java/com/google/samples/apps/topeka/adapter/CategoryAdapter.java +++ b/app/src/main/java/com/google/samples/apps/topeka/adapter/CategoryAdapter.java @@ -23,12 +23,12 @@ import android.support.annotation.ColorRes; import android.support.v4.content.ContextCompat; import android.support.v4.graphics.drawable.DrawableCompat; +import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.BaseAdapter; import android.widget.ImageView; -import android.widget.LinearLayout; +import android.widget.TextView; import com.google.samples.apps.topeka.R; import com.google.samples.apps.topeka.model.Category; @@ -37,10 +37,7 @@ import java.util.List; -/** - * An adapter that allows display of {@link Category} data. - */ -public class CategoryAdapter extends BaseAdapter { +public class CategoryAdapter extends RecyclerView.Adapter { public static final String DRAWABLE = "drawable"; private static final String ICON_CATEGORY = "icon_category_"; @@ -50,54 +47,78 @@ public class CategoryAdapter extends BaseAdapter { private final Activity mActivity; private List mCategories; + private OnItemClickListener mOnItemClickListener; + + public interface OnItemClickListener { + void onClick(View view, int position); + } + public CategoryAdapter(Activity activity) { - mResources = activity.getResources(); mActivity = activity; + mResources = mActivity.getResources(); mPackageName = mActivity.getPackageName(); mLayoutInflater = LayoutInflater.from(activity.getApplicationContext()); updateCategories(activity); } @Override - public View getView(int position, View convertView, ViewGroup parent) { - if (null == convertView) { - convertView = mLayoutInflater.inflate(R.layout.item_category, parent, false); - convertView.setTag(new CategoryViewHolder((LinearLayout) convertView)); - } - CategoryViewHolder holder = (CategoryViewHolder) convertView.getTag(); - Category category = getItem(position); + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + return new ViewHolder(mLayoutInflater + .inflate(R.layout.item_category, parent, false)); + } + + @Override + public void onBindViewHolder(ViewHolder holder, final int position) { + Category category = mCategories.get(position); Theme theme = category.getTheme(); setCategoryIcon(category, holder.icon); - convertView.setBackgroundColor(getColor(theme.getWindowBackgroundColor())); + holder.itemView.setBackgroundColor(getColor(theme.getWindowBackgroundColor())); holder.title.setText(category.getName()); holder.title.setTextColor(getColor(theme.getTextPrimaryColor())); holder.title.setBackgroundColor(getColor(theme.getPrimaryColor())); - return convertView; + holder.itemView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + mOnItemClickListener.onClick(v, position); + } + }); } @Override - public int getCount() { - return mCategories.size(); + public long getItemId(int position) { + return mCategories.get(position).getId().hashCode(); } @Override + public int getItemCount() { + return mCategories.size(); + } + public Category getItem(int position) { return mCategories.get(position); } - @Override - public long getItemId(int position) { - return mCategories.get(position).getId().hashCode(); + /** + * @see android.support.v7.widget.RecyclerView.Adapter#notifyItemChanged(int) + * @param id Id of changed category. + */ + public final void notifyItemChanged(String id) { + updateCategories(mActivity); + notifyItemChanged(getItemPositionById(id)); } - @Override - public boolean hasStableIds() { - return true; + private int getItemPositionById(String id) { + for (int i = 0; i < mCategories.size(); i++) { + if (mCategories.get(i).getId().equals(id)) { + return i; + } + + } + return -1; } - @Override - public boolean areAllItemsEnabled() { - return false; + public void setOnItemClickListener(OnItemClickListener onItemClickListener) { + mOnItemClickListener = onItemClickListener; } private void setCategoryIcon(Category category, ImageView icon) { @@ -112,12 +133,6 @@ private void setCategoryIcon(Category category, ImageView icon) { } } - @Override - public void notifyDataSetChanged() { - super.notifyDataSetChanged(); - updateCategories(mActivity); - } - private void updateCategories(Activity activity) { mCategories = TopekaDatabaseHelper.getCategories(activity, true); } @@ -144,9 +159,9 @@ private LayerDrawable loadSolvedIcon(Category category, int categoryImageResourc * @return The tinted resource */ private Drawable loadTintedCategoryDrawable(Category category, int categoryImageResource) { - final Drawable categoryIcon = ContextCompat.getDrawable(mActivity, categoryImageResource); - DrawableCompat.setTint(categoryIcon, getColor(category.getTheme().getPrimaryColor())); - return categoryIcon; + final Drawable categoryIcon = ContextCompat + .getDrawable(mActivity, categoryImageResource).mutate(); + return wrapAndTint(categoryIcon, category.getTheme().getPrimaryColor()); } /** @@ -156,8 +171,13 @@ private Drawable loadTintedCategoryDrawable(Category category, int categoryImage */ private Drawable loadTintedDoneDrawable() { final Drawable done = ContextCompat.getDrawable(mActivity, R.drawable.ic_tick); - DrawableCompat.setTint(done, getColor(android.R.color.white)); - return done; + return wrapAndTint(done, android.R.color.white); + } + + private Drawable wrapAndTint(Drawable done, @ColorRes int color) { + Drawable compatDrawable = DrawableCompat.wrap(done); + DrawableCompat.setTint(compatDrawable, getColor(color)); + return compatDrawable; } /** @@ -169,4 +189,16 @@ private Drawable loadTintedDoneDrawable() { private int getColor(@ColorRes int colorRes) { return ContextCompat.getColor(mActivity, colorRes); } + + static class ViewHolder extends RecyclerView.ViewHolder { + + final ImageView icon; + final TextView title; + + public ViewHolder(View container) { + super(container); + icon = (ImageView) container.findViewById(R.id.category_icon); + title = (TextView) container.findViewById(R.id.category_title); + } + } } diff --git a/app/src/main/java/com/google/samples/apps/topeka/adapter/CategoryViewHolder.java b/app/src/main/java/com/google/samples/apps/topeka/adapter/CategoryViewHolder.java deleted file mode 100644 index 34b28bdd..00000000 --- a/app/src/main/java/com/google/samples/apps/topeka/adapter/CategoryViewHolder.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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.adapter; - -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.TextView; - -import com.google.samples.apps.topeka.R; - -public class CategoryViewHolder { - - protected TextView title; - protected ImageView icon; - - public CategoryViewHolder(LinearLayout container) { - icon = (ImageView) container.findViewById(R.id.category_icon); - title = (TextView) container.findViewById(R.id.category_title); - } -} diff --git a/app/src/main/java/com/google/samples/apps/topeka/adapter/ScoreAdapter.java b/app/src/main/java/com/google/samples/apps/topeka/adapter/ScoreAdapter.java index 1d661f45..5ea60572 100644 --- a/app/src/main/java/com/google/samples/apps/topeka/adapter/ScoreAdapter.java +++ b/app/src/main/java/com/google/samples/apps/topeka/adapter/ScoreAdapter.java @@ -126,7 +126,7 @@ private Drawable loadAndTint(Context context, @DrawableRes int drawableId, throw new IllegalArgumentException("The drawable with id " + drawableId + " does not exist"); } - DrawableCompat.setTint(imageDrawable, tintColor); + DrawableCompat.setTint(DrawableCompat.wrap(imageDrawable), tintColor); return imageDrawable; } diff --git a/app/src/main/java/com/google/samples/apps/topeka/fragment/CategorySelectionFragment.java b/app/src/main/java/com/google/samples/apps/topeka/fragment/CategorySelectionFragment.java index 61d0058c..d7988df9 100644 --- a/app/src/main/java/com/google/samples/apps/topeka/fragment/CategorySelectionFragment.java +++ b/app/src/main/java/com/google/samples/apps/topeka/fragment/CategorySelectionFragment.java @@ -17,26 +17,28 @@ package com.google.samples.apps.topeka.fragment; import android.app.Activity; +import android.content.Intent; import android.os.Bundle; import android.support.v4.app.ActivityCompat; import android.support.v4.app.ActivityOptionsCompat; import android.support.v4.app.Fragment; import android.support.v4.util.Pair; +import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.GridView; import com.google.samples.apps.topeka.R; import com.google.samples.apps.topeka.activity.QuizActivity; import com.google.samples.apps.topeka.adapter.CategoryAdapter; import com.google.samples.apps.topeka.helper.TransitionHelper; import com.google.samples.apps.topeka.model.Category; +import com.google.samples.apps.topeka.model.JsonAttributes; +import com.google.samples.apps.topeka.widget.OffsetDecoration; public class CategorySelectionFragment extends Fragment { - private CategoryAdapter mCategoryAdapter; + private CategoryAdapter mAdapter; public static CategorySelectionFragment newInstance() { return new CategorySelectionFragment(); @@ -50,29 +52,42 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, @Override public void onViewCreated(View view, Bundle savedInstanceState) { - setUpQuizGrid((GridView) view.findViewById(R.id.categories)); + setUpQuizGrid((RecyclerView) view.findViewById(R.id.categories)); super.onViewCreated(view, savedInstanceState); } - private void setUpQuizGrid(GridView categoriesView) { - categoriesView.setOnItemClickListener(new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - Activity activity = getActivity(); - startQuizActivityWithTransition(activity, view.findViewById(R.id.category_title), - mCategoryAdapter.getItem(position)); - } - }); - mCategoryAdapter = new CategoryAdapter(getActivity()); - categoriesView.setAdapter(mCategoryAdapter); + private void setUpQuizGrid(RecyclerView categoriesView) { + final int spacing = getContext().getResources() + .getDimensionPixelSize(R.dimen.spacing_nano); + categoriesView.addItemDecoration(new OffsetDecoration(spacing)); + mAdapter = new CategoryAdapter(getActivity()); + mAdapter.setOnItemClickListener( + new CategoryAdapter.OnItemClickListener() { + @Override + public void onClick(View v, int position) { + Activity activity = getActivity(); + startQuizActivityWithTransition(activity, + v.findViewById(R.id.category_title), + mAdapter.getItem(position)); + } + }); + categoriesView.setAdapter(mAdapter); } @Override public void onResume() { - mCategoryAdapter.notifyDataSetChanged(); + getActivity().supportStartPostponedEnterTransition(); super.onResume(); } + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == R.id.request_category && resultCode == R.id.solved) { + mAdapter.notifyItemChanged(data.getStringExtra(JsonAttributes.ID)); + } + super.onActivityResult(requestCode, resultCode, data); + } + private void startQuizActivityWithTransition(Activity activity, View toolbar, Category category) { @@ -83,8 +98,11 @@ private void startQuizActivityWithTransition(Activity activity, View toolbar, // Start the activity with the participants, animating from one to the other. final Bundle transitionBundle = sceneTransitionAnimation.toBundle(); - ActivityCompat.startActivity(getActivity(), - QuizActivity.getStartIntent(activity, category), transitionBundle); + Intent startIntent = QuizActivity.getStartIntent(activity, category); + ActivityCompat.startActivityForResult(activity, + startIntent, + R.id.request_category, + transitionBundle); } } diff --git a/app/src/main/java/com/google/samples/apps/topeka/fragment/QuizFragment.java b/app/src/main/java/com/google/samples/apps/topeka/fragment/QuizFragment.java index e29ebc8d..24d5c963 100644 --- a/app/src/main/java/com/google/samples/apps/topeka/fragment/QuizFragment.java +++ b/app/src/main/java/com/google/samples/apps/topeka/fragment/QuizFragment.java @@ -243,6 +243,16 @@ public void showSummary() { mQuizView.setVisibility(View.GONE); } + public boolean hasSolvedStateListener() { + return mSolvedStateListener != null; + } + public void setSolvedStateListener(SolvedStateListener solvedStateListener) { + mSolvedStateListener = solvedStateListener; + if (mCategory.isSolved() && null != mSolvedStateListener) { + mSolvedStateListener.onCategorySolved(); + } + } + private ScoreAdapter getScoreAdapter() { if (null == mScoreAdapter) { mScoreAdapter = new ScoreAdapter(mCategory); diff --git a/app/src/main/java/com/google/samples/apps/topeka/helper/AnswerHelper.java b/app/src/main/java/com/google/samples/apps/topeka/helper/AnswerHelper.java index 87a5f2c7..5fdbb8cf 100644 --- a/app/src/main/java/com/google/samples/apps/topeka/helper/AnswerHelper.java +++ b/app/src/main/java/com/google/samples/apps/topeka/helper/AnswerHelper.java @@ -31,7 +31,6 @@ private AnswerHelper() { //no instance } - /** * Converts an array of answers to a readable answer. * diff --git a/app/src/main/java/com/google/samples/apps/topeka/helper/ApiLevelHelper.java b/app/src/main/java/com/google/samples/apps/topeka/helper/ApiLevelHelper.java index 7f855541..b1ff5410 100644 --- a/app/src/main/java/com/google/samples/apps/topeka/helper/ApiLevelHelper.java +++ b/app/src/main/java/com/google/samples/apps/topeka/helper/ApiLevelHelper.java @@ -23,6 +23,10 @@ */ public class ApiLevelHelper { + private ApiLevelHelper() { + //no instance + } + /** * Checks if the current api level is at least the provided value. * diff --git a/app/src/main/java/com/google/samples/apps/topeka/helper/TransitionHelper.java b/app/src/main/java/com/google/samples/apps/topeka/helper/TransitionHelper.java index 0f34f45a..4353d1e3 100644 --- a/app/src/main/java/com/google/samples/apps/topeka/helper/TransitionHelper.java +++ b/app/src/main/java/com/google/samples/apps/topeka/helper/TransitionHelper.java @@ -16,7 +16,9 @@ package com.google.samples.apps.topeka.helper; +import android.annotation.TargetApi; import android.app.Activity; +import android.os.Build; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.util.Pair; @@ -31,6 +33,9 @@ */ public class TransitionHelper { + private TransitionHelper() { + //no instance + } /** * Create the transition participants required during a activity transition while * avoiding glitches with the system UI. @@ -40,7 +45,8 @@ public class TransitionHelper { * participant. * @return All transition participants. */ - public static Pair[] createSafeTransitionParticipants(@NonNull Activity activity, + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public static Pair[] createSafeTransitionParticipants(@NonNull Activity activity, boolean includeStatusBar, @Nullable Pair... otherParticipants) { // Avoid system UI glitches as described here: @@ -64,6 +70,7 @@ public static Pair[] createSafeTransitionParticipants(@NonNull Activity activity return participants.toArray(new Pair[participants.size()]); } + @TargetApi(Build.VERSION_CODES.LOLLIPOP) private static void addNonNullViewToTransitionParticipants(View view, List participants) { if (view == null) { return; diff --git a/app/src/main/java/com/google/samples/apps/topeka/helper/ViewUtils.java b/app/src/main/java/com/google/samples/apps/topeka/helper/ViewUtils.java new file mode 100644 index 00000000..5d6904b7 --- /dev/null +++ b/app/src/main/java/com/google/samples/apps/topeka/helper/ViewUtils.java @@ -0,0 +1,157 @@ +/* + * 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.helper; + +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.support.v4.view.ViewCompat; +import android.transition.ChangeBounds; +import android.util.Property; +import android.util.TypedValue; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.TextView; + +public class ViewUtils { + + private ViewUtils() { + //no instance + } + + public static final Property FOREGROUND_COLOR = + new IntProperty("foregroundColor") { + + @Override + public void setValue(FrameLayout layout, int value) { + if (layout.getForeground() instanceof ColorDrawable) { + ((ColorDrawable) layout.getForeground().mutate()).setColor(value); + } else { + layout.setForeground(new ColorDrawable(value)); + } + } + + @Override + public Integer get(FrameLayout layout) { + if (layout.getForeground() instanceof ColorDrawable) { + return ((ColorDrawable) layout.getForeground()).getColor(); + } else { + return Color.TRANSPARENT; + } + } + }; + + public static final Property BACKGROUND_COLOR = + new IntProperty("backgroundColor") { + + @Override + public void setValue(View view, int value) { + view.setBackgroundColor(value); + } + + @Override + public Integer get(View view) { + Drawable d = view.getBackground(); + if (d instanceof ColorDrawable) { + return ((ColorDrawable) d).getColor(); + } + return Color.TRANSPARENT; + } + }; + + /** + * Allows changes to the text size in transitions and animations. + * Using this with something else than {@link ChangeBounds} + * can result in a severe performance penalty due to layout passes. + */ + public static final Property PROPERTY_TEXT_SIZE = + new FloatProperty("textSize") { + @Override + public Float get(TextView view) { + return view.getTextSize(); + } + + @Override + public void setValue(TextView view, float textSize) { + view.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize); + } + }; + + /** + * Allows making changes to the start padding of a view. + * Using this with something else than {@link ChangeBounds} + * can result in a severe performance penalty due to layout passes. + */ + public static final Property PROPERTY_TEXT_PADDING_START = + new IntProperty("paddingStart") { + @Override + public Integer get(TextView view) { + return ViewCompat.getPaddingStart(view); + } + + @Override + public void setValue(TextView view, int paddingStart) { + ViewCompat.setPaddingRelative(view, paddingStart, view.getPaddingTop(), + ViewCompat.getPaddingEnd(view), view.getPaddingBottom()); + } + }; + + public static abstract class IntProperty extends Property { + + public IntProperty(String name) { + super(Integer.class, name); + } + + /** + * A type-specific override of the {@link #set(Object, Integer)} that is faster when + * dealing + * with fields of type int. + */ + public abstract void setValue(T object, int value); + + @Override + final public void set(T object, Integer value) { + //noinspection UnnecessaryUnboxing + setValue(object, value.intValue()); + } + } + + public static abstract class FloatProperty extends Property { + + public FloatProperty(String name) { + super(Float.class, name); + } + + /** + * A type-specific override of the {@link #set(Object, Float)} that is faster when dealing + * with fields of type int. + */ + public abstract void setValue(T object, float value); + + @Override + final public void set(T object, Float value) { + //noinspection UnnecessaryUnboxing + setValue(object, value.floatValue()); + } + } + + public static void setPaddingStart(TextView target, int paddingStart) { + ViewCompat.setPaddingRelative(target, paddingStart, target.getPaddingTop(), + ViewCompat.getPaddingEnd(target), target.getPaddingBottom()); + } + +} diff --git a/app/src/main/java/com/google/samples/apps/topeka/widget/OffsetDecoration.java b/app/src/main/java/com/google/samples/apps/topeka/widget/OffsetDecoration.java new file mode 100644 index 00000000..473f8b72 --- /dev/null +++ b/app/src/main/java/com/google/samples/apps/topeka/widget/OffsetDecoration.java @@ -0,0 +1,39 @@ +/* + * Copyright 2015 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.samples.apps.topeka.widget; + +import android.graphics.Rect; +import android.support.v7.widget.RecyclerView; +import android.view.View; + +public class OffsetDecoration extends RecyclerView.ItemDecoration { + + private final int mOffset; + + public OffsetDecoration(int offset) { + mOffset = offset; + } + + @Override + public void getItemOffsets(Rect outRect, View view, + RecyclerView parent, RecyclerView.State state) { + outRect.left = mOffset; + outRect.right = mOffset; + outRect.bottom = mOffset; + outRect.top = mOffset; + } +} diff --git a/app/src/main/java/com/google/samples/apps/topeka/widget/TextResizeTransition.java b/app/src/main/java/com/google/samples/apps/topeka/widget/TextResizeTransition.java new file mode 100644 index 00000000..601b5428 --- /dev/null +++ b/app/src/main/java/com/google/samples/apps/topeka/widget/TextResizeTransition.java @@ -0,0 +1,106 @@ +/* + * Copyright 2015 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.samples.apps.topeka.widget; + +import com.google.samples.apps.topeka.helper.ViewUtils; + +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.annotation.TargetApi; +import android.content.Context; +import android.os.Build; +import android.support.v4.view.ViewCompat; +import android.transition.Transition; +import android.transition.TransitionValues; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.ViewGroup; +import android.widget.TextView; + +/** + * A transition that resizes text of a TextView. + */ +@TargetApi(Build.VERSION_CODES.KITKAT) +public class TextResizeTransition extends Transition { + + private static final String PROPERTY_NAME_TEXT_RESIZE = + "com.google.samples.apps.topeka.widget:TextResizeTransition:textSize"; + private static final String PROPERTY_NAME_PADDING_RESIZE = + "com.google.samples.apps.topeka.widget:TextResizeTransition:paddingStart"; + + private static final String[] TRANSITION_PROPERTIES = {PROPERTY_NAME_TEXT_RESIZE, + PROPERTY_NAME_PADDING_RESIZE }; + + public TextResizeTransition(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public void captureStartValues(TransitionValues transitionValues) { + captureValues(transitionValues); + } + + @Override + public void captureEndValues(TransitionValues transitionValues) { + captureValues(transitionValues); + } + + private void captureValues(TransitionValues transitionValues) { + if (!(transitionValues.view instanceof TextView)) { + throw new UnsupportedOperationException("Doesn't work on " + + transitionValues.view.getClass().getName()); + } + TextView view = (TextView) transitionValues.view; + transitionValues.values.put(PROPERTY_NAME_TEXT_RESIZE, view.getTextSize()); + transitionValues.values.put(PROPERTY_NAME_PADDING_RESIZE, + ViewCompat.getPaddingStart(view)); + } + + @Override + public String[] getTransitionProperties() { + return TRANSITION_PROPERTIES; + } + + @Override + public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, + TransitionValues endValues) { + if (startValues == null || endValues == null) { + return null; + } + + float initialTextSize = (float) startValues.values.get(PROPERTY_NAME_TEXT_RESIZE); + float targetTextSize = (float) endValues.values.get(PROPERTY_NAME_TEXT_RESIZE); + TextView targetView = (TextView) endValues.view; + targetView.setTextSize(TypedValue.COMPLEX_UNIT_PX, initialTextSize); + + int initialPaddingStart = (int) startValues.values.get(PROPERTY_NAME_PADDING_RESIZE); + int targetPaddingStart = (int) endValues.values.get(PROPERTY_NAME_PADDING_RESIZE); + + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.playTogether( + ObjectAnimator.ofFloat(targetView, + ViewUtils.PROPERTY_TEXT_SIZE, + initialTextSize, + targetTextSize), + ObjectAnimator.ofInt(targetView, + ViewUtils.PROPERTY_TEXT_PADDING_START, + initialPaddingStart, + targetPaddingStart)); + return animatorSet; + } +} diff --git a/app/src/main/java/com/google/samples/apps/topeka/widget/TextSharedElementCallback.java b/app/src/main/java/com/google/samples/apps/topeka/widget/TextSharedElementCallback.java new file mode 100644 index 00000000..407ce411 --- /dev/null +++ b/app/src/main/java/com/google/samples/apps/topeka/widget/TextSharedElementCallback.java @@ -0,0 +1,73 @@ +/* + * Copyright 2015 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.samples.apps.topeka.widget; + +import com.google.samples.apps.topeka.helper.ViewUtils; + +import android.annotation.TargetApi; +import android.os.Build; +import android.support.v4.app.SharedElementCallback; +import android.util.TypedValue; +import android.view.View; +import android.widget.TextView; + +import java.util.List; + +/** + * This callback allows a shared TextView to resize text and start padding during transition. + */ +@TargetApi(Build.VERSION_CODES.LOLLIPOP) +public class TextSharedElementCallback extends SharedElementCallback { + + private final int mInitialPaddingStart; + private float mInitialTextSize; + private float mTargetViewTextSize; + private int mTargetViewPaddingStart; + + public TextSharedElementCallback(float initialTextSize, int initialPaddingStart) { + mInitialTextSize = initialTextSize; + mInitialPaddingStart = initialPaddingStart; + } + + @Override + public void onSharedElementStart(List sharedElementNames, List sharedElements, + List sharedElementSnapshots) { + TextView targetView = (TextView) sharedElements.get(0); + mTargetViewTextSize = targetView.getTextSize(); + mTargetViewPaddingStart = targetView.getPaddingStart(); + // Setup the TextView's start values. + targetView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mInitialTextSize); + ViewUtils.setPaddingStart(targetView, mInitialPaddingStart); + } + + @Override + public void onSharedElementEnd(List sharedElementNames, List sharedElements, + List sharedElementSnapshots) { + TextView initialView = (TextView) sharedElements.get(0); + + // Setup the TextView's end values. + initialView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTargetViewTextSize); + ViewUtils.setPaddingStart(initialView, mTargetViewPaddingStart); + + // Re-measure the TextView (since the text size has changed). + int widthSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); + int heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); + initialView.measure(widthSpec, heightSpec); + initialView.requestLayout(); + } + +} diff --git a/app/src/main/java/com/google/samples/apps/topeka/widget/quiz/AbsQuizView.java b/app/src/main/java/com/google/samples/apps/topeka/widget/quiz/AbsQuizView.java index 22f4c9b0..716a8aee 100644 --- a/app/src/main/java/com/google/samples/apps/topeka/widget/quiz/AbsQuizView.java +++ b/app/src/main/java/com/google/samples/apps/topeka/widget/quiz/AbsQuizView.java @@ -21,7 +21,6 @@ import android.content.Context; import android.content.res.ColorStateList; import android.graphics.Color; -import android.graphics.drawable.ColorDrawable; import android.os.Build; import android.os.Bundle; import android.os.Handler; @@ -30,7 +29,6 @@ import android.support.v4.content.ContextCompat; import android.support.v4.view.MarginLayoutParamsCompat; import android.support.v4.view.animation.LinearOutSlowInInterpolator; -import android.util.IntProperty; import android.util.Property; import android.view.Gravity; import android.view.LayoutInflater; @@ -45,6 +43,7 @@ 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; @@ -61,7 +60,7 @@ *

* * @param The type of {@link com.google.samples.apps.topeka.model.quiz.Quiz} you want to - * display. + * display. */ public abstract class AbsQuizView extends FrameLayout { @@ -81,34 +80,12 @@ public abstract class AbsQuizView extends FrameLayout { private Runnable mMoveOffScreenRunnable; private InputMethodManager mInputMethodManager; - private static final Property FOREGROUND_COLOR = - new IntProperty("foregroundColor") { - - @Override - public void setValue(AbsQuizView layout, int value) { - if (layout.getForeground() instanceof ColorDrawable) { - ((ColorDrawable) layout.getForeground().mutate()).setColor(value); - } else { - layout.setForeground(new ColorDrawable(value)); - } - } - - @Override - public Integer get(AbsQuizView layout) { - if (layout.getForeground() instanceof ColorDrawable) { - return ((ColorDrawable) layout.getForeground()).getColor(); - } else { - return Color.TRANSPARENT; - } - } - }; - /** * Enables creation of views for quizzes. * - * @param context The context for this view. + * @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. + * @param quiz The actual {@link Quiz} that is going to be displayed. */ public AbsQuizView(Context context, Category category, Q quiz) { super(context); @@ -207,6 +184,7 @@ public void onClick(View v) { if (mInputMethodManager.isAcceptingText()) { mInputMethodManager.hideSoftInputFromWindow(v.getWindowToken(), 0); } + mSubmitAnswer.setEnabled(false); } }); } @@ -334,7 +312,7 @@ private void resizeView() { // 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, 250); + resizeViewProperty(View.SCALE_Y, .5f / widthHeightRatio, 300); } private void resizeViewProperty(Property property, @@ -358,7 +336,7 @@ protected void onDetachedFromWindow() { } private void animateForegroundColor(@ColorInt final int targetColor) { - ObjectAnimator animator = ObjectAnimator.ofInt(this, FOREGROUND_COLOR, + ObjectAnimator animator = ObjectAnimator.ofInt(this, ViewUtils.FOREGROUND_COLOR, Color.TRANSPARENT, targetColor); animator.setEvaluator(new ArgbEvaluator()); animator.setStartDelay(FOREGROUND_COLOR_CHANGE_DELAY); diff --git a/app/src/main/res/animator-v21/cross_to_tick_line_1.xml b/app/src/main/res/animator-v21/cross_to_tick_line_1.xml index adf92a88..ae06ab94 100644 --- a/app/src/main/res/animator-v21/cross_to_tick_line_1.xml +++ b/app/src/main/res/animator-v21/cross_to_tick_line_1.xml @@ -19,6 +19,6 @@ android:propertyName="pathData" android:valueFrom="@string/path_cross_line_1" android:valueTo="@string/path_tick_line_1" - android:duration="@integer/duration" + android:duration="@integer/tick_cross_duration" android:interpolator="@android:interpolator/fast_out_slow_in" android:valueType="pathType" /> diff --git a/app/src/main/res/animator-v21/cross_to_tick_line_2.xml b/app/src/main/res/animator-v21/cross_to_tick_line_2.xml index deb4a1d5..190036c5 100644 --- a/app/src/main/res/animator-v21/cross_to_tick_line_2.xml +++ b/app/src/main/res/animator-v21/cross_to_tick_line_2.xml @@ -19,6 +19,6 @@ android:propertyName="pathData" android:valueFrom="@string/path_cross_line_2" android:valueTo="@string/path_tick_line_2" - android:duration="@integer/duration" + android:duration="@integer/tick_cross_duration" android:interpolator="@android:interpolator/fast_out_slow_in" android:valueType="pathType" /> diff --git a/app/src/main/res/animator-v21/rotate_cross_to_tick.xml b/app/src/main/res/animator-v21/rotate_cross_to_tick.xml index d73e2994..72f5c471 100644 --- a/app/src/main/res/animator-v21/rotate_cross_to_tick.xml +++ b/app/src/main/res/animator-v21/rotate_cross_to_tick.xml @@ -19,5 +19,5 @@ android:propertyName="rotation" android:valueFrom="-180" android:valueTo="0" - android:duration="@integer/duration" + android:duration="@integer/tick_cross_duration" android:interpolator="@android:interpolator/fast_out_slow_in" /> diff --git a/app/src/main/res/animator-v21/rotate_tick_to_cross.xml b/app/src/main/res/animator-v21/rotate_tick_to_cross.xml index 437d5fa4..6904f936 100644 --- a/app/src/main/res/animator-v21/rotate_tick_to_cross.xml +++ b/app/src/main/res/animator-v21/rotate_tick_to_cross.xml @@ -19,5 +19,5 @@ android:propertyName="rotation" android:valueFrom="0" android:valueTo="180" - android:duration="@integer/duration" + android:duration="@integer/tick_cross_duration" android:interpolator="@android:interpolator/fast_out_slow_in" /> diff --git a/app/src/main/res/animator-v21/tick_to_cross_line_1.xml b/app/src/main/res/animator-v21/tick_to_cross_line_1.xml index 96118cd3..96b25cd1 100644 --- a/app/src/main/res/animator-v21/tick_to_cross_line_1.xml +++ b/app/src/main/res/animator-v21/tick_to_cross_line_1.xml @@ -19,6 +19,6 @@ android:propertyName="pathData" android:valueFrom="@string/path_tick_line_1" android:valueTo="@string/path_cross_line_1" - android:duration="@integer/duration" + android:duration="@integer/tick_cross_duration" android:interpolator="@android:interpolator/fast_out_slow_in" android:valueType="pathType" /> diff --git a/app/src/main/res/animator-v21/tick_to_cross_line_2.xml b/app/src/main/res/animator-v21/tick_to_cross_line_2.xml index 10cf1924..35094d7a 100644 --- a/app/src/main/res/animator-v21/tick_to_cross_line_2.xml +++ b/app/src/main/res/animator-v21/tick_to_cross_line_2.xml @@ -19,6 +19,6 @@ android:propertyName="pathData" android:valueFrom="@string/path_tick_line_2" android:valueTo="@string/path_cross_line_2" - android:duration="@integer/duration" + android:duration="@integer/tick_cross_duration" android:interpolator="@android:interpolator/fast_out_slow_in" android:valueType="pathType" /> diff --git a/app/src/main/res/drawable-v21/selector_categories.xml b/app/src/main/res/drawable-v21/selector_subtle.xml similarity index 100% rename from app/src/main/res/drawable-v21/selector_categories.xml rename to app/src/main/res/drawable-v21/selector_subtle.xml diff --git a/app/src/main/res/drawable/selector_categories.xml b/app/src/main/res/drawable/selector_subtle.xml similarity index 100% rename from app/src/main/res/drawable/selector_categories.xml rename to app/src/main/res/drawable/selector_subtle.xml diff --git a/app/src/main/res/layout-land/fragment_sign_in.xml b/app/src/main/res/layout-land/fragment_sign_in.xml index 6eb47355..0797fcce 100644 --- a/app/src/main/res/layout-land/fragment_sign_in.xml +++ b/app/src/main/res/layout-land/fragment_sign_in.xml @@ -15,8 +15,10 @@ --> diff --git a/app/src/main/res/layout/activity_category_selection.xml b/app/src/main/res/layout/activity_category_selection.xml index c5806842..85cc1ed4 100644 --- a/app/src/main/res/layout/activity_category_selection.xml +++ b/app/src/main/res/layout/activity_category_selection.xml @@ -18,6 +18,8 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" + android:background="@color/topeka_blank" + android:transitionGroup="false" android:orientation="vertical"> - - + + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/quiz_container" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> - - + - - + android:layout_height="wrap_content"> + + + + android:layout_gravity="center"> + android:scaleY="0.7"/> + android:visibility="invisible"/> + android:transitionName="@string/transition_avatar"/> diff --git a/app/src/main/res/layout/activity_sign_in.xml b/app/src/main/res/layout/activity_sign_in.xml index 58c1230c..87d5372e 100644 --- a/app/src/main/res/layout/activity_sign_in.xml +++ b/app/src/main/res/layout/activity_sign_in.xml @@ -14,10 +14,12 @@ ~ limitations under the License. --> - diff --git a/app/src/main/res/layout/fragment_categories.xml b/app/src/main/res/layout/fragment_categories.xml index 584bb927..2f30e4d5 100644 --- a/app/src/main/res/layout/fragment_categories.xml +++ b/app/src/main/res/layout/fragment_categories.xml @@ -14,20 +14,16 @@ ~ limitations under the License. --> - + android:scrollbars="vertical" + app:layoutManager="android.support.v7.widget.GridLayoutManager" + android:transitionGroup="false" + app:spanCount="2" /> diff --git a/app/src/main/res/layout/fragment_quiz.xml b/app/src/main/res/layout/fragment_quiz.xml index 168c8ccb..f02d54aa 100644 --- a/app/src/main/res/layout/fragment_quiz.xml +++ b/app/src/main/res/layout/fragment_quiz.xml @@ -17,7 +17,8 @@ + android:orientation="vertical" + android:background="?android:attr/windowBackground"> + android:visibility="gone"/> + android:layout_weight="1"/> + android:progressTint="?android:colorAccent"/> + android:layout_height="wrap_content" + android:textColor="?android:attr/textColorPrimary"/> diff --git a/app/src/main/res/layout/fragment_sign_in.xml b/app/src/main/res/layout/fragment_sign_in.xml index 4489179f..0b17107e 100644 --- a/app/src/main/res/layout/fragment_sign_in.xml +++ b/app/src/main/res/layout/fragment_sign_in.xml @@ -15,6 +15,7 @@ --> @@ -34,20 +35,18 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="center" - android:orientation="vertical" - android:paddingEnd="@dimen/spacing_double" - android:paddingStart="@dimen/spacing_double"> + android:orientation="vertical"> diff --git a/app/src/main/res/layout/item_category.xml b/app/src/main/res/layout/item_category.xml index 9f8f753a..8efd8df0 100644 --- a/app/src/main/res/layout/item_category.xml +++ b/app/src/main/res/layout/item_category.xml @@ -14,23 +14,27 @@ ~ limitations under the License. --> - + - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/sign_in_avatars.xml b/app/src/main/res/layout/sign_in_avatars.xml index 27f1dc39..b8aa0112 100644 --- a/app/src/main/res/layout/sign_in_avatars.xml +++ b/app/src/main/res/layout/sign_in_avatars.xml @@ -24,18 +24,23 @@ + android:text="@string/choose_avatar" + android:paddingEnd="@dimen/spacing_double" + android:paddingStart="@dimen/spacing_double" + android:clipToPadding="false" /> + android:verticalSpacing="@dimen/spacing_double" + android:paddingEnd="@dimen/spacing_double" + android:paddingStart="@dimen/spacing_double" + android:clipToPadding="false" /> \ No newline at end of file diff --git a/app/src/main/res/layout/sign_in_username.xml b/app/src/main/res/layout/sign_in_username.xml index 41a7a875..a46adb45 100644 --- a/app/src/main/res/layout/sign_in_username.xml +++ b/app/src/main/res/layout/sign_in_username.xml @@ -24,35 +24,54 @@ + android:text="@string/sign_in" + android:paddingEnd="@dimen/spacing_double" + android:paddingStart="@dimen/spacing_double" + android:clipToPadding="false" /> - + android:paddingEnd="@dimen/spacing_double" + android:paddingBottom="@dimen/spacing_micro" + android:clipToPadding="false" + android:transitionGroup="true"> + + - + android:paddingEnd="@dimen/spacing_double" + android:paddingBottom="@dimen/spacing_micro" + android:clipToPadding="false" + android:transitionGroup="true"> + + \ No newline at end of file diff --git a/app/src/main/res/transition/category_enter.xml b/app/src/main/res/transition/category_enter.xml index dbc1a273..fd4e24ca 100644 --- a/app/src/main/res/transition/category_enter.xml +++ b/app/src/main/res/transition/category_enter.xml @@ -15,9 +15,16 @@ --> - - - - - - \ No newline at end of file + + + + + + + + + + + + + diff --git a/app/src/main/res/transition/category_exit.xml b/app/src/main/res/transition/category_exit.xml new file mode 100644 index 00000000..a67ba032 --- /dev/null +++ b/app/src/main/res/transition/category_exit.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/transition/quiz_enter.xml b/app/src/main/res/transition/category_shared_enter.xml similarity index 88% rename from app/src/main/res/transition/quiz_enter.xml rename to app/src/main/res/transition/category_shared_enter.xml index 52eb8c02..42e7f1b7 100644 --- a/app/src/main/res/transition/quiz_enter.xml +++ b/app/src/main/res/transition/category_shared_enter.xml @@ -15,10 +15,7 @@ --> - - - - + \ No newline at end of file diff --git a/app/src/main/res/transition/quiz_shared_enter.xml b/app/src/main/res/transition/quiz_shared_enter.xml new file mode 100644 index 00000000..e488c2d3 --- /dev/null +++ b/app/src/main/res/transition/quiz_shared_enter.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/transition/signin_enter.xml b/app/src/main/res/transition/signin_enter.xml new file mode 100644 index 00000000..078c31ea --- /dev/null +++ b/app/src/main/res/transition/signin_enter.xml @@ -0,0 +1,22 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/transition/signin_exit.xml b/app/src/main/res/transition/signin_exit.xml new file mode 100644 index 00000000..b446049c --- /dev/null +++ b/app/src/main/res/transition/signin_exit.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-v21/styles.xml b/app/src/main/res/values-v21/styles.xml index c0f2d5a0..13dcf8f4 100644 --- a/app/src/main/res/values-v21/styles.xml +++ b/app/src/main/res/values-v21/styles.xml @@ -1,4 +1,3 @@ - + 2dp 4dp 8dp 16dp @@ -32,4 +33,5 @@ 4dp 18sp + 14sp diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml index f8bc9f76..b4b8ac2f 100644 --- a/app/src/main/res/values/ids.xml +++ b/app/src/main/res/values/ids.xml @@ -20,4 +20,7 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 2416129d..7ee26920 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -28,7 +28,6 @@ @color/topeka_primary_dark @color/text_dark @style/Topeka.CompoundButton.Radio - true @style/Topeka.CompoundButton @color/light_grey @color/light_grey @@ -36,6 +35,7 @@ + + diff --git a/app/src/main/res/values/tick_cross.xml b/app/src/main/res/values/tick_cross.xml index 785e81ad..1b49cba0 100644 --- a/app/src/main/res/values/tick_cross.xml +++ b/app/src/main/res/values/tick_cross.xml @@ -36,6 +36,6 @@ @android:color/black - 450 + 450 diff --git a/app/src/main/res/values/transition_names.xml b/app/src/main/res/values/transition_names.xml index e028cb70..a4cdf229 100644 --- a/app/src/main/res/values/transition_names.xml +++ b/app/src/main/res/values/transition_names.xml @@ -16,6 +16,5 @@ AvatarTransition - BackgroundTransition ToolbarTransition \ No newline at end of file diff --git a/app/src/main/res/values/transitions.xml b/app/src/main/res/values/transitions.xml new file mode 100644 index 00000000..1e70ce7e --- /dev/null +++ b/app/src/main/res/values/transitions.xml @@ -0,0 +1,20 @@ + + + + + 350 + \ No newline at end of file