blob: 4c3a9adcb4b3e21a89901681971e6a9b1afe13d4 [file] [log] [blame]
/*
* Copyright (C) 2017 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;
import android.animation.Animator;
import android.animation.ObjectAnimator;
import android.animation.RectEvaluator;
import android.annotation.TargetApi;
import android.app.ActivityManager.RunningTaskInfo;
import android.app.ActivityOptions;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Rect;
import android.os.Build;
import android.os.Handler;
import android.os.UserHandle;
import android.support.annotation.UiThread;
import android.view.View;
import android.view.ViewTreeObserver.OnPreDrawListener;
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.Hotseat;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherState;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimationSuccessListener;
import com.android.launcher3.anim.Interpolators;
import com.android.launcher3.dragndrop.DragLayer;
import com.android.launcher3.states.InternalStateHandler;
import com.android.launcher3.util.TraceHelper;
import com.android.systemui.shared.recents.model.RecentsTaskLoadPlan;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.recents.model.Task.TaskKey;
import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.WindowManagerWrapper;
@TargetApi(Build.VERSION_CODES.O)
public class NavBarSwipeInteractionHandler extends InternalStateHandler {
private static final int STATE_LAUNCHER_READY = 1 << 0;
private static final int STATE_RECENTS_DELAY_COMPLETE = 1 << 1;
private static final int STATE_LOAD_PLAN_READY = 1 << 2;
private static final int STATE_RECENTS_FULLY_VISIBLE = 1 << 3;
private static final int STATE_ACTIVITY_MULTIPLIER_COMPLETE = 1 << 4;
private static final int STATE_SCALED_SNAPSHOT_RECENTS = 1 << 5;
private static final int STATE_SCALED_SNAPSHOT_APP = 1 << 6;
private static final long RECENTS_VIEW_VISIBILITY_DELAY = 120;
private static final long RECENTS_VIEW_VISIBILITY_DURATION = 150;
private static final long MAX_SWIPE_DURATION = 200;
private static final long MIN_SWIPE_DURATION = 80;
// Ideal velocity for a smooth transition
private static final float PIXEL_PER_MS = 2f;
private static final float MIN_PROGRESS_FOR_OVERVIEW = 0.5f;
private final Rect mStableInsets = new Rect();
private final Rect mSourceRect = new Rect();
private final Rect mTargetRect = new Rect();
private final Rect mCurrentRect = new Rect();
private final RectEvaluator mRectEvaluator = new RectEvaluator(mCurrentRect);
// Shift in the range of [0, 1].
// 0 => preview snapShot is completely visible, and hotseat is completely translated down
// 1 => preview snapShot is completely aligned with the recents view and hotseat is completely
// visible.
private final AnimatedFloat mCurrentShift = new AnimatedFloat(this::updateFinalShift);
// Activity multiplier in the range of [0, 1]. When the activity becomes visible, this is
// animated to 1, so allow for a smooth transition.
private final AnimatedFloat mActivityMultiplier = new AnimatedFloat(this::updateFinalShift);
private final int mRunningTaskId;
private final Context mContext;
private final MultiStateCallback mStateCallback;
private Launcher mLauncher;
private SnapshotDragView mDragView;
private RecentsView mRecentsView;
private Hotseat mHotseat;
private RecentsTaskLoadPlan mLoadPlan;
private boolean mLauncherReady;
private boolean mTouchEndHandled;
private float mCurrentDisplacement;
private Bitmap mTaskSnapshot;
NavBarSwipeInteractionHandler(RunningTaskInfo runningTaskInfo, Context context) {
mRunningTaskId = runningTaskInfo.id;
mContext = context;
WindowManagerWrapper.getInstance().getStableInsets(mStableInsets);
// Build the state callback
mStateCallback = new MultiStateCallback();
mStateCallback.addCallback(STATE_LAUNCHER_READY, this::onLauncherReady);
mStateCallback.addCallback(STATE_LOAD_PLAN_READY | STATE_RECENTS_DELAY_COMPLETE,
this::setTaskPlanToUi);
mStateCallback.addCallback(STATE_SCALED_SNAPSHOT_APP, this::resumeLastTask);
mStateCallback.addCallback(STATE_RECENTS_FULLY_VISIBLE | STATE_SCALED_SNAPSHOT_RECENTS
| STATE_ACTIVITY_MULTIPLIER_COMPLETE,
this::onAnimationToLauncherComplete);
mStateCallback.addCallback(STATE_LAUNCHER_READY | STATE_SCALED_SNAPSHOT_APP,
this::cleanupLauncher);
}
private void onLauncherReady() {
mLauncherReady = true;
executeFrameUpdate();
// Wait for some time before loading recents so that the first frame is fast
new Handler().postDelayed(() -> mStateCallback.setState(STATE_RECENTS_DELAY_COMPLETE),
RECENTS_VIEW_VISIBILITY_DELAY);
long duration = Math.min(MAX_SWIPE_DURATION,
Math.max((long) (-mCurrentDisplacement / PIXEL_PER_MS), MIN_SWIPE_DURATION));
if (mCurrentShift.getCurrentAnimation() != null) {
ObjectAnimator anim = mCurrentShift.getCurrentAnimation();
long theirDuration = anim.getDuration() - anim.getCurrentPlayTime();
// TODO: Find a better heuristic
duration = (duration + theirDuration) / 2;
}
ObjectAnimator anim = mActivityMultiplier.animateToValue(1)
.setDuration(duration);
anim.addListener(new AnimationSuccessListener() {
@Override
public void onAnimationSuccess(Animator animator) {
mStateCallback.setState(STATE_ACTIVITY_MULTIPLIER_COMPLETE);
}
});
anim.start();
}
public void setTaskSnapshot(Bitmap taskSnapshot) {
mTaskSnapshot = taskSnapshot;
}
@Override
public void onLauncherResume() {
TraceHelper.partitionSection("TouchInt", "Launcher On resume");
mDragView.getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() {
@Override
public boolean onPreDraw() {
mDragView.getViewTreeObserver().removeOnPreDrawListener(this);
mStateCallback.setState(STATE_LAUNCHER_READY);
TraceHelper.partitionSection("TouchInt", "Launcher drawn");
return true;
}
});
}
@Override
protected void init(Launcher launcher, boolean alreadyOnHome) {
AbstractFloatingView.closeAllOpenViews(launcher, alreadyOnHome);
launcher.getStateManager().goToState(LauncherState.OVERVIEW, alreadyOnHome);
mLauncher = launcher;
mDragView = new SnapshotDragView(mLauncher, mTaskSnapshot);
mLauncher.getDragLayer().addView(mDragView);
mDragView.setPivotX(0);
mDragView.setPivotY(0);
mRecentsView = mLauncher.getOverviewPanel();
mHotseat = mLauncher.getHotseat();
// Optimization
mLauncher.getAppsView().setVisibility(View.GONE);
mRecentsView.setVisibility(View.GONE);
TraceHelper.partitionSection("TouchInt", "Launcher on new intent");
}
@UiThread
public void updateDisplacement(float displacement) {
mCurrentDisplacement = displacement;
executeFrameUpdate();
}
private void executeFrameUpdate() {
if (mLauncherReady) {
final float displacement = -mCurrentDisplacement;
int hotseatHeight = mHotseat.getHeight();
float translation = Utilities.boundToRange(displacement, 0, hotseatHeight);
float shift = hotseatHeight == 0 ? 0 : translation / hotseatHeight;
mCurrentShift.updateValue(shift);
}
}
@UiThread
private void updateFinalShift() {
if (!mLauncherReady) {
return;
}
if (mTargetRect.isEmpty()) {
DragLayer dl = mLauncher.getDragLayer();
mSourceRect.set(0, 0, dl.getWidth(), dl.getHeight());
Rect targetPadding = RecentsView.getPadding(mLauncher);
Rect insets = dl.getInsets();
mTargetRect.set(
targetPadding.left + insets.left,
targetPadding.top + insets.top,
mSourceRect.right - targetPadding.right - insets.right,
mSourceRect.bottom - targetPadding.bottom - insets.bottom);
mTargetRect.top += mLauncher.getResources()
.getDimensionPixelSize(R.dimen.task_thumbnail_top_margin);
}
float shift = mCurrentShift.value * mActivityMultiplier.value;
int hotseatHeight = mHotseat.getHeight();
mHotseat.setTranslationY((1 - shift) * hotseatHeight);
mRectEvaluator.evaluate(shift, mSourceRect, mTargetRect);
float scale = (float) mCurrentRect.width() / mSourceRect.width();
mDragView.setTranslationX(mCurrentRect.left - mStableInsets.left * scale * shift);
mDragView.setTranslationY(mCurrentRect.top - mStableInsets.top * scale * shift);
mDragView.setScaleX(scale);
mDragView.setScaleY(scale);
mDragView.getViewBounds().setClipTop((int) (mStableInsets.top * shift));
mDragView.getViewBounds().setClipBottom((int) (mStableInsets.bottom * shift));
}
@UiThread
public void setRecentsTaskLoadPlan(RecentsTaskLoadPlan loadPlan) {
mLoadPlan = loadPlan;
mStateCallback.setState(STATE_LOAD_PLAN_READY);
}
private void setTaskPlanToUi() {
mRecentsView.update(mLoadPlan);
mRecentsView.setVisibility(View.VISIBLE);
// Animate alpha
mRecentsView.setAlpha(0);
mRecentsView.animate().alpha(1).setDuration(RECENTS_VIEW_VISIBILITY_DURATION)
.withEndAction(() -> mStateCallback.setState(STATE_RECENTS_FULLY_VISIBLE));
}
@UiThread
public void endTouch(float endVelocity) {
if (mTouchEndHandled) {
return;
}
mTouchEndHandled = true;
Resources res = mContext.getResources();
float flingThreshold = res.getDimension(R.dimen.quickstep_fling_threshold_velocity);
boolean isFling = Math.abs(endVelocity) > flingThreshold;
long duration = MAX_SWIPE_DURATION;
final float endShift;
if (!isFling) {
endShift = mCurrentShift.value >= MIN_PROGRESS_FOR_OVERVIEW ? 1 : 0;
} else {
endShift = endVelocity < 0 ? 1 : 0;
float minFlingVelocity = res.getDimension(R.dimen.quickstep_fling_min_velocity);
if (Math.abs(endVelocity) > minFlingVelocity && mLauncherReady) {
float distanceToTravel = (endShift - mCurrentShift.value) * mHotseat.getHeight();
// we want the page's snap velocity to approximately match the velocity at
// which the user flings, so we scale the duration by a value near to the
// derivative of the scroll interpolator at zero, ie. 5.
duration = 5 * Math.round(1000 * Math.abs(distanceToTravel / endVelocity));
}
}
ObjectAnimator anim = mCurrentShift.animateToValue(endShift).setDuration(duration);
anim.setInterpolator(Interpolators.SCROLL);
anim.addListener(new AnimationSuccessListener() {
@Override
public void onAnimationSuccess(Animator animator) {
mStateCallback.setState((Float.compare(mCurrentShift.value, 0) == 0)
? STATE_SCALED_SNAPSHOT_APP : STATE_SCALED_SNAPSHOT_RECENTS);
}
});
anim.start();
}
@UiThread
private void resumeLastTask() {
TaskKey key = null;
if (mLoadPlan != null) {
Task task = mLoadPlan.getTaskStack().findTaskWithId(mRunningTaskId);
if (task != null) {
key = task.key;
}
}
if (key == null) {
// TODO: We need a better way for this
key = new TaskKey(mRunningTaskId, 0, null, UserHandle.myUserId(), 0);
}
ActivityOptions opts = ActivityOptions.makeCustomAnimation(mContext, 0, 0);
ActivityManagerWrapper.getInstance().startActivityFromRecentsAsync(key, opts, null, null);
}
private void cleanupLauncher() {
// TODO: These should be done as part of ActivityOptions#OnAnimationStarted
mHotseat.setTranslationY(0);
mLauncher.setOnResumeCallback(() -> mDragView.close(false));
}
private void onAnimationToLauncherComplete() {
mDragView.close(false);
}
}