blob: 222f6e6728f933e36ac8b4195725fe5f6cdf34de [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.views;
import static android.view.Gravity.BOTTOM;
import static android.view.Gravity.CENTER_HORIZONTAL;
import static android.view.Gravity.CENTER_VERTICAL;
import static android.view.Gravity.END;
import static android.view.Gravity.START;
import static android.view.Gravity.TOP;
import static android.widget.Toast.LENGTH_SHORT;
import static com.android.launcher3.QuickstepAppTransitionManagerImpl.RECENTS_LAUNCH_DURATION;
import static com.android.launcher3.Utilities.comp;
import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
import static com.android.launcher3.anim.Interpolators.LINEAR;
import static com.android.launcher3.anim.Interpolators.TOUCH_RESPONSE_INTERPOLATOR;
import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent
.LAUNCHER_TASK_ICON_TAP_OR_LONGPRESS;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_LAUNCH_TAP;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.animation.TimeInterpolator;
import android.animation.ValueAnimator;
import android.app.ActivityOptions;
import android.content.Context;
import android.content.Intent;
import android.graphics.Outline;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.InsetDrawable;
import android.os.Bundle;
import android.os.Handler;
import android.util.AttributeSet;
import android.util.FloatProperty;
import android.util.Log;
import android.view.Surface;
import android.view.View;
import android.view.ViewOutlineProvider;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.FrameLayout;
import android.widget.Toast;
import com.android.launcher3.BaseDraggingActivity;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimatorPlaybackController;
import com.android.launcher3.anim.Interpolators;
import com.android.launcher3.anim.PendingAnimation;
import com.android.launcher3.logging.UserEventDispatcher;
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.popup.SystemShortcut;
import com.android.launcher3.testing.TestLogging;
import com.android.launcher3.testing.TestProtocol;
import com.android.launcher3.touch.PagedOrientationHandler;
import com.android.launcher3.userevent.nano.LauncherLogProto;
import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.ViewPool.Reusable;
import com.android.quickstep.RecentsModel;
import com.android.quickstep.TaskIconCache;
import com.android.quickstep.TaskOverlayFactory;
import com.android.quickstep.TaskThumbnailCache;
import com.android.quickstep.TaskUtils;
import com.android.quickstep.util.RecentsOrientedState;
import com.android.quickstep.util.TaskCornerRadius;
import com.android.quickstep.views.RecentsView.PageCallbacks;
import com.android.quickstep.views.RecentsView.ScrollState;
import com.android.quickstep.views.TaskThumbnailView.PreviewPositionHelper;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.ActivityOptionsCompat;
import com.android.systemui.shared.system.QuickStepContract;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
/**
* A task in the Recents view.
*/
public class TaskView extends FrameLayout implements PageCallbacks, Reusable {
private static final String TAG = TaskView.class.getSimpleName();
/** A curve of x from 0 to 1, where 0 is the center of the screen and 1 is the edge. */
private static final TimeInterpolator CURVE_INTERPOLATOR
= x -> (float) -Math.cos(x * Math.PI) / 2f + .5f;
/**
* The alpha of a black scrim on a page in the carousel as it leaves the screen.
* In the resting position of the carousel, the adjacent pages have about half this scrim.
*/
public static final float MAX_PAGE_SCRIM_ALPHA = 0.4f;
/**
* How much to scale down pages near the edge of the screen.
*/
public static final float EDGE_SCALE_DOWN_FACTOR = 0.03f;
public static final long SCALE_ICON_DURATION = 120;
private static final long DIM_ANIM_DURATION = 700;
private static final List<Rect> SYSTEM_GESTURE_EXCLUSION_RECT =
Collections.singletonList(new Rect());
private static final FloatProperty<TaskView> FOCUS_TRANSITION =
new FloatProperty<TaskView>("focusTransition") {
@Override
public void setValue(TaskView taskView, float v) {
taskView.setIconAndDimTransitionProgress(v, false /* invert */);
}
@Override
public Float get(TaskView taskView) {
return taskView.mFocusTransitionProgress;
}
};
private final OnAttachStateChangeListener mTaskMenuStateListener =
new OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View view) {
}
@Override
public void onViewDetachedFromWindow(View view) {
if (mMenuView != null) {
mMenuView.removeOnAttachStateChangeListener(this);
mMenuView = null;
}
}
};
private final TaskOutlineProvider mOutlineProvider;
private Task mTask;
private TaskThumbnailView mSnapshotView;
private TaskMenuView mMenuView;
private IconView mIconView;
private final DigitalWellBeingToast mDigitalWellBeingToast;
private float mCurveScale;
private float mFullscreenProgress;
private final FullscreenDrawParams mCurrentFullscreenParams;
private final BaseDraggingActivity mActivity;
private ObjectAnimator mIconAndDimAnimator;
private float mIconScaleAnimStartProgress = 0;
private float mFocusTransitionProgress = 1;
private float mModalness = 0;
private float mStableAlpha = 1;
private boolean mShowScreenshot;
// The current background requests to load the task thumbnail and icon
private TaskThumbnailCache.ThumbnailLoadRequest mThumbnailLoadRequest;
private TaskIconCache.IconLoadRequest mIconLoadRequest;
// Order in which the footers appear. Lower order appear below higher order.
public static final int INDEX_DIGITAL_WELLBEING_TOAST = 0;
private final FooterWrapper[] mFooters = new FooterWrapper[2];
private float mFooterVerticalOffset = 0;
private float mFooterAlpha = 1;
private int mStackHeight;
private View mContextualChipWrapper;
private View mContextualChip;
public TaskView(Context context) {
this(context, null);
}
public TaskView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public TaskView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mActivity = BaseDraggingActivity.fromContext(context);
setOnClickListener((view) -> {
if (getTask() == null) {
return;
}
if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
if (isRunningTask()) {
createLaunchAnimationForRunningTask().start();
} else {
launchTask(true /* animate */);
}
} else {
launchTask(true /* animate */);
}
mActivity.getUserEventDispatcher().logTaskLaunchOrDismiss(
Touch.TAP, Direction.NONE, getRecentsView().indexOfChild(this),
TaskUtils.getLaunchComponentKeyForTask(getTask().key));
mActivity.getStatsLogManager().logger().withItemInfo(getItemInfo())
.log(LAUNCHER_TASK_LAUNCH_TAP);
});
mCurrentFullscreenParams = new FullscreenDrawParams(context);
mDigitalWellBeingToast = new DigitalWellBeingToast(mActivity, this);
mOutlineProvider = new TaskOutlineProvider(getContext(), mCurrentFullscreenParams);
setOutlineProvider(mOutlineProvider);
}
/**
* Builds proto for logging
*/
public WorkspaceItemInfo getItemInfo() {
ComponentKey componentKey = TaskUtils.getLaunchComponentKeyForTask(getTask().key);
WorkspaceItemInfo dummyInfo = new WorkspaceItemInfo();
dummyInfo.itemType = LauncherSettings.Favorites.ITEM_TYPE_TASK;
dummyInfo.container = LauncherSettings.Favorites.CONTAINER_TASKSWITCHER;
dummyInfo.user = componentKey.user;
dummyInfo.intent = new Intent().setComponent(componentKey.componentName);
dummyInfo.title = TaskUtils.getTitle(getContext(), getTask());
dummyInfo.screenId = getRecentsView().indexOfChild(this);
return dummyInfo;
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mSnapshotView = findViewById(R.id.snapshot);
mIconView = findViewById(R.id.icon);
}
/**
* The modalness of this view is how it should be displayed when it is shown on its own in the
* modal state of overview.
*
* @param modalness [0, 1] 0 being in context with other tasks, 1 being shown on its own.
*/
public void setModalness(float modalness) {
mModalness = modalness;
mIconView.setAlpha(comp(modalness));
if (mContextualChip != null) {
mContextualChip.setScaleX(comp(modalness));
mContextualChip.setScaleY(comp(modalness));
}
if (mContextualChipWrapper != null) {
mContextualChipWrapper.setAlpha(comp(modalness));
}
updateFooterVerticalOffset(mFooterVerticalOffset);
}
public TaskMenuView getMenuView() {
return mMenuView;
}
public DigitalWellBeingToast getDigitalWellBeingToast() {
return mDigitalWellBeingToast;
}
/**
* Updates this task view to the given {@param task}.
*
* TODO(b/142282126) Re-evaluate if we need to pass in isMultiWindowMode after
* that issue is fixed
*/
public void bind(Task task, RecentsOrientedState orientedState) {
cancelPendingLoadTasks();
mTask = task;
mSnapshotView.bind(task);
setOrientationState(orientedState);
}
public Task getTask() {
return mTask;
}
public TaskThumbnailView getThumbnail() {
return mSnapshotView;
}
public IconView getIconView() {
return mIconView;
}
public AnimatorPlaybackController createLaunchAnimationForRunningTask() {
final PendingAnimation pendingAnimation = getRecentsView().createTaskLaunchAnimation(
this, RECENTS_LAUNCH_DURATION, TOUCH_RESPONSE_INTERPOLATOR);
AnimatorPlaybackController currentAnimation = pendingAnimation.createPlaybackController();
currentAnimation.setEndAction(() -> {
pendingAnimation.finish(true, Touch.SWIPE);
launchTask(false);
});
return currentAnimation;
}
public void launchTask(boolean animate) {
launchTask(animate, false /* freezeTaskList */);
}
public void launchTask(boolean animate, boolean freezeTaskList) {
launchTask(animate, freezeTaskList, (result) -> {
if (!result) {
notifyTaskLaunchFailed(TAG);
}
}, getHandler());
}
public void launchTask(boolean animate, Consumer<Boolean> resultCallback,
Handler resultCallbackHandler) {
launchTask(animate, false /* freezeTaskList */, resultCallback, resultCallbackHandler);
}
public void launchTask(boolean animate, boolean freezeTaskList, Consumer<Boolean> resultCallback,
Handler resultCallbackHandler) {
if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
RecentsView recentsView = getRecentsView();
if (isRunningTask()) {
recentsView.finishRecentsAnimation(false /* toRecents */,
() -> resultCallbackHandler.post(() -> resultCallback.accept(true)));
} else {
// This is a workaround against the WM issue that app open is not correctly animated
// when recents animation is being cleaned up (b/143774568). When that's possible,
// we should rely on the framework side to cancel the recents animation, and we will
// clean up the screenshot on the launcher side while we launch the next task.
recentsView.switchToScreenshot(null,
() -> recentsView.finishRecentsAnimation(true /* toRecents */,
() -> launchTaskInternal(animate, freezeTaskList, resultCallback,
resultCallbackHandler)));
}
} else {
launchTaskInternal(animate, freezeTaskList, resultCallback, resultCallbackHandler);
}
}
private void launchTaskInternal(boolean animate, boolean freezeTaskList,
Consumer<Boolean> resultCallback, Handler resultCallbackHandler) {
if (mTask != null) {
final ActivityOptions opts;
TestLogging.recordEvent(
TestProtocol.SEQUENCE_MAIN, "startActivityFromRecentsAsync", mTask);
if (animate) {
opts = mActivity.getActivityLaunchOptions(this);
if (freezeTaskList) {
ActivityOptionsCompat.setFreezeRecentTasksList(opts);
}
ActivityManagerWrapper.getInstance().startActivityFromRecentsAsync(mTask.key,
opts, resultCallback, resultCallbackHandler);
} else {
opts = ActivityOptionsCompat.makeCustomAnimation(getContext(), 0, 0, () -> {
if (resultCallback != null) {
// Only post the animation start after the system has indicated that the
// transition has started
resultCallbackHandler.post(() -> resultCallback.accept(true));
}
}, resultCallbackHandler);
if (freezeTaskList) {
ActivityOptionsCompat.setFreezeRecentTasksList(opts);
}
ActivityManagerWrapper.getInstance().startActivityFromRecentsAsync(mTask.key,
opts, (success) -> {
if (resultCallback != null && !success) {
// If the call to start activity failed, then post the result
// immediately, otherwise, wait for the animation start callback
// from the activity options above
resultCallbackHandler.post(() -> resultCallback.accept(false));
}
}, resultCallbackHandler);
}
getRecentsView().onTaskLaunched(mTask);
}
}
public void onTaskListVisibilityChanged(boolean visible) {
if (mTask == null) {
return;
}
cancelPendingLoadTasks();
if (visible) {
// These calls are no-ops if the data is already loaded, try and load the high
// resolution thumbnail if the state permits
RecentsModel model = RecentsModel.INSTANCE.get(getContext());
TaskThumbnailCache thumbnailCache = model.getThumbnailCache();
TaskIconCache iconCache = model.getIconCache();
mThumbnailLoadRequest = thumbnailCache.updateThumbnailInBackground(
mTask, thumbnail -> mSnapshotView.setThumbnail(mTask, thumbnail));
mIconLoadRequest = iconCache.updateIconInBackground(mTask,
(task) -> {
setIcon(task.icon);
if (ENABLE_QUICKSTEP_LIVE_TILE.get() && isRunningTask()) {
getRecentsView().updateLiveTileIcon(task.icon);
}
mDigitalWellBeingToast.initialize(mTask);
});
} else {
mSnapshotView.setThumbnail(null, null);
setIcon(null);
// Reset the task thumbnail reference as well (it will be fetched from the cache or
// reloaded next time we need it)
mTask.thumbnail = null;
}
}
private void cancelPendingLoadTasks() {
if (mThumbnailLoadRequest != null) {
mThumbnailLoadRequest.cancel();
mThumbnailLoadRequest = null;
}
if (mIconLoadRequest != null) {
mIconLoadRequest.cancel();
mIconLoadRequest = null;
}
}
private boolean showTaskMenu(int action) {
if (!getRecentsView().isClearAllHidden()) {
getRecentsView().snapToPage(getRecentsView().indexOfChild(this));
} else {
mMenuView = TaskMenuView.showForTask(this);
mActivity.getStatsLogManager().logger().withItemInfo(getItemInfo())
.log(LAUNCHER_TASK_ICON_TAP_OR_LONGPRESS);
UserEventDispatcher.newInstance(getContext()).logActionOnItem(action, Direction.NONE,
LauncherLogProto.ItemType.TASK_ICON);
if (mMenuView != null) {
mMenuView.addOnAttachStateChangeListener(mTaskMenuStateListener);
}
}
return mMenuView != null;
}
private void setIcon(Drawable icon) {
if (icon != null) {
mIconView.setDrawable(icon);
mIconView.setOnClickListener(v -> showTaskMenu(Touch.TAP));
mIconView.setOnLongClickListener(v -> {
requestDisallowInterceptTouchEvent(true);
return showTaskMenu(Touch.LONGPRESS);
});
} else {
mIconView.setDrawable(null);
mIconView.setOnClickListener(null);
mIconView.setOnLongClickListener(null);
}
}
public void setOrientationState(RecentsOrientedState orientationState) {
PagedOrientationHandler orientationHandler = orientationState.getOrientationHandler();
boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
LayoutParams snapshotParams = (LayoutParams) mSnapshotView.getLayoutParams();
int thumbnailPadding = (int) getResources().getDimension(R.dimen.task_thumbnail_top_margin);
LayoutParams iconParams = (LayoutParams) mIconView.getLayoutParams();
switch (orientationHandler.getRotation()) {
case Surface.ROTATION_90:
iconParams.gravity = (isRtl ? START : END) | CENTER_VERTICAL;
iconParams.rightMargin = -thumbnailPadding;
iconParams.leftMargin = 0;
iconParams.topMargin = snapshotParams.topMargin / 2;
break;
case Surface.ROTATION_180:
iconParams.gravity = BOTTOM | CENTER_HORIZONTAL;
iconParams.bottomMargin = -thumbnailPadding;
iconParams.leftMargin = iconParams.topMargin = iconParams.rightMargin = 0;
break;
case Surface.ROTATION_270:
iconParams.gravity = (isRtl ? END : START) | CENTER_VERTICAL;
iconParams.leftMargin = -thumbnailPadding;
iconParams.rightMargin = 0;
iconParams.topMargin = snapshotParams.topMargin / 2;
break;
case Surface.ROTATION_0:
default:
iconParams.gravity = TOP | CENTER_HORIZONTAL;
iconParams.leftMargin = iconParams.topMargin = iconParams.rightMargin = 0;
break;
}
mIconView.setLayoutParams(iconParams);
mIconView.setRotation(orientationHandler.getDegreesRotated());
if (mMenuView != null) {
mMenuView.onRotationChanged();
}
}
private void setIconAndDimTransitionProgress(float progress, boolean invert) {
if (invert) {
progress = 1 - progress;
}
mFocusTransitionProgress = progress;
mSnapshotView.setDimAlphaMultipler(progress);
float iconScalePercentage = (float) SCALE_ICON_DURATION / DIM_ANIM_DURATION;
float lowerClamp = invert ? 1f - iconScalePercentage : 0;
float upperClamp = invert ? 1 : iconScalePercentage;
float scale = Interpolators.clampToProgress(FAST_OUT_SLOW_IN, lowerClamp, upperClamp)
.getInterpolation(progress);
mIconView.setScaleX(scale);
mIconView.setScaleY(scale);
updateFooterVerticalOffset(1.0f - scale);
}
public void setIconScaleAnimStartProgress(float startProgress) {
mIconScaleAnimStartProgress = startProgress;
}
public void animateIconScaleAndDimIntoView() {
if (mIconAndDimAnimator != null) {
mIconAndDimAnimator.cancel();
}
mIconAndDimAnimator = ObjectAnimator.ofFloat(this, FOCUS_TRANSITION, 1);
mIconAndDimAnimator.setCurrentFraction(mIconScaleAnimStartProgress);
mIconAndDimAnimator.setDuration(DIM_ANIM_DURATION).setInterpolator(LINEAR);
mIconAndDimAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mIconAndDimAnimator = null;
}
});
mIconAndDimAnimator.start();
}
protected void setIconScaleAndDim(float iconScale) {
setIconScaleAndDim(iconScale, false);
}
private void setIconScaleAndDim(float iconScale, boolean invert) {
if (mIconAndDimAnimator != null) {
mIconAndDimAnimator.cancel();
}
setIconAndDimTransitionProgress(iconScale, invert);
}
protected void resetViewTransforms() {
setCurveScale(1);
setTranslationX(0f);
setTranslationY(0f);
setTranslationZ(0);
setAlpha(mStableAlpha);
setIconScaleAndDim(1);
}
public void setStableAlpha(float parentAlpha) {
mStableAlpha = parentAlpha;
setAlpha(mStableAlpha);
}
@Override
public void onRecycle() {
resetViewTransforms();
// Clear any references to the thumbnail (it will be re-read either from the cache or the
// system on next bind)
mSnapshotView.setThumbnail(mTask, null);
setOverlayEnabled(false);
onTaskListVisibilityChanged(false);
}
@Override
public void onPageScroll(ScrollState scrollState) {
// Don't do anything if it's modal.
if (mModalness > 0) {
return;
}
float curveInterpolation =
CURVE_INTERPOLATOR.getInterpolation(scrollState.linearInterpolation);
float curveScaleForCurveInterpolation = getCurveScaleForCurveInterpolation(
curveInterpolation);
mSnapshotView.setDimAlpha(curveInterpolation * MAX_PAGE_SCRIM_ALPHA);
setCurveScale(curveScaleForCurveInterpolation);
mFooterAlpha = Utilities.boundToRange(1.0f - 2 * scrollState.linearInterpolation, 0f, 1f);
for (FooterWrapper footer : mFooters) {
if (footer != null) {
footer.mView.setAlpha(mFooterAlpha);
}
}
if (mMenuView != null) {
PagedOrientationHandler pagedOrientationHandler = getPagedOrientationHandler();
RecentsView recentsView = getRecentsView();
mMenuView.setPosition(getX() - recentsView.getScrollX(),
getY() - recentsView.getScrollY(), pagedOrientationHandler);
mMenuView.setScaleX(getScaleX());
mMenuView.setScaleY(getScaleY());
}
}
/**
* Sets the footer at the specific index and returns the previously set footer.
*/
public View setFooter(int index, View view) {
View oldFooter = null;
// If the footer are is already collapsed, do not animate entry
boolean shouldAnimateEntry = mFooterVerticalOffset <= 0;
if (mFooters[index] != null) {
oldFooter = mFooters[index].mView;
mFooters[index].release();
removeView(oldFooter);
// If we are replacing an existing footer, do not animate entry
shouldAnimateEntry = false;
}
if (view != null) {
int indexToAdd = getChildCount();
for (int i = index - 1; i >= 0; i--) {
if (mFooters[i] != null) {
indexToAdd = indexOfChild(mFooters[i].mView);
break;
}
}
addView(view, indexToAdd);
LayoutParams layoutParams = (LayoutParams) view.getLayoutParams();
layoutParams.gravity = BOTTOM | CENTER_HORIZONTAL;
layoutParams.bottomMargin =
((MarginLayoutParams) mSnapshotView.getLayoutParams()).bottomMargin;
view.setAlpha(mFooterAlpha);
mFooters[index] = new FooterWrapper(view);
if (shouldAnimateEntry) {
mFooters[index].animateEntry();
}
} else {
mFooters[index] = null;
}
mStackHeight = 0;
for (FooterWrapper footer : mFooters) {
if (footer != null) {
footer.setVerticalShift(mStackHeight);
mStackHeight += footer.mExpectedHeight;
}
}
return oldFooter;
}
/**
* Sets the contextual chip.
*
* @param view Wrapper view containing contextual chip.
*/
public void setContextualChip(View view) {
if (mContextualChipWrapper != null) {
removeView(mContextualChipWrapper);
}
if (view != null) {
mContextualChipWrapper = view;
LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT,
LayoutParams.WRAP_CONTENT);
layoutParams.gravity = BOTTOM | CENTER_HORIZONTAL;
int expectedChipHeight = getExpectedViewHeight(view);
float chipOffset = getResources().getDimension(R.dimen.chip_hint_vertical_offset);
layoutParams.bottomMargin = (int)
(((MarginLayoutParams) mSnapshotView.getLayoutParams()).bottomMargin
- expectedChipHeight + chipOffset);
mContextualChip = ((FrameLayout) mContextualChipWrapper).getChildAt(0);
mContextualChip.setScaleX(0f);
mContextualChip.setScaleY(0f);
GradientDrawable scrimDrawable = (GradientDrawable) getResources().getDrawable(
R.drawable.chip_scrim_gradient, mActivity.getTheme());
float cornerRadius = getTaskCornerRadius();
scrimDrawable.setCornerRadii(
new float[]{0, 0, 0, 0, cornerRadius, cornerRadius, cornerRadius,
cornerRadius});
InsetDrawable scrimDrawableInset = new InsetDrawable(scrimDrawable, 0, 0, 0,
(int) (expectedChipHeight - chipOffset));
mContextualChipWrapper.setBackground(scrimDrawableInset);
mContextualChipWrapper.setPadding(0, 0, 0, 0);
mContextualChipWrapper.setAlpha(0f);
addView(view, getChildCount(), layoutParams);
if (mContextualChip != null) {
mContextualChip.animate().scaleX(1f).scaleY(1f).setDuration(50);
}
if (mContextualChipWrapper != null) {
mContextualChipWrapper.animate().alpha(1f).setDuration(50);
}
}
}
public float getTaskCornerRadius() {
return TaskCornerRadius.get(mActivity);
}
/**
* Clears the contextual chip from TaskView.
*
* @return The contextual chip wrapper view to be recycled.
*/
public View clearContextualChip() {
if (mContextualChipWrapper != null) {
removeView(mContextualChipWrapper);
}
View oldContextualChipWrapper = mContextualChipWrapper;
mContextualChipWrapper = null;
mContextualChip = null;
return oldContextualChipWrapper;
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
setPivotX((right - left) * 0.5f);
setPivotY(mSnapshotView.getTop() + mSnapshotView.getHeight() * 0.5f);
if (Utilities.ATLEAST_Q) {
SYSTEM_GESTURE_EXCLUSION_RECT.get(0).set(0, 0, getWidth(), getHeight());
setSystemGestureExclusionRects(SYSTEM_GESTURE_EXCLUSION_RECT);
}
mStackHeight = 0;
for (FooterWrapper footer : mFooters) {
if (footer != null) {
mStackHeight += footer.mView.getHeight();
}
}
updateFooterVerticalOffset(0);
}
private void updateFooterVerticalOffset(float offset) {
mFooterVerticalOffset = offset;
for (FooterWrapper footer : mFooters) {
if (footer != null) {
footer.updateFooterOffset();
}
}
}
public static float getCurveScaleForInterpolation(float linearInterpolation) {
float curveInterpolation = CURVE_INTERPOLATOR.getInterpolation(linearInterpolation);
return getCurveScaleForCurveInterpolation(curveInterpolation);
}
private static float getCurveScaleForCurveInterpolation(float curveInterpolation) {
return 1 - curveInterpolation * EDGE_SCALE_DOWN_FACTOR;
}
private void setCurveScale(float curveScale) {
mCurveScale = curveScale;
setScaleX(mCurveScale);
setScaleY(mCurveScale);
}
public float getCurveScale() {
return mCurveScale;
}
@Override
public boolean hasOverlappingRendering() {
// TODO: Clip-out the icon region from the thumbnail, since they are overlapping.
return false;
}
private static final class TaskOutlineProvider extends ViewOutlineProvider {
private final int mMarginTop;
private FullscreenDrawParams mFullscreenParams;
TaskOutlineProvider(Context context, FullscreenDrawParams fullscreenParams) {
mMarginTop = context.getResources().getDimensionPixelSize(
R.dimen.task_thumbnail_top_margin);
mFullscreenParams = fullscreenParams;
}
public void setFullscreenParams(FullscreenDrawParams params) {
mFullscreenParams = params;
}
@Override
public void getOutline(View view, Outline outline) {
RectF insets = mFullscreenParams.mCurrentDrawnInsets;
float scale = mFullscreenParams.mScale;
outline.setRoundRect(0,
(int) (mMarginTop * scale),
(int) ((insets.left + view.getWidth() + insets.right) * scale),
(int) ((insets.top + view.getHeight() + insets.bottom) * scale),
mFullscreenParams.mCurrentDrawnCornerRadius);
}
}
private class FooterWrapper extends ViewOutlineProvider {
final View mView;
final ViewOutlineProvider mOldOutlineProvider;
final ViewOutlineProvider mDelegate;
final int mExpectedHeight;
final int mOldPaddingBottom;
int mAnimationOffset = 0;
int mEntryAnimationOffset = 0;
public FooterWrapper(View view) {
mView = view;
mOldOutlineProvider = view.getOutlineProvider();
mDelegate = mOldOutlineProvider == null
? ViewOutlineProvider.BACKGROUND : mOldOutlineProvider;
mExpectedHeight = getExpectedViewHeight(view);
mOldPaddingBottom = view.getPaddingBottom();
if (mOldOutlineProvider != null) {
view.setOutlineProvider(this);
view.setClipToOutline(true);
}
}
public void setVerticalShift(int shift) {
mView.setPadding(mView.getPaddingLeft(), mView.getPaddingTop(),
mView.getPaddingRight(), mOldPaddingBottom + shift);
}
@Override
public void getOutline(View view, Outline outline) {
mDelegate.getOutline(view, outline);
outline.offset(0, -mAnimationOffset - mEntryAnimationOffset);
}
void updateFooterOffset() {
float offset = Utilities.or(mFooterVerticalOffset, mModalness);
mAnimationOffset = Math.round(mStackHeight * offset);
mView.setTranslationY(mAnimationOffset + mEntryAnimationOffset
+ mCurrentFullscreenParams.mCurrentDrawnInsets.bottom
+ mCurrentFullscreenParams.mCurrentDrawnInsets.top);
mView.invalidateOutline();
}
void release() {
mView.setOutlineProvider(mOldOutlineProvider);
setVerticalShift(0);
}
void animateEntry() {
ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
animator.addUpdateListener(anim -> {
float factor = 1 - anim.getAnimatedFraction();
int totalShift = mExpectedHeight + mView.getPaddingBottom() - mOldPaddingBottom;
mEntryAnimationOffset = Math.round(factor * totalShift);
updateFooterOffset();
});
animator.setDuration(100);
animator.start();
}
}
private int getExpectedViewHeight(View view) {
int expectedHeight;
int h = view.getLayoutParams().height;
if (h > 0) {
expectedHeight = h;
} else {
int m = MeasureSpec.makeMeasureSpec(MeasureSpec.EXACTLY - 1, MeasureSpec.AT_MOST);
view.measure(m, m);
expectedHeight = view.getMeasuredHeight();
}
return expectedHeight;
}
@Override
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfo(info);
info.addAction(
new AccessibilityNodeInfo.AccessibilityAction(R.string.accessibility_close,
getContext().getText(R.string.accessibility_close)));
final Context context = getContext();
for (SystemShortcut s : TaskOverlayFactory.getEnabledShortcuts(this)) {
info.addAction(s.createAccessibilityAction(context));
}
if (mDigitalWellBeingToast.hasLimit()) {
info.addAction(
new AccessibilityNodeInfo.AccessibilityAction(
R.string.accessibility_app_usage_settings,
getContext().getText(R.string.accessibility_app_usage_settings)));
}
final RecentsView recentsView = getRecentsView();
final AccessibilityNodeInfo.CollectionItemInfo itemInfo =
AccessibilityNodeInfo.CollectionItemInfo.obtain(
0, 1, recentsView.getTaskViewCount() - recentsView.indexOfChild(this) - 1,
1, false);
info.setCollectionItemInfo(itemInfo);
}
@Override
public boolean performAccessibilityAction(int action, Bundle arguments) {
if (action == R.string.accessibility_close) {
getRecentsView().dismissTask(this, true /*animateTaskView*/,
true /*removeTask*/);
return true;
}
if (action == R.string.accessibility_app_usage_settings) {
mDigitalWellBeingToast.openAppUsageSettings(this);
return true;
}
for (SystemShortcut s : TaskOverlayFactory.getEnabledShortcuts(this)) {
if (s.hasHandlerForAction(action)) {
s.onClick(this);
return true;
}
}
return super.performAccessibilityAction(action, arguments);
}
public RecentsView getRecentsView() {
return (RecentsView) getParent();
}
PagedOrientationHandler getPagedOrientationHandler() {
return getRecentsView().mOrientationState.getOrientationHandler();
}
public void notifyTaskLaunchFailed(String tag) {
String msg = "Failed to launch task";
if (mTask != null) {
msg += " (task=" + mTask.key.baseIntent + " userId=" + mTask.key.userId + ")";
}
Log.w(tag, msg);
Toast.makeText(getContext(), R.string.activity_not_available, LENGTH_SHORT).show();
}
/**
* Hides the icon and shows insets when this TaskView is about to be shown fullscreen.
*
* @param progress: 0 = show icon and no insets; 1 = don't show icon and show full insets.
*/
public void setFullscreenProgress(float progress) {
progress = Utilities.boundToRange(progress, 0, 1);
mFullscreenProgress = progress;
boolean isFullscreen = mFullscreenProgress > 0;
mIconView.setVisibility(progress < 1 ? VISIBLE : INVISIBLE);
setClipChildren(!isFullscreen);
setClipToPadding(!isFullscreen);
TaskThumbnailView thumbnail = getThumbnail();
updateCurrentFullscreenParams(thumbnail.getPreviewPositionHelper());
if (!getRecentsView().isTaskIconScaledDown(this)) {
// Some of the items in here are dependent on the current fullscreen params, but don't
// update them if the icon is supposed to be scaled down.
setIconScaleAndDim(progress, true /* invert */);
}
thumbnail.setFullscreenParams(mCurrentFullscreenParams);
mOutlineProvider.setFullscreenParams(mCurrentFullscreenParams);
invalidateOutline();
}
void updateCurrentFullscreenParams(PreviewPositionHelper previewPositionHelper) {
if (getRecentsView() == null) {
return;
}
mCurrentFullscreenParams.setProgress(
mFullscreenProgress,
getRecentsView().getScaleX(),
getWidth(), mActivity.getDeviceProfile(),
previewPositionHelper);
}
public boolean isRunningTask() {
if (getRecentsView() == null) {
return false;
}
return this == getRecentsView().getRunningTaskView();
}
public void setShowScreenshot(boolean showScreenshot) {
mShowScreenshot = showScreenshot;
}
public boolean showScreenshot() {
if (!isRunningTask()) {
return true;
}
return mShowScreenshot;
}
public void setOverlayEnabled(boolean overlayEnabled) {
mSnapshotView.setOverlayEnabled(overlayEnabled);
}
/**
* We update and subsequently draw these in {@link #setFullscreenProgress(float)}.
*/
public static class FullscreenDrawParams {
private final float mCornerRadius;
private final float mWindowCornerRadius;
public RectF mCurrentDrawnInsets = new RectF();
public float mCurrentDrawnCornerRadius;
/** The current scale we apply to the thumbnail to adjust for new left/right insets. */
public float mScale = 1;
public FullscreenDrawParams(Context context) {
mCornerRadius = TaskCornerRadius.get(context);
mWindowCornerRadius = QuickStepContract.getWindowCornerRadius(context.getResources());
mCurrentDrawnCornerRadius = mCornerRadius;
}
/**
* Sets the progress in range [0, 1]
*/
public void setProgress(float fullscreenProgress, float parentScale, int previewWidth,
DeviceProfile dp, PreviewPositionHelper pph) {
RectF insets = pph.getInsetsToDrawInFullscreen();
float currentInsetsLeft = insets.left * fullscreenProgress;
float currentInsetsRight = insets.right * fullscreenProgress;
mCurrentDrawnInsets.set(currentInsetsLeft, insets.top * fullscreenProgress,
currentInsetsRight, insets.bottom * fullscreenProgress);
float fullscreenCornerRadius = dp.isMultiWindowMode ? 0 : mWindowCornerRadius;
mCurrentDrawnCornerRadius =
Utilities.mapRange(fullscreenProgress, mCornerRadius, fullscreenCornerRadius)
/ parentScale;
// We scaled the thumbnail to fit the content (excluding insets) within task view width.
// Now that we are drawing left/right insets again, we need to scale down to fit them.
if (previewWidth > 0) {
mScale = previewWidth / (previewWidth + currentInsetsLeft + currentInsetsRight);
}
}
}
}