Implement non-disappearing View for split staging instructions
The instructions for how to perform a splitscreen operation, previously conveyed through a disappearing Toast, are now conveyed through a custom View object.
Fixes: 219987907
Test: Manual
Change-Id: Iff2bb6e334e0325e8a091d76a5f9b8767071365f
diff --git a/quickstep/res/drawable/split_instructions_background.xml b/quickstep/res/drawable/split_instructions_background.xml
new file mode 100644
index 0000000..6d0e7db
--- /dev/null
+++ b/quickstep/res/drawable/split_instructions_background.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2022 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.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:shape="rectangle">
+ <solid android:color="?androidprv:attr/colorAccentPrimary" />
+ <corners android:radius="@dimen/split_instructions_radius" />
+</shape>
\ No newline at end of file
diff --git a/quickstep/res/layout/split_instructions_view.xml b/quickstep/res/layout/split_instructions_view.xml
new file mode 100644
index 0000000..91fb05c
--- /dev/null
+++ b/quickstep/res/layout/split_instructions_view.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2022 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.
+-->
+<com.android.quickstep.views.SplitInstructionsView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="@drawable/split_instructions_background"
+ android:paddingRight="@dimen/split_instructions_horizontal_padding"
+ android:paddingLeft="@dimen/split_instructions_horizontal_padding"
+ android:paddingTop="@dimen/split_instructions_vertical_padding"
+ android:paddingBottom="@dimen/split_instructions_vertical_padding"
+ android:elevation="@dimen/split_instructions_elevation"
+ android:visibility="gone">
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/split_instructions_text"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:gravity="center"
+ android:textColor="?androidprv:attr/textColorOnAccent"
+ android:text="@string/toast_split_select_app" />
+</com.android.quickstep.views.SplitInstructionsView>
\ No newline at end of file
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 2360396..a2f6792 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -630,6 +630,8 @@
private final Toast mSplitUnsupportedToast = Toast.makeText(getContext(),
R.string.toast_split_app_unsupported, Toast.LENGTH_SHORT);
+ private SplitInstructionsView mSplitInstructionsView;
+
@Nullable
private QuickstepSystemShortcut.SplitSelectSource mSplitSelectSource;
@@ -2764,11 +2766,15 @@
mFirstFloatingTaskView.addAnimation(anim, startingTaskRect, mTempRect,
false /* fadeWithThumbnail */, true /* isStagedTask */);
}
+
+ mSplitInstructionsView = SplitInstructionsView.getSplitInstructionsView(mActivity);
+ mSplitInstructionsView.setAlpha(0);
+ anim.addFloat(mSplitInstructionsView, SplitInstructionsView.ALPHA_FLOAT, 0, 1, ACCEL);
+
InteractionJankMonitorWrapper.begin(this,
InteractionJankMonitorWrapper.CUJ_SPLIT_SCREEN_ENTER, "First tile selected");
anim.addEndListener(success -> {
if (success) {
- mSplitToast.show();
InteractionJankMonitorWrapper.end(
InteractionJankMonitorWrapper.CUJ_SPLIT_SCREEN_ENTER);
} else {
@@ -4099,6 +4105,10 @@
@SuppressLint("WrongCall")
protected void resetFromSplitSelectionState() {
if (mSplitSelectSource != null || mSplitHiddenTaskViewIndex != -1) {
+ if (mSplitInstructionsView != null) {
+ mActivity.getDragLayer().removeView(mSplitInstructionsView);
+ mSplitInstructionsView = null;
+ }
if (mFirstFloatingTaskView != null) {
mActivity.getRootView().removeView(mFirstFloatingTaskView);
mFirstFloatingTaskView = null;
@@ -4164,6 +4174,10 @@
taskViewsFloat.first.set(this, getSplitSelectTranslation());
taskViewsFloat.second.set(this, 0f);
+ if (mSplitInstructionsView != null) {
+ mSplitInstructionsView.ensureProperRotation();
+ }
+
applySplitPrimaryScrollOffset();
}
diff --git a/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java b/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java
new file mode 100644
index 0000000..7d94505
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2022 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.views;
+
+import static com.android.launcher3.util.DisplayController.NavigationMode.THREE_BUTTONS;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.util.FloatProperty;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.R;
+import com.android.launcher3.statemanager.StatefulActivity;
+import com.android.launcher3.util.DisplayController;
+
+/**
+ * A rounded rectangular component containing a single TextView.
+ * Appears when a split is in progress, and tells the user to select a second app to initiate
+ * splitscreen.
+ *
+ * Appears and disappears concurrently with a FloatingTaskView.
+ */
+public class SplitInstructionsView extends FrameLayout {
+ private final StatefulActivity mLauncher;
+
+ public static final FloatProperty<SplitInstructionsView> ALPHA_FLOAT =
+ new FloatProperty<SplitInstructionsView>("SplitInstructionsAlpha") {
+ @Override
+ public void setValue(SplitInstructionsView splitInstructionsView, float v) {
+ splitInstructionsView.setVisibility(v != 0 ? VISIBLE : GONE);
+ splitInstructionsView.setAlpha(v);
+ }
+
+ @Override
+ public Float get(SplitInstructionsView splitInstructionsView) {
+ return splitInstructionsView.getAlpha();
+ }
+ };
+
+ public SplitInstructionsView(Context context) {
+ this(context, null);
+ }
+
+ public SplitInstructionsView(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public SplitInstructionsView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ mLauncher = (StatefulActivity) context;
+ }
+
+ static SplitInstructionsView getSplitInstructionsView(StatefulActivity launcher) {
+ ViewGroup dragLayer = launcher.getDragLayer();
+ final SplitInstructionsView splitInstructionsView =
+ (SplitInstructionsView) launcher.getLayoutInflater().inflate(
+ R.layout.split_instructions_view,
+ dragLayer,
+ false
+ );
+
+ dragLayer.addView(splitInstructionsView);
+ return splitInstructionsView;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ ensureProperRotation();
+ }
+
+ void ensureProperRotation() {
+ ((RecentsView) mLauncher.getOverviewPanel()).getPagedOrientationHandler()
+ .setSplitInstructionsParams(
+ this,
+ mLauncher.getDeviceProfile(),
+ getMeasuredHeight(),
+ getMeasuredWidth(),
+ getThreeButtonNavShift()
+ );
+ }
+
+ // In some cases, when user is using 3-button nav, there isn't enough room for both the
+ // 3-button nav and a centered SplitInstructionsView. This function will return an int that will
+ // be used to shift the SplitInstructionsView over a bit so that everything looks well-spaced.
+ // In many cases, this will return 0, since we don't need to shift it away from the center.
+ int getThreeButtonNavShift() {
+ DeviceProfile dp = mLauncher.getDeviceProfile();
+ if ((DisplayController.getNavigationMode(getContext()) == THREE_BUTTONS)
+ && ((dp.isTwoPanels) || (dp.isTablet && !dp.isLandscape))) {
+ int navButtonWidth = getResources().getDimensionPixelSize(
+ R.dimen.taskbar_nav_buttons_size);
+ int extraMargin = getResources().getDimensionPixelSize(
+ R.dimen.taskbar_contextual_button_margin);
+ // Explanation: The 3-button nav for non-phones sits on one side of the screen, taking
+ // up 3 buttons + a side margin worth of space. Our splitInstructionsView starts in the
+ // center of the screen and we want to center it in the remaining space, therefore we
+ // want to shift it over by half the 3-button layout's width.
+ // If the user is using an RtL layout, we shift it the opposite way.
+ return -((3 * navButtonWidth + extraMargin) / 2) * (isLayoutRtl() ? -1 : 1);
+ } else {
+ return 0;
+ }
+ }
+}
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 766dca3..e7b3375 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -390,8 +390,17 @@
<dimen name="split_placeholder_inset">16dp</dimen>
<dimen name="split_placeholder_icon_size">44dp</dimen>
<dimen name="task_menu_width_grid">216dp</dimen>
-
-
+ <dimen name="split_instructions_radius">22dp</dimen>
+ <dimen name="split_instructions_elevation">1dp</dimen>
+ <dimen name="split_instructions_horizontal_padding">24dp</dimen>
+ <dimen name="split_instructions_vertical_padding">12dp</dimen>
+ <dimen name="split_instructions_bottom_margin_tablet_landscape">32dp</dimen>
+ <dimen name="split_instructions_bottom_margin_tablet_portrait">44dp</dimen>
+ <dimen name="split_instructions_bottom_margin_twopanels_landscape">33dp</dimen>
+ <dimen name="split_instructions_bottom_margin_twopanels_portrait">51dp</dimen>
+ <dimen name="split_instructions_bottom_margin_phone_landscape">24dp</dimen>
+ <dimen name="split_instructions_bottom_margin_phone_portrait">60dp</dimen>
+
<!-- Workspace grid visualization parameters -->
<dimen name="grid_visualization_rounding_radius">28dp</dimen>
<dimen name="grid_visualization_horizontal_cell_spacing">6dp</dimen>
diff --git a/src/com/android/launcher3/touch/LandscapePagedViewHandler.java b/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
index 121088a..21372cf 100644
--- a/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
+++ b/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
@@ -19,6 +19,7 @@
import static android.view.Gravity.BOTTOM;
import static android.view.Gravity.CENTER_VERTICAL;
import static android.view.Gravity.END;
+import static android.view.Gravity.LEFT;
import static android.view.Gravity.START;
import static android.view.Gravity.TOP;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
@@ -425,6 +426,28 @@
}
@Override
+ public void setSplitInstructionsParams(View out, DeviceProfile dp, int splitInstructionsHeight,
+ int splitInstructionsWidth, int threeButtonNavShift) {
+ out.setPivotX(0);
+ out.setPivotY(0);
+ out.setRotation(getDegreesRotated());
+ int distanceToEdge = out.getResources().getDimensionPixelSize(
+ R.dimen.split_instructions_bottom_margin_phone_landscape);
+ // Adjust for any insets on the left edge
+ int insetCorrectionX = dp.getInsets().left;
+ // Center the view in case of unbalanced insets on top or bottom of screen
+ int insetCorrectionY = (dp.getInsets().bottom - dp.getInsets().top) / 2;
+ out.setTranslationX(splitInstructionsHeight + distanceToEdge - insetCorrectionX);
+ out.setTranslationY(((splitInstructionsHeight - splitInstructionsWidth) / 2f)
+ + insetCorrectionY);
+ FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) out.getLayoutParams();
+ // Setting gravity to LEFT instead of the lint-recommended START because we always want this
+ // view to be screen-left when phone is in landscape, regardless of the RtL setting.
+ lp.gravity = LEFT | CENTER_VERTICAL;
+ out.setLayoutParams(lp);
+ }
+
+ @Override
public void getFinalSplitPlaceholderBounds(int splitDividerSize, DeviceProfile dp,
@StagePosition int stagePosition, Rect out1, Rect out2) {
// In fake land/seascape, the window bounds are always top and bottom half
diff --git a/src/com/android/launcher3/touch/PagedOrientationHandler.java b/src/com/android/launcher3/touch/PagedOrientationHandler.java
index 4fcf378..74e2020 100644
--- a/src/com/android/launcher3/touch/PagedOrientationHandler.java
+++ b/src/com/android/launcher3/touch/PagedOrientationHandler.java
@@ -135,6 +135,16 @@
@StagePosition int stagePosition);
/**
+ * Sets positioning and rotation for a SplitInstructionsView.
+ * @param out The SplitInstructionsView that needs to be positioned.
+ * @param dp The device profile, used to report rotation and device type.
+ * @param splitInstructionsHeight The SplitInstructionView's height.
+ * @param splitInstructionsWidth The SplitInstructionView's width.
+ */
+ void setSplitInstructionsParams(View out, DeviceProfile dp, int splitInstructionsHeight,
+ int splitInstructionsWidth, int threeButtonNavShift);
+
+ /**
* @param splitDividerSize height of split screen drag handle in portrait, width in landscape
* @param stagePosition the split position option (top/left, bottom/right) of the first
* task selected for entering split
diff --git a/src/com/android/launcher3/touch/PortraitPagedViewHandler.java b/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
index 80a8c19..816e396 100644
--- a/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
+++ b/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
@@ -27,6 +27,7 @@
import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
import static com.android.launcher3.touch.SingleAxisSwipeDetector.VERTICAL;
+import static com.android.launcher3.util.DisplayController.NavigationMode.THREE_BUTTONS;
import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT;
@@ -47,7 +48,9 @@
import android.widget.LinearLayout;
import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.R;
import com.android.launcher3.Utilities;
+import com.android.launcher3.util.DisplayController;
import com.android.launcher3.util.SplitConfigurationOptions;
import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
import com.android.launcher3.util.SplitConfigurationOptions.StagePosition;
@@ -485,6 +488,58 @@
}
@Override
+ public void setSplitInstructionsParams(View out, DeviceProfile dp, int splitInstructionsHeight,
+ int splitInstructionsWidth, int threeButtonNavShift) {
+ out.setPivotX(0);
+ out.setPivotY(0);
+ out.setRotation(getDegreesRotated());
+ int distanceToEdge;
+ if ((DisplayController.getNavigationMode(out.getContext()) == THREE_BUTTONS)
+ && (dp.isTwoPanels || dp.isTablet)) {
+ // If 3-button nav is active, align the splitInstructionsView with it.
+ distanceToEdge = dp.getTaskbarOffsetY()
+ + ((dp.taskbarSize - splitInstructionsHeight) / 2);
+ } else {
+ // If 3-button nav is not active, set bottom margin according to spec.
+ if (dp.isPhone) {
+ if (dp.isLandscape) {
+ distanceToEdge = out.getResources().getDimensionPixelSize(
+ R.dimen.split_instructions_bottom_margin_phone_landscape);
+ } else {
+ distanceToEdge = out.getResources().getDimensionPixelSize(
+ R.dimen.split_instructions_bottom_margin_phone_portrait);
+ }
+ } else if (dp.isTwoPanels) {
+ if (dp.isLandscape) {
+ distanceToEdge = out.getResources().getDimensionPixelSize(
+ R.dimen.split_instructions_bottom_margin_twopanels_landscape);
+ } else {
+ distanceToEdge = out.getResources().getDimensionPixelSize(
+ R.dimen.split_instructions_bottom_margin_twopanels_portrait);
+ }
+ } else {
+ if (dp.isLandscape) {
+ distanceToEdge = out.getResources().getDimensionPixelSize(
+ R.dimen.split_instructions_bottom_margin_tablet_landscape);
+ } else {
+ distanceToEdge = out.getResources().getDimensionPixelSize(
+ R.dimen.split_instructions_bottom_margin_tablet_portrait);
+ }
+ }
+ }
+
+ // Center the view in case of unbalanced insets on left or right of screen
+ int insetCorrectionX = (dp.getInsets().right - dp.getInsets().left) / 2;
+ // Adjust for any insets on the bottom edge
+ int insetCorrectionY = dp.getInsets().bottom;
+ out.setTranslationX(insetCorrectionX + threeButtonNavShift);
+ out.setTranslationY(-distanceToEdge + insetCorrectionY);
+ FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) out.getLayoutParams();
+ lp.gravity = CENTER_HORIZONTAL | BOTTOM;
+ out.setLayoutParams(lp);
+ }
+
+ @Override
public void getFinalSplitPlaceholderBounds(int splitDividerSize, DeviceProfile dp,
@StagePosition int stagePosition, Rect out1, Rect out2) {
int screenHeight = dp.heightPx;
diff --git a/src/com/android/launcher3/touch/SeascapePagedViewHandler.java b/src/com/android/launcher3/touch/SeascapePagedViewHandler.java
index 74b6a5b..71adb7a 100644
--- a/src/com/android/launcher3/touch/SeascapePagedViewHandler.java
+++ b/src/com/android/launcher3/touch/SeascapePagedViewHandler.java
@@ -19,6 +19,7 @@
import static android.view.Gravity.BOTTOM;
import static android.view.Gravity.CENTER_VERTICAL;
import static android.view.Gravity.END;
+import static android.view.Gravity.RIGHT;
import static android.view.Gravity.START;
import static com.android.launcher3.touch.SingleAxisSwipeDetector.HORIZONTAL;
@@ -167,6 +168,29 @@
}
@Override
+ public void setSplitInstructionsParams(View out, DeviceProfile dp, int splitInstructionsHeight,
+ int splitInstructionsWidth, int threeButtonNavShift) {
+ out.setPivotX(0);
+ out.setPivotY(0);
+ out.setRotation(getDegreesRotated());
+ int distanceToEdge = out.getResources().getDimensionPixelSize(
+ R.dimen.split_instructions_bottom_margin_phone_landscape);
+ // Adjust for any insets on the right edge
+ int insetCorrectionX = dp.getInsets().right;
+ // Center the view in case of unbalanced insets on top or bottom of screen
+ int insetCorrectionY = (dp.getInsets().bottom - dp.getInsets().top) / 2;
+ out.setTranslationX(splitInstructionsWidth - splitInstructionsHeight - distanceToEdge
+ + insetCorrectionX);
+ out.setTranslationY(((splitInstructionsHeight + splitInstructionsWidth) / 2f)
+ + insetCorrectionY);
+ FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) out.getLayoutParams();
+ // Setting gravity to RIGHT instead of the lint-recommended END because we always want this
+ // view to be screen-right when phone is in seascape, regardless of the RtL setting.
+ lp.gravity = RIGHT | CENTER_VERTICAL;
+ out.setLayoutParams(lp);
+ }
+
+ @Override
public void setTaskIconParams(FrameLayout.LayoutParams iconParams,
int taskIconMargin, int taskIconHeight, int thumbnailTopMargin, boolean isRtl) {
iconParams.gravity = (isRtl ? END : START) | CENTER_VERTICAL;