diff --git a/.google/packaging.yaml b/.google/packaging.yaml index 8cf1302c..baace339 100644 --- a/.google/packaging.yaml +++ b/.google/packaging.yaml @@ -26,5 +26,4 @@ apiRefs: - android:android.view.ViewOutlineProvider - android:android.view.ContextThemeWrapper - license: apache2-google diff --git a/README.markdown b/README.markdown index f875bbc6..1e92cd72 100644 --- a/README.markdown +++ b/README.markdown @@ -4,14 +4,29 @@ A fun to play quiz that showcases material design on Android ### Introduction -Material design is a new system for visual, interaction and motion design. We -originally launched the [Topeka web app](https://github.com/Polymer/topeka) -as an Open Source example of material design on the web. - +Material design is a new system for visual, interaction and motion design. The Android version of Topeka demonstrates that the same branding and material design principles can be used to create a consistent experience across -platforms. You can read more about it on the -[Android Developers +platforms. + +We originally launched the [Topeka web app](https://github.com/Polymer/topeka) +as an Open Source example of material design on the web. + +The current release of Topeka is available to users down to API level 16 aka [Jelly +Bean](http://developer.android.com/about/versions/android-4.1.html). +This is being accomplished by utilizing several [support +libraries](https://developer.android.com/tools/support-library/index.html). +Especially +[AppCompat](https://developer.android.com/tools/support-library/features.html#v7-appcompat) +and the [design support +library](https://developer.android.com/tools/support-library/features.html#design) +play important roles. + +Topeka also features a set of [Espresso +tests](http://google.github.io/android-testing-support-library) which can be +executed with the `connectedAndroidTest` gradle task. + +You can read more about the project on the [Android Developers blog](http://android-developers.blogspot.co.uk/2015/06/more-material-design-with-topeka-for_16.html). ### Screenshots @@ -22,7 +37,8 @@ blog](http://android-developers.blogspot.co.uk/2015/06/more-material-design-with ### Getting Started -Clone this repository, enter the top level directory and run ./gradlew tasks to get an overview of all the tasks available for this project. +Clone this repository, enter the top level directory and run ./gradlew tasks +to get an overview of all the tasks available for this project. Some important tasks are: diff --git a/app/build.gradle b/app/build.gradle index 294a3316..b89b44ce 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -22,7 +22,7 @@ android { defaultConfig { applicationId "com.google.samples.apps.topeka" - minSdkVersion 21 + minSdkVersion 16 targetSdkVersion 23 versionCode 1 versionName "1.0" @@ -31,7 +31,7 @@ android { buildTypes { release { - minifyEnabled false + minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } @@ -46,7 +46,13 @@ dependencies { compile 'com.android.support:cardview-v7:23.0.1' compile 'com.android.support:design:23.0.1' compile 'com.android.support:recyclerview-v7:23.0.1' + compile 'com.android.support.test.espresso:espresso-idling-resource:2.2.1' testCompile 'junit:junit:4.12' - androidTestCompile 'com.android.support.test:testing-support-lib:0.1' + + androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.1') { + exclude module: 'espresso-idling-resource' + } + 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/src/androidTest/java/com/google/samples/apps/topeka/activity/CategorySelectionActivityTest.java b/app/src/androidTest/java/com/google/samples/apps/topeka/activity/CategorySelectionActivityTest.java new file mode 100644 index 00000000..e7395f04 --- /dev/null +++ b/app/src/androidTest/java/com/google/samples/apps/topeka/activity/CategorySelectionActivityTest.java @@ -0,0 +1,97 @@ +/* + * 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.activity; + +import android.content.Context; +import android.content.Intent; +import android.support.test.InstrumentationRegistry; +import android.support.test.rule.ActivityTestRule; +import android.support.test.runner.AndroidJUnit4; +import android.test.suitebuilder.annotation.LargeTest; + +import com.google.samples.apps.topeka.R; +import com.google.samples.apps.topeka.helper.PreferencesHelper; +import com.google.samples.apps.topeka.model.Avatar; +import com.google.samples.apps.topeka.model.Category; +import com.google.samples.apps.topeka.model.Player; +import com.google.samples.apps.topeka.persistence.TopekaDatabaseHelper; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +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.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 +public class CategorySelectionActivityTest { + + private Context mTargetContext; + private List mCategories; + + @Rule + public ActivityTestRule mActivityRule = + new ActivityTestRule(CategorySelectionActivity.class) { + + @Override + protected void beforeActivityLaunched() { + PreferencesHelper.signOut(InstrumentationRegistry.getTargetContext()); + } + + @Override + protected Intent getActivityIntent() { + mTargetContext = InstrumentationRegistry.getTargetContext(); + final Player player = new Player("Zaphod", "B", Avatar.EIGHT); + return CategorySelectionActivity.getStartIntent(mTargetContext, player); + } + }; + + @Before + public void loadCategories() { + mCategories = TopekaDatabaseHelper.getCategories(mTargetContext, false); + } + + @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())); + } + } + + @Test + public void signOut() { + openActionBarOverflowOrOptionsMenu(mTargetContext); + onView(withText(R.string.sign_out)).perform(click()); + assertFalse(PreferencesHelper.isSignedIn(mTargetContext)); + } +} diff --git a/app/src/androidTest/java/com/google/samples/apps/topeka/activity/SignInActivityTest.java b/app/src/androidTest/java/com/google/samples/apps/topeka/activity/SignInActivityTest.java new file mode 100644 index 00000000..cfea81d8 --- /dev/null +++ b/app/src/androidTest/java/com/google/samples/apps/topeka/activity/SignInActivityTest.java @@ -0,0 +1,84 @@ +/* + * 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.activity; + +import android.support.test.InstrumentationRegistry; +import android.support.test.rule.ActivityTestRule; +import android.support.test.runner.AndroidJUnit4; +import android.test.suitebuilder.annotation.LargeTest; + +import com.google.samples.apps.topeka.R; +import com.google.samples.apps.topeka.helper.PreferencesHelper; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import static android.support.test.espresso.Espresso.onView; +import static android.support.test.espresso.action.ViewActions.closeSoftKeyboard; +import static android.support.test.espresso.action.ViewActions.typeText; +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 org.hamcrest.Matchers.not; + +@RunWith(AndroidJUnit4.class) +@LargeTest +public class SignInActivityTest { + + private static final String TEST_FIRST_NAME = "Zaphod"; + private static final String TEST_LAST_INITIAL = "B"; + + @Rule + public ActivityTestRule mActivityRule = + new ActivityTestRule(SignInActivity.class) { + @Override + protected void beforeActivityLaunched() { + PreferencesHelper.signOut(InstrumentationRegistry.getTargetContext()); + } + }; + + @Before + public void clearPreferences() throws Exception { + PreferencesHelper.signOut(InstrumentationRegistry.getTargetContext()); + } + + @Test + public void checkFab_initiallyNotDisplayed() { + onView(withId(R.id.done)).check(matches(not(isDisplayed()))); + } + + @Test + public void signIn_performSuccessful() { + inputData(); + onView(withId(R.id.done)).check(matches(isDisplayed())); + } + + @Test + public void signIn_withLongLastName() { + inputData(); + onView(withId(R.id.last_initial)).perform(typeText("somelongtext"), closeSoftKeyboard()); + onView(withId(R.id.last_initial)).check(matches(withText(TEST_LAST_INITIAL))); + } + + private void inputData() { + onView(withId(R.id.first_name)).perform(typeText(TEST_FIRST_NAME), closeSoftKeyboard()); + onView(withId(R.id.last_initial)).perform(typeText(TEST_LAST_INITIAL), closeSoftKeyboard()); + } +} diff --git a/app/src/androidTest/java/com/google/samples/apps/topeka/activity/quiz/BaseQuizActivityTest.java b/app/src/androidTest/java/com/google/samples/apps/topeka/activity/quiz/BaseQuizActivityTest.java new file mode 100644 index 00000000..6c8fe0e0 --- /dev/null +++ b/app/src/androidTest/java/com/google/samples/apps/topeka/activity/quiz/BaseQuizActivityTest.java @@ -0,0 +1,116 @@ +/* + * 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.activity.quiz; + +import android.content.Context; +import android.content.Intent; +import android.support.test.InstrumentationRegistry; +import android.support.test.espresso.Espresso; +import android.support.test.rule.ActivityTestRule; +import android.support.test.runner.AndroidJUnit4; +import android.test.suitebuilder.annotation.LargeTest; + +import com.google.samples.apps.topeka.R; +import com.google.samples.apps.topeka.activity.QuizActivity; +import com.google.samples.apps.topeka.helper.PreferencesHelper; +import com.google.samples.apps.topeka.helper.SolveQuizHelper; +import com.google.samples.apps.topeka.model.Avatar; +import com.google.samples.apps.topeka.model.Category; +import com.google.samples.apps.topeka.model.Player; +import com.google.samples.apps.topeka.model.quiz.Quiz; +import com.google.samples.apps.topeka.persistence.TopekaDatabaseHelper; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.List; + +import static android.support.test.espresso.Espresso.onView; +import static android.support.test.espresso.action.ViewActions.click; +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 org.hamcrest.Matchers.allOf; + +@RunWith(AndroidJUnit4.class) +@LargeTest +public abstract class BaseQuizActivityTest { + + private List mCategories; + @Rule + public ActivityTestRule mActivityRule = + new ActivityTestRule(QuizActivity.class) { + @Override + protected void beforeActivityLaunched() { + Context targetContext = InstrumentationRegistry.getTargetContext(); + PreferencesHelper.signOut(targetContext); + TopekaDatabaseHelper.reset(targetContext); + PreferencesHelper.writeToPreferences(targetContext, + new Player("Zaphod", "B", Avatar.FIVE)); + } + + @Override + protected Intent getActivityIntent() { + Context targetContext = InstrumentationRegistry.getTargetContext(); + mCategories = TopekaDatabaseHelper.getCategories(targetContext, false); + return QuizActivity.getStartIntent(targetContext, + getCurrentCategory()); + } + }; + + abstract int getCategory(); + + @Before + public void registerIdlingResources() { + Espresso.registerIdlingResources(mActivityRule.getActivity().getCountingIdlingResource()); + } + + @Test + public void categoryName_isDisplayed() { + onView(withText(getCurrentCategory().getName())).check(matches(isDisplayed())); + } + + @Test + public void category_solveCorrectly() { + testCategory(); + } + + protected void testCategory() { + final Category category = getCurrentCategory(); + onView(withId(R.id.fab_quiz)).perform(click()); + for (Quiz quiz : category.getQuizzes()) { + SolveQuizHelper.solveQuiz(quiz); + onView(allOf(withId(R.id.submitAnswer), isDisplayed())) + .check(matches(isDisplayed())) + .perform(click()); + } + } + + private Category getCurrentCategory() { + return mCategories.get(getCategory()); + } + + @After + public void unregisterIdlingResources() { + Espresso.unregisterIdlingResources(mActivityRule.getActivity().getCountingIdlingResource()); + } + +} diff --git a/app/src/androidTest/java/com/google/samples/apps/topeka/activity/quiz/EntertainmentQuizTest.java b/app/src/androidTest/java/com/google/samples/apps/topeka/activity/quiz/EntertainmentQuizTest.java new file mode 100644 index 00000000..769d8695 --- /dev/null +++ b/app/src/androidTest/java/com/google/samples/apps/topeka/activity/quiz/EntertainmentQuizTest.java @@ -0,0 +1,25 @@ +/* + * 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.activity.quiz; + +public class EntertainmentQuizTest extends BaseQuizActivityTest { + + @Override + int getCategory() { + return 7; + } +} diff --git a/app/src/androidTest/java/com/google/samples/apps/topeka/activity/quiz/FoodAndDrinkQuizTest.java b/app/src/androidTest/java/com/google/samples/apps/topeka/activity/quiz/FoodAndDrinkQuizTest.java new file mode 100644 index 00000000..4bd2fd26 --- /dev/null +++ b/app/src/androidTest/java/com/google/samples/apps/topeka/activity/quiz/FoodAndDrinkQuizTest.java @@ -0,0 +1,25 @@ +/* + * 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.activity.quiz; + +public class FoodAndDrinkQuizTest extends BaseQuizActivityTest { + + @Override + int getCategory() { + return 0; + } +} diff --git a/app/src/androidTest/java/com/google/samples/apps/topeka/activity/quiz/GeneralKnowledgeQuizTest.java b/app/src/androidTest/java/com/google/samples/apps/topeka/activity/quiz/GeneralKnowledgeQuizTest.java new file mode 100644 index 00000000..63e18efc --- /dev/null +++ b/app/src/androidTest/java/com/google/samples/apps/topeka/activity/quiz/GeneralKnowledgeQuizTest.java @@ -0,0 +1,25 @@ +/* + * 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.activity.quiz; + +public class GeneralKnowledgeQuizTest extends BaseQuizActivityTest { + + @Override + int getCategory() { + return 1; + } +} diff --git a/app/src/androidTest/java/com/google/samples/apps/topeka/activity/quiz/GeographyQuizTest.java b/app/src/androidTest/java/com/google/samples/apps/topeka/activity/quiz/GeographyQuizTest.java new file mode 100644 index 00000000..f4dd28d7 --- /dev/null +++ b/app/src/androidTest/java/com/google/samples/apps/topeka/activity/quiz/GeographyQuizTest.java @@ -0,0 +1,25 @@ +/* + * 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.activity.quiz; + +public class GeographyQuizTest extends BaseQuizActivityTest { + + @Override + int getCategory() { + return 3; + } +} diff --git a/app/src/androidTest/java/com/google/samples/apps/topeka/activity/quiz/HistoryQuizTest.java b/app/src/androidTest/java/com/google/samples/apps/topeka/activity/quiz/HistoryQuizTest.java new file mode 100644 index 00000000..39a1b00d --- /dev/null +++ b/app/src/androidTest/java/com/google/samples/apps/topeka/activity/quiz/HistoryQuizTest.java @@ -0,0 +1,25 @@ +/* + * 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.activity.quiz; + +public class HistoryQuizTest extends BaseQuizActivityTest { + + @Override + int getCategory() { + return 2; + } +} diff --git a/app/src/androidTest/java/com/google/samples/apps/topeka/activity/quiz/MusicQuizTest.java b/app/src/androidTest/java/com/google/samples/apps/topeka/activity/quiz/MusicQuizTest.java new file mode 100644 index 00000000..b16b6d41 --- /dev/null +++ b/app/src/androidTest/java/com/google/samples/apps/topeka/activity/quiz/MusicQuizTest.java @@ -0,0 +1,25 @@ +/* + * 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.activity.quiz; + +public class MusicQuizTest extends BaseQuizActivityTest { + + @Override + int getCategory() { + return 6; + } +} diff --git a/app/src/androidTest/java/com/google/samples/apps/topeka/activity/quiz/ScienceAndNatureQuizTest.java b/app/src/androidTest/java/com/google/samples/apps/topeka/activity/quiz/ScienceAndNatureQuizTest.java new file mode 100644 index 00000000..2f70a950 --- /dev/null +++ b/app/src/androidTest/java/com/google/samples/apps/topeka/activity/quiz/ScienceAndNatureQuizTest.java @@ -0,0 +1,25 @@ +/* + * 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.activity.quiz; + +public class ScienceAndNatureQuizTest extends BaseQuizActivityTest { + + @Override + int getCategory() { + return 4; + } +} diff --git a/app/src/androidTest/java/com/google/samples/apps/topeka/activity/quiz/SportsQuizTest.java b/app/src/androidTest/java/com/google/samples/apps/topeka/activity/quiz/SportsQuizTest.java new file mode 100644 index 00000000..9d08d8ee --- /dev/null +++ b/app/src/androidTest/java/com/google/samples/apps/topeka/activity/quiz/SportsQuizTest.java @@ -0,0 +1,25 @@ +/* + * 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.activity.quiz; + +public class SportsQuizTest extends BaseQuizActivityTest { + + @Override + int getCategory() { + return 8; + } +} diff --git a/app/src/androidTest/java/com/google/samples/apps/topeka/activity/quiz/TVAndMoviesQuizTest.java b/app/src/androidTest/java/com/google/samples/apps/topeka/activity/quiz/TVAndMoviesQuizTest.java new file mode 100644 index 00000000..85001bd7 --- /dev/null +++ b/app/src/androidTest/java/com/google/samples/apps/topeka/activity/quiz/TVAndMoviesQuizTest.java @@ -0,0 +1,25 @@ +/* + * 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.activity.quiz; + +public class TVAndMoviesQuizTest extends BaseQuizActivityTest { + + @Override + int getCategory() { + return 5; + } +} diff --git a/app/src/androidTest/java/com/google/samples/apps/topeka/helper/PreferencesHelperTest.java b/app/src/androidTest/java/com/google/samples/apps/topeka/helper/PreferencesHelperTest.java index ca79e3ed..f4b9ea2e 100644 --- a/app/src/androidTest/java/com/google/samples/apps/topeka/helper/PreferencesHelperTest.java +++ b/app/src/androidTest/java/com/google/samples/apps/topeka/helper/PreferencesHelperTest.java @@ -24,6 +24,7 @@ import com.google.samples.apps.topeka.model.Avatar; import com.google.samples.apps.topeka.model.Player; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -36,6 +37,11 @@ public class PreferencesHelperTest { private static final Player TEST_PLAYER = new Player("Zaphod", "B", Avatar.FOUR); + @Before + public void clearPreferences() { + PreferencesHelper.signOut(InstrumentationRegistry.getTargetContext()); + } + @Test public void performPreferenceCycle() throws Exception { final Context context = InstrumentationRegistry.getTargetContext(); diff --git a/app/src/androidTest/java/com/google/samples/apps/topeka/helper/SolveQuizHelper.java b/app/src/androidTest/java/com/google/samples/apps/topeka/helper/SolveQuizHelper.java new file mode 100644 index 00000000..e22cbcba --- /dev/null +++ b/app/src/androidTest/java/com/google/samples/apps/topeka/helper/SolveQuizHelper.java @@ -0,0 +1,189 @@ +/* + * 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.helper; + +import android.support.annotation.NonNull; +import android.support.test.InstrumentationRegistry; +import android.support.test.espresso.UiController; +import android.support.test.espresso.ViewAction; +import android.support.test.espresso.matcher.ViewMatchers; +import android.text.TextUtils; +import android.view.View; +import android.widget.AdapterView; +import android.widget.GridView; +import android.widget.ListView; +import android.widget.SeekBar; + +import com.google.samples.apps.topeka.R; +import com.google.samples.apps.topeka.model.quiz.AlphaPickerQuiz; +import com.google.samples.apps.topeka.model.quiz.FillBlankQuiz; +import com.google.samples.apps.topeka.model.quiz.FillTwoBlanksQuiz; +import com.google.samples.apps.topeka.model.quiz.OptionsQuiz; +import com.google.samples.apps.topeka.model.quiz.PickerQuiz; +import com.google.samples.apps.topeka.model.quiz.Quiz; +import com.google.samples.apps.topeka.model.quiz.ToggleTranslateQuiz; +import com.google.samples.apps.topeka.model.quiz.TrueFalseQuiz; + +import org.hamcrest.Matcher; + +import java.util.Arrays; +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.action.ViewActions.click; +import static android.support.test.espresso.action.ViewActions.closeSoftKeyboard; +import static android.support.test.espresso.action.ViewActions.typeText; +import static android.support.test.espresso.matcher.ViewMatchers.hasSibling; +import static android.support.test.espresso.matcher.ViewMatchers.isDescendantOfA; +import static android.support.test.espresso.matcher.ViewMatchers.withId; +import static android.support.test.espresso.matcher.ViewMatchers.withText; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; + +public class SolveQuizHelper { + + public static void solveQuiz(Quiz quiz) { + switch (quiz.getType()) { + case ALPHA_PICKER: + setAlphaPickerProgress((AlphaPickerQuiz) quiz); + break; + case PICKER: + setPickerProgress((PickerQuiz) quiz); + break; + case FILL_BLANK: + FillBlankQuiz fillBlankQuiz = (FillBlankQuiz) quiz; + String siblingText = fillBlankQuiz.getStart(); + if (TextUtils.isEmpty(siblingText)) { + siblingText = fillBlankQuiz.getEnd(); + } + int viewId = R.id.quiz_edit_text; + if (TextUtils.isEmpty(siblingText)) { + siblingText = quiz.getQuestion(); + viewId = R.id.quiz_content; + } + typeAndCloseOnView(fillBlankQuiz.getAnswer(), siblingText, viewId); + break; + case FILL_TWO_BLANKS: + FillTwoBlanksQuiz fillTwoBlanksQuiz = (FillTwoBlanksQuiz) quiz; + typeAndCloseOnView(fillTwoBlanksQuiz.getAnswer()[0], R.id.quiz_edit_text); + typeAndCloseOnView(fillTwoBlanksQuiz.getAnswer()[1], R.id.quiz_edit_text_two); + break; + case FOUR_QUARTER: + testOptionsQuizWithType(quiz, GridView.class); + break; + case SINGLE_SELECT: + case SINGLE_SELECT_ITEM: + case MULTI_SELECT: + testOptionsQuizWithType(quiz, ListView.class); + break; + case TOGGLE_TRANSLATE: + ToggleTranslateQuiz toggleTranslateQuiz = (ToggleTranslateQuiz) quiz; + for (int i : toggleTranslateQuiz.getAnswer()) { + onData(equalTo(toggleTranslateQuiz.getReadableOptions()[i])) + .inAdapterView(allOf(instanceOf(AdapterView.class), + withId(R.id.quiz_content), + hasSiblingWith(quiz.getQuestion()))) + .perform(click()); + } + break; + case TRUE_FALSE: + TrueFalseQuiz trueFalseQuiz = (TrueFalseQuiz) quiz; + onView(allOf(isDescendantOfA(hasSibling(withText(quiz.getQuestion()))), withText( + trueFalseQuiz.getAnswer() ? R.string.btn_true : R.string.btn_false))) + .perform(click()); + break; + } + } + + private static void testOptionsQuizWithType(Quiz quiz, Class type) { + OptionsQuiz stringOptionsQuiz = (OptionsQuiz) quiz; + for (int i : stringOptionsQuiz.getAnswer()) { + onData(equalTo(stringOptionsQuiz.getOptions()[i])) + .inAdapterView(allOf(instanceOf(type), + withId(R.id.quiz_content), + hasSiblingWith(quiz.getQuestion()))) + .perform(click()); + } + } + + private static void setAlphaPickerProgress(final AlphaPickerQuiz quiz) { + onView(allOf(isDescendantOfA(hasSibling(withText(quiz.getQuestion()))), + withId(R.id.seekbar))) + .perform(new ViewAction() { + @Override + public Matcher getConstraints() { + return ViewMatchers.isAssignableFrom(SeekBar.class); + } + + @Override + public String getDescription() { + return "Set progress on AlphaPickerQuizView"; + } + + @Override + public void perform(UiController uiController, View view) { + List alphabet = Arrays.asList(InstrumentationRegistry. + getTargetContext() + .getResources() + .getStringArray(R.array.alphabet)); + + SeekBar seekBar = (SeekBar) view; + seekBar.setProgress(alphabet.indexOf(quiz.getAnswer())); + } + }); + } + + private static void setPickerProgress(final PickerQuiz pickerQuiz) { + onView(allOf(isDescendantOfA(hasSibling(withText(pickerQuiz.getQuestion()))), + withId(R.id.seekbar))) + .perform(click()) + .perform(new ViewAction() { + @Override + public Matcher getConstraints() { + return ViewMatchers.isAssignableFrom(SeekBar.class); + } + + @Override + public String getDescription() { + return "Set progress on PickerQuizView"; + } + + @Override + public void perform(UiController uiController, View view) { + SeekBar seekBar = (SeekBar) view; + seekBar.setProgress(pickerQuiz.getAnswer()); + } + }); + } + + private static void typeAndCloseOnView(String answer, String siblingText, int viewId) { + onView(allOf(withId(viewId), hasSiblingWith(siblingText))) + .perform(typeText(answer), closeSoftKeyboard()); + } + + private static void typeAndCloseOnView(String answer, int viewId) { + onView(withId(viewId)) + .perform(typeText(answer), closeSoftKeyboard()); + } + + @NonNull + private static Matcher hasSiblingWith(String text) { + return hasSibling(withText(text)); + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c03aac00..cdf4de1d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -36,7 +36,8 @@ android:theme="@style/Topeka.CategorySelectionActivity" /> + android:windowSoftInputMode="adjustPan" + android:theme="@style/Topeka.QuizActivity"/> diff --git a/app/src/main/java/android/support/test/espresso/contrib/CountingIdlingResource.java b/app/src/main/java/android/support/test/espresso/contrib/CountingIdlingResource.java new file mode 100644 index 00000000..dbcd8ed6 --- /dev/null +++ b/app/src/main/java/android/support/test/espresso/contrib/CountingIdlingResource.java @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2014 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.support.test.espresso.contrib; + +import android.os.SystemClock; +import android.support.test.espresso.IdlingResource; +import android.text.TextUtils; +import android.util.Log; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * An implementation of {@link IdlingResource} that determines idleness by maintaining an internal + * counter. When the counter is 0 - it is considered to be idle, when it is non-zero it is not + * idle. This is very similar to the way a {@link java.util.concurrent.Semaphore} behaves. + *

