1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.quickstep.interaction; 17 18 import android.content.Context; 19 import android.graphics.drawable.RippleDrawable; 20 import android.view.View; 21 import android.view.View.OnClickListener; 22 import android.widget.Button; 23 import android.widget.ImageButton; 24 import android.widget.ImageView; 25 import android.widget.TextView; 26 27 import androidx.annotation.CallSuper; 28 import androidx.annotation.Nullable; 29 30 import com.android.launcher3.R; 31 import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureAttemptCallback; 32 import com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureAttemptCallback; 33 34 abstract class TutorialController implements BackGestureAttemptCallback, 35 NavBarGestureAttemptCallback { 36 37 private static final int FEEDBACK_VISIBLE_MS = 3000; 38 private static final int FEEDBACK_ANIMATION_MS = 500; 39 private static final int RIPPLE_VISIBLE_MS = 300; 40 41 final TutorialFragment mTutorialFragment; 42 TutorialType mTutorialType; 43 final Context mContext; 44 45 final ImageButton mCloseButton; 46 final TextView mTitleTextView; 47 final TextView mSubtitleTextView; 48 final TextView mFeedbackView; 49 final View mFakeTaskView; 50 final View mRippleView; 51 final RippleDrawable mRippleDrawable; 52 final TutorialHandAnimation mHandCoachingAnimation; 53 final ImageView mHandCoachingView; 54 final Button mActionTextButton; 55 final Button mActionButton; 56 private final Runnable mHideFeedbackRunnable; 57 TutorialController(TutorialFragment tutorialFragment, TutorialType tutorialType)58 TutorialController(TutorialFragment tutorialFragment, TutorialType tutorialType) { 59 mTutorialFragment = tutorialFragment; 60 mTutorialType = tutorialType; 61 mContext = mTutorialFragment.getContext(); 62 63 View rootView = tutorialFragment.getRootView(); 64 mCloseButton = rootView.findViewById(R.id.gesture_tutorial_fragment_close_button); 65 mCloseButton.setOnClickListener(button -> mTutorialFragment.closeTutorial()); 66 mTitleTextView = rootView.findViewById(R.id.gesture_tutorial_fragment_title_view); 67 mSubtitleTextView = rootView.findViewById(R.id.gesture_tutorial_fragment_subtitle_view); 68 mFeedbackView = rootView.findViewById(R.id.gesture_tutorial_fragment_feedback_view); 69 mFakeTaskView = rootView.findViewById(R.id.gesture_tutorial_fake_task_view); 70 mRippleView = rootView.findViewById(R.id.gesture_tutorial_ripple_view); 71 mRippleDrawable = (RippleDrawable) mRippleView.getBackground(); 72 mHandCoachingAnimation = tutorialFragment.getHandAnimation(); 73 mHandCoachingView = rootView.findViewById(R.id.gesture_tutorial_fragment_hand_coaching); 74 mHandCoachingView.bringToFront(); 75 mActionTextButton = 76 rootView.findViewById(R.id.gesture_tutorial_fragment_action_text_button); 77 mActionButton = rootView.findViewById(R.id.gesture_tutorial_fragment_action_button); 78 79 mHideFeedbackRunnable = 80 () -> mFeedbackView.animate().alpha(0).setDuration(FEEDBACK_ANIMATION_MS) 81 .withEndAction(this::showHandCoachingAnimation).start(); 82 } 83 setTutorialType(TutorialType tutorialType)84 void setTutorialType(TutorialType tutorialType) { 85 mTutorialType = tutorialType; 86 } 87 88 @Nullable getTitleStringId()89 Integer getTitleStringId() { 90 return null; 91 } 92 93 @Nullable getSubtitleStringId()94 Integer getSubtitleStringId() { 95 return null; 96 } 97 98 @Nullable getActionButtonStringId()99 Integer getActionButtonStringId() { 100 return null; 101 } 102 103 @Nullable getActionTextButtonStringId()104 Integer getActionTextButtonStringId() { 105 return null; 106 } 107 showFeedback(int resId)108 void showFeedback(int resId) { 109 hideHandCoachingAnimation(); 110 mFeedbackView.setText(resId); 111 mFeedbackView.animate().alpha(1).setDuration(FEEDBACK_ANIMATION_MS).start(); 112 mFeedbackView.removeCallbacks(mHideFeedbackRunnable); 113 mFeedbackView.postDelayed(mHideFeedbackRunnable, FEEDBACK_VISIBLE_MS); 114 } 115 hideFeedback()116 void hideFeedback() { 117 mFeedbackView.setText(null); 118 mFeedbackView.removeCallbacks(mHideFeedbackRunnable); 119 mFeedbackView.clearAnimation(); 120 mFeedbackView.setAlpha(0); 121 } 122 setRippleHotspot(float x, float y)123 void setRippleHotspot(float x, float y) { 124 mRippleDrawable.setHotspot(x, y); 125 } 126 showRippleEffect(@ullable Runnable onCompleteRunnable)127 void showRippleEffect(@Nullable Runnable onCompleteRunnable) { 128 mRippleDrawable.setState( 129 new int[] {android.R.attr.state_pressed, android.R.attr.state_enabled}); 130 mRippleView.postDelayed(() -> { 131 mRippleDrawable.setState(new int[] {}); 132 if (onCompleteRunnable != null) { 133 onCompleteRunnable.run(); 134 } 135 }, RIPPLE_VISIBLE_MS); 136 } 137 onActionButtonClicked(View button)138 void onActionButtonClicked(View button) {} 139 onActionTextButtonClicked(View button)140 void onActionTextButtonClicked(View button) {} 141 showHandCoachingAnimation()142 void showHandCoachingAnimation() { 143 if (isComplete()) { 144 return; 145 } 146 mHandCoachingAnimation.startLoopedAnimation(mTutorialType); 147 } 148 hideHandCoachingAnimation()149 void hideHandCoachingAnimation() { 150 mHandCoachingAnimation.stop(); 151 mHandCoachingView.setVisibility(View.INVISIBLE); 152 } 153 154 @CallSuper transitToController()155 void transitToController() { 156 hideFeedback(); 157 updateTitles(); 158 updateActionButtons(); 159 160 if (isComplete()) { 161 hideHandCoachingAnimation(); 162 } else { 163 showHandCoachingAnimation(); 164 } 165 } 166 updateTitles()167 private void updateTitles() { 168 updateTitleView(mTitleTextView, getTitleStringId(), 169 R.style.TextAppearance_GestureTutorial_Title); 170 updateTitleView(mSubtitleTextView, getSubtitleStringId(), 171 R.style.TextAppearance_GestureTutorial_Subtitle); 172 } 173 updateTitleView(TextView textView, @Nullable Integer stringId, int styleId)174 private void updateTitleView(TextView textView, @Nullable Integer stringId, int styleId) { 175 if (stringId == null) { 176 textView.setVisibility(View.GONE); 177 return; 178 } 179 180 textView.setVisibility(View.VISIBLE); 181 textView.setText(stringId); 182 textView.setTextAppearance(styleId); 183 } 184 updateActionButtons()185 private void updateActionButtons() { 186 updateButton(mActionButton, getActionButtonStringId(), this::onActionButtonClicked); 187 updateButton( 188 mActionTextButton, getActionTextButtonStringId(), this::onActionTextButtonClicked); 189 } 190 updateButton(Button button, @Nullable Integer stringId, OnClickListener listener)191 private void updateButton(Button button, @Nullable Integer stringId, OnClickListener listener) { 192 if (stringId == null) { 193 button.setVisibility(View.INVISIBLE); 194 return; 195 } 196 197 button.setVisibility(View.VISIBLE); 198 button.setText(stringId); 199 button.setOnClickListener(listener); 200 } 201 isComplete()202 private boolean isComplete() { 203 return mTutorialType == TutorialType.BACK_NAVIGATION_COMPLETE 204 || mTutorialType == TutorialType.HOME_NAVIGATION_COMPLETE 205 || mTutorialType == TutorialType.OVERVIEW_NAVIGATION_COMPLETE 206 || mTutorialType == TutorialType.ASSISTANT_COMPLETE; 207 } 208 209 /** Denotes the type of the tutorial. */ 210 enum TutorialType { 211 RIGHT_EDGE_BACK_NAVIGATION, 212 LEFT_EDGE_BACK_NAVIGATION, 213 BACK_NAVIGATION_COMPLETE, 214 HOME_NAVIGATION, 215 HOME_NAVIGATION_COMPLETE, 216 OVERVIEW_NAVIGATION, 217 OVERVIEW_NAVIGATION_COMPLETE, 218 ASSISTANT, 219 ASSISTANT_COMPLETE 220 } 221 } 222