From 9e0d0ed0de7b230eed21872eed45e9bc6ae6a7b1 Mon Sep 17 00:00:00 2001 From: Nick Chameyev Date: Wed, 8 Sep 2021 16:39:15 +0100 Subject: [PATCH 1/8] [Taskbar icons unfold animation issue] Do not take into account initial translation Removes taking into account initial translation in move from center animation. Initially it was done because in StatusBar items are aligned using translations but we decided to use different approach there so it is not necessary anymore. Now move from center animator can only set translations from positive/negative value when progress is 0 and sets it to 0 when progress is 1 (previously it was 'capturing' the initial translation and animated the translation to its original value). Taskbar icons are pre-aligned using translations when user is on launcher. After several folds/unfolds these translations were accumulating that led to incorrect positioning of the icons. Found one more issue that we didn't clear the animated views list after finishing the animation in taskbar unfold animation controller but it didn't affect the animation itself. Test: atest com.android.systemui.shared.animation.UnfoldMoveFromCenterAnimatorTest Test: unfold on launcher and use gesture nav to swipe to an app Fixes: 199126683 Change-Id: Icfb29d79fdd8f04eb5a9a369a3d01c15fd321c75 --- .../launcher3/taskbar/TaskbarUnfoldAnimationController.java | 1 + 1 file changed, 1 insertion(+) diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUnfoldAnimationController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUnfoldAnimationController.java index 43f015cd2d..978bd470ff 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUnfoldAnimationController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUnfoldAnimationController.java @@ -77,6 +77,7 @@ public class TaskbarUnfoldAnimationController { @Override public void onTransitionFinished() { mMoveFromCenterAnimator.onTransitionFinished(); + mMoveFromCenterAnimator.clearRegisteredViews(); } @Override From 84511f29976859607127f679e65dfed2f94e8db8 Mon Sep 17 00:00:00 2001 From: Vinit Nayak Date: Fri, 3 Sep 2021 14:09:47 -0700 Subject: [PATCH 2/8] Call exitSplitScreen() when user leaves split tasks * Also call it when user swipes to home Fixes: 198310766 Test: Tested QS'ing split to fullscreen, full to split, swipe to home from split and fullscreen Change-Id: I6a465ead7fcd68dc85e0ab401789f4a5c3feb0d1 --- .../android/quickstep/AbsSwipeUpHandler.java | 6 ++++++ .../quickstep/OverviewCommandHelper.java | 3 +++ .../com/android/quickstep/SystemUiProxy.java | 11 ++++++++-- .../util/LauncherSplitScreenListener.java | 21 +++++++++++++++---- .../android/quickstep/views/RecentsView.java | 3 --- .../com/android/quickstep/views/TaskView.java | 6 ++++++ 6 files changed, 41 insertions(+), 9 deletions(-) diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java index be0c980219..627dd64827 100644 --- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java +++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java @@ -942,6 +942,7 @@ public abstract class AbsSwipeUpHandler, mStateCallback.setState(STATE_SCALED_CONTROLLER_HOME | STATE_CAPTURE_SCREENSHOT); // Notify swipe-to-home (recents animation) is finished SystemUiProxy.INSTANCE.get(mContext).notifySwipeToHomeFinished(); + LauncherSplitScreenListener.INSTANCE.getNoCreate().notifySwipingToHome(); break; case RECENTS: mStateCallback.setState(STATE_SCALED_CONTROLLER_RECENTS | STATE_CAPTURE_SCREENSHOT @@ -1802,8 +1803,13 @@ public abstract class AbsSwipeUpHandler, mGestureState.updateLastStartedTaskId(taskId); boolean hasTaskPreviouslyAppeared = mGestureState.getPreviouslyAppearedTaskIds() .contains(taskId); + boolean isOldTaskSplit = LauncherSplitScreenListener.INSTANCE.getNoCreate() + .getRunningSplitTaskIds().length > 0; nextTask.launchTask(success -> { resultCallback.accept(success); + if (isOldTaskSplit) { + SystemUiProxy.INSTANCE.getNoCreate().exitSplitScreen(taskId); + } if (success) { if (hasTaskPreviouslyAppeared) { onRestartPreviouslyAppearedTask(); diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java index 8fb851c399..b2324640e5 100644 --- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java +++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java @@ -32,6 +32,7 @@ import androidx.annotation.UiThread; import com.android.launcher3.statemanager.StatefulActivity; import com.android.launcher3.util.RunnableList; import com.android.quickstep.RecentsAnimationCallbacks.RecentsAnimationListener; +import com.android.quickstep.util.LauncherSplitScreenListener; import com.android.quickstep.views.RecentsView; import com.android.quickstep.views.TaskView; import com.android.systemui.shared.recents.model.ThumbnailData; @@ -157,6 +158,7 @@ public class OverviewCommandHelper { } if (cmd.type == TYPE_HOME) { mService.startActivity(mOverviewComponentObserver.getHomeIntent()); + LauncherSplitScreenListener.INSTANCE.getNoCreate().notifySwipingToHome(); return true; } } else { @@ -175,6 +177,7 @@ public class OverviewCommandHelper { return launchTask(recents, getNextTask(recents), cmd); case TYPE_HOME: recents.startHome(); + LauncherSplitScreenListener.INSTANCE.getNoCreate().notifySwipingToHome(); return true; } } diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java index 7d2d41326c..aea2d4c645 100644 --- a/quickstep/src/com/android/quickstep/SystemUiProxy.java +++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java @@ -533,10 +533,17 @@ public class SystemUiProxy implements ISystemUiProxy, } } - public void exitSplitScreen() { + /** + * To be called whenever the user exits out of split screen apps (either by launching another + * app or by swiping home) + * @param topTaskId The taskId of the new app that was launched. System will then move this task + * to the front of what the user sees while removing all other split stages. + * If swiping to home (or there is no task to put at the top), can pass in -1. + */ + public void exitSplitScreen(int topTaskId) { if (mSplitScreen != null) { try { - mSplitScreen.exitSplitScreen(); + mSplitScreen.exitSplitScreen(topTaskId); } catch (RemoteException e) { Log.w(TAG, "Failed call exitSplitScreen"); } diff --git a/quickstep/src/com/android/quickstep/util/LauncherSplitScreenListener.java b/quickstep/src/com/android/quickstep/util/LauncherSplitScreenListener.java index 0f4ed014f0..fa4cddc426 100644 --- a/quickstep/src/com/android/quickstep/util/LauncherSplitScreenListener.java +++ b/quickstep/src/com/android/quickstep/util/LauncherSplitScreenListener.java @@ -44,8 +44,6 @@ public class LauncherSplitScreenListener extends ISplitScreenListener.Stub { if (frozen) { mPersistentGroupedIds = getRunningSplitTaskIds(); } else { - // TODO(b/198310766) Need to also explicitly exit split screen if - // we're not currently viewing split screened apps mPersistentGroupedIds = EMPTY_ARRAY; } } @@ -53,8 +51,11 @@ public class LauncherSplitScreenListener extends ISplitScreenListener.Stub { /** * Gets set to current split taskIDs whenever the task list is frozen, and set to empty array - * whenever task list unfreezes. - * When not null, this indicates that we need to load a GroupedTaskView as the most recent + * whenever task list unfreezes. This also gets set to empty array whenever the user swipes to + * home - in that case the task list does not unfreeze immediately after the gesture, so it's + * done via {@link #notifySwipingToHome()}. + * + * When not empty, this indicates that we need to load a GroupedTaskView as the most recent * page, so user can quickswitch back to a grouped task. */ private int[] mPersistentGroupedIds; @@ -140,6 +141,18 @@ public class LauncherSplitScreenListener extends ISplitScreenListener.Stub { } } + /** Notifies SystemUi to remove any split screen state */ + public void notifySwipingToHome() { + boolean hasSplitTasks = LauncherSplitScreenListener.INSTANCE.getNoCreate() + .getPersistentSplitIds().length > 0; + if (!hasSplitTasks) { + return; + } + + SystemUiProxy.INSTANCE.getNoCreate().exitSplitScreen(-1); + mPersistentGroupedIds = EMPTY_ARRAY; + } + private void resetTaskId(StagedSplitTaskPosition taskPosition) { taskPosition.taskId = -1; } diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java index b690982efb..192cef80a9 100644 --- a/quickstep/src/com/android/quickstep/views/RecentsView.java +++ b/quickstep/src/com/android/quickstep/views/RecentsView.java @@ -2001,9 +2001,6 @@ public abstract class RecentsView 0; if (ActivityManagerWrapper.getInstance() .startActivityFromRecents(mTask.key, opts.options)) { + if (isOldTaskSplit) { + SystemUiProxy.INSTANCE.getNoCreate().exitSplitScreen(mTask.key.id); + } RecentsView recentsView = getRecentsView(); if (ENABLE_QUICKSTEP_LIVE_TILE.get() && recentsView.getRunningTaskViewId() != -1) { recentsView.onTaskLaunchedInLiveTileMode(); From 281786a841d9230f223118e4c61e580647806c12 Mon Sep 17 00:00:00 2001 From: Tony Wickham Date: Wed, 8 Sep 2021 11:29:36 -0700 Subject: [PATCH 3/8] Add option to reset Onboarding Flows in Developer Options Test: Go to Developer Options, reset Hybrid Hotseat Education, then long press on a predicted hotseat item and ensure edu toast appears Bug: 199299382 Change-Id: I384d7af08fc8b842bc3a1333b9f8017cca3680e6 --- .../settings/DeveloperOptionsFragment.java | 28 +++++++++++++++++++ .../launcher3/util/OnboardingPrefs.java | 8 ++++++ 2 files changed, 36 insertions(+) diff --git a/src/com/android/launcher3/settings/DeveloperOptionsFragment.java b/src/com/android/launcher3/settings/DeveloperOptionsFragment.java index 4d63218785..f098b002b5 100644 --- a/src/com/android/launcher3/settings/DeveloperOptionsFragment.java +++ b/src/com/android/launcher3/settings/DeveloperOptionsFragment.java @@ -29,6 +29,7 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.net.Uri; @@ -44,6 +45,7 @@ import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.widget.EditText; +import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -57,12 +59,15 @@ import androidx.preference.PreferenceViewHolder; import androidx.preference.SwitchPreference; import com.android.launcher3.R; +import com.android.launcher3.Utilities; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.config.FlagTogglerPrefUi; import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper; +import com.android.launcher3.util.OnboardingPrefs; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -104,6 +109,7 @@ public class DeveloperOptionsFragment extends PreferenceFragmentCompat { initFlags(); loadPluginPrefs(); maybeAddSandboxCategory(); + addOnboardingPrefsCatergory(); if (getActivity() != null) { getActivity().setTitle("Developer Options"); @@ -355,6 +361,28 @@ public class DeveloperOptionsFragment extends PreferenceFragmentCompat { sandboxCategory.addPreference(launchSandboxModeTutorialPreference); } + private void addOnboardingPrefsCatergory() { + PreferenceCategory onboardingCategory = newCategory("Onboarding Flows"); + onboardingCategory.setSummary("Reset these if you want to see the education again."); + for (Map.Entry titleAndKeys : OnboardingPrefs.ALL_PREF_KEYS.entrySet()) { + String title = titleAndKeys.getKey(); + String[] keys = titleAndKeys.getValue(); + Preference onboardingPref = new Preference(getContext()); + onboardingPref.setTitle(title); + onboardingPref.setSummary("Tap to reset"); + onboardingPref.setOnPreferenceClickListener(preference -> { + SharedPreferences.Editor sharedPrefsEdit = Utilities.getPrefs(getContext()).edit(); + for (String key : keys) { + sharedPrefsEdit.remove(key); + } + sharedPrefsEdit.apply(); + Toast.makeText(getContext(), "Reset " + title, Toast.LENGTH_SHORT).show(); + return true; + }); + onboardingCategory.addPreference(onboardingPref); + } + } + private String toName(String action) { String str = action.replace("com.android.systemui.action.PLUGIN_", "") .replace("com.android.launcher3.action.PLUGIN_", ""); diff --git a/src/com/android/launcher3/util/OnboardingPrefs.java b/src/com/android/launcher3/util/OnboardingPrefs.java index cf1467ade4..5ba0d30256 100644 --- a/src/com/android/launcher3/util/OnboardingPrefs.java +++ b/src/com/android/launcher3/util/OnboardingPrefs.java @@ -39,6 +39,14 @@ public class OnboardingPrefs { public static final String SEARCH_EDU_SEEN = "launcher.search_edu_seen"; public static final String SEARCH_SNACKBAR_COUNT = "launcher.keyboard_snackbar_count"; public static final String TASKBAR_EDU_SEEN = "launcher.taskbar_edu_seen"; + // When adding a new key, add it here as well, to be able to reset it from Developer Options. + public static final Map ALL_PREF_KEYS = Map.of( + "All Apps Bounce", new String[] { HOME_BOUNCE_SEEN, HOME_BOUNCE_COUNT }, + "Hybrid Hotseat Education", new String[] { HOTSEAT_DISCOVERY_TIP_COUNT, + HOTSEAT_LONGPRESS_TIP_SEEN }, + "Search Education", new String[] { SEARCH_EDU_SEEN, SEARCH_SNACKBAR_COUNT }, + "Taskbar Education", new String[] { TASKBAR_EDU_SEEN } + ); /** * Events that either have happened or have not (booleans). From 534bd2205b2051baeee6b1fa3e71a3b4e8ce4d3e Mon Sep 17 00:00:00 2001 From: Tony Wickham Date: Wed, 8 Sep 2021 13:46:56 -0700 Subject: [PATCH 4/8] Add ability to start DeveloperOptionsFragment with filter pre-applied Repurposing EXTRA_FRAGMENT_ARG_KEY to set an initial filter. Test: adb shell 'am start -n com.google.android.apps.nexuslauncher/com.android.launcher3.settings.SettingsActivity --es ":settings:fragment" "com.android.launcher3.settings.DeveloperOptionsFragment" --es ":settings:fragment_args_key" "Education"' Bug: 199299382 Change-Id: I032f7ffc373daed51c8d381f3cacf722fb3fb198 --- .../launcher3/settings/DeveloperOptionsFragment.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/com/android/launcher3/settings/DeveloperOptionsFragment.java b/src/com/android/launcher3/settings/DeveloperOptionsFragment.java index f098b002b5..b06b8a10bf 100644 --- a/src/com/android/launcher3/settings/DeveloperOptionsFragment.java +++ b/src/com/android/launcher3/settings/DeveloperOptionsFragment.java @@ -20,6 +20,7 @@ import static android.content.pm.PackageManager.MATCH_DISABLED_COMPONENTS; import static android.view.View.GONE; import static android.view.View.VISIBLE; +import static com.android.launcher3.settings.SettingsActivity.EXTRA_FRAGMENT_ARG_KEY; import static com.android.launcher3.uioverrides.plugins.PluginManagerWrapper.PLUGIN_CHANGED; import static com.android.launcher3.uioverrides.plugins.PluginManagerWrapper.pluginEnabledKey; @@ -159,6 +160,15 @@ public class DeveloperOptionsFragment extends PreferenceFragmentCompat { } }); + if (getArguments() != null) { + String filter = getArguments().getString(EXTRA_FRAGMENT_ARG_KEY); + // Normally EXTRA_FRAGMENT_ARG_KEY is used to highlight the preference with the given + // key. This is a slight variation where we instead filter by the human-readable titles. + if (filter != null) { + filterBox.setText(filter); + } + } + View listView = getListView(); final int bottomPadding = listView.getPaddingBottom(); listView.setOnApplyWindowInsetsListener((v, insets) -> { From cc5c8843dfebfa92057b6ce8904ac58cd05ffaef Mon Sep 17 00:00:00 2001 From: Lais Andrade Date: Fri, 4 Jun 2021 18:20:30 +0100 Subject: [PATCH 5/8] Introduces haptic feedback to launcher overview Haptics introduced at the key moments: - Task scroll in overview or quick switch, trigger when a new task comes to the center of the screen; - Task scroll in overview when overscroll animation is triggered; - Task dismissed in overview; There is also a configured min gap between two scroll haptics set to 20ms to prevent fast scrolls from creating a chain of cancelled effects. Fix: 182382085 Test: manual Change-Id: I43c0f8c879a06f317e8a660240dafb7f7abe79f7 --- quickstep/res/values/config.xml | 1 + ...ButtonNavbarToOverviewTouchController.java | 4 +- .../NoButtonQuickSwitchTouchController.java | 13 ++++++- .../TaskViewTouchController.java | 14 ++++++- .../android/quickstep/AbsSwipeUpHandler.java | 4 +- .../interaction/EdgeBackGesturePanel.java | 2 +- .../interaction/NavBarGestureHandler.java | 4 +- .../quickstep}/util/VibratorWrapper.java | 38 +++++++++++++++++-- .../android/quickstep/views/RecentsView.java | 32 ++++++++++++++++ src/com/android/launcher3/PagedView.java | 31 +++++++++++++++ 10 files changed, 130 insertions(+), 13 deletions(-) rename {src/com/android/launcher3 => quickstep/src/com/android/quickstep}/util/VibratorWrapper.java (66%) diff --git a/quickstep/res/values/config.xml b/quickstep/res/values/config.xml index d67b23be0a..31c0f5f2ab 100644 --- a/quickstep/res/values/config.xml +++ b/quickstep/res/values/config.xml @@ -30,6 +30,7 @@ determines how many thumbnails will be fetched in the background. --> 3 12 + 20 200 diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java index 283743dc64..ef6f53e8f5 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java +++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java @@ -24,7 +24,7 @@ import static com.android.launcher3.LauncherState.OVERVIEW; import static com.android.launcher3.Utilities.EDGE_NAV_BAR; import static com.android.launcher3.anim.AnimatorListeners.forSuccessCallback; import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL; -import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC; +import static com.android.quickstep.util.VibratorWrapper.OVERVIEW_HAPTIC; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED; import android.animation.ObjectAnimator; @@ -38,11 +38,11 @@ import com.android.launcher3.LauncherState; import com.android.launcher3.Utilities; import com.android.launcher3.anim.AnimatorPlaybackController; import com.android.launcher3.states.StateAnimationConfig; -import com.android.launcher3.util.VibratorWrapper; import com.android.quickstep.SystemUiProxy; import com.android.quickstep.util.AnimatorControllerWithResistance; import com.android.quickstep.util.MotionPauseDetector; import com.android.quickstep.util.OverviewToHomeAnim; +import com.android.quickstep.util.VibratorWrapper; import com.android.quickstep.views.RecentsView; /** diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java index 40c3e02238..ff3c517949 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java +++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java @@ -41,7 +41,7 @@ import static com.android.launcher3.states.StateAnimationConfig.SKIP_SCRIM; import static com.android.launcher3.touch.BothAxesSwipeDetector.DIRECTION_RIGHT; import static com.android.launcher3.touch.BothAxesSwipeDetector.DIRECTION_UP; import static com.android.launcher3.util.DisplayController.getSingleFrameMs; -import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC; +import static com.android.quickstep.util.VibratorWrapper.OVERVIEW_HAPTIC; import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_HORIZONTAL_OFFSET; import static com.android.quickstep.views.RecentsView.CONTENT_ALPHA; import static com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS; @@ -67,14 +67,15 @@ import com.android.launcher3.states.StateAnimationConfig; import com.android.launcher3.touch.BaseSwipeDetector; import com.android.launcher3.touch.BothAxesSwipeDetector; import com.android.launcher3.util.TouchController; -import com.android.launcher3.util.VibratorWrapper; import com.android.quickstep.AnimatedFloat; import com.android.quickstep.SystemUiProxy; import com.android.quickstep.util.AnimatorControllerWithResistance; import com.android.quickstep.util.LayoutUtils; import com.android.quickstep.util.MotionPauseDetector; +import com.android.quickstep.util.VibratorWrapper; import com.android.quickstep.util.WorkspaceRevealAnim; import com.android.quickstep.views.LauncherRecentsView; +import com.android.quickstep.views.RecentsView; /** * Handles quick switching to a recent task from the home screen. To give as much flexibility to @@ -398,6 +399,14 @@ public class NoButtonQuickSwitchTouchController implements TouchController, nonOverviewAnim.setFloatValues(startProgress, endProgress); mNonOverviewAnim.dispatchOnStart(); } + if (targetState == QUICK_SWITCH) { + // Navigating to quick switch, add scroll feedback since the first time is not + // considered a scroll by the RecentsView. + VibratorWrapper.INSTANCE.get(mLauncher).vibrate( + RecentsView.SCROLL_VIBRATION_PRIMITIVE, + RecentsView.SCROLL_VIBRATION_PRIMITIVE_SCALE, + RecentsView.SCROLL_VIBRATION_FALLBACK); + } nonOverviewAnim.setDuration(Math.max(xDuration, yDuration)); mNonOverviewAnim.setEndAction(() -> onAnimationToStateCompleted(targetState)); diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java index 0603ba502a..010f463438 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java +++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java @@ -22,6 +22,7 @@ import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_BOTH import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.os.SystemClock; +import android.os.VibrationEffect; import android.view.MotionEvent; import android.view.View; import android.view.animation.Interpolator; @@ -41,6 +42,7 @@ import com.android.launcher3.util.FlingBlockCheck; import com.android.launcher3.util.TouchController; import com.android.launcher3.views.BaseDragLayer; import com.android.quickstep.SysUINavigationMode; +import com.android.quickstep.util.VibratorWrapper; import com.android.quickstep.views.RecentsView; import com.android.quickstep.views.TaskView; @@ -55,6 +57,12 @@ public abstract class TaskViewTouchController private static final long MIN_TASK_DISMISS_ANIMATION_DURATION = 300; private static final long MAX_TASK_DISMISS_ANIMATION_DURATION = 600; + public static final int TASK_DISMISS_VIBRATION_PRIMITIVE = + Utilities.ATLEAST_R ? VibrationEffect.Composition.PRIMITIVE_TICK : -1; + public static final float TASK_DISMISS_VIBRATION_PRIMITIVE_SCALE = 1f; + public static final VibrationEffect TASK_DISMISS_VIBRATION_FALLBACK = + VibratorWrapper.EFFECT_TEXTURE_TICK; + protected final T mActivity; private final SingleAxisSwipeDetector mDetector; private final RecentsView mRecentsView; @@ -334,10 +342,10 @@ public abstract class TaskViewTouchController fling = false; } PagedOrientationHandler orientationHandler = mRecentsView.getPagedOrientationHandler(); + boolean goingUp = orientationHandler.isGoingUp(velocity, mIsRtl); float progress = mCurrentAnimation.getProgressFraction(); float interpolatedProgress = mCurrentAnimation.getInterpolatedProgress(); if (fling) { - boolean goingUp = orientationHandler.isGoingUp(velocity, mIsRtl); goingToEnd = goingUp == mCurrentAnimationIsGoingUp; } else { goingToEnd = interpolatedProgress > SUCCESS_TRANSITION_PROGRESS; @@ -357,6 +365,10 @@ public abstract class TaskViewTouchController mCurrentAnimation.startWithVelocity(mActivity, goingToEnd, velocity * orientationHandler.getSecondaryTranslationDirectionFactor(), mEndDisplacement, animationDuration); + if (goingUp && goingToEnd) { + VibratorWrapper.INSTANCE.get(mActivity).vibrate(TASK_DISMISS_VIBRATION_PRIMITIVE, + TASK_DISMISS_VIBRATION_PRIMITIVE_SCALE, TASK_DISMISS_VIBRATION_FALLBACK); + } } private void clearState() { diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java index 42c89fd006..48a4e77f9f 100644 --- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java +++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java @@ -36,7 +36,6 @@ import static com.android.launcher3.util.DisplayController.getSingleFrameMs; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; import static com.android.launcher3.util.SystemUiController.UI_STATE_FULLSCREEN_TASK; -import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC; import static com.android.quickstep.GestureState.GestureEndTarget.HOME; import static com.android.quickstep.GestureState.GestureEndTarget.LAST_TASK; import static com.android.quickstep.GestureState.GestureEndTarget.NEW_TASK; @@ -46,6 +45,7 @@ import static com.android.quickstep.GestureState.STATE_END_TARGET_SET; import static com.android.quickstep.GestureState.STATE_RECENTS_ANIMATION_CANCELED; import static com.android.quickstep.GestureState.STATE_RECENTS_SCROLLING_FINISHED; import static com.android.quickstep.MultiStateCallback.DEBUG_STATES; +import static com.android.quickstep.util.VibratorWrapper.OVERVIEW_HAPTIC; import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD; import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS; import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME; @@ -90,7 +90,6 @@ import com.android.launcher3.statemanager.StatefulActivity; import com.android.launcher3.tracing.InputConsumerProto; import com.android.launcher3.tracing.SwipeHandlerProto; import com.android.launcher3.util.TraceHelper; -import com.android.launcher3.util.VibratorWrapper; import com.android.launcher3.util.WindowBounds; import com.android.quickstep.BaseActivityInterface.AnimationFactory; import com.android.quickstep.GestureState.GestureEndTarget; @@ -107,6 +106,7 @@ import com.android.quickstep.util.StaggeredWorkspaceAnim; import com.android.quickstep.util.SurfaceTransactionApplier; import com.android.quickstep.util.SwipePipToHomeAnimator; import com.android.quickstep.util.TransformParams; +import com.android.quickstep.util.VibratorWrapper; import com.android.quickstep.views.RecentsView; import com.android.quickstep.views.TaskView; import com.android.systemui.shared.recents.model.ThumbnailData; diff --git a/quickstep/src/com/android/quickstep/interaction/EdgeBackGesturePanel.java b/quickstep/src/com/android/quickstep/interaction/EdgeBackGesturePanel.java index 0521db4d1b..063eb6a8e5 100644 --- a/quickstep/src/com/android/quickstep/interaction/EdgeBackGesturePanel.java +++ b/quickstep/src/com/android/quickstep/interaction/EdgeBackGesturePanel.java @@ -43,7 +43,7 @@ import androidx.dynamicanimation.animation.SpringForce; import com.android.launcher3.R; import com.android.launcher3.ResourceUtils; import com.android.launcher3.anim.Interpolators; -import com.android.launcher3.util.VibratorWrapper; +import com.android.quickstep.util.VibratorWrapper; /** Forked from platform/frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarEdgePanel.java. */ public class EdgeBackGesturePanel extends View { diff --git a/quickstep/src/com/android/quickstep/interaction/NavBarGestureHandler.java b/quickstep/src/com/android/quickstep/interaction/NavBarGestureHandler.java index a9a9e2a104..5e127d45ee 100644 --- a/quickstep/src/com/android/quickstep/interaction/NavBarGestureHandler.java +++ b/quickstep/src/com/android/quickstep/interaction/NavBarGestureHandler.java @@ -16,7 +16,6 @@ package com.android.quickstep.interaction; import static com.android.launcher3.Utilities.squaredHypot; -import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC; import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.ASSISTANT_COMPLETED; import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.ASSISTANT_NOT_STARTED_BAD_ANGLE; import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.ASSISTANT_NOT_STARTED_SWIPE_TOO_SHORT; @@ -26,6 +25,7 @@ import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestu import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.HOME_OR_OVERVIEW_NOT_STARTED_WRONG_SWIPE_DIRECTION; import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.OVERVIEW_GESTURE_COMPLETED; import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.OVERVIEW_NOT_STARTED_TOO_FAR_FROM_EDGE; +import static com.android.quickstep.util.VibratorWrapper.OVERVIEW_HAPTIC; import android.animation.ValueAnimator; import android.content.Context; @@ -47,11 +47,11 @@ import androidx.annotation.Nullable; import com.android.launcher3.R; import com.android.launcher3.ResourceUtils; import com.android.launcher3.anim.Interpolators; -import com.android.launcher3.util.VibratorWrapper; import com.android.quickstep.SysUINavigationMode.Mode; import com.android.quickstep.util.MotionPauseDetector; import com.android.quickstep.util.NavBarPosition; import com.android.quickstep.util.TriggerSwipeUpTouchTracker; +import com.android.quickstep.util.VibratorWrapper; import com.android.systemui.shared.system.QuickStepContract; /** Utility class to handle Home and Assistant gestures. */ diff --git a/src/com/android/launcher3/util/VibratorWrapper.java b/quickstep/src/com/android/quickstep/util/VibratorWrapper.java similarity index 66% rename from src/com/android/launcher3/util/VibratorWrapper.java rename to quickstep/src/com/android/quickstep/util/VibratorWrapper.java index b0defd445a..211bd08b33 100644 --- a/src/com/android/launcher3/util/VibratorWrapper.java +++ b/quickstep/src/com/android/quickstep/util/VibratorWrapper.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 The Android Open Source Project + * Copyright (C) 2021 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. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.launcher3.util; +package com.android.quickstep.util; import static android.os.VibrationEffect.createPredefined; import static android.provider.Settings.System.HAPTIC_FEEDBACK_ENABLED; @@ -21,15 +21,20 @@ import static android.provider.Settings.System.HAPTIC_FEEDBACK_ENABLED; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; +import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.ContentResolver; import android.content.Context; import android.database.ContentObserver; +import android.media.AudioAttributes; import android.os.Build; import android.os.VibrationEffect; import android.os.Vibrator; import android.provider.Settings; +import com.android.launcher3.Utilities; +import com.android.launcher3.util.MainThreadInitializedObject; + /** * Wrapper around {@link Vibrator} to easily perform haptic feedback where necessary. */ @@ -39,8 +44,15 @@ public class VibratorWrapper { public static final MainThreadInitializedObject INSTANCE = new MainThreadInitializedObject<>(VibratorWrapper::new); + public static final AudioAttributes VIBRATION_ATTRS = new AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION) + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .build(); + public static final VibrationEffect EFFECT_CLICK = createPredefined(VibrationEffect.EFFECT_CLICK); + public static final VibrationEffect EFFECT_TEXTURE_TICK = + VibrationEffect.createPredefined(VibrationEffect.EFFECT_TEXTURE_TICK); /** * Haptic when entering overview. @@ -78,7 +90,27 @@ public class VibratorWrapper { /** Vibrates with the given effect if haptic feedback is available and enabled. */ public void vibrate(VibrationEffect vibrationEffect) { if (mHasVibrator && mIsHapticFeedbackEnabled) { - UI_HELPER_EXECUTOR.execute(() -> mVibrator.vibrate(vibrationEffect)); + UI_HELPER_EXECUTOR.execute(() -> mVibrator.vibrate(vibrationEffect, VIBRATION_ATTRS)); + } + } + + /** + * Vibrates with a single primitive, if supported, or use a fallback effect instead. This only + * vibrates if haptic feedback is available and enabled. + */ + @SuppressLint("NewApi") + public void vibrate(int primitiveId, float primitiveScale, VibrationEffect fallbackEffect) { + if (mHasVibrator && mIsHapticFeedbackEnabled) { + UI_HELPER_EXECUTOR.execute(() -> { + if (Utilities.ATLEAST_R && primitiveId >= 0 + && mVibrator.areAllPrimitivesSupported(primitiveId)) { + mVibrator.vibrate(VibrationEffect.startComposition() + .addPrimitive(primitiveId, primitiveScale) + .compose(), VIBRATION_ATTRS); + } else { + mVibrator.vibrate(fallbackEffect, VIBRATION_ATTRS); + } + }); } } } diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java index 3aed7cc630..9ec5138a9c 100644 --- a/quickstep/src/com/android/quickstep/views/RecentsView.java +++ b/quickstep/src/com/android/quickstep/views/RecentsView.java @@ -77,7 +77,9 @@ import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Bundle; +import android.os.SystemClock; import android.os.UserHandle; +import android.os.VibrationEffect; import android.text.Layout; import android.text.StaticLayout; import android.text.TextPaint; @@ -154,6 +156,7 @@ import com.android.quickstep.util.SplitSelectStateController; import com.android.quickstep.util.SurfaceTransactionApplier; import com.android.quickstep.util.TaskViewSimulator; import com.android.quickstep.util.TransformParams; +import com.android.quickstep.util.VibratorWrapper; import com.android.systemui.plugins.ResourceProvider; import com.android.systemui.shared.recents.model.Task; import com.android.systemui.shared.recents.model.Task.TaskKey; @@ -238,6 +241,12 @@ public abstract class RecentsView mScrollHapticMinGapMillis) { + mScrollLastHapticTimestamp = now; + VibratorWrapper.INSTANCE.get(mContext).vibrate(SCROLL_VIBRATION_PRIMITIVE, + SCROLL_VIBRATION_PRIMITIVE_SCALE, SCROLL_VIBRATION_FALLBACK); + } + } + @Override protected void determineScrollingStart(MotionEvent ev, float touchSlopScale) { // Enables swiping to the left or right only if the task overlay is not modal. diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java index eb3f94ce7c..e4ee50a0b1 100644 --- a/src/com/android/launcher3/PagedView.java +++ b/src/com/android/launcher3/PagedView.java @@ -108,6 +108,8 @@ public abstract class PagedView extends ViewGrou // relative scroll position unchanged in updateCurrentPageScroll. Cleared when snapping to a // page. protected int mCurrentPageScrollDiff; + // The current page the PagedView is scrolling over on it's way to the destination page. + protected int mCurrentScrollOverPage; @ViewDebug.ExportedProperty(category = "launcher") protected int mNextPage = INVALID_PAGE; @@ -180,6 +182,7 @@ public abstract class PagedView extends ViewGrou mScroller = new OverScroller(context, SCROLL); mCurrentPage = 0; + mCurrentScrollOverPage = 0; final ViewConfiguration configuration = ViewConfiguration.get(context); mTouchSlop = configuration.getScaledTouchSlop(); @@ -437,6 +440,7 @@ public abstract class PagedView extends ViewGrou } int prevPage = overridePrevPage != INVALID_PAGE ? overridePrevPage : mCurrentPage; mCurrentPage = validateNewPage(currentPage); + mCurrentScrollOverPage = mCurrentPage; updateCurrentPageScroll(); notifyPageSwitchListener(prevPage); invalidate(); @@ -557,9 +561,11 @@ public abstract class PagedView extends ViewGrou if (newPos < mMinScroll && oldPos >= mMinScroll) { mEdgeGlowLeft.onAbsorb((int) mScroller.getCurrVelocity()); mScroller.abortAnimation(); + onEdgeAbsorbingScroll(); } else if (newPos > mMaxScroll && oldPos <= mMaxScroll) { mEdgeGlowRight.onAbsorb((int) mScroller.getCurrVelocity()); mScroller.abortAnimation(); + onEdgeAbsorbingScroll(); } } @@ -577,6 +583,7 @@ public abstract class PagedView extends ViewGrou sendScrollAccessibilityEvent(); int prevPage = mCurrentPage; mCurrentPage = validateNewPage(mNextPage); + mCurrentScrollOverPage = mCurrentPage; mNextPage = INVALID_PAGE; notifyPageSwitchListener(prevPage); @@ -837,6 +844,7 @@ public abstract class PagedView extends ViewGrou public void onViewRemoved(View child) { super.onViewRemoved(child); mCurrentPage = validateNewPage(mCurrentPage); + mCurrentScrollOverPage = mCurrentPage; dispatchPageCountChanged(); } @@ -1408,6 +1416,20 @@ public abstract class PagedView extends ViewGrou protected void onNotSnappingToPageInFreeScroll() { } + /** + * Called when the view edges absorb part of the scroll. Subclasses can override this + * to provide custom behavior during animation. + */ + protected void onEdgeAbsorbingScroll() { + } + + /** + * Called when the current page closest to the center of the screen changes as part of the + * scroll. Subclasses can override this to provide custom behavior during scroll. + */ + protected void onScrollOverPageChanged() { + } + protected boolean shouldFlingForVelocity(int velocity) { float threshold = mAllowEasyFling ? mEasyFlingThresholdVelocity : mFlingThresholdVelocity; return Math.abs(velocity) > threshold; @@ -1691,6 +1713,15 @@ public abstract class PagedView extends ViewGrou return mAllowOverScroll; } + @Override + protected void onScrollChanged(int l, int t, int oldl, int oldt) { + int newDestinationPage = getDestinationPage(); + if (newDestinationPage >= 0 && newDestinationPage != mCurrentScrollOverPage) { + mCurrentScrollOverPage = newDestinationPage; + onScrollOverPageChanged(); + } + } + @Override public CharSequence getAccessibilityClassName() { // Some accessibility services have special logic for ScrollView. Since we provide same From fef4d0e08185619d791cce1668468a8bc172999c Mon Sep 17 00:00:00 2001 From: Alex Chau Date: Thu, 9 Sep 2021 18:24:51 +0100 Subject: [PATCH 6/8] Allow restore between TYPE_PHONE AND TYPE_MULTI_DISPLAY Bug: 199407109 Test: manual Change-Id: I6eb982a79f4130925a9d7e0972faf0f01a6aac20 --- src/com/android/launcher3/model/DeviceGridState.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/com/android/launcher3/model/DeviceGridState.java b/src/com/android/launcher3/model/DeviceGridState.java index 761053d2d0..1076e8834b 100644 --- a/src/com/android/launcher3/model/DeviceGridState.java +++ b/src/com/android/launcher3/model/DeviceGridState.java @@ -28,6 +28,7 @@ import android.text.TextUtils; import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.Utilities; import com.android.launcher3.logging.StatsLogManager.LauncherEvent; +import com.android.launcher3.util.IntSet; import java.util.Locale; import java.util.Objects; @@ -44,6 +45,7 @@ public class DeviceGridState { public static final int TYPE_PHONE = 0; public static final int TYPE_MULTI_DISPLAY = 1; public static final int TYPE_TABLET = 2; + public static final IntSet COMPATIBLE_TYPES = IntSet.wrap(TYPE_PHONE, TYPE_MULTI_DISPLAY); private final String mGridSizeString; private final int mNumHotseat; @@ -109,7 +111,9 @@ public class DeviceGridState { if (o == null || getClass() != o.getClass()) return false; DeviceGridState that = (DeviceGridState) o; return mNumHotseat == that.mNumHotseat - && mDeviceType == that.mDeviceType + && (mDeviceType == that.mDeviceType + || (COMPATIBLE_TYPES.contains(mDeviceType) + && COMPATIBLE_TYPES.contains(that.mDeviceType))) && Objects.equals(mGridSizeString, that.mGridSizeString); } } From c2a6f6f4ae7966bfbfde5c33885384e240ad9951 Mon Sep 17 00:00:00 2001 From: Thales Lima Date: Thu, 9 Sep 2021 17:06:22 +0100 Subject: [PATCH 7/8] launcher: correct page spacing for multiple panels in RTL Fixes 199043583 Test: manual testing in unfolded state in portrait and landscape Change-Id: Idf5bdabb50742498701681e4654e72260054ab47 --- src/com/android/launcher3/PagedView.java | 3 ++- src/com/android/launcher3/Workspace.java | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java index 2f9b5af050..7f0186fc83 100644 --- a/src/com/android/launcher3/PagedView.java +++ b/src/com/android/launcher3/PagedView.java @@ -766,7 +766,8 @@ public abstract class PagedView extends ViewGrou childStart += primaryDimension + getChildGap(); // This makes sure that the space is added after the page, not after each panel - if (i % panelCount == panelCount - 1) { + int lastPanel = mIsRtl ? 0 : panelCount - 1; + if (i % panelCount == lastPanel) { childStart += mPageSpacing; } } diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java index a21c8e30f1..3d1b7128b4 100644 --- a/src/com/android/launcher3/Workspace.java +++ b/src/com/android/launcher3/Workspace.java @@ -338,15 +338,17 @@ public class Workspace extends PagedView int paddingBottom = grid.cellLayoutBottomPaddingPx; int panelCount = getPanelCount(); + int rightPanelModulus = mIsRtl ? 0 : panelCount - 1; + int leftPanelModulus = mIsRtl ? panelCount - 1 : 0; int numberOfScreens = mScreenOrder.size(); for (int i = 0; i < numberOfScreens; i++) { int paddingLeft = paddingLeftRight; int paddingRight = paddingLeftRight; if (panelCount > 1) { - if (i % panelCount == 0) { // left side panel + if (i % panelCount == leftPanelModulus) { paddingLeft = paddingLeftRight; paddingRight = 0; - } else if (i % panelCount == panelCount - 1) { // right side panel + } else if (i % panelCount == rightPanelModulus) { paddingLeft = 0; paddingRight = paddingLeftRight; } else { // middle panel From 9732df1addac665f61f0b7ebd63e1e08607bce3a Mon Sep 17 00:00:00 2001 From: Andras Kloczl Date: Thu, 9 Sep 2021 19:40:42 +0100 Subject: [PATCH 8/8] Fix two dragging related bugs in Launcher home - accessibility bug: 199394124 - dragging on side bug: http://shortn/_LFBdIllupk Test: manual Bug: 199394124 Change-Id: I478e31332119f337bf0f8b6c92926845eb4889c4 --- src/com/android/launcher3/PagedView.java | 2 +- src/com/android/launcher3/Workspace.java | 31 ++++++++++++++++-------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java index 2f9b5af050..978886d96d 100644 --- a/src/com/android/launcher3/PagedView.java +++ b/src/com/android/launcher3/PagedView.java @@ -1553,7 +1553,7 @@ public abstract class PagedView extends ViewGrou return getDisplacementFromScreenCenter(childIndex, screenCenter); } - private int getScreenCenter(int primaryScroll) { + protected int getScreenCenter(int primaryScroll) { float primaryScale = mOrientationHandler.getPrimaryScale(this); float primaryPivot = mOrientationHandler.getPrimaryValue(getPivotX(), getPivotY()); int pageOrientationSize = mOrientationHandler.getMeasuredSize(this); diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java index a21c8e30f1..8a43ca991f 100644 --- a/src/com/android/launcher3/Workspace.java +++ b/src/com/android/launcher3/Workspace.java @@ -2453,21 +2453,32 @@ public class Workspace extends PagedView } int nextPage = getNextPage(); - if (layout == null && !isPageInTransition()) { - layout = verifyInsidePage(nextPage + (mIsRtl ? 1 : -1), Math.min(centerX, d.x), d.y); + IntSet pageIndexesToVerify = IntSet.wrap(nextPage - 1, nextPage + 1); + if (isTwoPanelEnabled()) { + // If two panel is enabled, users can also drag items to nextPage + 2 + pageIndexesToVerify.add(nextPage + 2); } - if (layout == null && !isPageInTransition()) { - layout = verifyInsidePage(nextPage + (mIsRtl ? -1 : 1), Math.max(centerX, d.x), d.y); + int touchX = (int) Math.min(centerX, d.x); + int touchY = d.y; + + // Go through the pages and check if the dragged item is inside one of them + for (int pageIndex : pageIndexesToVerify) { + if (layout != null || isPageInTransition()) { + break; + } + layout = verifyInsidePage(pageIndex, touchX, touchY); } - // If two panel is enabled, users can also drag items to currentPage + 2 - if (isTwoPanelEnabled() && layout == null && !isPageInTransition()) { - layout = verifyInsidePage(nextPage + (mIsRtl ? -2 : 2), Math.max(centerX, d.x), d.y); - } - - // Always pick the current page. + // If the dragged item isn't located in one of the pages above, the icon will stay on the + // current screen. For two panel pick the closest panel on the current screen, + // on one panel just choose the current page. if (layout == null && nextPage >= 0 && nextPage < getPageCount()) { + if (isTwoPanelEnabled()) { + nextPage = getScreenCenter(getScrollX()) > touchX + ? (mIsRtl ? nextPage + 1 : nextPage) // left side + : (mIsRtl ? nextPage : nextPage + 1); // right side + } layout = (CellLayout) getChildAt(nextPage); } if (layout != mDragTargetLayout) {