Refactors Sandbox to more easily add new tutorials.

- Extracts abstract classes for common functionality
 - Renames some layouts, etc. to not be back-specific
 - Consolidates more logic in the controllers rather
   than classes like BackGestureTutorialTypeInfo
 - Removes redundant TutorialStep enum (combining it
   with TutorialType)

Still considering removing additional layers of
abstraction like the Fragment itself (instead
keeping UI within the Activity, which would still
be controlled by a TutorialController).

Test: Built and went through the Back tutorial to
confirm existing funcitonality was not disrupted.
Bug: 148542211
Change-Id: I590af62f5fe3186a15deb5883e6ef6e6cb7e4665

Change-Id: Id893869cb59609141dcdbdca01744d0f5952b546
This commit is contained in:
Andy Wickham
2020-04-14 23:08:46 +00:00
parent af4c525054
commit 9c89358c7d
20 changed files with 429 additions and 628 deletions
@@ -16,5 +16,5 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="?android:attr/dialogCornerRadius"/>
<solid android:color="@color/back_gesture_tutorial_primary_color"/>
<solid android:color="@color/gesture_tutorial_primary_color"/>
</shape>
@@ -16,5 +16,5 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="@dimen/default_dialog_corner_radius"/>
<solid android:color="@color/back_gesture_tutorial_primary_color"/>
<solid android:color="@color/gesture_tutorial_primary_color"/>
</shape>
@@ -14,6 +14,6 @@
limitations under the License.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/back_gesture_tutorial_fragment_container"
android:id="@+id/gesture_tutorial_fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
@@ -16,27 +16,27 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/back_gesture_tutorial_background_color">
android:background="@color/gesture_tutorial_background_color">
<ImageView
android:id="@+id/back_gesture_tutorial_fragment_hand_coaching"
android:id="@+id/gesture_tutorial_fragment_hand_coaching"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"/>
<ImageButton
android:id="@+id/back_gesture_tutorial_fragment_close_button"
android:id="@+id/gesture_tutorial_fragment_close_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="18dp"
android:layout_marginTop="30dp"
android:layout_marginStart="4dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:background="@android:color/transparent"
android:accessibilityTraversalAfter="@id/back_gesture_tutorial_fragment_titles_container"
android:contentDescription="@string/back_gesture_tutorial_close_button_content_description"
android:src="@drawable/back_gesture_tutorial_close_button"/>
android:accessibilityTraversalAfter="@id/gesture_tutorial_fragment_titles_container"
android:contentDescription="@string/gesture_tutorial_close_button_content_description"
android:src="@drawable/gesture_tutorial_close_button"/>
<LinearLayout
android:layout_width="match_parent"
@@ -45,29 +45,29 @@
android:orientation="vertical">
<LinearLayout
android:id="@+id/back_gesture_tutorial_fragment_titles_container"
android:id="@+id/gesture_tutorial_fragment_titles_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:focusable="true">
<TextView
android:id="@+id/back_gesture_tutorial_fragment_title_view"
android:id="@+id/gesture_tutorial_fragment_title_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginStart="@dimen/back_gesture_tutorial_title_margin_start_end"
android:layout_marginEnd="@dimen/back_gesture_tutorial_title_margin_start_end"
android:layout_marginStart="@dimen/gesture_tutorial_title_margin_start_end"
android:layout_marginEnd="@dimen/gesture_tutorial_title_margin_start_end"
style="@style/TextAppearance.BackGestureTutorial.Title"/>
<TextView
android:id="@+id/back_gesture_tutorial_fragment_subtitle_view"
android:id="@+id/gesture_tutorial_fragment_subtitle_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="10dp"
android:layout_marginStart="@dimen/back_gesture_tutorial_subtitle_margin_start_end"
android:layout_marginEnd="@dimen/back_gesture_tutorial_subtitle_margin_start_end"
android:layout_marginStart="@dimen/gesture_tutorial_subtitle_margin_start_end"
android:layout_marginEnd="@dimen/gesture_tutorial_subtitle_margin_start_end"
style="@style/TextAppearance.BackGestureTutorial.Subtitle"/>
</LinearLayout>
@@ -91,21 +91,21 @@
android:layout_gravity="center_horizontal">
<Button
android:id="@+id/back_gesture_tutorial_fragment_action_button"
android:id="@+id/gesture_tutorial_fragment_action_button"
android:layout_width="142dp"
android:layout_height="49dp"
android:layout_marginEnd="@dimen/back_gesture_tutorial_button_margin_start_end"
android:layout_marginEnd="@dimen/gesture_tutorial_button_margin_start_end"
android:layout_alignParentEnd="true"
android:stateListAnimator="@null"
android:background="@drawable/back_gesture_tutorial_action_button_background"
android:background="@drawable/gesture_tutorial_action_button_background"
android:foreground="?android:attr/selectableItemBackgroundBorderless"
style="@style/TextAppearance.BackGestureTutorial.ButtonLabel"/>
<Button
android:id="@+id/back_gesture_tutorial_fragment_action_text_button"
android:id="@+id/gesture_tutorial_fragment_action_text_button"
android:layout_width="142dp"
android:layout_height="49dp"
android:layout_marginStart="@dimen/back_gesture_tutorial_button_margin_start_end"
android:layout_marginStart="@dimen/gesture_tutorial_button_margin_start_end"
android:layout_alignParentStart="true"
android:stateListAnimator="@null"
android:background="@null"
@@ -113,7 +113,5 @@
style="@style/TextAppearance.BackGestureTutorial.TextButtonLabel"/>
</RelativeLayout>
</LinearLayout>
</RelativeLayout>
+3 -3
View File
@@ -81,7 +81,7 @@
<dimen name="gestures_overscroll_fling_threshold">40dp</dimen>
<!-- Tips Gesture Tutorial -->
<dimen name="back_gesture_tutorial_title_margin_start_end">40dp</dimen>
<dimen name="back_gesture_tutorial_subtitle_margin_start_end">16dp</dimen>
<dimen name="back_gesture_tutorial_button_margin_start_end">18dp</dimen>
<dimen name="gesture_tutorial_title_margin_start_end">40dp</dimen>
<dimen name="gesture_tutorial_subtitle_margin_start_end">16dp</dimen>
<dimen name="gesture_tutorial_button_margin_start_end">18dp</dimen>
</resources>
+3 -3
View File
@@ -61,7 +61,7 @@
<string name="all_apps_prediction_tip">Your predicted apps</string>
<!-- Content description for a close button. [CHAR LIMIT=NONE] -->
<string name="back_gesture_tutorial_close_button_content_description" translatable="false">Close</string>
<string name="gesture_tutorial_close_button_content_description" translatable="false">Close</string>
<!-- Hotseat migration notification title -->
@@ -110,9 +110,9 @@
<string name="back_gesture_tutorial_confirm_subtitle" translatable="false">To change the sensitivity of the back gesture, go to Settings</string>
<!-- Button text shown on a button on the confirm screen. [CHAR LIMIT=14] -->
<string name="back_gesture_tutorial_action_button_label" translatable="false">Done</string>
<string name="gesture_tutorial_action_button_label" translatable="false">Done</string>
<!-- Button text shown on a text button on the confirm screen. [CHAR LIMIT=14] -->
<string name="back_gesture_tutorial_action_text_button_label" translatable="false">Settings</string>
<string name="gesture_tutorial_action_text_button_label" translatable="false">Settings</string>
<!-- ******* Overview ******* -->
<!-- Label for a button that causes the current overview app to be shared. [CHAR_LIMIT=40] -->
+4 -4
View File
@@ -35,14 +35,14 @@
<style name="TextAppearance.BackGestureTutorial.Title"
parent="TextAppearance.BackGestureTutorial">
<item name="android:gravity">center</item>
<item name="android:textColor">@color/back_gesture_tutorial_title_color</item>
<item name="android:textColor">@color/gesture_tutorial_title_color</item>
<item name="android:textSize">28sp</item>
</style>
<style name="TextAppearance.BackGestureTutorial.Subtitle"
parent="TextAppearance.BackGestureTutorial">
<item name="android:gravity">center</item>
<item name="android:textColor">@color/back_gesture_tutorial_subtitle_color</item>
<item name="android:textColor">@color/gesture_tutorial_subtitle_color</item>
<item name="android:letterSpacing">0.03</item>
<item name="android:textSize">21sp</item>
</style>
@@ -50,7 +50,7 @@
<style name="TextAppearance.BackGestureTutorial.ButtonLabel"
parent="TextAppearance.BackGestureTutorial.CallToAction">
<item name="android:gravity">center</item>
<item name="android:textColor">@color/back_gesture_tutorial_action_button_label_color</item>
<item name="android:textColor">@color/gesture_tutorial_action_button_label_color</item>
<item name="android:letterSpacing">0.02</item>
<item name="android:textSize">16sp</item>
<item name="android:textAllCaps">false</item>
@@ -58,7 +58,7 @@
<style name="TextAppearance.BackGestureTutorial.TextButtonLabel"
parent="TextAppearance.BackGestureTutorial.ButtonLabel">
<item name="android:textColor">@color/back_gesture_tutorial_primary_color</item>
<item name="android:textColor">@color/gesture_tutorial_primary_color</item>
</style>
<style name="OverviewActionButton"
@@ -1,64 +0,0 @@
/*
* Copyright (C) 2020 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 com.android.quickstep.interaction;
import android.view.View;
import com.android.launcher3.R;
import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialStep;
import java.util.Optional;
/**
* An implementation of {@link BackGestureTutorialController} that defines the behavior of the
* {@link TutorialStep#CONFIRM}.
*/
final class BackGestureTutorialConfirmController extends BackGestureTutorialController {
BackGestureTutorialConfirmController(BackGestureTutorialFragment fragment,
BackGestureTutorialTypeInfo tutorialTypeInfo) {
super(fragment, TutorialStep.CONFIRM, Optional.of(tutorialTypeInfo));
}
@Override
Optional<Integer> getTitleStringId() {
return Optional.of(mTutorialTypeInfo.get().getTutorialConfirmTitleId());
}
@Override
Optional<Integer> getSubtitleStringId() {
return Optional.of(mTutorialTypeInfo.get().getTutorialConfirmSubtitleId());
}
@Override
Optional<Integer> getActionButtonStringId() {
return Optional.of(R.string.back_gesture_tutorial_action_button_label);
}
@Override
Optional<Integer> getActionTextButtonStringId() {
return Optional.of(R.string.back_gesture_tutorial_action_text_button_label);
}
@Override
void onActionButtonClicked(View button) {
hideHandCoachingAnimation();
if (button == mActionTextButton) {
mFragment.startSystemNavigationSetting();
}
mFragment.closeTutorial();
}
}
@@ -15,164 +15,101 @@
*/
package com.android.quickstep.interaction;
import static com.android.quickstep.interaction.TutorialController.TutorialType.BACK_NAVIGATION_COMPLETE;
import static com.android.quickstep.interaction.TutorialController.TutorialType.LEFT_EDGE_BACK_NAVIGATION;
import android.view.View;
import android.widget.Button;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.android.launcher3.R;
import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialStep;
import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialType;
import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureResult;
import java.util.Optional;
/** A {@link TutorialController} for the Back tutorial. */
final class BackGestureTutorialController extends TutorialController {
/**
* Defines the behavior of the particular {@link TutorialStep} and implements the transition to it.
*/
abstract class BackGestureTutorialController {
final BackGestureTutorialFragment mFragment;
final TutorialStep mTutorialStep;
final Optional<BackGestureTutorialTypeInfo> mTutorialTypeInfo;
final Button mActionTextButton;
final Button mActionButton;
final TextView mSubtitleTextView;
final ImageButton mCloseButton;
final BackGestureTutorialHandAnimation mHandCoachingAnimation;
final LinearLayout mTitlesContainer;
private final TextView mTitleTextView;
private final ImageView mHandCoachingView;
BackGestureTutorialController(
BackGestureTutorialFragment fragment,
TutorialStep tutorialStep,
Optional<BackGestureTutorialTypeInfo> tutorialTypeInfo) {
mFragment = fragment;
mTutorialStep = tutorialStep;
mTutorialTypeInfo = tutorialTypeInfo;
View rootView = fragment.getRootView();
mActionTextButton = rootView.findViewById(
R.id.back_gesture_tutorial_fragment_action_text_button);
mActionButton = rootView.findViewById(R.id.back_gesture_tutorial_fragment_action_button);
mSubtitleTextView = rootView.findViewById(
R.id.back_gesture_tutorial_fragment_subtitle_view);
mTitleTextView = rootView.findViewById(R.id.back_gesture_tutorial_fragment_title_view);
mHandCoachingView = rootView.findViewById(
R.id.back_gesture_tutorial_fragment_hand_coaching);
mHandCoachingAnimation = mFragment.getHandAnimation();
mHandCoachingView.bringToFront();
mCloseButton = rootView.findViewById(R.id.back_gesture_tutorial_fragment_close_button);
mTitlesContainer = rootView.findViewById(
R.id.back_gesture_tutorial_fragment_titles_container);
BackGestureTutorialController(BackGestureTutorialFragment fragment, TutorialType tutorialType) {
super(fragment, tutorialType);
}
@Override
void transitToController() {
updateTitles();
updateActionButtons();
super.transitToController();
if (mTutorialType != BACK_NAVIGATION_COMPLETE) {
mHandCoachingAnimation.startLoopedAnimation(mTutorialType);
}
}
void hideHandCoachingAnimation() {
mHandCoachingAnimation.stop();
@Override
Integer getTitleStringId() {
switch (mTutorialType) {
case RIGHT_EDGE_BACK_NAVIGATION:
return R.string.back_gesture_tutorial_playground_title_swipe_inward_right_edge;
case LEFT_EDGE_BACK_NAVIGATION:
return R.string.back_gesture_tutorial_playground_title_swipe_inward_left_edge;
case BACK_NAVIGATION_COMPLETE:
return R.string.back_gesture_tutorial_confirm_title;
}
return null;
}
void onGestureAttempted(BackGestureResult result) {
if (mTutorialStep == TutorialStep.CONFIRM
&& (result == BackGestureResult.BACK_COMPLETED_FROM_LEFT
|| result == BackGestureResult.BACK_COMPLETED_FROM_RIGHT)) {
mFragment.closeTutorial();
return;
@Override
Integer getSubtitleStringId() {
switch (mTutorialType) {
case RIGHT_EDGE_BACK_NAVIGATION:
return R.string.back_gesture_tutorial_engaged_subtitle_swipe_inward_right_edge;
case LEFT_EDGE_BACK_NAVIGATION:
return R.string.back_gesture_tutorial_engaged_subtitle_swipe_inward_left_edge;
case BACK_NAVIGATION_COMPLETE:
return R.string.back_gesture_tutorial_confirm_subtitle;
}
return null;
}
if (!mTutorialTypeInfo.isPresent()) {
return;
@Override
Integer getActionButtonStringId() {
if (mTutorialType == BACK_NAVIGATION_COMPLETE) {
return R.string.gesture_tutorial_action_button_label;
}
return null;
}
switch (mTutorialTypeInfo.get().getTutorialType()) {
@Override
Integer getActionTextButtonStringId() {
if (mTutorialType == BACK_NAVIGATION_COMPLETE) {
return R.string.gesture_tutorial_action_text_button_label;
}
return null;
}
@Override
void onActionButtonClicked(View button) {
hideHandCoachingAnimation();
if (button == mActionTextButton) {
mTutorialFragment.startSystemNavigationSetting();
}
mTutorialFragment.closeTutorial();
}
@Override
public void onBackGestureAttempted(BackGestureResult result) {
switch (mTutorialType) {
case RIGHT_EDGE_BACK_NAVIGATION:
if (result == BackGestureResult.BACK_COMPLETED_FROM_RIGHT) {
hideHandCoachingAnimation();
mFragment.changeController(
TutorialStep.ENGAGED, TutorialType.LEFT_EDGE_BACK_NAVIGATION);
mTutorialFragment.changeController(LEFT_EDGE_BACK_NAVIGATION);
}
break;
case LEFT_EDGE_BACK_NAVIGATION:
if (result == BackGestureResult.BACK_COMPLETED_FROM_LEFT) {
hideHandCoachingAnimation();
mFragment.changeController(TutorialStep.CONFIRM);
mTutorialFragment.changeController(BACK_NAVIGATION_COMPLETE);
}
break;
case BACK_NAVIGATION_COMPLETE:
if (result == BackGestureResult.BACK_COMPLETED_FROM_LEFT
|| result == BackGestureResult.BACK_COMPLETED_FROM_RIGHT) {
mTutorialFragment.closeTutorial();
}
break;
}
}
abstract Optional<Integer> getTitleStringId();
abstract Optional<Integer> getSubtitleStringId();
abstract Optional<Integer> getActionButtonStringId();
abstract Optional<Integer> getActionTextButtonStringId();
abstract void onActionButtonClicked(View button);
private void updateActionButtons() {
updateButton(mActionButton, getActionButtonStringId(), this::onActionButtonClicked);
updateButton(mActionTextButton, getActionTextButtonStringId(), this::onActionButtonClicked);
}
private static void updateButton(Button button, Optional<Integer> stringId,
View.OnClickListener listener) {
if (!stringId.isPresent()) {
button.setVisibility(View.INVISIBLE);
return;
}
button.setVisibility(View.VISIBLE);
button.setText(stringId.get());
button.setOnClickListener(listener);
}
private void updateTitles() {
updateTitleView(mTitleTextView, getTitleStringId(),
R.style.TextAppearance_BackGestureTutorial_Title);
updateTitleView(mSubtitleTextView, getSubtitleStringId(),
R.style.TextAppearance_BackGestureTutorial_Subtitle);
}
private static void updateTitleView(TextView textView, Optional<Integer> stringId,
int styleId) {
if (!stringId.isPresent()) {
textView.setVisibility(View.GONE);
return;
}
textView.setVisibility(View.VISIBLE);
textView.setText(stringId.get());
textView.setTextAppearance(styleId);
}
/**
* Constructs {@link BackGestureTutorialController} for providing {@link TutorialType} and
* {@link TutorialStep}.
*/
static Optional<BackGestureTutorialController> getTutorialController(
BackGestureTutorialFragment fragment, TutorialStep tutorialStep,
TutorialType tutorialType) {
BackGestureTutorialTypeInfo tutorialTypeInfo =
BackGestureTutorialTypeInfoProvider.getTutorialTypeInfo(tutorialType);
switch (tutorialStep) {
case ENGAGED:
return Optional.of(
new BackGestureTutorialEngagedController(fragment, tutorialTypeInfo));
case CONFIRM:
return Optional.of(
new BackGestureTutorialConfirmController(fragment, tutorialTypeInfo));
default:
throw new AssertionError("Unexpected tutorial step: " + tutorialStep);
}
}
}
@@ -1,64 +0,0 @@
/*
* Copyright (C) 2020 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 com.android.quickstep.interaction;
import android.view.View;
import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialStep;
import java.util.Optional;
/**
* An implementation of {@link BackGestureTutorialController} that defines the behavior of the
* {@link TutorialStep#ENGAGED}.
*/
final class BackGestureTutorialEngagedController extends BackGestureTutorialController {
BackGestureTutorialEngagedController(
BackGestureTutorialFragment fragment, BackGestureTutorialTypeInfo tutorialTypeInfo) {
super(fragment, TutorialStep.ENGAGED, Optional.of(tutorialTypeInfo));
}
@Override
void transitToController() {
super.transitToController();
mHandCoachingAnimation.startLoopedAnimation(mTutorialTypeInfo.get().getTutorialType());
}
@Override
Optional<Integer> getTitleStringId() {
return Optional.of(mTutorialTypeInfo.get().getTutorialPlaygroundTitleId());
}
@Override
Optional<Integer> getSubtitleStringId() {
return Optional.of(mTutorialTypeInfo.get().getTutorialEngagedSubtitleId());
}
@Override
Optional<Integer> getActionButtonStringId() {
return Optional.empty();
}
@Override
Optional<Integer> getActionTextButtonStringId() {
return Optional.empty();
}
@Override
void onActionButtonClicked(View button) {
}
}
@@ -15,172 +15,43 @@
*/
package com.android.quickstep.interaction;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.graphics.Insets;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import com.android.launcher3.R;
import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureAttemptCallback;
import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureResult;
import java.net.URISyntaxException;
import java.util.Optional;
import com.android.quickstep.interaction.TutorialController.TutorialType;
/** Shows the Back gesture interactive tutorial. */
public class BackGestureTutorialFragment extends Fragment implements BackGestureAttemptCallback {
private static final String LOG_TAG = "TutorialFragment";
private static final String KEY_TUTORIAL_STEP = "tutorialStep";
private static final String KEY_TUTORIAL_TYPE = "tutorialType";
private static final String SYSTEM_NAVIGATION_SETTING_INTENT =
"#Intent;action=com.android.settings.SEARCH_RESULT_TRAMPOLINE;S"
+ ".:settings:fragment_args_key=gesture_system_navigation_input_summary;S"
+ ".:settings:show_fragment=com.android.settings.gestures"
+ ".SystemNavigationGestureSettings;end";
private TutorialStep mTutorialStep;
private TutorialType mTutorialType;
private Optional<BackGestureTutorialController> mTutorialController = Optional.empty();
private View mRootView;
private BackGestureTutorialHandAnimation mHandCoachingAnimation;
private EdgeBackGestureHandler mEdgeBackGestureHandler;
public static BackGestureTutorialFragment newInstance(
TutorialStep tutorialStep, TutorialType tutorialType) {
BackGestureTutorialFragment fragment = new BackGestureTutorialFragment();
Bundle args = new Bundle();
args.putSerializable(KEY_TUTORIAL_STEP, tutorialStep);
args.putSerializable(KEY_TUTORIAL_TYPE, tutorialType);
fragment.setArguments(args);
return fragment;
}
public class BackGestureTutorialFragment extends TutorialFragment
implements BackGestureAttemptCallback {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle args = savedInstanceState != null ? savedInstanceState : getArguments();
mTutorialStep = (TutorialStep) args.getSerializable(KEY_TUTORIAL_STEP);
mTutorialType = (TutorialType) args.getSerializable(KEY_TUTORIAL_TYPE);
mEdgeBackGestureHandler = new EdgeBackGestureHandler(getContext());
mEdgeBackGestureHandler.registerBackGestureAttemptCallback(this);
}
@Override
public View onCreateView(
@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
mRootView = inflater.inflate(R.layout.back_gesture_tutorial_fragment,
container, /* attachToRoot= */ false);
mRootView.findViewById(R.id.back_gesture_tutorial_fragment_close_button)
.setOnClickListener(this::onCloseButtonClicked);
mRootView.setOnApplyWindowInsetsListener((view, insets) -> {
Insets systemInsets = insets.getInsets(WindowInsets.Type.systemBars());
mEdgeBackGestureHandler.setInsets(systemInsets.left, systemInsets.right);
return insets;
});
mRootView.setOnTouchListener(mEdgeBackGestureHandler);
mHandCoachingAnimation = new BackGestureTutorialHandAnimation(getContext(), mRootView);
return mRootView;
public void onDestroy() {
super.onDestroy();
mEdgeBackGestureHandler.unregisterBackGestureAttemptCallback();
}
@Override
public void onResume() {
super.onResume();
changeController(mTutorialStep, mTutorialType);
int getHandAnimationResId() {
return R.drawable.back_gesture;
}
@Override
public void onPause() {
super.onPause();
mHandCoachingAnimation.stop();
}
void onAttachedToWindow() {
mEdgeBackGestureHandler.setViewGroupParent((ViewGroup) getRootView());
}
void onDetachedFromWindow() {
mEdgeBackGestureHandler.setViewGroupParent(null);
}
@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
savedInstanceState.putSerializable(KEY_TUTORIAL_STEP, mTutorialStep);
savedInstanceState.putSerializable(KEY_TUTORIAL_TYPE, mTutorialType);
super.onSaveInstanceState(savedInstanceState);
}
View getRootView() {
return mRootView;
}
BackGestureTutorialHandAnimation getHandAnimation() {
return mHandCoachingAnimation;
}
void changeController(TutorialStep tutorialStep) {
changeController(tutorialStep, mTutorialType);
}
void changeController(TutorialStep tutorialStep, TutorialType tutorialType) {
Optional<BackGestureTutorialController> tutorialController =
BackGestureTutorialController.getTutorialController(/* fragment= */ this,
tutorialStep, tutorialType);
if (!tutorialController.isPresent()) {
return;
}
mTutorialController = tutorialController;
mTutorialController.get().transitToController();
this.mTutorialStep = mTutorialController.get().mTutorialStep;
this.mTutorialType = tutorialType;
TutorialController createController(TutorialType type) {
return new BackGestureTutorialController(this, type);
}
@Override
public void onBackGestureAttempted(BackGestureResult result) {
mTutorialController.ifPresent(controller -> controller.onGestureAttempted(result));
}
void closeTutorial() {
getActivity().finish();
}
void startSystemNavigationSetting() {
try {
startActivityForResult(
Intent.parseUri(SYSTEM_NAVIGATION_SETTING_INTENT, /* flags= */ 0),
/* requestCode= */ 0);
} catch (URISyntaxException e) {
Log.e(LOG_TAG, "The launch Intent Uri is wrong syntax: " + e);
} catch (ActivityNotFoundException e) {
Log.e(LOG_TAG, "The launch Activity not found: " + e);
if (mTutorialController != null) {
mTutorialController.onBackGestureAttempted(result);
}
}
private void onCloseButtonClicked(View button) {
closeTutorial();
}
/** Denotes the step of the tutorial. */
enum TutorialStep {
ENGAGED,
CONFIRM,
}
/** Denotes the type of the tutorial. */
enum TutorialType {
RIGHT_EDGE_BACK_NAVIGATION,
LEFT_EDGE_BACK_NAVIGATION,
}
}
@@ -1,109 +0,0 @@
/*
* Copyright (C) 2020 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 com.android.quickstep.interaction;
import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialType;
/** Defines the UI element identifiers for the particular {@link TutorialType}. */
final class BackGestureTutorialTypeInfo {
private final TutorialType mTutorialType;
private final int mTutorialPlaygroundTitleId;
private final int mTutorialEngagedSubtitleId;
private final int mTutorialConfirmTitleId;
private final int mTutorialConfirmSubtitleId;
TutorialType getTutorialType() {
return mTutorialType;
}
int getTutorialPlaygroundTitleId() {
return mTutorialPlaygroundTitleId;
}
int getTutorialEngagedSubtitleId() {
return mTutorialEngagedSubtitleId;
}
int getTutorialConfirmTitleId() {
return mTutorialConfirmTitleId;
}
int getTutorialConfirmSubtitleId() {
return mTutorialConfirmSubtitleId;
}
static Builder builder() {
return new Builder();
}
private BackGestureTutorialTypeInfo(
TutorialType tutorialType,
int tutorialPlaygroundTitleId,
int tutorialEngagedSubtitleId,
int tutorialConfirmTitleId,
int tutorialConfirmSubtitleId) {
mTutorialType = tutorialType;
mTutorialPlaygroundTitleId = tutorialPlaygroundTitleId;
mTutorialEngagedSubtitleId = tutorialEngagedSubtitleId;
mTutorialConfirmTitleId = tutorialConfirmTitleId;
mTutorialConfirmSubtitleId = tutorialConfirmSubtitleId;
}
/** Builder for producing {@link BackGestureTutorialTypeInfo} objects. */
static class Builder {
private TutorialType mTutorialType;
private Integer mTutorialPlaygroundTitleId;
private Integer mTutorialEngagedSubtitleId;
private Integer mTutorialConfirmTitleId;
private Integer mTutorialConfirmSubtitleId;
Builder setTutorialType(TutorialType tutorialType) {
mTutorialType = tutorialType;
return this;
}
Builder setTutorialPlaygroundTitleId(int stringId) {
mTutorialPlaygroundTitleId = stringId;
return this;
}
Builder setTutorialEngagedSubtitleId(int stringId) {
mTutorialEngagedSubtitleId = stringId;
return this;
}
Builder setTutorialConfirmTitleId(int stringId) {
mTutorialConfirmTitleId = stringId;
return this;
}
Builder setTutorialConfirmSubtitleId(int stringId) {
mTutorialConfirmSubtitleId = stringId;
return this;
}
BackGestureTutorialTypeInfo build() {
return new BackGestureTutorialTypeInfo(
mTutorialType,
mTutorialPlaygroundTitleId,
mTutorialEngagedSubtitleId,
mTutorialConfirmTitleId,
mTutorialConfirmSubtitleId);
}
}
}
@@ -1,59 +0,0 @@
/*
* Copyright (C) 2020 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 com.android.quickstep.interaction;
import com.android.launcher3.R;
import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialType;
/** Provides instances of {@link BackGestureTutorialTypeInfo} for each {@link TutorialType}. */
final class BackGestureTutorialTypeInfoProvider {
private static final BackGestureTutorialTypeInfo RIGHT_EDGE_BACK_NAV_TUTORIAL_INFO =
BackGestureTutorialTypeInfo.builder()
.setTutorialType(TutorialType.RIGHT_EDGE_BACK_NAVIGATION)
.setTutorialPlaygroundTitleId(
R.string.back_gesture_tutorial_playground_title_swipe_inward_right_edge)
.setTutorialEngagedSubtitleId(
R.string.back_gesture_tutorial_engaged_subtitle_swipe_inward_right_edge)
.setTutorialConfirmTitleId(R.string.back_gesture_tutorial_confirm_title)
.setTutorialConfirmSubtitleId(R.string.back_gesture_tutorial_confirm_subtitle)
.build();
private static final BackGestureTutorialTypeInfo LEFT_EDGE_BACK_NAV_TUTORIAL_INFO =
BackGestureTutorialTypeInfo.builder()
.setTutorialType(TutorialType.LEFT_EDGE_BACK_NAVIGATION)
.setTutorialPlaygroundTitleId(
R.string.back_gesture_tutorial_playground_title_swipe_inward_left_edge)
.setTutorialEngagedSubtitleId(
R.string.back_gesture_tutorial_engaged_subtitle_swipe_inward_left_edge)
.setTutorialConfirmTitleId(R.string.back_gesture_tutorial_confirm_title)
.setTutorialConfirmSubtitleId(R.string.back_gesture_tutorial_confirm_subtitle)
.build();
static BackGestureTutorialTypeInfo getTutorialTypeInfo(TutorialType tutorialType) {
switch (tutorialType) {
case RIGHT_EDGE_BACK_NAVIGATION:
return RIGHT_EDGE_BACK_NAV_TUTORIAL_INFO;
case LEFT_EDGE_BACK_NAVIGATION:
return LEFT_EDGE_BACK_NAV_TUTORIAL_INFO;
default:
throw new AssertionError("Unexpected tutorial type: " + tutorialType);
}
}
private BackGestureTutorialTypeInfoProvider() {
}
}
@@ -20,7 +20,6 @@ import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.PointF;
import android.os.SystemProperties;
import android.view.Display;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
@@ -46,7 +45,6 @@ public class EdgeBackGestureHandler implements OnTouchListener {
private final Context mContext;
private final Point mDisplaySize = new Point();
private final int mDisplayId;
// The edge width where touch down is allowed
private int mEdgeWidth;
@@ -91,8 +89,6 @@ public class EdgeBackGestureHandler implements OnTouchListener {
EdgeBackGestureHandler(Context context) {
final Resources res = context.getResources();
mContext = context;
mDisplayId = context.getDisplay() == null
? Display.DEFAULT_DISPLAY : context.getDisplay().getDisplayId();
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
mLongPressTimeout = Math.min(MAX_LONG_PRESS_TIMEOUT,
@@ -126,6 +122,10 @@ public class EdgeBackGestureHandler implements OnTouchListener {
mGestureCallback = callback;
}
void unregisterBackGestureAttemptCallback() {
mGestureCallback = null;
}
private LayoutParams createLayoutParams() {
Resources resources = mContext.getResources();
return new LayoutParams(
@@ -19,6 +19,7 @@ import android.graphics.Color;
import android.graphics.Rect;
import android.os.Bundle;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Display;
import android.view.View;
import android.view.Window;
@@ -26,26 +27,32 @@ import android.view.Window;
import androidx.fragment.app.FragmentActivity;
import com.android.launcher3.R;
import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialStep;
import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialType;
import com.android.quickstep.interaction.TutorialController.TutorialType;
import java.util.List;
/** Shows the gesture interactive sandbox in full screen mode. */
public class GestureSandboxActivity extends FragmentActivity {
private BackGestureTutorialFragment mFragment;
private static final String LOG_TAG = "GestureSandboxActivity";
private TutorialFragment mFragment;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.back_gesture_tutorial_activity);
setContentView(R.layout.gesture_tutorial_activity);
mFragment = BackGestureTutorialFragment.newInstance(
TutorialStep.ENGAGED, TutorialType.RIGHT_EDGE_BACK_NAVIGATION);
try {
mFragment = TutorialFragment.newInstance(BackGestureTutorialFragment.class,
TutorialType.RIGHT_EDGE_BACK_NAVIGATION);
} catch (InstantiationException | IllegalAccessException e) {
Log.wtf(LOG_TAG, "Failed to create tutorial fragment!", e);
mFragment = new BackGestureTutorialFragment();
}
getSupportFragmentManager().beginTransaction()
.add(R.id.back_gesture_tutorial_fragment_container, mFragment)
.add(R.id.gesture_tutorial_fragment_container, mFragment)
.commit();
}
@@ -0,0 +1,135 @@
/*
* Copyright (C) 2020 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 com.android.quickstep.interaction;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.CallSuper;
import androidx.annotation.Nullable;
import com.android.launcher3.R;
import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureResult;
abstract class TutorialController {
final TutorialFragment mTutorialFragment;
final TutorialType mTutorialType;
final ImageButton mCloseButton;
final TextView mTitleTextView;
final TextView mSubtitleTextView;
final TutorialHandAnimation mHandCoachingAnimation;
final ImageView mHandCoachingView;
final Button mActionTextButton;
final Button mActionButton;
TutorialController(TutorialFragment tutorialFragment, TutorialType tutorialType) {
mTutorialFragment = tutorialFragment;
mTutorialType = tutorialType;
View rootView = tutorialFragment.getRootView();
mCloseButton = rootView.findViewById(R.id.gesture_tutorial_fragment_close_button);
mCloseButton.setOnClickListener(button -> mTutorialFragment.closeTutorial());
mTitleTextView = rootView.findViewById(R.id.gesture_tutorial_fragment_title_view);
mSubtitleTextView = rootView.findViewById(R.id.gesture_tutorial_fragment_subtitle_view);
mHandCoachingAnimation = tutorialFragment.getHandAnimation();
mHandCoachingView = rootView.findViewById(R.id.gesture_tutorial_fragment_hand_coaching);
mHandCoachingView.bringToFront();
mActionTextButton =
rootView.findViewById(R.id.gesture_tutorial_fragment_action_text_button);
mActionButton = rootView.findViewById(R.id.gesture_tutorial_fragment_action_button);
}
abstract void onBackGestureAttempted(BackGestureResult result);
@Nullable
Integer getTitleStringId() {
return null;
}
@Nullable
Integer getSubtitleStringId() {
return null;
}
@Nullable
Integer getActionButtonStringId() {
return null;
}
@Nullable
Integer getActionTextButtonStringId() {
return null;
}
void onActionButtonClicked(View button) {}
void hideHandCoachingAnimation() {
mHandCoachingAnimation.stop();
}
@CallSuper
void transitToController() {
updateTitles();
updateActionButtons();
}
private void updateTitles() {
updateTitleView(mTitleTextView, getTitleStringId(),
R.style.TextAppearance_BackGestureTutorial_Title);
updateTitleView(mSubtitleTextView, getSubtitleStringId(),
R.style.TextAppearance_BackGestureTutorial_Subtitle);
}
private void updateTitleView(TextView textView, @Nullable Integer stringId, int styleId) {
if (stringId == null) {
textView.setVisibility(View.GONE);
return;
}
textView.setVisibility(View.VISIBLE);
textView.setText(stringId);
textView.setTextAppearance(styleId);
}
private void updateActionButtons() {
updateButton(mActionButton, getActionButtonStringId(), this::onActionButtonClicked);
updateButton(mActionTextButton, getActionTextButtonStringId(), this::onActionButtonClicked);
}
private void updateButton(Button button, @Nullable Integer stringId, OnClickListener listener) {
if (stringId == null) {
button.setVisibility(View.INVISIBLE);
return;
}
button.setVisibility(View.VISIBLE);
button.setText(stringId);
button.setOnClickListener(listener);
}
/** Denotes the type of the tutorial. */
enum TutorialType {
RIGHT_EDGE_BACK_NAVIGATION,
LEFT_EDGE_BACK_NAVIGATION,
BACK_NAVIGATION_COMPLETE,
}
}
@@ -0,0 +1,153 @@
/*
* Copyright (C) 2020 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 com.android.quickstep.interaction;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.graphics.Insets;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import com.android.launcher3.R;
import com.android.quickstep.interaction.TutorialController.TutorialType;
import java.net.URISyntaxException;
abstract class TutorialFragment extends Fragment {
private static final String LOG_TAG = "TutorialFragment";
private static final String SYSTEM_NAVIGATION_SETTING_INTENT =
"#Intent;action=com.android.settings.SEARCH_RESULT_TRAMPOLINE;S"
+ ".:settings:fragment_args_key=gesture_system_navigation_input_summary;S"
+ ".:settings:show_fragment=com.android.settings.gestures"
+ ".SystemNavigationGestureSettings;end";
private static final String KEY_TUTORIAL_TYPE = "tutorialType";
TutorialType mTutorialType;
@Nullable TutorialController mTutorialController = null;
View mRootView;
TutorialHandAnimation mHandCoachingAnimation;
EdgeBackGestureHandler mEdgeBackGestureHandler;
public static TutorialFragment newInstance(
Class<? extends TutorialFragment> fragmentClass, TutorialType tutorialType)
throws java.lang.InstantiationException, IllegalAccessException {
TutorialFragment fragment = fragmentClass.newInstance();
Bundle args = new Bundle();
args.putSerializable(KEY_TUTORIAL_TYPE, tutorialType);
fragment.setArguments(args);
return fragment;
}
abstract int getHandAnimationResId();
abstract TutorialController createController(TutorialType type);
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle args = savedInstanceState != null ? savedInstanceState : getArguments();
mTutorialType = (TutorialType) args.getSerializable(KEY_TUTORIAL_TYPE);
mEdgeBackGestureHandler = new EdgeBackGestureHandler(getContext());
}
@Override
public View onCreateView(
@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
mRootView = inflater.inflate(R.layout.gesture_tutorial_fragment,
container, /* attachToRoot= */ false);
mRootView.setOnApplyWindowInsetsListener((view, insets) -> {
Insets systemInsets = insets.getInsets(WindowInsets.Type.systemBars());
mEdgeBackGestureHandler.setInsets(systemInsets.left, systemInsets.right);
return insets;
});
mRootView.setOnTouchListener(mEdgeBackGestureHandler);
mHandCoachingAnimation = new TutorialHandAnimation(getContext(), mRootView,
getHandAnimationResId());
return mRootView;
}
@Override
public void onResume() {
super.onResume();
changeController(mTutorialType);
}
@Override
public void onPause() {
super.onPause();
mHandCoachingAnimation.stop();
}
void onAttachedToWindow() {
mEdgeBackGestureHandler.setViewGroupParent((ViewGroup) getRootView());
}
void onDetachedFromWindow() {
mEdgeBackGestureHandler.setViewGroupParent(null);
}
void changeController(TutorialType tutorialType) {
mTutorialController = createController(tutorialType);
mTutorialController.transitToController();
mTutorialType = tutorialType;
}
@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
savedInstanceState.putSerializable(KEY_TUTORIAL_TYPE, mTutorialType);
super.onSaveInstanceState(savedInstanceState);
}
View getRootView() {
return mRootView;
}
TutorialHandAnimation getHandAnimation() {
return mHandCoachingAnimation;
}
void closeTutorial() {
FragmentActivity activity = getActivity();
if (activity != null) {
activity.finish();
}
}
void startSystemNavigationSetting() {
try {
startActivityForResult(
Intent.parseUri(SYSTEM_NAVIGATION_SETTING_INTENT, /* flags= */ 0),
/* requestCode= */ 0);
} catch (URISyntaxException e) {
Log.e(LOG_TAG, "The launch Intent Uri is wrong syntax: " + e);
} catch (ActivityNotFoundException e) {
Log.e(LOG_TAG, "The launch Activity not found: " + e);
}
}
}
@@ -25,12 +25,12 @@ import android.widget.ImageView;
import androidx.core.content.ContextCompat;
import com.android.launcher3.R;
import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialType;
import com.android.quickstep.interaction.TutorialController.TutorialType;
import java.time.Duration;
/** Hand coaching animation. */
final class BackGestureTutorialHandAnimation {
final class TutorialHandAnimation {
// A delay for waiting the Activity fully launches.
private static final Duration ANIMATION_START_DELAY = Duration.ofMillis(300L);
@@ -38,16 +38,12 @@ final class BackGestureTutorialHandAnimation {
private final ImageView mHandCoachingView;
private final AnimatedVectorDrawable mGestureAnimation;
BackGestureTutorialHandAnimation(Context context, View rootView) {
mHandCoachingView = rootView.findViewById(
R.id.back_gesture_tutorial_fragment_hand_coaching);
mGestureAnimation = (AnimatedVectorDrawable) ContextCompat.getDrawable(context,
R.drawable.back_gesture);
TutorialHandAnimation(Context context, View rootView, int resId) {
mHandCoachingView = rootView.findViewById(R.id.gesture_tutorial_fragment_hand_coaching);
mGestureAnimation = (AnimatedVectorDrawable) ContextCompat.getDrawable(context, resId);
}
/**
* [Re]starts animation for the given tutorial.
*/
/** [Re]starts animation for the given tutorial. */
void startLoopedAnimation(TutorialType tutorialType) {
if (mGestureAnimation.isRunning()) {
stop();
+5 -5
View File
@@ -38,11 +38,11 @@
<color name="all_apps_bg_hand_fill">#E5E5E5</color>
<color name="all_apps_bg_hand_fill_dark">#9AA0A6</color>
<color name="back_gesture_tutorial_background_color">#FFFFFFFF</color>
<color name="back_gesture_tutorial_subtitle_color">#99000000</color> <!-- 60% black -->
<color name="back_gesture_tutorial_title_color">#FF000000</color>
<color name="back_gesture_tutorial_action_button_label_color">#FFFFFFFF</color>
<color name="back_gesture_tutorial_primary_color">#1A73E8</color> <!-- Blue -->
<color name="gesture_tutorial_background_color">#FFFFFFFF</color>
<color name="gesture_tutorial_subtitle_color">#99000000</color> <!-- 60% black -->
<color name="gesture_tutorial_title_color">#FF000000</color>
<color name="gesture_tutorial_action_button_label_color">#FFFFFFFF</color>
<color name="gesture_tutorial_primary_color">#1A73E8</color> <!-- Blue -->
</resources>