diff --git a/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java b/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java index c3b342babc..cf523d054d 100644 --- a/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java +++ b/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java @@ -29,8 +29,6 @@ import androidx.fragment.app.FragmentActivity; import com.android.launcher3.R; import com.android.quickstep.interaction.TutorialController.TutorialType; -import java.util.ArrayDeque; -import java.util.Deque; import java.util.List; /** Shows the gesture interactive sandbox in full screen mode. */ @@ -39,8 +37,9 @@ public class GestureSandboxActivity extends FragmentActivity { private static final String LOG_TAG = "GestureSandboxActivity"; private static final String KEY_TUTORIAL_STEPS = "tutorial_steps"; + private static final String KEY_CURRENT_STEP = "current_step"; - private Deque mTutorialSteps; + private TutorialType[] mTutorialSteps; private TutorialType mCurrentTutorialStep; private TutorialFragment mFragment; @@ -55,9 +54,7 @@ public class GestureSandboxActivity extends FragmentActivity { Bundle args = savedInstanceState == null ? getIntent().getExtras() : savedInstanceState; mTutorialSteps = getTutorialSteps(args); - mCurrentStep = 1; - mNumSteps = mTutorialSteps.size(); - mCurrentTutorialStep = mTutorialSteps.pop(); + mCurrentTutorialStep = mTutorialSteps[mCurrentStep - 1]; mFragment = TutorialFragment.newInstance(mCurrentTutorialStep); getSupportFragmentManager().beginTransaction() .add(R.id.gesture_tutorial_fragment_container, mFragment) @@ -88,12 +85,13 @@ public class GestureSandboxActivity extends FragmentActivity { @Override protected void onSaveInstanceState(@NonNull Bundle savedInstanceState) { savedInstanceState.putStringArray(KEY_TUTORIAL_STEPS, getTutorialStepNames()); + savedInstanceState.putInt(KEY_CURRENT_STEP, mCurrentStep); super.onSaveInstanceState(savedInstanceState); } /** Returns true iff there aren't anymore tutorial types to display to the user. */ public boolean isTutorialComplete() { - return mTutorialSteps.isEmpty(); + return mCurrentStep >= mNumSteps; } public int getCurrentStep() { @@ -121,7 +119,7 @@ public class GestureSandboxActivity extends FragmentActivity { mFragment.closeTutorial(); return; } - mCurrentTutorialStep = mTutorialSteps.pop(); + mCurrentTutorialStep = mTutorialSteps[mCurrentStep]; mFragment = TutorialFragment.newInstance(mCurrentTutorialStep); getSupportFragmentManager().beginTransaction() .replace(R.id.gesture_tutorial_fragment_container, mFragment) @@ -131,10 +129,9 @@ public class GestureSandboxActivity extends FragmentActivity { } private String[] getTutorialStepNames() { - String[] tutorialStepNames = new String[mTutorialSteps.size() + 1]; + String[] tutorialStepNames = new String[mTutorialSteps.length]; - int i = 1; - tutorialStepNames[0] = mCurrentTutorialStep.name(); + int i = 0; for (TutorialType tutorialStep : mTutorialSteps) { tutorialStepNames[i++] = tutorialStep.name(); } @@ -142,25 +139,28 @@ public class GestureSandboxActivity extends FragmentActivity { return tutorialStepNames; } - private Deque getTutorialSteps(Bundle extras) { - Deque defaultSteps = new ArrayDeque<>(); - defaultSteps.push(TutorialType.RIGHT_EDGE_BACK_NAVIGATION); + private TutorialType[] getTutorialSteps(Bundle extras) { + TutorialType[] defaultSteps = new TutorialType[] {TutorialType.LEFT_EDGE_BACK_NAVIGATION}; if (extras == null || !extras.containsKey(KEY_TUTORIAL_STEPS)) { return defaultSteps; } String[] tutorialStepNames = extras.getStringArray(KEY_TUTORIAL_STEPS); + int currentStep = extras.getInt(KEY_CURRENT_STEP, -1); if (tutorialStepNames == null) { return defaultSteps; } - Deque tutorialSteps = new ArrayDeque<>(); - for (String tutorialStepName : tutorialStepNames) { - tutorialSteps.addLast(TutorialType.valueOf(tutorialStepName)); + TutorialType[] tutorialSteps = new TutorialType[tutorialStepNames.length]; + for (int i = 0; i < tutorialStepNames.length; i++) { + tutorialSteps[i] = TutorialType.valueOf(tutorialStepNames[i]); } + mCurrentStep = Math.max(currentStep, 1); + mNumSteps = tutorialSteps.length; + return tutorialSteps; } diff --git a/quickstep/src/com/android/quickstep/interaction/TutorialController.java b/quickstep/src/com/android/quickstep/interaction/TutorialController.java index c9da609a08..dfe0c7270a 100644 --- a/quickstep/src/com/android/quickstep/interaction/TutorialController.java +++ b/quickstep/src/com/android/quickstep/interaction/TutorialController.java @@ -58,6 +58,7 @@ abstract class TutorialController implements BackGestureAttemptCallback, private static final int FEEDBACK_ANIMATION_MS = 250; private static final int RIPPLE_VISIBLE_MS = 300; + private static final int GESTURE_ANIMATION_DELAY_MS = 1500; final TutorialFragment mTutorialFragment; TutorialType mTutorialType; @@ -65,6 +66,7 @@ abstract class TutorialController implements BackGestureAttemptCallback, final TextView mCloseButton; final ViewGroup mFeedbackView; + final TextView mFeedbackTitleView; final ImageView mFeedbackVideoView; final ImageView mGestureVideoView; final RelativeLayout mFakeLauncherView; @@ -80,6 +82,12 @@ abstract class TutorialController implements BackGestureAttemptCallback, protected boolean mGestureCompleted = false; + // These runnables should be used when posting callbacks to their views and cleared from their + // views before posting new callbacks. + private final Runnable mTitleViewCallback; + @Nullable private Runnable mFeedbackViewCallback; + @Nullable private Runnable mFeedbackVideoViewCallback; + TutorialController(TutorialFragment tutorialFragment, TutorialType tutorialType) { mTutorialFragment = tutorialFragment; mTutorialType = tutorialType; @@ -89,6 +97,8 @@ abstract class TutorialController implements BackGestureAttemptCallback, mCloseButton = rootView.findViewById(R.id.gesture_tutorial_fragment_close_button); mCloseButton.setOnClickListener(button -> showSkipTutorialDialog()); mFeedbackView = rootView.findViewById(R.id.gesture_tutorial_fragment_feedback_view); + mFeedbackTitleView = mFeedbackView.findViewById( + R.id.gesture_tutorial_fragment_feedback_title); mFeedbackVideoView = rootView.findViewById(R.id.gesture_tutorial_feedback_video); mGestureVideoView = rootView.findViewById(R.id.gesture_tutorial_gesture_video); mFakeLauncherView = rootView.findViewById(R.id.gesture_tutorial_fake_launcher_view); @@ -103,6 +113,9 @@ abstract class TutorialController implements BackGestureAttemptCallback, mTutorialStepView = rootView.findViewById(R.id.gesture_tutorial_fragment_feedback_tutorial_step); mSkipTutorialDialog = createSkipTutorialDialog(); + + mTitleViewCallback = () -> mFeedbackTitleView.sendAccessibilityEvent( + AccessibilityEvent.TYPE_VIEW_FOCUSED); } private void showSkipTutorialDialog() { @@ -169,7 +182,7 @@ abstract class TutorialController implements BackGestureAttemptCallback, playFeedbackVideo(tutorialAnimation, gestureAnimation, () -> { mFeedbackView.setTranslationY(0); title.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); - }); + }, true); } } @@ -192,15 +205,17 @@ abstract class TutorialController implements BackGestureAttemptCallback, isGestureSuccessful ? R.string.gesture_tutorial_nice : R.string.gesture_tutorial_try_again, subtitleResId, - isGestureSuccessful); + isGestureSuccessful, + false); } void showFeedback( int titleResId, int subtitleResId, - boolean isGestureSuccessful) { - TextView title = mFeedbackView.findViewById(R.id.gesture_tutorial_fragment_feedback_title); - title.setText(titleResId); + boolean isGestureSuccessful, + boolean useGestureAnimationDelay) { + mFeedbackTitleView.setText(titleResId); + mFeedbackTitleView.removeCallbacks(mTitleViewCallback); TextView subtitle = mFeedbackView.findViewById(R.id.gesture_tutorial_fragment_feedback_subtitle); subtitle.setText(subtitleResId); @@ -221,11 +236,8 @@ abstract class TutorialController implements BackGestureAttemptCallback, .setDuration(FEEDBACK_ANIMATION_MS) .translationY(0) .start(); - title.postDelayed( - () -> title.sendAccessibilityEvent( - AccessibilityEvent.TYPE_VIEW_FOCUSED), - FEEDBACK_ANIMATION_MS); - }); + mFeedbackTitleView.postDelayed(mTitleViewCallback, FEEDBACK_ANIMATION_MS); + }, useGestureAnimationDelay); return; } else { mTutorialFragment.releaseFeedbackVideoView(); @@ -237,10 +249,7 @@ abstract class TutorialController implements BackGestureAttemptCallback, .setDuration(FEEDBACK_ANIMATION_MS) .translationY(0) .start(); - title.postDelayed( - () -> title.sendAccessibilityEvent( - AccessibilityEvent.TYPE_VIEW_FOCUSED), - FEEDBACK_ANIMATION_MS); + mFeedbackTitleView.postDelayed(mTitleViewCallback, FEEDBACK_ANIMATION_MS); } void hideFeedback(boolean releaseFeedbackVideo) { @@ -254,7 +263,8 @@ abstract class TutorialController implements BackGestureAttemptCallback, private void playFeedbackVideo( @NonNull AnimatedVectorDrawable tutorialAnimation, @NonNull AnimatedVectorDrawable gestureAnimation, - @NonNull Runnable onStartRunnable) { + @NonNull Runnable onStartRunnable, + boolean useGestureAnimationDelay) { if (tutorialAnimation.isRunning()) { tutorialAnimation.reset(); @@ -270,7 +280,9 @@ abstract class TutorialController implements BackGestureAttemptCallback, gestureAnimation.stop(); } - onStartRunnable.run(); + if (!useGestureAnimationDelay) { + onStartRunnable.run(); + } } @Override @@ -284,8 +296,28 @@ abstract class TutorialController implements BackGestureAttemptCallback, } }); - tutorialAnimation.start(); - mFeedbackVideoView.setVisibility(View.VISIBLE); + if (mFeedbackViewCallback != null) { + mFeedbackVideoView.removeCallbacks(mFeedbackViewCallback); + mFeedbackViewCallback = null; + } + if (mFeedbackVideoViewCallback != null) { + mFeedbackVideoView.removeCallbacks(mFeedbackVideoViewCallback); + mFeedbackVideoViewCallback = null; + } + if (useGestureAnimationDelay) { + mFeedbackViewCallback = onStartRunnable; + mFeedbackVideoViewCallback = () -> { + mFeedbackVideoView.setVisibility(View.VISIBLE); + tutorialAnimation.start(); + }; + + mFeedbackVideoView.setVisibility(View.GONE); + mFeedbackView.post(mFeedbackViewCallback); + mFeedbackVideoView.postDelayed(mFeedbackVideoViewCallback, GESTURE_ANIMATION_DELAY_MS); + } else { + mFeedbackVideoView.setVisibility(View.VISIBLE); + tutorialAnimation.start(); + } } void setRippleHotspot(float x, float y) { diff --git a/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java b/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java index ec26022adf..da0fc668ac 100644 --- a/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java +++ b/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java @@ -170,7 +170,8 @@ abstract class TutorialFragment extends Fragment implements OnTouchListener { Integer introTileStringResId = mTutorialController.getIntroductionTitle(); Integer introSubtitleResId = mTutorialController.getIntroductionSubtitle(); if (introTileStringResId != null && introSubtitleResId != null) { - mTutorialController.showFeedback(introTileStringResId, introSubtitleResId, false); + mTutorialController.showFeedback( + introTileStringResId, introSubtitleResId, false, true); mIntroductionShown = true; } } @@ -252,7 +253,7 @@ abstract class TutorialFragment extends Fragment implements OnTouchListener { @Override public void onResume() { super.onResume(); - if (mFragmentStopped) { + if (mFragmentStopped && mTutorialController != null) { mTutorialController.showFeedback(); mFragmentStopped = false; } else { diff --git a/quickstep/src/com/android/quickstep/interaction/TutorialStepIndicator.java b/quickstep/src/com/android/quickstep/interaction/TutorialStepIndicator.java index eda61583fa..d880d74e5d 100644 --- a/quickstep/src/com/android/quickstep/interaction/TutorialStepIndicator.java +++ b/quickstep/src/com/android/quickstep/interaction/TutorialStepIndicator.java @@ -23,6 +23,7 @@ import android.widget.ImageView; import android.widget.LinearLayout; import androidx.appcompat.content.res.AppCompatResources; +import androidx.core.graphics.ColorUtils; import com.android.launcher3.R; import com.android.launcher3.Utilities; @@ -86,6 +87,8 @@ public class TutorialStepIndicator extends LinearLayout { for (int i = mTotalSteps; i < getChildCount(); i++) { removeViewAt(i); } + int stepIndicatorColor = GraphicsUtils.getAttrColor( + getContext(), android.R.attr.textColorPrimary); for (int i = 0; i < mTotalSteps; i++) { Drawable pageIndicatorPillDrawable = AppCompatResources.getDrawable( getContext(), R.drawable.tutorial_step_indicator_pill); @@ -104,13 +107,10 @@ public class TutorialStepIndicator extends LinearLayout { } if (pageIndicatorPillDrawable != null) { if (i < mCurrentStep) { - pageIndicatorPillDrawable.setTint( - GraphicsUtils.getAttrColor(getContext(), - android.R.attr.textColorPrimary)); + pageIndicatorPillDrawable.setTint(stepIndicatorColor); } else { pageIndicatorPillDrawable.setTint( - GraphicsUtils.getAttrColor(getContext(), - android.R.attr.textColorPrimaryInverse)); + ColorUtils.setAlphaComponent(stepIndicatorColor, 0x22)); } } } diff --git a/src/com/android/launcher3/views/RecyclerViewFastScroller.java b/src/com/android/launcher3/views/RecyclerViewFastScroller.java index e607ab3f7e..f49d1b5d2a 100644 --- a/src/com/android/launcher3/views/RecyclerViewFastScroller.java +++ b/src/com/android/launcher3/views/RecyclerViewFastScroller.java @@ -28,6 +28,7 @@ import android.graphics.Paint; import android.graphics.Point; import android.graphics.Rect; import android.graphics.RectF; +import android.os.Build; import android.util.AttributeSet; import android.util.Property; import android.view.MotionEvent; @@ -37,6 +38,7 @@ import android.view.WindowInsets; import android.widget.TextView; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import androidx.recyclerview.widget.RecyclerView; import com.android.launcher3.BaseRecyclerView; @@ -342,16 +344,21 @@ public class RecyclerViewFastScroller extends View { // swiping very close to the thumb area (not just within it's bound) // will also prevent back gesture SYSTEM_GESTURE_EXCLUSION_RECT.get(0).offset(mThumbDrawOffset.x, mThumbDrawOffset.y); - SYSTEM_GESTURE_EXCLUSION_RECT.get(0).left = SYSTEM_GESTURE_EXCLUSION_RECT.get(0).right - - mSystemGestureInsets.right; + if (Utilities.ATLEAST_Q) { + SYSTEM_GESTURE_EXCLUSION_RECT.get(0).left = + SYSTEM_GESTURE_EXCLUSION_RECT.get(0).right - mSystemGestureInsets.right; + } setSystemGestureExclusionRects(SYSTEM_GESTURE_EXCLUSION_RECT); } canvas.restoreToCount(saveCount); } @Override + @RequiresApi(Build.VERSION_CODES.Q) public WindowInsets onApplyWindowInsets(WindowInsets insets) { - mSystemGestureInsets = insets.getSystemGestureInsets(); + if (Utilities.ATLEAST_Q) { + mSystemGestureInsets = insets.getSystemGestureInsets(); + } return super.onApplyWindowInsets(insets); } diff --git a/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java b/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java index 150bd9902f..e89aea7507 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java +++ b/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java @@ -298,6 +298,12 @@ public class WidgetsListAdapter extends Adapter implements OnHeaderC scrollToPositionAndMaintainOffset(positionForPackageUserKey, topForPackageUserKey); } + /** Returns the position of the currently expanded header, or empty if it's not present. */ + public OptionalInt getSelectedHeaderPosition() { + if (mWidgetsContentVisiblePackageUserKey == null) return OptionalInt.empty(); + return getPositionForPackageUserKey(mWidgetsContentVisiblePackageUserKey); + } + /** * Returns the position of {@code key} in {@link #mVisibleEntries}, or empty if it's not * present. diff --git a/src/com/android/launcher3/widget/picker/WidgetsRecyclerView.java b/src/com/android/launcher3/widget/picker/WidgetsRecyclerView.java index 090362ba33..4f4f1a3566 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsRecyclerView.java +++ b/src/com/android/launcher3/widget/picker/WidgetsRecyclerView.java @@ -53,6 +53,7 @@ public class WidgetsRecyclerView extends BaseRecyclerView implements OnItemTouch private HeaderViewDimensionsProvider mHeaderViewDimensionsProvider; private int mLastVisibleWidgetContentTableHeight = 0; private int mWidgetHeaderHeight = 0; + private final int mCollapsedHeaderBottomMarginSize; @Nullable private OnContentChangeListener mOnContentChangeListener; public WidgetsRecyclerView(Context context) { @@ -71,6 +72,10 @@ public class WidgetsRecyclerView extends BaseRecyclerView implements OnItemTouch ActivityContext activity = ActivityContext.lookupContext(getContext()); DeviceProfile grid = activity.getDeviceProfile(); + + // The bottom margin used when the header is not expanded. + mCollapsedHeaderBottomMarginSize = + getResources().getDimensionPixelSize(R.dimen.widget_list_entry_bottom_margin); } @Override @@ -182,10 +187,7 @@ public class WidgetsRecyclerView extends BaseRecyclerView implements OnItemTouch && mLastVisibleWidgetContentTableHeight == 0 && view.getMeasuredHeight() > 0) { // This assumes all header views are of the same height. - RecyclerView.LayoutParams layoutParams = - (RecyclerView.LayoutParams) view.getLayoutParams(); - mWidgetHeaderHeight = view.getMeasuredHeight() + layoutParams.topMargin - + layoutParams.bottomMargin; + mWidgetHeaderHeight = view.getMeasuredHeight(); } } @@ -279,12 +281,17 @@ public class WidgetsRecyclerView extends BaseRecyclerView implements OnItemTouch if (untilIndex > mAdapter.getItems().size()) { untilIndex = mAdapter.getItems().size(); } + int expandedHeaderPosition = mAdapter.getSelectedHeaderPosition().orElse(-1); int totalItemsHeight = 0; for (int i = 0; i < untilIndex; i++) { WidgetsListBaseEntry entry = mAdapter.getItems().get(i); if (entry instanceof WidgetsListHeaderEntry || entry instanceof WidgetsListSearchHeaderEntry) { totalItemsHeight += mWidgetHeaderHeight; + if (expandedHeaderPosition != i) { + // If the header is collapsed, include the bottom margin it will use. + totalItemsHeight += mCollapsedHeaderBottomMarginSize; + } } else if (entry instanceof WidgetsListContentEntry) { totalItemsHeight += mLastVisibleWidgetContentTableHeight; } else {