Swipe up on nav bar to go home from -1 and widgets

More specifically, any window (e.g. qsb search) or AbstractFloatingView.

NavBarToHomeTouchController now implements TouchController directly instead
of AbstractStateChangeTouchController, as it not only dealing with
launcher states. This makes it easier to override intercept logic to
handle cases like not having window focus, for example.

AbstractFloatingViews can createHintCloseAnim() to play an animation
hinting that it is about to be closed by swiping up. Widgets sheets use
this to pull back similar to the all apps transition to home.

Bug: 129976669
Change-Id: Ie157b978d9f1ee36d5fd32cea72ec02ce40878c0
This commit is contained in:
Tony Wickham 2019-04-05 13:52:35 -07:00
parent 2a059c7802
commit 9791bd1555
9 changed files with 230 additions and 70 deletions

View File

@ -15,74 +15,131 @@
*/
package com.android.launcher3.uioverrides.touchcontrollers;
import static android.view.View.TRANSLATION_X;
import static com.android.launcher3.AbstractFloatingView.TYPE_ALL;
import static com.android.launcher3.AbstractFloatingView.TYPE_HIDE_BACK_BUTTON;
import static com.android.launcher3.LauncherState.ALL_APPS;
import static com.android.launcher3.LauncherState.NORMAL;
import static com.android.launcher3.LauncherState.OVERVIEW;
import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS;
import static com.android.launcher3.anim.Interpolators.DEACCEL_3;
import static com.android.launcher3.touch.AbstractStateChangeTouchController.SUCCESS_TRANSITION_PROGRESS;
import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.Interpolator;
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherState;
import com.android.launcher3.LauncherStateManager.AnimationConfig;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.allapps.AllAppsTransitionController;
import com.android.launcher3.anim.AnimationSuccessListener;
import com.android.launcher3.anim.AnimatorPlaybackController;
import com.android.launcher3.anim.AnimatorSetBuilder;
import com.android.launcher3.anim.Interpolators;
import com.android.launcher3.touch.AbstractStateChangeTouchController;
import com.android.launcher3.touch.SwipeDetector;
import com.android.launcher3.userevent.nano.LauncherLogProto;
import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Command;
import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
import com.android.launcher3.util.TouchController;
import com.android.quickstep.views.RecentsView;
/**
* Handles swiping up on the nav bar to go home from overview or all apps.
* Handles swiping up on the nav bar to go home from launcher, e.g. overview or all apps.
*/
public class NavBarToHomeTouchController extends AbstractStateChangeTouchController {
public class NavBarToHomeTouchController implements TouchController, SwipeDetector.Listener {
private static final Interpolator PULLBACK_INTERPOLATOR = DEACCEL_3;
private final Launcher mLauncher;
private final SwipeDetector mSwipeDetector;
private final float mPullbackDistance;
private boolean mNoIntercept;
private LauncherState mStartState;
private LauncherState mEndState = NORMAL;
private AnimatorPlaybackController mCurrentAnimation;
public NavBarToHomeTouchController(Launcher launcher) {
super(launcher, SwipeDetector.VERTICAL);
mLauncher = launcher;
mSwipeDetector = new SwipeDetector(mLauncher, this, SwipeDetector.VERTICAL);
mPullbackDistance = mLauncher.getResources().getDimension(R.dimen.home_pullback_distance);
}
@Override
protected boolean canInterceptTouch(MotionEvent ev) {
boolean cameFromNavBar = (ev.getEdgeFlags() & Utilities.EDGE_NAV_BAR) != 0;
return cameFromNavBar && (mLauncher.isInState(OVERVIEW) || mLauncher.isInState(ALL_APPS));
}
@Override
protected LauncherState getTargetState(LauncherState fromState, boolean isDragTowardPositive) {
return isDragTowardPositive ? NORMAL : fromState;
}
@Override
protected float initCurrentAnimation(int animComponents) {
long accuracy = (long) (getShiftRange() * 2);
final AnimatorSet anim;
if (mFromState == OVERVIEW) {
anim = new AnimatorSet();
RecentsView recentsView = mLauncher.getOverviewPanel();
float pullbackDistance = recentsView.getPaddingStart() / 2;
if (!recentsView.isRtl()) {
pullbackDistance = -pullbackDistance;
public final boolean onControllerInterceptTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
mStartState = mLauncher.getStateManager().getState();
mNoIntercept = !canInterceptTouch(ev);
if (mNoIntercept) {
return false;
}
anim.play(ObjectAnimator.ofFloat(recentsView, View.TRANSLATION_X, pullbackDistance));
anim.setInterpolator(PULLBACK_INTERPOLATOR);
} else { // if (mFromState == ALL_APPS)
mSwipeDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_POSITIVE, false);
}
if (mNoIntercept) {
return false;
}
onControllerTouchEvent(ev);
return mSwipeDetector.isDraggingOrSettling();
}
private boolean canInterceptTouch(MotionEvent ev) {
boolean cameFromNavBar = (ev.getEdgeFlags() & Utilities.EDGE_NAV_BAR) != 0;
if (!cameFromNavBar) {
return false;
}
if (mStartState == OVERVIEW || mStartState == ALL_APPS) {
return true;
}
if (!mLauncher.hasWindowFocus()) {
return true;
}
if (AbstractFloatingView.getTopOpenView(mLauncher) != null) {
return true;
}
return false;
}
@Override
public final boolean onControllerTouchEvent(MotionEvent ev) {
return mSwipeDetector.onTouchEvent(ev);
}
private float getShiftRange() {
return mLauncher.getDeviceProfile().heightPx;
}
@Override
public void onDragStart(boolean start) {
initCurrentAnimation();
}
private void initCurrentAnimation() {
long accuracy = (long) (getShiftRange() * 2);
final AnimatorSet anim = new AnimatorSet();
if (mStartState == OVERVIEW) {
RecentsView recentsView = mLauncher.getOverviewPanel();
float pullbackDist = mPullbackDistance;
if (!recentsView.isRtl()) {
pullbackDist = -pullbackDist;
}
Animator pullback = ObjectAnimator.ofFloat(recentsView, TRANSLATION_X, pullbackDist);
pullback.setInterpolator(PULLBACK_INTERPOLATOR);
anim.play(pullback);
} else if (mStartState == ALL_APPS) {
AnimatorSetBuilder builder = new AnimatorSetBuilder();
AllAppsTransitionController allAppsController = mLauncher.getAllAppsController();
final float pullbackDistance = mLauncher.getDeviceProfile().allAppsIconSizePx / 2;
Animator allAppsProgress = ObjectAnimator.ofFloat(allAppsController, ALL_APPS_PROGRESS,
-pullbackDistance / allAppsController.getShiftRange());
-mPullbackDistance / allAppsController.getShiftRange());
allAppsProgress.setInterpolator(PULLBACK_INTERPOLATOR);
builder.play(allAppsProgress);
// Slightly fade out all apps content to further distinguish from scrolling.
@ -90,52 +147,79 @@ public class NavBarToHomeTouchController extends AbstractStateChangeTouchControl
.mapToProgress(PULLBACK_INTERPOLATOR, 0, 0.5f));
AnimationConfig config = new AnimationConfig();
config.duration = accuracy;
allAppsController.setAlphas(mToState.getVisibleElements(mLauncher), config, builder);
anim = builder.build();
allAppsController.setAlphas(mEndState.getVisibleElements(mLauncher), config, builder);
anim.play(builder.build());
}
AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mLauncher);
if (topView != null) {
Animator hintCloseAnim = topView.createHintCloseAnim(mPullbackDistance);
if (hintCloseAnim != null) {
hintCloseAnim.setInterpolator(PULLBACK_INTERPOLATOR);
anim.play(hintCloseAnim);
}
}
anim.setDuration(accuracy);
mCurrentAnimation = AnimatorPlaybackController.wrap(anim, accuracy, this::clearState);
return -1 / getShiftRange();
}
private void clearState() {
mCurrentAnimation = null;
mSwipeDetector.finishedScrolling();
mSwipeDetector.setDetectableScrollConditions(0, false);
}
@Override
public void onDragStart(boolean start) {
super.onDragStart(start);
mStartContainerType = LauncherLogProto.ContainerType.NAVBAR;
public boolean onDrag(float displacement) {
// Only allow swipe up.
displacement = Math.min(0, displacement);
float progress = Utilities.getProgress(displacement, 0, getShiftRange());
mCurrentAnimation.setPlayFraction(progress);
return true;
}
@Override
public void onDragEnd(float velocity, boolean fling) {
final int logAction = fling ? Touch.FLING : Touch.SWIPE;
float interpolatedProgress = PULLBACK_INTERPOLATOR.getInterpolation(
mCurrentAnimation.getProgressFraction());
if (interpolatedProgress >= SUCCESS_TRANSITION_PROGRESS || velocity < 0 && fling) {
mLauncher.getStateManager().goToState(mToState, true,
() -> onSwipeInteractionCompleted(mToState, logAction));
float progress = mCurrentAnimation.getProgressFraction();
float interpolatedProgress = PULLBACK_INTERPOLATOR.getInterpolation(progress);
boolean success = interpolatedProgress >= SUCCESS_TRANSITION_PROGRESS
|| (velocity < 0 && fling);
if (success) {
mLauncher.getStateManager().goToState(mEndState, true,
() -> onSwipeInteractionCompleted(mEndState));
if (mStartState != mEndState) {
logStateChange(mStartState.containerType, logAction);
}
AbstractFloatingView topOpenView = AbstractFloatingView.getTopOpenView(mLauncher);
if (topOpenView != null) {
AbstractFloatingView.closeAllOpenViews(mLauncher);
logStateChange(topOpenView.getLogContainerType(), logAction);
}
} else {
// Quickly return to the state we came from (we didn't move far).
AnimatorPlaybackController anim = mLauncher.getStateManager()
.createAnimationToNewWorkspace(mFromState, 80);
anim.setEndAction(() -> onSwipeInteractionCompleted(mFromState, logAction));
anim.start();
ValueAnimator anim = mCurrentAnimation.getAnimationPlayer();
anim.setFloatValues(progress, 0);
anim.addListener(new AnimationSuccessListener() {
@Override
public void onAnimationSuccess(Animator animator) {
onSwipeInteractionCompleted(mStartState);
}
});
anim.setDuration(80).start();
}
mCurrentAnimation.dispatchOnCancel();
}
@Override
protected int getDirectionForLog() {
return LauncherLogProto.Action.Direction.UP;
private void onSwipeInteractionCompleted(LauncherState targetState) {
clearState();
mLauncher.getStateManager().goToState(targetState, false /* animated */);
}
@Override
protected boolean goingBetweenNormalAndOverview(LauncherState fromState,
LauncherState toState) {
// We don't want to create an atomic animation to/from overview.
return false;
}
@Override
protected int getLogContainerTypeForNormalState() {
return LauncherLogProto.ContainerType.NAVBAR;
private void logStateChange(int startContainerType, int logAction) {
mLauncher.getUserEventDispatcher().logStateChangeAction(logAction,
LauncherLogProto.Action.Direction.UP,
LauncherLogProto.ContainerType.NAVBAR,
startContainerType,
mEndState.containerType,
mLauncher.getWorkspace().getCurrentPage());
}
}

View File

@ -68,4 +68,7 @@
<!-- Assistant Gestures -->
<dimen name="gestures_assistant_size">28dp</dimen>
<dimen name="gestures_assistant_drag_threshold">70dp</dimen>
<!-- Distance to move elements when swiping up to go home from launcher -->
<dimen name="home_pullback_distance">28dp</dimen>
</resources>

View File

@ -19,9 +19,11 @@ package com.android.launcher3;
import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_FOCUSED;
import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED;
import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED;
import static com.android.launcher3.compat.AccessibilityManagerCompat.isAccessibilityEnabled;
import static com.android.launcher3.compat.AccessibilityManagerCompat.sendCustomAccessibilityEvent;
import android.animation.Animator;
import android.annotation.SuppressLint;
import android.content.Context;
import android.util.AttributeSet;
@ -30,7 +32,11 @@ import android.view.MotionEvent;
import android.view.View;
import android.widget.LinearLayout;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
import com.android.launcher3.util.TouchController;
import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.views.BaseDragLayer;
@ -38,8 +44,6 @@ import com.android.launcher3.views.BaseDragLayer;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import androidx.annotation.IntDef;
/**
* Base class for a View which shows a floating UI on top of the launcher UI.
*/
@ -124,8 +128,20 @@ public abstract class AbstractFloatingView extends LinearLayout implements Touch
protected abstract void handleClose(boolean animate);
/**
* Creates a user-controlled animation to hint that the view will be closed if completed.
* @param distanceToMove The max distance that elements should move from their starting point.
*/
public @Nullable Animator createHintCloseAnim(float distanceToMove) {
return null;
}
public abstract void logActionCommand(int command);
public int getLogContainerType() {
return ContainerType.DEFAULT_CONTAINERTYPE;
}
public final boolean isOpen() {
return mIsOpen;
}

View File

@ -1406,7 +1406,12 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo
@Override
public void logActionCommand(int command) {
mLauncher.getUserEventDispatcher().logActionCommand(
command, getFolderIcon(), ContainerType.FOLDER);
command, getFolderIcon(), getLogContainerType());
}
@Override
public int getLogContainerType() {
return ContainerType.FOLDER;
}
@Override

View File

@ -157,7 +157,12 @@ public class PopupContainerWithArrow extends ArrowPopup implements DragSource,
@Override
public void logActionCommand(int command) {
mLauncher.getUserEventDispatcher().logActionCommand(
command, mOriginalIcon, ContainerType.DEEPSHORTCUTS);
command, mOriginalIcon, getLogContainerType());
}
@Override
public int getLogContainerType() {
return ContainerType.DEEPSHORTCUTS;
}
public OnClickListener getItemClickListener() {

View File

@ -15,6 +15,8 @@
*/
package com.android.launcher3.views;
import static com.android.launcher3.compat.AccessibilityManagerCompat.sendCustomAccessibilityEvent;
import android.animation.PropertyValuesHolder;
import android.content.Context;
import android.graphics.Rect;
@ -28,8 +30,7 @@ import com.android.launcher3.Insettable;
import com.android.launcher3.Launcher;
import com.android.launcher3.R;
import com.android.launcher3.anim.Interpolators;
import static com.android.launcher3.compat.AccessibilityManagerCompat.sendCustomAccessibilityEvent;
import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
public class BottomUserEducationView extends AbstractSlideInView implements Insettable {
@ -70,6 +71,11 @@ public class BottomUserEducationView extends AbstractSlideInView implements Inse
// Since this is on-boarding popup, it is not a user controlled action.
}
@Override
public int getLogContainerType() {
return ContainerType.TIP;
}
@Override
protected boolean isOfType(int type) {
return (type & TYPE_ON_BOARD_POPUP) != 0;

View File

@ -162,11 +162,16 @@ abstract class BaseWidgetSheet extends AbstractSlideInView
@Override
public final void logActionCommand(int command) {
Target target = newContainerTarget(ContainerType.WIDGETS);
Target target = newContainerTarget(getLogContainerType());
target.cardinality = getElementsRowCount();
mLauncher.getUserEventDispatcher().logActionCommand(command, target);
}
@Override
public int getLogContainerType() {
return ContainerType.WIDGETS;
}
protected abstract int getElementsRowCount();
protected SystemUiController getSystemUiController() {

View File

@ -16,10 +16,13 @@
package com.android.launcher3.widget;
import android.animation.Animator;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.IntProperty;
import android.util.Pair;
import android.view.Gravity;
import android.view.LayoutInflater;
@ -27,6 +30,8 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.Nullable;
import com.android.launcher3.Insettable;
import com.android.launcher3.ItemInfo;
import com.android.launcher3.LauncherAppState;
@ -43,6 +48,20 @@ import java.util.List;
*/
public class WidgetsBottomSheet extends BaseWidgetSheet implements Insettable {
private static final IntProperty<View> PADDING_BOTTOM =
new IntProperty<View>("paddingBottom") {
@Override
public void setValue(View view, int paddingBottom) {
view.setPadding(view.getPaddingLeft(), view.getPaddingTop(),
view.getPaddingRight(), paddingBottom);
}
@Override
public Integer get(View view) {
return view.getPaddingBottom();
}
};
private static final int DEFAULT_CLOSE_DURATION = 200;
private ItemInfo mOriginalItemInfo;
private Rect mInsets;
@ -158,8 +177,7 @@ public class WidgetsBottomSheet extends BaseWidgetSheet implements Insettable {
int rightInset = insets.right - mInsets.right;
int bottomInset = insets.bottom - mInsets.bottom;
mInsets.set(insets);
setPadding(getPaddingLeft() + leftInset, getPaddingTop(),
getPaddingRight() + rightInset, getPaddingBottom() + bottomInset);
setPadding(leftInset, getPaddingTop(), rightInset, bottomInset);
}
@Override
@ -172,4 +190,10 @@ public class WidgetsBottomSheet extends BaseWidgetSheet implements Insettable {
return Pair.create(findViewById(R.id.title), getContext().getString(
mIsOpen ? R.string.widgets_list : R.string.widgets_list_closed));
}
@Nullable
@Override
public Animator createHintCloseAnim(float distanceToMove) {
return ObjectAnimator.ofInt(this, PADDING_BOTTOM, (int) (distanceToMove + mInsets.bottom));
}
}

View File

@ -17,6 +17,8 @@ package com.android.launcher3.widget;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.content.Context;
import android.graphics.Rect;
@ -27,6 +29,9 @@ import android.view.MotionEvent;
import android.view.View;
import android.view.animation.AnimationUtils;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.android.launcher3.Insettable;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherAppState;
@ -35,8 +40,6 @@ import com.android.launcher3.R;
import com.android.launcher3.views.RecyclerViewFastScroller;
import com.android.launcher3.views.TopRoundedCornerView;
import androidx.annotation.VisibleForTesting;
/**
* Popup for showing the full list of available widgets
*/
@ -235,4 +238,13 @@ public class WidgetsFullSheet extends BaseWidgetSheet
protected int getElementsRowCount() {
return mAdapter.getItemCount();
}
@Nullable
@Override
public Animator createHintCloseAnim(float distanceToMove) {
AnimatorSet anim = new AnimatorSet();
anim.play(ObjectAnimator.ofFloat(mRecyclerView, TRANSLATION_Y, -distanceToMove));
anim.play(ObjectAnimator.ofFloat(mRecyclerView, ALPHA, 0.5f));
return anim;
}
}