Show split select instructions toast when starting split on workspace
* Used same animation properties that RecentsView uses to
show the instructions view unfolding
* TODOs:
* UI polish (correct asset, animation timings, etc)
* Hook into existing animations (if possible)
* Migrate recentsView splitInstructionsView to use same/similar code path;
remove RecentsView#safeRemoveDragLayerView
Bug: 276361926
Test: Create split w/ and w/o flag, works as expected
Flag: ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE
Change-Id: I7a51b71c358902399ad73b650287cdbf1dad07c6
diff --git a/quickstep/res/layout/split_instructions_view.xml b/quickstep/res/layout/split_instructions_view.xml
index 91fb05c..5f037f8 100644
--- a/quickstep/res/layout/split_instructions_view.xml
+++ b/quickstep/res/layout/split_instructions_view.xml
@@ -24,12 +24,15 @@
android:paddingTop="@dimen/split_instructions_vertical_padding"
android:paddingBottom="@dimen/split_instructions_vertical_padding"
android:elevation="@dimen/split_instructions_elevation"
- android:visibility="gone">
+ android:visibility="gone"
+ android:importantForAccessibility="yes">
<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:drawableEnd="@drawable/ic_split_horizontal"
+ android:drawablePadding="@dimen/split_instructions_drawable_padding"
android:text="@string/toast_split_select_app" />
</com.android.quickstep.views.SplitInstructionsView>
\ No newline at end of file
diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml
index 77799e6..dd9fc63 100644
--- a/quickstep/res/values/strings.xml
+++ b/quickstep/res/values/strings.xml
@@ -230,6 +230,7 @@
<string name="action_split">Split</string>
<!-- Label for toast with instructions for split screen selection mode. [CHAR_LIMIT=50] -->
<string name="toast_split_select_app">Tap another app to use split screen</string>
+ <string name="toast_split_select_cont_desc">Exit split screen selection</string>
<!-- Label for toast when app selected for split isn't supported. [CHAR_LIMIT=50] -->
<string name="toast_split_app_unsupported">Choose another app to use split screen</string>
<!-- Message shown when an action is blocked by a policy enforced by the app or the organization managing the device. [CHAR_LIMIT=NONE] -->
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index fbe0a8f..6c73a2d 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -544,17 +544,9 @@
ArrayList<TouchController> list = new ArrayList<>();
list.add(getDragController());
- Consumer<AnimatorSet> splitAnimator = animatorSet -> {
- AnimatorSet anim = mSplitSelectStateController.getSplitAnimationController()
- .createPlaceholderDismissAnim(QuickstepLauncher.this);
- anim.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- mSplitSelectStateController.resetState();
- }
- });
- animatorSet.play(anim);
- };
+ Consumer<AnimatorSet> splitAnimator = animatorSet ->
+ animatorSet.play(mSplitSelectStateController.getSplitAnimationController()
+ .createPlaceholderDismissAnim(this));
switch (mode) {
case NO_BUTTON:
list.add(new NoButtonQuickSwitchTouchController(this));
@@ -673,6 +665,8 @@
mSplitSelectStateController.resetState();
}
});
+ anim.add(mSplitSelectStateController.getSplitAnimationController()
+ .getShowSplitInstructionsAnim(this).buildAnim());
anim.buildAnim().start();
}
diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
index 5740991..56d6857 100644
--- a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
@@ -26,15 +26,17 @@
import android.graphics.RectF
import android.graphics.drawable.Drawable
import android.view.View
+import com.android.app.animation.Interpolators
import com.android.launcher3.DeviceProfile
-import com.android.launcher3.Launcher
import com.android.launcher3.Utilities
import com.android.launcher3.anim.PendingAnimation
-import com.android.launcher3.dragndrop.DragLayer
+import com.android.launcher3.statemanager.StatefulActivity
import com.android.launcher3.util.SplitConfigurationOptions.SplitSelectSource
+import com.android.launcher3.views.BaseDragLayer
import com.android.quickstep.views.FloatingTaskView
import com.android.quickstep.views.IconView
import com.android.quickstep.views.RecentsView
+import com.android.quickstep.views.SplitInstructionsView
import com.android.quickstep.views.TaskThumbnailView
import com.android.quickstep.views.TaskView
import com.android.quickstep.views.TaskView.TaskIdAttributeContainer
@@ -58,6 +60,8 @@
)
}
+ var splitInstructionsView: SplitInstructionsView? = null
+
/**
* Returns different elements to animate for the initial split selection animation
* depending on the state of the surface from which the split was initiated
@@ -188,31 +192,26 @@
}
/** Does not play any animation if user is not currently in split selection state. */
- fun playPlaceholderDismissAnim(launcher: Launcher) {
+ fun playPlaceholderDismissAnim(launcher: StatefulActivity<*>) {
if (!splitSelectStateController.isSplitSelectActive) {
return
}
val anim = createPlaceholderDismissAnim(launcher)
- anim.addListener(object : AnimatorListenerAdapter() {
- override fun onAnimationEnd(animation: Animator) {
- splitSelectStateController.resetState()
- }
- })
anim.start()
}
/** Returns [AnimatorSet] which slides initial split placeholder view offscreen. */
- fun createPlaceholderDismissAnim(launcher: Launcher) : AnimatorSet {
+ fun createPlaceholderDismissAnim(launcher: StatefulActivity<*>) : AnimatorSet {
val animatorSet = AnimatorSet()
val recentsView : RecentsView<*, *> = launcher.getOverviewPanel()
val floatingTask: FloatingTaskView = splitSelectStateController.firstFloatingTaskView
?: return animatorSet
// We are in split selection state currently, transitioning to another state
- val dragLayer: DragLayer = launcher.dragLayer
+ val dragLayer: BaseDragLayer<*> = launcher.dragLayer
val onScreenRectF = RectF()
- Utilities.getBoundsForViewInDragLayer(launcher.dragLayer, floatingTask,
+ Utilities.getBoundsForViewInDragLayer(dragLayer, floatingTask,
Rect(0, 0, floatingTask.width, floatingTask.height),
false, null, onScreenRectF)
// Get the part of the floatingTask that intersects with the DragLayer (i.e. the
@@ -233,6 +232,42 @@
floatingTask.stagePosition,
launcher.deviceProfile
)))
+ animatorSet.addListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator) {
+ splitSelectStateController.resetState()
+ safeRemoveViewFromDragLayer(launcher, splitInstructionsView)
+ }
+ })
return animatorSet
}
+
+ /**
+ * Returns a [PendingAnimation] to animate in the chip to instruct a user to select a second
+ * app for splitscreen
+ */
+ fun getShowSplitInstructionsAnim(launcher: StatefulActivity<*>) : PendingAnimation {
+ safeRemoveViewFromDragLayer(launcher, splitInstructionsView)
+ splitInstructionsView = SplitInstructionsView.getSplitInstructionsView(launcher)
+ val timings = AnimUtils.getDeviceOverviewToSplitTimings(launcher.deviceProfile.isTablet)
+ val anim = PendingAnimation(100 /*duration */)
+ anim.setViewAlpha(splitInstructionsView, 1f,
+ Interpolators.clampToProgress(Interpolators.LINEAR,
+ timings.instructionsContainerFadeInStartOffset,
+ timings.instructionsContainerFadeInEndOffset))
+ anim.setViewAlpha(splitInstructionsView!!.textView, 1f,
+ Interpolators.clampToProgress(Interpolators.LINEAR,
+ timings.instructionsTextFadeInStartOffset,
+ timings.instructionsTextFadeInEndOffset))
+ anim.addFloat(splitInstructionsView, SplitInstructionsView.UNFOLD, 0.1f, 1f,
+ Interpolators.clampToProgress(Interpolators.EMPHASIZED_DECELERATE,
+ timings.instructionsUnfoldStartOffset,
+ timings.instructionsUnfoldEndOffset))
+ return anim
+ }
+
+ private fun safeRemoveViewFromDragLayer(launcher: StatefulActivity<*>, view: View?) {
+ if (view != null) {
+ launcher.dragLayer.removeView(view)
+ }
+ }
}
diff --git a/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java b/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java
index 0d9e412..4ca02e0 100644
--- a/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java
+++ b/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java
@@ -17,15 +17,21 @@
package com.android.quickstep.views;
import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
import android.util.AttributeSet;
import android.util.FloatProperty;
+import android.view.MotionEvent;
import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.FrameLayout;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatTextView;
+import com.android.launcher3.LauncherState;
import com.android.launcher3.R;
+import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.statemanager.StatefulActivity;
/**
@@ -65,7 +71,7 @@
mLauncher = (StatefulActivity) context;
}
- static SplitInstructionsView getSplitInstructionsView(StatefulActivity launcher) {
+ public static SplitInstructionsView getSplitInstructionsView(StatefulActivity launcher) {
ViewGroup dragLayer = launcher.getDragLayer();
final SplitInstructionsView splitInstructionsView =
(SplitInstructionsView) launcher.getLayoutInflater().inflate(
@@ -73,9 +79,7 @@
dragLayer,
false
);
-
- splitInstructionsView.mTextView = splitInstructionsView.findViewById(
- R.id.split_instructions_text);
+ splitInstructionsView.init();
// Since textview overlays base view, and we sometimes manipulate the alpha of each
// simultaneously, force overlapping rendering to false prevents redrawing of pixels,
@@ -92,6 +96,71 @@
ensureProperRotation();
}
+ private void init() {
+ mTextView = findViewById(R.id.split_instructions_text);
+
+ if (!FeatureFlags.ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE.get()) {
+ mTextView.setCompoundDrawables(null, null, null, null);
+ return;
+ }
+
+ mTextView.setOnTouchListener((v, event) -> {
+ if (isTouchInsideRightCompoundDrawable(event)) {
+ if (event.getAction() == MotionEvent.ACTION_UP) {
+ exitSplitSelection();
+ }
+ return true;
+ }
+ return false;
+ });
+ }
+
+ private void exitSplitSelection() {
+ ((RecentsView) mLauncher.getOverviewPanel()).getSplitSelectController()
+ .getSplitAnimationController().playPlaceholderDismissAnim(mLauncher);
+ mLauncher.getStateManager().goToState(LauncherState.NORMAL);
+ }
+
+ private boolean isTouchInsideRightCompoundDrawable(MotionEvent event) {
+ // Get the right compound drawable of the TextView.
+ Drawable rightDrawable = mTextView.getCompoundDrawablesRelative()[2];
+
+ // Check if the touch event intersects with the drawable's bounds.
+ if (rightDrawable != null) {
+ // We can get away w/o caring about the Y bounds since it's such a small view, if it's
+ // above/below the drawable just assume they meant to touch it. ¯\_(ツ)_/¯
+ return event.getX() >= (mTextView.getWidth() - rightDrawable.getBounds().width());
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ if (!FeatureFlags.ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE.get()) {
+ return;
+ }
+
+ info.addAction(new AccessibilityNodeInfo.AccessibilityAction(
+ R.string.toast_split_select_cont_desc,
+ getResources().getString(R.string.toast_split_select_cont_desc)
+ ));
+ }
+
+ @Override
+ public boolean performAccessibilityAction(int action, Bundle arguments) {
+ if (!FeatureFlags.ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE.get()) {
+ return super.performAccessibilityAction(action, arguments);
+ }
+
+ if (action == R.string.toast_split_select_cont_desc) {
+ exitSplitSelection();
+ return true;
+ }
+ return super.performAccessibilityAction(action, arguments);
+ }
+
void ensureProperRotation() {
((RecentsView) mLauncher.getOverviewPanel()).getPagedOrientationHandler()
.setSplitInstructionsParams(
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 6796d4b..1079e00 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -411,6 +411,7 @@
<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_drawable_padding">10dp</dimen>
<dimen name="split_instructions_bottom_margin_phone_landscape">24dp</dimen>
<dimen name="split_instructions_bottom_margin_phone_portrait">60dp</dimen>