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
This commit is contained in:
Jeremy Sim
2022-05-09 17:18:56 -07:00
parent f072b75964
commit b0cce86385
9 changed files with 317 additions and 3 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -630,6 +630,8 @@ public abstract class RecentsView<ACTIVITY_TYPE extends StatefulActivity<STATE_T
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 @@ public abstract class RecentsView<ACTIVITY_TYPE extends StatefulActivity<STATE_T
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 @@ public abstract class RecentsView<ACTIVITY_TYPE extends StatefulActivity<STATE_T
@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 @@ public abstract class RecentsView<ACTIVITY_TYPE extends StatefulActivity<STATE_T
taskViewsFloat.first.set(this, getSplitSelectTranslation());
taskViewsFloat.second.set(this, 0f);
if (mSplitInstructionsView != null) {
mSplitInstructionsView.ensureProperRotation();
}
applySplitPrimaryScrollOffset();
}

View File

@@ -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;
}
}
}

View File

@@ -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>

View File

@@ -19,6 +19,7 @@ package com.android.launcher3.touch;
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;
@@ -424,6 +425,28 @@ public class LandscapePagedViewHandler implements PagedOrientationHandler {
- 1.0f * drawableHeight / 2));
}
@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) {

View File

@@ -134,6 +134,16 @@ public interface PagedOrientationHandler {
int drawableWidth, int drawableHeight, DeviceProfile dp,
@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

View File

@@ -27,6 +27,7 @@ import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
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.FrameLayout;
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;
@@ -484,6 +487,58 @@ public class PortraitPagedViewHandler implements PagedOrientationHandler {
}
}
@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) {

View File

@@ -19,6 +19,7 @@ package com.android.launcher3.touch;
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;
@@ -166,6 +167,29 @@ public class SeascapePagedViewHandler extends LandscapePagedViewHandler {
STAGE_POSITION_BOTTOM_OR_RIGHT, STAGE_TYPE_MAIN));
}
@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) {