| /* |
| * 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.widget.Toast.LENGTH_SHORT; |
| |
| import static com.android.launcher3.QuickstepAppTransitionManagerImpl.RECENTS_LAUNCH_DURATION; |
| import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN; |
| import static com.android.launcher3.anim.Interpolators.LINEAR; |
| import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE; |
| |
| import android.animation.Animator; |
| import android.animation.AnimatorListenerAdapter; |
| import android.animation.ObjectAnimator; |
| import android.animation.TimeInterpolator; |
| import android.animation.ValueAnimator; |
| import android.animation.ValueAnimator.AnimatorUpdateListener; |
| import android.app.ActivityOptions; |
| import android.content.Context; |
| import android.content.res.Resources; |
| import android.graphics.Outline; |
| import android.graphics.Rect; |
| import android.graphics.RectF; |
| import android.graphics.drawable.Drawable; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.util.AttributeSet; |
| import android.util.FloatProperty; |
| import android.util.Log; |
| import android.view.Gravity; |
| 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.R; |
| import com.android.launcher3.Utilities; |
| import com.android.launcher3.anim.AnimatorPlaybackController; |
| import com.android.launcher3.anim.Interpolators; |
| import com.android.launcher3.logging.UserEventDispatcher; |
| import com.android.launcher3.testing.TestProtocol; |
| 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.PendingAnimation; |
| 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.TaskSystemShortcut; |
| import com.android.quickstep.TaskThumbnailCache; |
| import com.android.quickstep.TaskUtils; |
| import com.android.quickstep.util.TaskCornerRadius; |
| import com.android.quickstep.views.RecentsView.PageCallbacks; |
| import com.android.quickstep.views.RecentsView.ScrollState; |
| 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()); |
| |
| public static final FloatProperty<TaskView> FULLSCREEN_PROGRESS = |
| new FloatProperty<TaskView>("fullscreenProgress") { |
| @Override |
| public void setValue(TaskView taskView, float v) { |
| taskView.setFullscreenProgress(v); |
| } |
| |
| @Override |
| public Float get(TaskView taskView) { |
| return taskView.mFullscreenProgress; |
| } |
| }; |
| |
| 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 float mCornerRadius; |
| private final float mWindowCornerRadius; |
| private final BaseDraggingActivity mActivity; |
| |
| private ObjectAnimator mIconAndDimAnimator; |
| private float mIconScaleAnimStartProgress = 0; |
| private float mFocusTransitionProgress = 1; |
| 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; |
| public static final int INDEX_PROACTIVE_SUGGEST = 1; |
| private final FooterWrapper[] mFooters = new FooterWrapper[2]; |
| private float mFooterVerticalOffset = 0; |
| private float mFooterAlpha = 1; |
| private int mStackHeight; |
| |
| 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 (com.android.launcher3.testing.TestProtocol.sDebugTracing) { |
| android.util.Log.d(TestProtocol.NO_START_TASK_TAG, "TaskView onClick"); |
| } |
| 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().logTaskLaunch(getRecentsView(), |
| TaskUtils.getLaunchComponentKeyForTask(getTask().key)); |
| }); |
| mCornerRadius = TaskCornerRadius.get(context); |
| mWindowCornerRadius = QuickStepContract.getWindowCornerRadius(context.getResources()); |
| mCurrentFullscreenParams = new FullscreenDrawParams(mCornerRadius); |
| mDigitalWellBeingToast = new DigitalWellBeingToast(mActivity, this); |
| |
| mOutlineProvider = new TaskOutlineProvider(getResources(), mCurrentFullscreenParams); |
| setOutlineProvider(mOutlineProvider); |
| } |
| |
| @Override |
| protected void onFinishInflate() { |
| super.onFinishInflate(); |
| mSnapshotView = findViewById(R.id.snapshot); |
| mIconView = findViewById(R.id.icon); |
| } |
| |
| public TaskMenuView getMenuView() { |
| return mMenuView; |
| } |
| |
| public DigitalWellBeingToast getDigitalWellBeingToast() { |
| return mDigitalWellBeingToast; |
| } |
| |
| /** |
| * Updates this task view to the given {@param task}. |
| */ |
| public void bind(Task task) { |
| cancelPendingLoadTasks(); |
| mTask = task; |
| mSnapshotView.bind(task); |
| } |
| |
| public Task getTask() { |
| return mTask; |
| } |
| |
| public TaskThumbnailView getThumbnail() { |
| return mSnapshotView; |
| } |
| |
| public IconView getIconView() { |
| return mIconView; |
| } |
| |
| public AnimatorPlaybackController createLaunchAnimationForRunningTask() { |
| final PendingAnimation pendingAnimation = |
| getRecentsView().createTaskLauncherAnimation(this, RECENTS_LAUNCH_DURATION); |
| pendingAnimation.anim.setInterpolator(Interpolators.TOUCH_RESPONSE_INTERPOLATOR); |
| AnimatorPlaybackController currentAnimation = AnimatorPlaybackController |
| .wrap(pendingAnimation.anim, RECENTS_LAUNCH_DURATION, null); |
| 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 (com.android.launcher3.testing.TestProtocol.sDebugTracing) { |
| android.util.Log.d(TestProtocol.NO_START_TASK_TAG, "launchTask"); |
| } |
| if (ENABLE_QUICKSTEP_LIVE_TILE.get()) { |
| if (isRunningTask()) { |
| getRecentsView().finishRecentsAnimation(false /* toRecents */, |
| () -> resultCallbackHandler.post(() -> resultCallback.accept(true))); |
| } else { |
| launchTaskInternal(animate, freezeTaskList, resultCallback, resultCallbackHandler); |
| } |
| } else { |
| launchTaskInternal(animate, freezeTaskList, resultCallback, resultCallbackHandler); |
| } |
| } |
| |
| private void launchTaskInternal(boolean animate, boolean freezeTaskList, |
| Consumer<Boolean> resultCallback, Handler resultCallbackHandler) { |
| if (com.android.launcher3.testing.TestProtocol.sDebugTracing) { |
| android.util.Log.d(TestProtocol.NO_START_TASK_TAG, "launchTaskInternal"); |
| } |
| if (mTask != null) { |
| final ActivityOptions opts; |
| 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); |
| } |
| } |
| } |
| |
| 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) { |
| getRecentsView().snapToPage(getRecentsView().indexOfChild(this)); |
| mMenuView = TaskMenuView.showForTask(this); |
| 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); |
| } |
| } |
| |
| 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); |
| |
| mFooterVerticalOffset = 1.0f - scale; |
| for (FooterWrapper footer : mFooters) { |
| if (footer != null) { |
| footer.updateFooterOffset(); |
| } |
| } |
| } |
| |
| 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); |
| } |
| |
| private void resetViewTransforms() { |
| setCurveScale(1); |
| setTranslationX(0f); |
| setTranslationY(0f); |
| setTranslationZ(0); |
| setAlpha(mStableAlpha); |
| setIconScaleAndDim(1); |
| } |
| |
| public void resetVisualProperties() { |
| resetViewTransforms(); |
| setFullscreenProgress(0); |
| } |
| |
| 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) { |
| float curveInterpolation = |
| CURVE_INTERPOLATOR.getInterpolation(scrollState.linearInterpolation); |
| |
| mSnapshotView.setDimAlpha(curveInterpolation * MAX_PAGE_SCRIM_ALPHA); |
| setCurveScale(getCurveScaleForCurveInterpolation(curveInterpolation)); |
| |
| mFooterAlpha = Utilities.boundToRange(1.0f - 2 * scrollState.linearInterpolation, 0f, 1f); |
| for (FooterWrapper footer : mFooters) { |
| if (footer != null) { |
| footer.mView.setAlpha(mFooterAlpha); |
| } |
| } |
| |
| if (mMenuView != null) { |
| mMenuView.setPosition(getX() - getRecentsView().getScrollX(), getY()); |
| 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) view.getLayoutParams()).gravity = |
| Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL; |
| 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; |
| } |
| |
| @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(); |
| } |
| } |
| 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; |
| } |
| |
| public void setCurveScale(float curveScale) { |
| mCurveScale = curveScale; |
| onScaleChanged(); |
| } |
| |
| public float getCurveScale() { |
| return mCurveScale; |
| } |
| |
| private void onScaleChanged() { |
| float scale = mCurveScale; |
| setScaleX(scale); |
| setScaleY(scale); |
| } |
| |
| @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(Resources res, FullscreenDrawParams fullscreenParams) { |
| mMarginTop = res.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; |
| |
| int h = view.getLayoutParams().height; |
| if (h > 0) { |
| mExpectedHeight = h; |
| } else { |
| int m = MeasureSpec.makeMeasureSpec(MeasureSpec.EXACTLY - 1, MeasureSpec.AT_MOST); |
| view.measure(m, m); |
| mExpectedHeight = view.getMeasuredHeight(); |
| } |
| 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() { |
| mAnimationOffset = Math.round(mStackHeight * mFooterVerticalOffset); |
| 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(); |
| } |
| } |
| |
| @Override |
| public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { |
| super.onInitializeAccessibilityNodeInfo(info); |
| |
| info.addAction( |
| new AccessibilityNodeInfo.AccessibilityAction(R.string.accessibility_close_task, |
| getContext().getText(R.string.accessibility_close_task))); |
| |
| final Context context = getContext(); |
| final List<TaskSystemShortcut> shortcuts = |
| TaskOverlayFactory.INSTANCE.get(getContext()).getEnabledShortcuts(this); |
| final int count = shortcuts.size(); |
| for (int i = 0; i < count; ++i) { |
| final TaskSystemShortcut menuOption = shortcuts.get(i); |
| OnClickListener onClickListener = menuOption.getOnClickListener(mActivity, this); |
| if (onClickListener != null) { |
| info.addAction(menuOption.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.getChildCount() - recentsView.indexOfChild(this) - 1, 1, |
| false); |
| info.setCollectionItemInfo(itemInfo); |
| } |
| |
| @Override |
| public boolean performAccessibilityAction(int action, Bundle arguments) { |
| if (action == R.string.accessibility_close_task) { |
| getRecentsView().dismissTask(this, true /*animateTaskView*/, |
| true /*removeTask*/); |
| return true; |
| } |
| |
| if (action == R.string.accessibility_app_usage_settings) { |
| mDigitalWellBeingToast.openAppUsageSettings(this); |
| return true; |
| } |
| |
| final List<TaskSystemShortcut> shortcuts = |
| TaskOverlayFactory.INSTANCE.get(getContext()).getEnabledShortcuts(this); |
| final int count = shortcuts.size(); |
| for (int i = 0; i < count; ++i) { |
| final TaskSystemShortcut menuOption = shortcuts.get(i); |
| if (menuOption.hasHandlerForAction(action)) { |
| OnClickListener onClickListener = menuOption.getOnClickListener(mActivity, this); |
| if (onClickListener != null) { |
| onClickListener.onClick(this); |
| } |
| return true; |
| } |
| } |
| |
| return super.performAccessibilityAction(action, arguments); |
| } |
| |
| public RecentsView getRecentsView() { |
| return (RecentsView) getParent(); |
| } |
| |
| 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); |
| if (progress == mFullscreenProgress) { |
| return; |
| } |
| mFullscreenProgress = progress; |
| boolean isFullscreen = mFullscreenProgress > 0; |
| mIconView.setVisibility(progress < 1 ? VISIBLE : INVISIBLE); |
| setClipChildren(!isFullscreen); |
| setClipToPadding(!isFullscreen); |
| |
| TaskThumbnailView thumbnail = getThumbnail(); |
| boolean isMultiWindowMode = mActivity.getDeviceProfile().isMultiWindowMode; |
| RectF insets = thumbnail.getInsetsToDrawInFullscreen(isMultiWindowMode); |
| float currentInsetsLeft = insets.left * mFullscreenProgress; |
| float currentInsetsRight = insets.right * mFullscreenProgress; |
| mCurrentFullscreenParams.setInsets(currentInsetsLeft, |
| insets.top * mFullscreenProgress, |
| currentInsetsRight, |
| insets.bottom * mFullscreenProgress); |
| float fullscreenCornerRadius = isMultiWindowMode ? 0 : mWindowCornerRadius; |
| mCurrentFullscreenParams.setCornerRadius(Utilities.mapRange(mFullscreenProgress, |
| mCornerRadius, fullscreenCornerRadius) / getRecentsView().getScaleX()); |
| // 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 (getWidth() > 0) { |
| mCurrentFullscreenParams.setScale(getWidth() |
| / (getWidth() + currentInsetsLeft + currentInsetsRight)); |
| } |
| |
| // Some of the items in here are dependent on the current fullscreen params |
| setIconScaleAndDim(progress, true /* invert */); |
| |
| thumbnail.setFullscreenParams(mCurrentFullscreenParams); |
| mOutlineProvider.setFullscreenParams(mCurrentFullscreenParams); |
| invalidateOutline(); |
| } |
| |
| 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)}. |
| */ |
| static class FullscreenDrawParams { |
| RectF mCurrentDrawnInsets = new RectF(); |
| float mCurrentDrawnCornerRadius; |
| /** The current scale we apply to the thumbnail to adjust for new left/right insets. */ |
| float mScale = 1; |
| |
| public FullscreenDrawParams(float cornerRadius) { |
| setCornerRadius(cornerRadius); |
| } |
| |
| public void setInsets(float left, float top, float right, float bottom) { |
| mCurrentDrawnInsets.set(left, top, right, bottom); |
| } |
| |
| public void setCornerRadius(float cornerRadius) { |
| mCurrentDrawnCornerRadius = cornerRadius; |
| } |
| |
| public void setScale(float scale) { |
| mScale = scale; |
| } |
| } |
| } |