+ * The counter may be incremented or decremented from any thread. If it reaches an illogical state + * (like counter less than zero) it will throw an IllegalStateException. + *

+ *

+ * This class can then be used to wrap up operations that while in progress should block tests from + * accessing the UI. + *

+ * + *
+ * {@code
+ *   public interface FooServer {
+ *     public Foo newFoo();
+ *     public void updateFoo(Foo foo);
+ *   }
+ *
+ *   public DecoratedFooServer implements FooServer {
+ *     private final FooServer realFooServer;
+ *     private final CountingIdlingResource fooServerIdlingResource;
+ *
+ *     public DecoratedFooServer(FooServer realFooServer,
+ *         CountingIdlingResource fooServerIdlingResource) {
+ *       this.realFooServer = checkNotNull(realFooServer);
+ *       this.fooServerIdlingResource = checkNotNull(fooServerIdlingResource);
+ *     }
+ *
+ *     public Foo newFoo() {
+ *       fooServerIdlingResource.increment();
+ *       try {
+ *         return realFooServer.newFoo();
+ *       } finally {
+ *         fooServerIdlingResource.decrement();
+ *       }
+ *     }
+ *
+ *     public void updateFoo(Foo foo) {
+ *       fooServerIdlingResource.increment();
+ *       try {
+ *         realFooServer.updateFoo(foo);
+ *       } finally {
+ *         fooServerIdlingResource.decrement();
+ *       }
+ *     }
+ *   }
+ *   }
+ *   
+ * + * Then in your test setup: + *
+ *   {@code
+ *     public void setUp() throws Exception {
+ *       super.setUp();
+ *       FooServer realServer = FooApplication.getFooServer();
+ *       CountingIdlingResource countingResource = new CountingIdlingResource("FooServerCalls");
+ *       FooApplication.setFooServer(new DecoratedFooServer(realServer, countingResource));
+ *       Espresso.registerIdlingResource(countingResource);
+ *     }
+ *   }
+ *   
+ */ +@SuppressWarnings("javadoc") +public final class CountingIdlingResource implements IdlingResource { + private static final String TAG = "CountingIdlingResource"; + private final String resourceName; + private final AtomicInteger counter = new AtomicInteger(0); + private final boolean debugCounting; + // written from main thread, read from any thread. + private volatile ResourceCallback resourceCallback; + // read/written from any thread - used for debugging messages. + private volatile long becameBusyAt = 0; + private volatile long becameIdleAt = 0; + + /** + * Creates a CountingIdlingResource without debug tracing. + * + * @param resourceName the resource name this resource should report to Espresso. + */ + public CountingIdlingResource(String resourceName) { + this(resourceName, false); + } + + /** + * Creates a CountingIdlingResource. + * + * @param resourceName the resource name this resource should report to Espresso. + * @param debugCounting if true increment & decrement calls will print trace information to + * logs. + */ + public CountingIdlingResource(String resourceName, boolean debugCounting) { + if (TextUtils.isEmpty(resourceName)) { + throw new IllegalArgumentException("Resource name must not be empty or null"); + } + this.resourceName = resourceName; + this.debugCounting = debugCounting; + } + + @Override + public String getName() { + return resourceName; + } + + @Override + public boolean isIdleNow() { + return counter.get() == 0; + } + + @Override + public void registerIdleTransitionCallback(ResourceCallback resourceCallback) { + this.resourceCallback = resourceCallback; + } + + /** + * Increments the count of in-flight transactions to the resource being monitored. + * + * This method can be called from any thread. + */ + public void increment() { + int counterVal = counter.getAndIncrement(); + if (0 == counterVal) { + becameBusyAt = SystemClock.uptimeMillis(); + } + if (debugCounting) { + Log.i(TAG, "Resource: " + resourceName + " in-use-count incremented to: " + + (counterVal + 1)); + } + } + + /** + * Decrements the count of in-flight transactions to the resource being monitored. + * + * If this operation results in the counter falling below 0 - an exception is raised. + * + * @throws IllegalStateException if the counter is below 0. + */ + public void decrement() { + int counterVal = counter.decrementAndGet(); + if (counterVal == 0) { + // we've gone from non-zero to zero. That means we're idle now! Tell espresso. + if (null != resourceCallback) { + resourceCallback.onTransitionToIdle(); + } + becameIdleAt = SystemClock.uptimeMillis(); + } + if (debugCounting) { + if (counterVal == 0) { + Log.i(TAG, "Resource: " + resourceName + " went idle! (Time spent not idle: " + + (becameIdleAt - becameBusyAt) + ")"); + } else { + Log.i(TAG, "Resource: " + resourceName + " in-use-count decremented to: " + + counterVal); + } + } + if (counterVal < 0) { + throw new IllegalArgumentException("Counter has been corrupted!"); + } + } + + /** + * Prints the current state of this resource to the logcat at info level. + */ + public void dumpStateToLogs() { + StringBuilder message = new StringBuilder("Resource: ") + .append(resourceName) + .append(" inflight transaction count: ") + .append(counter.get()); + if (0 == becameBusyAt) { + Log.i(TAG, message.append(" and has never been busy!").toString()); + } else { + message.append(" and was last busy at: ") + .append(becameBusyAt); + if (0 == becameIdleAt) { + Log.w(TAG, message.append(" AND NEVER WENT IDLE!").toString()); + } else { + message.append(" and last went idle at: ") + .append(becameIdleAt); + Log.i(TAG, message.toString()); + } + } + } +} \ 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 b3b40aa9..e8c83aa6 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 @@ -19,6 +19,8 @@ import android.content.Context; import android.content.Intent; import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.ActivityCompat; import android.support.v4.app.ActivityOptionsCompat; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; @@ -39,15 +41,20 @@ public class CategorySelectionActivity extends AppCompatActivity { private static final String EXTRA_PLAYER = "player"; public static void start(Context context, Player player, ActivityOptionsCompat options) { - Intent starter = new Intent(context, CategorySelectionActivity.class); - starter.putExtra(EXTRA_PLAYER, player); + Intent starter = getStartIntent(context, player); context.startActivity(starter, options.toBundle()); } public static void start(Context context, Player player) { + Intent starter = getStartIntent(context, player); + context.startActivity(starter); + } + + @NonNull + static Intent getStartIntent(Context context, Player player) { Intent starter = new Intent(context, CategorySelectionActivity.class); starter.putExtra(EXTRA_PLAYER, player); - context.startActivity(starter); + return starter; } @Override @@ -56,6 +63,9 @@ protected void onCreate(Bundle savedInstanceState) { setContentView(R.layout.activity_category_selection); Player player = getIntent().getParcelableExtra(EXTRA_PLAYER); + if (!PreferencesHelper.isSignedIn(this) && player != null) { + PreferencesHelper.writeToPreferences(this, player); + } setUpToolbar(player); if (savedInstanceState == null) { attachCategoryGridFragment(); @@ -78,7 +88,7 @@ private void setUpToolbar(Player player) { //noinspection ConstantConditions getSupportActionBar().setDisplayShowTitleEnabled(false); final AvatarView avatarView = (AvatarView) toolbar.findViewById(R.id.avatar); - avatarView.setImageResource(player.getAvatar().getDrawableId()); + avatarView.setAvatar(player.getAvatar().getDrawableId()); //noinspection PrivateResource ((TextView) toolbar.findViewById(R.id.title)).setText(getDisplayName(player)); } @@ -104,7 +114,7 @@ private void signOut() { PreferencesHelper.signOut(this); TopekaDatabaseHelper.reset(this); SignInActivity.start(this, false, null); - finishAfterTransition(); + ActivityCompat.finishAfterTransition(this); } private String getDisplayName(Player player) { 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 db1fbc5e..f1f09310 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,30 +18,33 @@ import android.animation.Animator; import android.animation.AnimatorListenerAdapter; +import android.annotation.SuppressLint; +import android.annotation.TargetApi; import android.content.Context; import android.content.Intent; import android.os.Build; import android.os.Bundle; import android.support.annotation.NonNull; +import android.support.annotation.VisibleForTesting; import android.support.design.widget.FloatingActionButton; +import android.support.test.espresso.contrib.CountingIdlingResource; +import android.support.v4.app.ActivityCompat; import android.support.v4.content.ContextCompat; import android.support.v4.view.ViewCompat; import android.support.v4.view.ViewPropertyAnimatorListenerAdapter; +import android.support.v4.view.animation.FastOutSlowInInterpolator; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; -import android.transition.Transition; -import android.transition.TransitionInflater; import android.util.Log; import android.view.View; import android.view.ViewAnimationUtils; import android.view.Window; -import android.view.WindowManager; -import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; import android.widget.ImageView; 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.model.Category; import com.google.samples.apps.topeka.persistence.TopekaDatabaseHelper; @@ -54,6 +57,7 @@ public class QuizActivity extends AppCompatActivity { 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 QuizFragment mQuizFragment; @@ -62,6 +66,7 @@ public class QuizActivity extends AppCompatActivity { private boolean mSavedStateIsPlaying; private ImageView mIcon; private Animator mCircularReveal; + private CountingIdlingResource mCountingIdlingResource; private View.OnClickListener mOnClickListener = new View.OnClickListener() { @Override @@ -74,12 +79,12 @@ public void onClick(final View v) { submitAnswer(); break; case R.id.quiz_done: - finishAfterTransition(); + ActivityCompat.finishAfterTransition(QuizActivity.this); break; case UNDEFINED: final CharSequence contentDescription = v.getContentDescription(); - if (contentDescription != null && contentDescription.equals( - getString(R.string.up))) { + if (contentDescription != null && contentDescription + .equals(getString(R.string.up))) { onBackPressed(); break; } @@ -99,15 +104,9 @@ public static Intent getStartIntent(Context context, Category category) { @Override protected void onCreate(Bundle savedInstanceState) { - // Inflate and set the enter transition for this activity. - final Transition sharedElementEnterTransition = TransitionInflater.from(this) - .inflateTransition(R.transition.quiz_enter); - getWindow().setSharedElementEnterTransition(sharedElementEnterTransition); - + mCountingIdlingResource = new CountingIdlingResource("Quiz"); mCategoryId = getIntent().getStringExtra(Category.TAG); - //noinspection ResourceType - mInterpolator = AnimationUtils.loadInterpolator(this, - android.R.interpolator.fast_out_slow_in); + mInterpolator = new FastOutSlowInInterpolator(); if (null != savedInstanceState) { mSavedStateIsPlaying = savedInstanceState.getBoolean(STATE_IS_PLAYING); } @@ -153,9 +152,12 @@ public void onBackPressed() { .setInterpolator(mInterpolator) .setStartDelay(100) .setListener(new ViewPropertyAnimatorListenerAdapter() { + @SuppressLint("NewApi") @Override public void onAnimationEnd(View view) { - if (isFinishing() || isDestroyed()) { + if (isFinishing() || + (ApiLevelHelper.isAtLeast(Build.VERSION_CODES.JELLY_BEAN_MR1) + && isDestroyed())) { return; } QuizActivity.super.onBackPressed(); @@ -169,7 +171,24 @@ private void startQuizFromClickOn(final View clickedView) { getSupportFragmentManager().beginTransaction() .replace(R.id.quiz_fragment_container, mQuizFragment, FRAGMENT_TAG).commit(); final View fragmentContainer = findViewById(R.id.quiz_fragment_container); + revealFragmentContainer(clickedView, fragmentContainer); + // the toolbar should not have more elevation than the content while playing + setToolbarElevation(false); + } + + private void revealFragmentContainer(final View clickedView, final View fragmentContainer) { + if (ApiLevelHelper.isAtLeast(Build.VERSION_CODES.LOLLIPOP)) { + revealFragmentContainerLollipop(clickedView, fragmentContainer); + } else { + fragmentContainer.setVisibility(View.VISIBLE); + clickedView.setVisibility(View.GONE); + mIcon.setVisibility(View.GONE); + } + } + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private void revealFragmentContainerLollipop(final View clickedView, + final View fragmentContainer) { prepareCircularReveal(clickedView, fragmentContainer); ViewCompat.animate(clickedView) .scaleX(0) @@ -184,15 +203,13 @@ public void onAnimationEnd(View view) { } }) .start(); - - // the toolbar should not have more elevation than the content while playing - mToolbar.setElevation(0); } + @TargetApi(Build.VERSION_CODES.LOLLIPOP) private void prepareCircularReveal(View startView, View targetView) { int centerX = (startView.getLeft() + startView.getRight()) / 2; int centerY = (startView.getTop() + startView.getBottom()) / 2; - int finalRadius = Math.max(targetView.getWidth(), targetView.getHeight()); + float finalRadius = (float) Math.hypot((double) centerX, (double) centerY); mCircularReveal = ViewAnimationUtils.createCircularReveal( targetView, centerX, centerY, 0, finalRadius); @@ -205,8 +222,11 @@ public void onAnimationEnd(Animator animation) { }); } - public void elevateToolbar() { - mToolbar.setElevation(getResources().getDimension(R.dimen.elevation_header)); + public void setToolbarElevation(boolean shouldElevate) { + if (ApiLevelHelper.isAtLeast(Build.VERSION_CODES.LOLLIPOP)) { + mToolbar.setElevation(shouldElevate ? + getResources().getDimension(R.dimen.elevation_header) : 0); + } } private void initQuizFragment() { @@ -214,7 +234,7 @@ private void initQuizFragment() { new QuizFragment.SolvedStateListener() { @Override public void onCategorySolved() { - elevateToolbar(); + setToolbarElevation(true); displayDoneFab(); } @@ -250,8 +270,10 @@ private void showQuizFabWithDoneIcon() { .start(); } }); - // the toolbar should not have more elevation than the content while playing - mToolbar.setElevation(0); + if (ApiLevelHelper.isAtLeast(Build.VERSION_CODES.LOLLIPOP)) { + // the toolbar should not have more elevation than the content while playing + setToolbarElevation(false); + } } /** @@ -261,13 +283,20 @@ public void proceed() { submitAnswer(); } + /** + * Solely exists for testing purposes and making sure Espresso does not get confused. + */ + public void lockIdlingResource() { + mCountingIdlingResource.increment(); + } + private void submitAnswer() { - elevateToolbar(); + mCountingIdlingResource.decrement(); if (!mQuizFragment.showNextPage()) { mQuizFragment.showSummary(); return; } - mToolbar.setElevation(0); + setToolbarElevation(false); } private void populate(String categoryId) { @@ -277,13 +306,11 @@ private void populate(String categoryId) { } Category category = TopekaDatabaseHelper.getCategoryWith(this, categoryId); setTheme(category.getTheme().getStyleId()); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + if (ApiLevelHelper.isAtLeast(Build.VERSION_CODES.LOLLIPOP)) { Window window = getWindow(); - window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); window.setStatusBarColor(ContextCompat.getColor(this, category.getTheme().getPrimaryDarkColor())); } - initLayout(category.getId()); initToolbar(category); } @@ -305,14 +332,12 @@ private void initLayout(String categoryId) { .start(); mQuizFab = (FloatingActionButton) findViewById(R.id.fab_quiz); mQuizFab.setImageResource(R.drawable.ic_play); - mQuizFab.setVisibility(mSavedStateIsPlaying ? View.GONE : View.VISIBLE); + if (mSavedStateIsPlaying) { + mQuizFab.hide(); + } else { + mQuizFab.show(); + } mQuizFab.setOnClickListener(mOnClickListener); - ViewCompat.animate(mQuizFab) - .scaleX(1) - .scaleY(1) - .setInterpolator(mInterpolator) - .setStartDelay(400) - .start(); } private void initToolbar(Category category) { @@ -323,7 +348,12 @@ private void initToolbar(Category category) { mToolbar.setNavigationOnClickListener(mOnClickListener); if (mSavedStateIsPlaying) { // the toolbar should not have more elevation than the content while playing - mToolbar.setElevation(0); + setToolbarElevation(false); } } + + @VisibleForTesting + public CountingIdlingResource getCountingIdlingResource() { + return mCountingIdlingResource; + } } diff --git a/app/src/main/java/com/google/samples/apps/topeka/adapter/AvatarAdapter.java b/app/src/main/java/com/google/samples/apps/topeka/adapter/AvatarAdapter.java index 2d3e2696..1024fe8a 100644 --- a/app/src/main/java/com/google/samples/apps/topeka/adapter/AvatarAdapter.java +++ b/app/src/main/java/com/google/samples/apps/topeka/adapter/AvatarAdapter.java @@ -49,7 +49,7 @@ public View getView(int position, View convertView, ViewGroup parent) { } private void setAvatar(AvatarView mIcon, Avatar avatar) { - mIcon.setImageResource(avatar.getDrawableId()); + mIcon.setAvatar(avatar.getDrawableId()); mIcon.setContentDescription(avatar.getNameForAccessibility()); } 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 c74b3309..68c56af1 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 @@ -145,8 +145,7 @@ private LayerDrawable loadSolvedIcon(Category category, int categoryImageResourc */ private Drawable loadTintedCategoryDrawable(Category category, int categoryImageResource) { final Drawable categoryIcon = ContextCompat.getDrawable(mActivity, categoryImageResource); - - DrawableCompat.setTint(categoryIcon, category.getTheme().getPrimaryColor()); + DrawableCompat.setTint(categoryIcon, getColor(category.getTheme().getPrimaryColor())); return categoryIcon; } @@ -157,7 +156,7 @@ private Drawable loadTintedCategoryDrawable(Category category, int categoryImage */ private Drawable loadTintedDoneDrawable() { final Drawable done = ContextCompat.getDrawable(mActivity, R.drawable.ic_tick); - DrawableCompat.setTint(done, android.R.color.white); + DrawableCompat.setTint(done, getColor(android.R.color.white)); return done; } 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 b0e2694b..61d0058c 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 @@ -18,6 +18,7 @@ import android.app.Activity; 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; @@ -82,7 +83,8 @@ private void startQuizActivityWithTransition(Activity activity, View toolbar, // Start the activity with the participants, animating from one to the other. final Bundle transitionBundle = sceneTransitionAnimation.toBundle(); - activity.startActivity(QuizActivity.getStartIntent(activity, category), transitionBundle); + ActivityCompat.startActivity(getActivity(), + QuizActivity.getStartIntent(activity, 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 c53f4da4..e29ebc8d 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 @@ -16,8 +16,12 @@ package com.google.samples.apps.topeka.fragment; +import android.annotation.TargetApi; +import android.os.Build; import android.os.Bundle; import android.support.annotation.Nullable; +import android.support.v4.view.ViewCompat; +import android.support.v4.view.animation.FastOutLinearInInterpolator; import android.view.ContextThemeWrapper; import android.view.LayoutInflater; import android.view.View; @@ -30,6 +34,7 @@ import com.google.samples.apps.topeka.R; import com.google.samples.apps.topeka.adapter.QuizAdapter; import com.google.samples.apps.topeka.adapter.ScoreAdapter; +import com.google.samples.apps.topeka.helper.ApiLevelHelper; import com.google.samples.apps.topeka.helper.PreferencesHelper; import com.google.samples.apps.topeka.model.Category; import com.google.samples.apps.topeka.model.Player; @@ -94,14 +99,22 @@ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { mQuizView = (AdapterViewAnimator) view.findViewById(R.id.quiz_view); decideOnViewToDisplay(); - mQuizView.setInAnimation(getActivity(), R.animator.slide_in_bottom); - mQuizView.setOutAnimation(getActivity(), R.animator.slide_out_top); + setQuizViewAnimations(); final AvatarView avatar = (AvatarView) view.findViewById(R.id.avatar); setAvatarDrawable(avatar); initProgressToolbar(view); super.onViewCreated(view, savedInstanceState); } + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private void setQuizViewAnimations() { + if (ApiLevelHelper.isLowerThan(Build.VERSION_CODES.LOLLIPOP)) { + return; + } + mQuizView.setInAnimation(getActivity(), R.animator.slide_in_bottom); + mQuizView.setOutAnimation(getActivity(), R.animator.slide_out_top); + } + private void initProgressToolbar(View view) { final int firstUnsolvedQuizPosition = mCategory.getFirstUnsolvedQuizPosition(); final List quizzes = mCategory.getQuizzes(); @@ -125,7 +138,13 @@ private void setProgress(int currentQuizPosition) { @SuppressWarnings("ConstantConditions") private void setAvatarDrawable(AvatarView avatarView) { Player player = PreferencesHelper.getPlayer(getActivity()); - avatarView.setImageResource(player.getAvatar().getDrawableId()); + avatarView.setAvatar(player.getAvatar().getDrawableId()); + ViewCompat.animate(avatarView) + .setInterpolator(new FastOutLinearInInterpolator()) + .setStartDelay(500) + .scaleX(1) + .scaleY(1) + .start(); } private void decideOnViewToDisplay() { @@ -166,7 +185,8 @@ private void restoreQuizState(final Bundle savedInstanceState) { mQuizView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { @Override public void onLayoutChange(View v, int left, int top, int right, int bottom, - int oldLeft, int oldTop, int oldRight, int oldBottom) { + int oldLeft, + int oldTop, int oldRight, int oldBottom) { mQuizView.removeOnLayoutChangeListener(this); View currentChild = mQuizView.getChildAt(0); if (currentChild instanceof ViewGroup) { diff --git a/app/src/main/java/com/google/samples/apps/topeka/fragment/SignInFragment.java b/app/src/main/java/com/google/samples/apps/topeka/fragment/SignInFragment.java index 09fa26ca..74c8fb96 100644 --- a/app/src/main/java/com/google/samples/apps/topeka/fragment/SignInFragment.java +++ b/app/src/main/java/com/google/samples/apps/topeka/fragment/SignInFragment.java @@ -136,9 +136,9 @@ public void beforeTextChanged(CharSequence s, int start, int count, int after) { public void onTextChanged(CharSequence s, int start, int before, int count) { // showing the floating action button if text is entered if (s.length() == 0) { - mDoneFab.setVisibility(View.GONE); + mDoneFab.hide(); } else { - mDoneFab.setVisibility(View.VISIBLE); + mDoneFab.show(); } } 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 new file mode 100644 index 00000000..7f855541 --- /dev/null +++ b/app/src/main/java/com/google/samples/apps/topeka/helper/ApiLevelHelper.java @@ -0,0 +1,47 @@ +/* + * 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.helper; + +import android.os.Build; + +/** + * Encapsulates checking api levels. + */ +public class ApiLevelHelper { + + /** + * Checks if the current api level is at least the provided value. + * + * @param apiLevel One of the values within {@link Build.VERSION_CODES}. + * @return true if the calling version is at least apiLevel. + * Else false is returned. + */ + public static boolean isAtLeast(int apiLevel) { + return Build.VERSION.SDK_INT >= apiLevel; + } + + /** + * Checks if the current api level is at lower than the provided value. + * + * @param apiLevel One of the values within {@link Build.VERSION_CODES}. + * @return true if the calling version is lower than apiLevel. + * Else false is returned. + */ + public static boolean isLowerThan(int apiLevel) { + return Build.VERSION.SDK_INT < apiLevel; + } +} diff --git a/app/src/main/java/com/google/samples/apps/topeka/model/Theme.java b/app/src/main/java/com/google/samples/apps/topeka/model/Theme.java index 12112bf7..fd1b8805 100644 --- a/app/src/main/java/com/google/samples/apps/topeka/model/Theme.java +++ b/app/src/main/java/com/google/samples/apps/topeka/model/Theme.java @@ -33,36 +33,38 @@ public enum Theme { topeka(R.color.topeka_primary, R.color.topeka_primary_dark, R.color.theme_blue_background, R.color.theme_blue_text, - R.style.Topeka), + R.color.topeka_accent, R.style.Topeka), blue(R.color.theme_blue_primary, R.color.theme_blue_primary_dark, R.color.theme_blue_background, R.color.theme_blue_text, - R.style.Topeka_Blue), + R.color.theme_blue_accent, R.style.Topeka_Blue), green(R.color.theme_green_primary, R.color.theme_green_primary_dark, R.color.theme_green_background, R.color.theme_green_text, - R.style.Topeka_Green), + R.color.theme_green_accent, R.style.Topeka_Green), purple(R.color.theme_purple_primary, R.color.theme_purple_primary_dark, R.color.theme_purple_background, R.color.theme_purple_text, - R.style.Topeka_Purple), + R.color.theme_purple_accent, R.style.Topeka_Purple), red(R.color.theme_red_primary, R.color.theme_red_primary_dark, R.color.theme_red_background, R.color.theme_red_text, - R.style.Topeka_Red), + R.color.theme_red_accent, R.style.Topeka_Red), yellow(R.color.theme_yellow_primary, R.color.theme_yellow_primary_dark, R.color.theme_yellow_background, R.color.theme_yellow_text, - R.style.Topeka_Yellow); + R.color.theme_yellow_accent, R.style.Topeka_Yellow); private final int mColorPrimaryId; private final int mWindowBackgroundColorId; private final int mColorPrimaryDarkId; private final int mTextColorPrimaryId; + private final int mAccentColorId; private final int mStyleId; Theme(final int colorPrimaryId, final int colorPrimaryDarkId, - final int windowBackgroundColorId, final int textColorPrimaryId, - final int styleId) { + final int windowBackgroundColorId, final int textColorPrimaryId, + final int accentColorId, final int styleId) { mColorPrimaryId = colorPrimaryId; mWindowBackgroundColorId = windowBackgroundColorId; mColorPrimaryDarkId = colorPrimaryDarkId; mTextColorPrimaryId = textColorPrimaryId; + mAccentColorId = accentColorId; mStyleId = styleId; } @@ -81,6 +83,11 @@ public int getPrimaryColor() { return mColorPrimaryId; } + @ColorRes + public int getAccentColor() { + return mAccentColorId; + } + @ColorRes public int getPrimaryDarkColor() { return mColorPrimaryDarkId; diff --git a/app/src/main/java/com/google/samples/apps/topeka/widget/AvatarView.java b/app/src/main/java/com/google/samples/apps/topeka/widget/AvatarView.java index 208574ef..4b412643 100644 --- a/app/src/main/java/com/google/samples/apps/topeka/widget/AvatarView.java +++ b/app/src/main/java/com/google/samples/apps/topeka/widget/AvatarView.java @@ -18,9 +18,13 @@ import android.content.Context; import android.graphics.Canvas; +import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; +import android.os.Build; +import android.support.annotation.DrawableRes; import android.support.annotation.NonNull; import android.support.v4.content.ContextCompat; +import android.support.v4.content.res.ResourcesCompat; import android.support.v4.graphics.drawable.RoundedBitmapDrawable; import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory; import android.util.AttributeSet; @@ -28,6 +32,7 @@ import android.widget.ImageView; import com.google.samples.apps.topeka.R; +import com.google.samples.apps.topeka.helper.ApiLevelHelper; import com.google.samples.apps.topeka.widget.outlineprovider.RoundOutlineProvider; /** @@ -47,7 +52,6 @@ public AvatarView(Context context, AttributeSet attrs) { public AvatarView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); - setClipToOutline(true); } @Override @@ -66,6 +70,30 @@ public void toggle() { setChecked(!mChecked); } + /** + * Set the image for this avatar. Will be used to create a round version of this avatar. + * + * @param resId The image's resource id. + */ + public void setAvatar(@DrawableRes int resId) { + if (ApiLevelHelper.isAtLeast(Build.VERSION_CODES.LOLLIPOP)) { + setClipToOutline(true); + setImageResource(resId); + } else { + setAvatarPreLollipop(resId); + } + } + + private void setAvatarPreLollipop(@DrawableRes int resId) { + Drawable drawable = ResourcesCompat.getDrawable(getResources(), resId, + getContext().getTheme()); + BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable; + RoundedBitmapDrawable roundedDrawable = RoundedBitmapDrawableFactory.create(getResources(), + bitmapDrawable.getBitmap()); + roundedDrawable.setCircular(true); + setImageDrawable(roundedDrawable); + } + @Override protected void onDraw(@NonNull Canvas canvas) { super.onDraw(canvas); @@ -79,6 +107,9 @@ protected void onDraw(@NonNull Canvas canvas) { @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); + if (ApiLevelHelper.isLowerThan(Build.VERSION_CODES.LOLLIPOP)) { + return; + } if (w > 0 && h > 0) { setOutlineProvider(new RoundOutlineProvider(Math.min(w, h))); } diff --git a/app/src/main/java/com/google/samples/apps/topeka/widget/outlineprovider/RoundOutlineProvider.java b/app/src/main/java/com/google/samples/apps/topeka/widget/outlineprovider/RoundOutlineProvider.java index 6cd178b7..7cfac468 100644 --- a/app/src/main/java/com/google/samples/apps/topeka/widget/outlineprovider/RoundOutlineProvider.java +++ b/app/src/main/java/com/google/samples/apps/topeka/widget/outlineprovider/RoundOutlineProvider.java @@ -16,13 +16,16 @@ package com.google.samples.apps.topeka.widget.outlineprovider; +import android.annotation.TargetApi; import android.graphics.Outline; +import android.os.Build; import android.view.View; import android.view.ViewOutlineProvider; /** * Creates round outlines for views. */ +@TargetApi(Build.VERSION_CODES.LOLLIPOP) public class RoundOutlineProvider extends ViewOutlineProvider { private final int mSize; 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 5f0bf034..dfdd1d3c 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 @@ -16,23 +16,26 @@ package com.google.samples.apps.topeka.widget.quiz; +import android.animation.ArgbEvaluator; import android.animation.ObjectAnimator; 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; import android.support.annotation.ColorInt; import android.support.annotation.DimenRes; import android.support.v4.content.ContextCompat; -import android.support.v4.view.ViewCompat; +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; import android.view.View; import android.view.ViewGroup; -import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; import android.widget.FrameLayout; import android.widget.LinearLayout; @@ -40,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.model.Category; import com.google.samples.apps.topeka.model.quiz.Quiz; import com.google.samples.apps.topeka.widget.fab.CheckableFab; @@ -56,66 +60,65 @@ *

* * @param The type of {@link com.google.samples.apps.topeka.model.quiz.Quiz} you want to - * display. + * display. */ public abstract class AbsQuizView extends FrameLayout { - /** Property for animating the foreground color */ - public static final Property FOREGROUND_COLOR = - new IntProperty("foregroundColor") { + private static final int ANSWER_HIDE_DELAY = 500; + private static final int FOREGROUND_COLOR_CHANGE_DELAY = 750; + protected final int mMinHeightTouchTarget; + private final int mSpacingDouble; + private final LayoutInflater mLayoutInflater; + private final Category mCategory; + private final Q mQuiz; + private Interpolator mLinearOutSlowInInterpolator; + private boolean mAnswered; + private TextView mQuestionView; + private CheckableFab mSubmitAnswer; + private Handler mHandler; + private Runnable mHideFabRunnable; + private Runnable mMoveOffScreenRunnable; + + private static final Property FOREGROUND_COLOR = + new IntProperty("foregroundColor") { @Override - public void setValue(FrameLayout object, int value) { - if (object.getForeground() instanceof ColorDrawable) { - ((ColorDrawable) object.getForeground()).setColor(value); + public void setValue(AbsQuizView layout, int value) { + if (layout.getForeground() instanceof ColorDrawable) { + ((ColorDrawable) layout.getForeground().mutate()).setColor(value); } else { - object.setForeground(new ColorDrawable(value)); + layout.setForeground(new ColorDrawable(value)); } } @Override - public Integer get(FrameLayout object) { - return ((ColorDrawable) object.getForeground()).getColor(); + public Integer get(AbsQuizView layout) { + if (layout.getForeground() instanceof ColorDrawable) { + return ((ColorDrawable) layout.getForeground()).getColor(); + } else { + return Color.TRANSPARENT; + } } }; - protected final int mMinHeightTouchTarget; - private final int mSpacingDouble; - private final LayoutInflater mLayoutInflater; - private final Category mCategory; - private final Q mQuiz; - private final Interpolator mFastOutSlowInInterpolator; - private final Interpolator mLinearOutSlowInInterpolator; - private final int mIconAnimationDuration; - private final int mScaleAnimationDuration; - private boolean mAnswered; - private TextView mQuestionView; - private CheckableFab mSubmitAnswer; - /** * 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); mQuiz = quiz; mCategory = category; mSpacingDouble = getResources().getDimensionPixelSize(R.dimen.spacing_double); - mSubmitAnswer = getSubmitButton(context); mLayoutInflater = LayoutInflater.from(context); + mSubmitAnswer = getSubmitButton(); mMinHeightTouchTarget = getResources() .getDimensionPixelSize(R.dimen.min_height_touch_target); - //noinspection ResourceType - mFastOutSlowInInterpolator = AnimationUtils - .loadInterpolator(getContext(), android.R.interpolator.fast_out_slow_in); - //noinspection ResourceType - mLinearOutSlowInInterpolator = AnimationUtils - .loadInterpolator(getContext(), android.R.interpolator.linear_out_slow_in); - mIconAnimationDuration = 300; - mScaleAnimationDuration = 200; + mLinearOutSlowInInterpolator = new LinearOutSlowInInterpolator(); + mHandler = new Handler(); setId(quiz.getId()); setUpQuestionView(); @@ -180,17 +183,19 @@ private void addFloatingActionButton() { bottomOfQuestionView - halfAFab, //top 0, // right mSpacingDouble); // bottom - fabLayoutParams.setMarginEnd(mSpacingDouble); + 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(Context context) { + private CheckableFab getSubmitButton() { if (null == mSubmitAnswer) { - mSubmitAnswer = new CheckableFab(context); - mSubmitAnswer.setId(R.id.submitAnswer); - mSubmitAnswer.setVisibility(GONE); - mSubmitAnswer.setScaleY(0); - mSubmitAnswer.setScaleX(0); + mSubmitAnswer = (CheckableFab) getLayoutInflater() + .inflate(R.layout.answer_submit, this, false); + mSubmitAnswer.hide(); mSubmitAnswer.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { @@ -254,15 +259,11 @@ protected boolean isAnswered() { */ protected void allowAnswer(final boolean answered) { if (null != mSubmitAnswer) { - final float targetScale = answered ? 1f : 0f; if (answered) { - mSubmitAnswer.setVisibility(View.VISIBLE); + mSubmitAnswer.show(); + } else { + mSubmitAnswer.hide(); } - ViewCompat.animate(mSubmitAnswer) - .scaleX(targetScale) - .scaleY(targetScale) - .setInterpolator(mFastOutSlowInInterpolator) - .start(); mAnswered = answered; } } @@ -284,7 +285,6 @@ protected void submitAnswer() { submitAnswer(findViewById(R.id.submitAnswer)); } - @SuppressWarnings("UnusedParameters") private void submitAnswer(final View v) { final boolean answerCorrect = isAnswerCorrect(); @@ -298,14 +298,11 @@ private void submitAnswer(final View v) { * @param answerCorrect true if the answer was correct, else false. */ private void performScoreAnimation(final boolean answerCorrect) { - - mSubmitAnswer.setChecked(answerCorrect); - + ((QuizActivity) getContext()).lockIdlingResource(); // Decide which background color to use. final int backgroundColor = ContextCompat.getColor(getContext(), answerCorrect ? R.color.green : R.color.red); - mSubmitAnswer.setBackgroundTintList(ColorStateList.valueOf(backgroundColor)); - hideFab(); + adjustFab(answerCorrect, backgroundColor); resizeView(); moveViewOffScreen(answerCorrect); // Animate the foreground color to match the background color. @@ -313,58 +310,67 @@ private void performScoreAnimation(final boolean answerCorrect) { animateForegroundColor(backgroundColor); } - private void hideFab() { - ViewCompat.animate(mSubmitAnswer) - .setDuration(mScaleAnimationDuration) - .setStartDelay(mIconAnimationDuration * 2) - .scaleX(0f) - .scaleY(0f) - .setInterpolator(mLinearOutSlowInInterpolator) - .start(); + 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. - ViewCompat.animate(this) - .scaleY(.5f / widthHeightRatio) - .setDuration(300) - .setStartDelay(750) - .start(); - ViewCompat.animate(this) - .scaleX(.5f) - .setDuration(300) - .setStartDelay(800) - .start(); + // 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); } - private void animateForegroundColor(@ColorInt int targetColor) { - final ObjectAnimator foregroundAnimator = ObjectAnimator - .ofArgb(this, FOREGROUND_COLOR, Color.WHITE, targetColor); - foregroundAnimator - .setDuration(200) - .setInterpolator(mLinearOutSlowInInterpolator); - foregroundAnimator.setStartDelay(750); - foregroundAnimator.start(); + private void resizeViewProperty(Property 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, FOREGROUND_COLOR, + Color.TRANSPARENT, targetColor); + animator.setEvaluator(new ArgbEvaluator()); + animator.setStartDelay(FOREGROUND_COLOR_CHANGE_DELAY); + animator.start(); } private void moveViewOffScreen(final boolean answerCorrect) { - // Animate the current view off the screen. - ViewCompat.animate(this) - .setDuration(200) - .setStartDelay(1200) - .setInterpolator(mLinearOutSlowInInterpolator) - .withEndAction(new Runnable() { - @Override - public void run() { - mCategory.setScore(getQuiz(), answerCorrect); - if (getContext() instanceof QuizActivity) { - ((QuizActivity) getContext()).proceed(); - } - } - }) - .start(); + // 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, @DimenRes int resId) { diff --git a/app/src/main/java/com/google/samples/apps/topeka/widget/quiz/FillTwoBlanksQuizView.java b/app/src/main/java/com/google/samples/apps/topeka/widget/quiz/FillTwoBlanksQuizView.java index d7780848..0d2dac79 100644 --- a/app/src/main/java/com/google/samples/apps/topeka/widget/quiz/FillTwoBlanksQuizView.java +++ b/app/src/main/java/com/google/samples/apps/topeka/widget/quiz/FillTwoBlanksQuizView.java @@ -24,6 +24,7 @@ import android.widget.EditText; import android.widget.LinearLayout; +import com.google.samples.apps.topeka.R; import com.google.samples.apps.topeka.model.Category; import com.google.samples.apps.topeka.model.quiz.FillTwoBlanksQuiz; @@ -48,6 +49,7 @@ protected View createQuizContentView() { mAnswerOne = createEditText(); mAnswerOne.setImeOptions(EditorInfo.IME_ACTION_NEXT); mAnswerTwo = createEditText(); + mAnswerTwo.setId(R.id.quiz_edit_text_two); addEditText(layout, mAnswerOne); addEditText(layout, mAnswerTwo); return layout; diff --git a/app/src/main/java/com/google/samples/apps/topeka/widget/quiz/SelectItemQuizView.java b/app/src/main/java/com/google/samples/apps/topeka/widget/quiz/SelectItemQuizView.java index 74095e96..58ad1f3b 100644 --- a/app/src/main/java/com/google/samples/apps/topeka/widget/quiz/SelectItemQuizView.java +++ b/app/src/main/java/com/google/samples/apps/topeka/widget/quiz/SelectItemQuizView.java @@ -47,12 +47,13 @@ public SelectItemQuizView(Context context, Category category, SelectItemQuiz qui @Override protected View createQuizContentView() { - mListView = new ListView(getContext()); + Context context = getContext(); + mListView = new ListView(context); mListView.setDivider(null); mListView.setSelector(R.drawable.selector_button); mListView.setAdapter( new OptionsQuizAdapter(getQuiz().getOptions(), R.layout.item_answer_start, - getContext(), true)); + context, true)); mListView.setChoiceMode(AbsListView.CHOICE_MODE_SINGLE); mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override diff --git a/app/src/main/java/com/google/samples/apps/topeka/widget/quiz/ToggleTranslateQuizView.java b/app/src/main/java/com/google/samples/apps/topeka/widget/quiz/ToggleTranslateQuizView.java index 35953117..26d44d18 100644 --- a/app/src/main/java/com/google/samples/apps/topeka/widget/quiz/ToggleTranslateQuizView.java +++ b/app/src/main/java/com/google/samples/apps/topeka/widget/quiz/ToggleTranslateQuizView.java @@ -43,7 +43,11 @@ public class ToggleTranslateQuizView extends AbsQuizView { public ToggleTranslateQuizView(Context context, Category category, ToggleTranslateQuiz quiz) { super(context, category, quiz); - mAnswers = new boolean[quiz.getOptions().length]; + initAnswerSpace(); + } + + private void initAnswerSpace() { + mAnswers = new boolean[getQuiz().getOptions().length]; } @Override @@ -89,6 +93,7 @@ public void setUserInput(Bundle savedInput) { } mAnswers = savedInput.getBooleanArray(KEY_ANSWERS); if (mAnswers == null) { + initAnswerSpace(); return; } ListAdapter adapter = mListView.getAdapter(); diff --git a/app/src/main/java/com/google/samples/apps/topeka/widget/quiz/TrueFalseQuizView.java b/app/src/main/java/com/google/samples/apps/topeka/widget/quiz/TrueFalseQuizView.java index 05079395..615dfd6b 100644 --- a/app/src/main/java/com/google/samples/apps/topeka/widget/quiz/TrueFalseQuizView.java +++ b/app/src/main/java/com/google/samples/apps/topeka/widget/quiz/TrueFalseQuizView.java @@ -56,10 +56,10 @@ protected View createQuizContentView() { @Override public void onClick(View v) { switch (v.getId()) { - case R.id.answerTrue: + case R.id.answer_true: mAnswer = true; break; - case R.id.answerFalse: + case R.id.answer_false: mAnswer = false; break; } @@ -67,9 +67,9 @@ public void onClick(View v) { } }; - mAnswerTrue = container.findViewById(R.id.answerTrue); + mAnswerTrue = container.findViewById(R.id.answer_true); mAnswerTrue.setOnClickListener(clickListener); - mAnswerFalse = container.findViewById(R.id.answerFalse); + mAnswerFalse = container.findViewById(R.id.answer_false); mAnswerFalse.setOnClickListener(clickListener); return container; } diff --git a/app/src/main/res/drawable-hdpi/ic_arrow_back_black.png b/app/src/main/res/drawable-hdpi/ic_arrow_back_black.png new file mode 100644 index 00000000..da807e99 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_arrow_back_black.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_check_black.png b/app/src/main/res/drawable-hdpi/ic_check_black.png new file mode 100644 index 00000000..e802d90a Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_check_black.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_close_black.png b/app/src/main/res/drawable-hdpi/ic_close_black.png new file mode 100644 index 00000000..1a9cd75a Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_close_black.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_play_arrow_black.png b/app/src/main/res/drawable-hdpi/ic_play_arrow_black.png new file mode 100644 index 00000000..e9c288c9 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_play_arrow_black.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_arrow_back_black.png b/app/src/main/res/drawable-mdpi/ic_arrow_back_black.png new file mode 100644 index 00000000..ad388309 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_arrow_back_black.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_check_black.png b/app/src/main/res/drawable-mdpi/ic_check_black.png new file mode 100644 index 00000000..1c14c9c4 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_check_black.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_close_black.png b/app/src/main/res/drawable-mdpi/ic_close_black.png new file mode 100644 index 00000000..40a1a84e Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_close_black.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_play_arrow_black.png b/app/src/main/res/drawable-mdpi/ic_play_arrow_black.png new file mode 100644 index 00000000..d78c57ba Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_play_arrow_black.png differ diff --git a/app/src/main/res/drawable-v21/ic_arrow_back.xml b/app/src/main/res/drawable-v21/ic_arrow_back.xml new file mode 100644 index 00000000..4bd48eaf --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_arrow_back.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/app/src/main/res/drawable-v21/ic_cross.xml b/app/src/main/res/drawable-v21/ic_cross.xml new file mode 100644 index 00000000..a5f0b16c --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_cross.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable-v21/ic_play.xml b/app/src/main/res/drawable-v21/ic_play.xml new file mode 100644 index 00000000..21fa3d07 --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_play.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/app/src/main/res/drawable-v21/ic_tick.xml b/app/src/main/res/drawable-v21/ic_tick.xml new file mode 100644 index 00000000..30d2ef97 --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_tick.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ripple.xml b/app/src/main/res/drawable-v21/ripple.xml similarity index 100% rename from app/src/main/res/drawable/ripple.xml rename to app/src/main/res/drawable-v21/ripple.xml diff --git a/app/src/main/res/drawable-v21/selector_button.xml b/app/src/main/res/drawable-v21/selector_button.xml new file mode 100644 index 00000000..c70241a1 --- /dev/null +++ b/app/src/main/res/drawable-v21/selector_button.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable-v21/selector_categories.xml b/app/src/main/res/drawable-v21/selector_categories.xml new file mode 100644 index 00000000..0c85908b --- /dev/null +++ b/app/src/main/res/drawable-v21/selector_categories.xml @@ -0,0 +1,24 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable-v21/selector_checkable.xml b/app/src/main/res/drawable-v21/selector_checkable.xml new file mode 100644 index 00000000..d25b5d5e --- /dev/null +++ b/app/src/main/res/drawable-v21/selector_checkable.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable-v21/selector_false.xml b/app/src/main/res/drawable-v21/selector_false.xml new file mode 100644 index 00000000..4d52e228 --- /dev/null +++ b/app/src/main/res/drawable-v21/selector_false.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable-v21/selector_true.xml b/app/src/main/res/drawable-v21/selector_true.xml new file mode 100644 index 00000000..9c01f078 --- /dev/null +++ b/app/src/main/res/drawable-v21/selector_true.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable-xhdpi/ic_arrow_back_black.png b/app/src/main/res/drawable-xhdpi/ic_arrow_back_black.png new file mode 100644 index 00000000..68427253 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_arrow_back_black.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_check_black.png b/app/src/main/res/drawable-xhdpi/ic_check_black.png new file mode 100644 index 00000000..64a4944f Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_check_black.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_close_black.png b/app/src/main/res/drawable-xhdpi/ic_close_black.png new file mode 100644 index 00000000..6bc43729 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_close_black.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_play_arrow_black.png b/app/src/main/res/drawable-xhdpi/ic_play_arrow_black.png new file mode 100644 index 00000000..f208795f Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_play_arrow_black.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_arrow_back_black.png b/app/src/main/res/drawable-xxhdpi/ic_arrow_back_black.png new file mode 100644 index 00000000..fa432c23 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_arrow_back_black.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_check_black.png b/app/src/main/res/drawable-xxhdpi/ic_check_black.png new file mode 100644 index 00000000..b26a2c05 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_check_black.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_close_black.png b/app/src/main/res/drawable-xxhdpi/ic_close_black.png new file mode 100644 index 00000000..51b4401c Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_close_black.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_play_arrow_black.png b/app/src/main/res/drawable-xxhdpi/ic_play_arrow_black.png new file mode 100644 index 00000000..5345ee3c Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_play_arrow_black.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_arrow_back_black.png b/app/src/main/res/drawable-xxxhdpi/ic_arrow_back_black.png new file mode 100644 index 00000000..fb5235fb Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_arrow_back_black.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_check_black.png b/app/src/main/res/drawable-xxxhdpi/ic_check_black.png new file mode 100644 index 00000000..2f6d6386 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_check_black.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_close_black.png b/app/src/main/res/drawable-xxxhdpi/ic_close_black.png new file mode 100644 index 00000000..df42feec Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_close_black.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_play_arrow_black.png b/app/src/main/res/drawable-xxxhdpi/ic_play_arrow_black.png new file mode 100644 index 00000000..d12d4956 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_play_arrow_black.png differ diff --git a/app/src/main/res/drawable/ic_arrow_back.xml b/app/src/main/res/drawable/ic_arrow_back.xml index da7882e3..8c952697 100644 --- a/app/src/main/res/drawable/ic_arrow_back.xml +++ b/app/src/main/res/drawable/ic_arrow_back.xml @@ -1,3 +1,4 @@ + - - - - + diff --git a/app/src/main/res/drawable/ic_cross.xml b/app/src/main/res/drawable/ic_cross.xml index cfbfe6eb..6a66b9fa 100644 --- a/app/src/main/res/drawable/ic_cross.xml +++ b/app/src/main/res/drawable/ic_cross.xml @@ -1,3 +1,4 @@ + - - - - - - - - - - - - + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_play.xml b/app/src/main/res/drawable/ic_play.xml index e7aeaa62..ffa671f6 100644 --- a/app/src/main/res/drawable/ic_play.xml +++ b/app/src/main/res/drawable/ic_play.xml @@ -1,3 +1,4 @@ + - - - - + + diff --git a/app/src/main/res/drawable/ic_tick.xml b/app/src/main/res/drawable/ic_tick.xml index 79c2833b..dbcfacf6 100644 --- a/app/src/main/res/drawable/ic_tick.xml +++ b/app/src/main/res/drawable/ic_tick.xml @@ -1,3 +1,4 @@ + - - - - - - - - - - - - + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_avatar.xml b/app/src/main/res/drawable/selector_avatar.xml index e62a908f..38992b3d 100644 --- a/app/src/main/res/drawable/selector_avatar.xml +++ b/app/src/main/res/drawable/selector_avatar.xml @@ -19,4 +19,5 @@ + diff --git a/app/src/main/res/drawable/selector_button.xml b/app/src/main/res/drawable/selector_button.xml index c70241a1..a54e379f 100644 --- a/app/src/main/res/drawable/selector_button.xml +++ b/app/src/main/res/drawable/selector_button.xml @@ -14,16 +14,11 @@ ~ limitations under the License. --> - + + + + - - - - - - - - + - + diff --git a/app/src/main/res/drawable/selector_categories.xml b/app/src/main/res/drawable/selector_categories.xml new file mode 100644 index 00000000..d388107b --- /dev/null +++ b/app/src/main/res/drawable/selector_categories.xml @@ -0,0 +1,19 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_checkable.xml b/app/src/main/res/drawable/selector_checkable.xml index d25b5d5e..fac818c8 100644 --- a/app/src/main/res/drawable/selector_checkable.xml +++ b/app/src/main/res/drawable/selector_checkable.xml @@ -13,17 +13,8 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> - - - - - - - - - - - + + + - + diff --git a/app/src/main/res/drawable/selector_false.xml b/app/src/main/res/drawable/selector_false.xml index 85d31248..ad14966e 100644 --- a/app/src/main/res/drawable/selector_false.xml +++ b/app/src/main/res/drawable/selector_false.xml @@ -13,19 +13,11 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> - - - - - - - - - - - + + + - + + + + diff --git a/app/src/main/res/drawable/selector_list.xml b/app/src/main/res/drawable/selector_list.xml new file mode 100644 index 00000000..ba4677a9 --- /dev/null +++ b/app/src/main/res/drawable/selector_list.xml @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_true.xml b/app/src/main/res/drawable/selector_true.xml index 9c01f078..a4c2c908 100644 --- a/app/src/main/res/drawable/selector_true.xml +++ b/app/src/main/res/drawable/selector_true.xml @@ -14,18 +14,11 @@ ~ limitations under the License. --> - - - - - - - - - - + + + - + + + + 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 df400b30..6eb47355 100644 --- a/app/src/main/res/layout-land/fragment_sign_in.xml +++ b/app/src/main/res/layout-land/fragment_sign_in.xml @@ -32,10 +32,6 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - - + diff --git a/app/src/main/res/layout/activity_category_selection.xml b/app/src/main/res/layout/activity_category_selection.xml index b13c4d61..c5806842 100644 --- a/app/src/main/res/layout/activity_category_selection.xml +++ b/app/src/main/res/layout/activity_category_selection.xml @@ -33,6 +33,7 @@ android:layout_width="@dimen/size_avatar_toolbar" android:layout_height="@dimen/size_avatar_toolbar" android:layout_marginEnd="@dimen/spacing_double" + android:layout_marginRight="@dimen/spacing_double" android:transitionName="@string/transition_avatar" /> + + - - - - - - + @@ -78,4 +69,16 @@ android:visibility="invisible" /> + + + diff --git a/app/src/main/res/layout/answer_submit.xml b/app/src/main/res/layout/answer_submit.xml new file mode 100644 index 00000000..08b1fa61 --- /dev/null +++ b/app/src/main/res/layout/answer_submit.xml @@ -0,0 +1,24 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fab_done.xml b/app/src/main/res/layout/fab_done.xml index 04f262e7..1ea162df 100644 --- a/app/src/main/res/layout/fab_done.xml +++ b/app/src/main/res/layout/fab_done.xml @@ -18,10 +18,12 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/done" - android:layout_width="@dimen/size_fab" - android:layout_height="@dimen/size_fab" + app:fabSize="normal" + android:layout_width="wrap_content" + android:layout_height="wrap_content" android:layout_gravity="bottom|end" android:layout_marginBottom="@dimen/spacing_double" android:layout_marginEnd="@dimen/spacing_double" + android:layout_marginRight="@dimen/spacing_double" android:src="@drawable/ic_tick" app:backgroundTint="@color/topeka_primary" /> diff --git a/app/src/main/res/layout/fragment_categories.xml b/app/src/main/res/layout/fragment_categories.xml index 16b3e3a3..584bb927 100644 --- a/app/src/main/res/layout/fragment_categories.xml +++ b/app/src/main/res/layout/fragment_categories.xml @@ -24,7 +24,7 @@ android:clipToPadding="false" android:drawSelectorOnTop="true" android:horizontalSpacing="@dimen/spacing_micro" - android:listSelector="@drawable/ripple" + android:listSelector="@drawable/selector_categories" android:numColumns="auto_fit" android:padding="@dimen/spacing_micro" android:scrollbarStyle="outsideOverlay" diff --git a/app/src/main/res/layout/fragment_quiz.xml b/app/src/main/res/layout/fragment_quiz.xml index 3987bc85..168c8ccb 100644 --- a/app/src/main/res/layout/fragment_quiz.xml +++ b/app/src/main/res/layout/fragment_quiz.xml @@ -38,7 +38,7 @@ android:id="@+id/progress_toolbar" android:layout_width="match_parent" android:layout_height="wrap_content" - android:background="?android:colorPrimary" + android:background="?colorPrimary" android:elevation="@dimen/elevation_header"> + android:layout_marginEnd="@dimen/spacing_double" + android:layout_marginRight="@dimen/spacing_double" + android:scaleX="0" + android:scaleY="0"/> - - + diff --git a/app/src/main/res/layout/item_scorecard.xml b/app/src/main/res/layout/item_scorecard.xml index fcc3db5a..e6d876f4 100644 --- a/app/src/main/res/layout/item_scorecard.xml +++ b/app/src/main/res/layout/item_scorecard.xml @@ -15,20 +15,21 @@ --> + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@null" + android:orientation="horizontal" + android:paddingBottom="@dimen/spacing_normal" + android:paddingTop="@dimen/spacing_normal" + tools:ignore="Overdraw"> \ No newline at end of file diff --git a/app/src/main/res/layout/quiz_radio_group_true_false.xml b/app/src/main/res/layout/quiz_radio_group_true_false.xml index 3b70fbbe..e5b852de 100644 --- a/app/src/main/res/layout/quiz_radio_group_true_false.xml +++ b/app/src/main/res/layout/quiz_radio_group_true_false.xml @@ -20,20 +20,22 @@ android:orientation="horizontal"> - + \ No newline at end of file diff --git a/app/src/main/res/values-v21/styles.xml b/app/src/main/res/values-v21/styles.xml new file mode 100644 index 00000000..c0f2d5a0 --- /dev/null +++ b/app/src/main/res/values-v21/styles.xml @@ -0,0 +1,30 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml index 1b8d8f63..f8bc9f76 100644 --- a/app/src/main/res/values/ids.xml +++ b/app/src/main/res/values/ids.xml @@ -19,4 +19,5 @@ + \ 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 db40b24a..2416129d 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -17,8 +17,7 @@ - + - - + - - - @@ -116,7 +114,7 @@ 96sp - diff --git a/build.gradle b/build.gradle index eb242d01..e82f2a81 100644 --- a/build.gradle +++ b/build.gradle @@ -29,7 +29,7 @@ buildscript { } task wrapper(type: Wrapper) { - gradleVersion = "2.4" + gradleVersion = "2.7" } allprojects { diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index c97a8bdb..b5166dad 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 50f0706c..32b4e84b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Wed Jun 10 13:27:32 BST 2015 +#Mon Sep 21 17:26:58 BST 2015 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.7-all.zip diff --git a/screenrecord b/screenrecord new file mode 100644 index 00000000..3a121a3e Binary files /dev/null and b/screenrecord differ diff --git a/screenshots/categories.png b/screenshots/categories.png index a674a39f..ae6b0a28 100644 Binary files a/screenshots/categories.png and b/screenshots/categories.png differ diff --git a/screenshots/category_history.png b/screenshots/category_history.png index 8bf62d9a..7b212097 100644 Binary files a/screenshots/category_history.png and b/screenshots/category_history.png differ diff --git a/screenshots/quiz_shakespeare.png b/screenshots/quiz_shakespeare.png index 9fdc2e76..309b27e4 100644 Binary files a/screenshots/quiz_shakespeare.png and b/screenshots/quiz_shakespeare.png differ