Home | History | Annotate | Download | only in views
      1 /*
      2  * Copyright (C) 2017 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.quickstep.views;
     18 
     19 import static android.widget.Toast.LENGTH_SHORT;
     20 
     21 import static com.android.quickstep.views.TaskThumbnailView.DIM_ALPHA_MULTIPLIER;
     22 
     23 import android.animation.Animator;
     24 import android.animation.AnimatorListenerAdapter;
     25 import android.animation.ObjectAnimator;
     26 import android.animation.TimeInterpolator;
     27 import android.app.ActivityOptions;
     28 import android.content.Context;
     29 import android.content.res.Resources;
     30 import android.graphics.Outline;
     31 import android.os.Bundle;
     32 import android.os.Handler;
     33 import android.util.AttributeSet;
     34 import android.util.FloatProperty;
     35 import android.util.Log;
     36 import android.util.Property;
     37 import android.view.View;
     38 import android.view.ViewOutlineProvider;
     39 import android.view.accessibility.AccessibilityNodeInfo;
     40 import android.widget.FrameLayout;
     41 import android.widget.Toast;
     42 
     43 import com.android.launcher3.BaseActivity;
     44 import com.android.launcher3.BaseDraggingActivity;
     45 import com.android.launcher3.R;
     46 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
     47 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
     48 import com.android.quickstep.TaskSystemShortcut;
     49 import com.android.quickstep.TaskUtils;
     50 import com.android.quickstep.views.RecentsView.PageCallbacks;
     51 import com.android.quickstep.views.RecentsView.ScrollState;
     52 import com.android.systemui.shared.recents.model.Task;
     53 import com.android.systemui.shared.recents.model.Task.TaskCallbacks;
     54 import com.android.systemui.shared.recents.model.ThumbnailData;
     55 import com.android.systemui.shared.system.ActivityManagerWrapper;
     56 
     57 import java.util.function.Consumer;
     58 
     59 /**
     60  * A task in the Recents view.
     61  */
     62 public class TaskView extends FrameLayout implements TaskCallbacks, PageCallbacks {
     63 
     64     private static final String TAG = TaskView.class.getSimpleName();
     65 
     66     /** A curve of x from 0 to 1, where 0 is the center of the screen and 1 is the edge. */
     67     private static final TimeInterpolator CURVE_INTERPOLATOR
     68             = x -> (float) -Math.cos(x * Math.PI) / 2f + .5f;
     69 
     70     /**
     71      * The alpha of a black scrim on a page in the carousel as it leaves the screen.
     72      * In the resting position of the carousel, the adjacent pages have about half this scrim.
     73      */
     74     private static final float MAX_PAGE_SCRIM_ALPHA = 0.4f;
     75 
     76     /**
     77      * How much to scale down pages near the edge of the screen.
     78      */
     79     private static final float EDGE_SCALE_DOWN_FACTOR = 0.03f;
     80 
     81     public static final long SCALE_ICON_DURATION = 120;
     82     private static final long DIM_ANIM_DURATION = 700;
     83 
     84     public static final Property<TaskView, Float> ZOOM_SCALE =
     85             new FloatProperty<TaskView>("zoomScale") {
     86                 @Override
     87                 public void setValue(TaskView taskView, float v) {
     88                     taskView.setZoomScale(v);
     89                 }
     90 
     91                 @Override
     92                 public Float get(TaskView taskView) {
     93                     return taskView.mZoomScale;
     94                 }
     95             };
     96 
     97     private Task mTask;
     98     private TaskThumbnailView mSnapshotView;
     99     private IconView mIconView;
    100     private float mCurveScale;
    101     private float mZoomScale;
    102     private Animator mDimAlphaAnim;
    103 
    104     public TaskView(Context context) {
    105         this(context, null);
    106     }
    107 
    108     public TaskView(Context context, AttributeSet attrs) {
    109         this(context, attrs, 0);
    110     }
    111 
    112     public TaskView(Context context, AttributeSet attrs, int defStyleAttr) {
    113         super(context, attrs, defStyleAttr);
    114         setOnClickListener((view) -> {
    115             if (getTask() == null) {
    116                 return;
    117             }
    118             launchTask(true /* animate */);
    119             BaseActivity.fromContext(context).getUserEventDispatcher().logTaskLaunchOrDismiss(
    120                     Touch.TAP, Direction.NONE, getRecentsView().indexOfChild(this),
    121                     TaskUtils.getComponentKeyForTask(getTask().key));
    122         });
    123         setOutlineProvider(new TaskOutlineProvider(getResources()));
    124     }
    125 
    126     @Override
    127     protected void onFinishInflate() {
    128         super.onFinishInflate();
    129         mSnapshotView = findViewById(R.id.snapshot);
    130         mIconView = findViewById(R.id.icon);
    131     }
    132 
    133     /**
    134      * Updates this task view to the given {@param task}.
    135      */
    136     public void bind(Task task) {
    137         if (mTask != null) {
    138             mTask.removeCallback(this);
    139         }
    140         mTask = task;
    141         mSnapshotView.bind();
    142         task.addCallback(this);
    143         setContentDescription(task.titleDescription);
    144     }
    145 
    146     public Task getTask() {
    147         return mTask;
    148     }
    149 
    150     public TaskThumbnailView getThumbnail() {
    151         return mSnapshotView;
    152     }
    153 
    154     public IconView getIconView() {
    155         return mIconView;
    156     }
    157 
    158     public void launchTask(boolean animate) {
    159         launchTask(animate, (result) -> {
    160             if (!result) {
    161                 notifyTaskLaunchFailed(TAG);
    162             }
    163         }, getHandler());
    164     }
    165 
    166     public void launchTask(boolean animate, Consumer<Boolean> resultCallback,
    167             Handler resultCallbackHandler) {
    168         if (mTask != null) {
    169             final ActivityOptions opts;
    170             if (animate) {
    171                 opts = BaseDraggingActivity.fromContext(getContext())
    172                         .getActivityLaunchOptions(this);
    173             } else {
    174                 opts = ActivityOptions.makeCustomAnimation(getContext(), 0, 0);
    175             }
    176             ActivityManagerWrapper.getInstance().startActivityFromRecentsAsync(mTask.key,
    177                     opts, resultCallback, resultCallbackHandler);
    178         }
    179     }
    180 
    181     @Override
    182     public void onTaskDataLoaded(Task task, ThumbnailData thumbnailData) {
    183         mSnapshotView.setThumbnail(task, thumbnailData);
    184         mIconView.setDrawable(task.icon);
    185         mIconView.setOnClickListener(icon -> TaskMenuView.showForTask(this));
    186         mIconView.setOnLongClickListener(icon -> {
    187             requestDisallowInterceptTouchEvent(true);
    188             return TaskMenuView.showForTask(this);
    189         });
    190     }
    191 
    192     @Override
    193     public void onTaskDataUnloaded() {
    194         mSnapshotView.setThumbnail(null, null);
    195         mIconView.setDrawable(null);
    196         mIconView.setOnLongClickListener(null);
    197     }
    198 
    199     @Override
    200     public void onTaskWindowingModeChanged() {
    201         // Do nothing
    202     }
    203 
    204     public void animateIconToScaleAndDim(float scale) {
    205         mIconView.animate().scaleX(scale).scaleY(scale).setDuration(SCALE_ICON_DURATION).start();
    206         mDimAlphaAnim = ObjectAnimator.ofFloat(mSnapshotView, DIM_ALPHA_MULTIPLIER, 1 - scale,
    207                 scale);
    208         mDimAlphaAnim.setDuration(DIM_ANIM_DURATION);
    209         mDimAlphaAnim.addListener(new AnimatorListenerAdapter() {
    210             @Override
    211             public void onAnimationEnd(Animator animation) {
    212                 mDimAlphaAnim = null;
    213             }
    214         });
    215         mDimAlphaAnim.start();
    216     }
    217 
    218     protected void setIconScaleAndDim(float iconScale) {
    219         mIconView.animate().cancel();
    220         mIconView.setScaleX(iconScale);
    221         mIconView.setScaleY(iconScale);
    222         if (mDimAlphaAnim != null) {
    223             mDimAlphaAnim.cancel();
    224         }
    225         mSnapshotView.setDimAlphaMultipler(iconScale);
    226     }
    227 
    228     public void resetVisualProperties() {
    229         setZoomScale(1);
    230         setTranslationX(0f);
    231         setTranslationY(0f);
    232         setTranslationZ(0);
    233         setAlpha(1f);
    234         setIconScaleAndDim(1);
    235     }
    236 
    237     @Override
    238     public void onPageScroll(ScrollState scrollState) {
    239         float curveInterpolation =
    240                 CURVE_INTERPOLATOR.getInterpolation(scrollState.linearInterpolation);
    241 
    242         mSnapshotView.setDimAlpha(curveInterpolation * MAX_PAGE_SCRIM_ALPHA);
    243         setCurveScale(getCurveScaleForCurveInterpolation(curveInterpolation));
    244     }
    245 
    246     @Override
    247     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    248         super.onLayout(changed, left, top, right, bottom);
    249         setPivotX((right - left) * 0.5f);
    250         setPivotY(mSnapshotView.getTop() + mSnapshotView.getHeight() * 0.5f);
    251     }
    252 
    253     public static float getCurveScaleForInterpolation(float linearInterpolation) {
    254         float curveInterpolation = CURVE_INTERPOLATOR.getInterpolation(linearInterpolation);
    255         return getCurveScaleForCurveInterpolation(curveInterpolation);
    256     }
    257 
    258     private static float getCurveScaleForCurveInterpolation(float curveInterpolation) {
    259         return 1 - curveInterpolation * EDGE_SCALE_DOWN_FACTOR;
    260     }
    261 
    262     private void setCurveScale(float curveScale) {
    263         mCurveScale = curveScale;
    264         onScaleChanged();
    265     }
    266 
    267     public float getCurveScale() {
    268         return mCurveScale;
    269     }
    270 
    271     public void setZoomScale(float adjacentScale) {
    272         mZoomScale = adjacentScale;
    273         onScaleChanged();
    274     }
    275 
    276     private void onScaleChanged() {
    277         float scale = mCurveScale * mZoomScale;
    278         setScaleX(scale);
    279         setScaleY(scale);
    280     }
    281 
    282     @Override
    283     public boolean hasOverlappingRendering() {
    284         // TODO: Clip-out the icon region from the thumbnail, since they are overlapping.
    285         return false;
    286     }
    287 
    288     private static final class TaskOutlineProvider extends ViewOutlineProvider {
    289 
    290         private final int mMarginTop;
    291         private final float mRadius;
    292 
    293         TaskOutlineProvider(Resources res) {
    294             mMarginTop = res.getDimensionPixelSize(R.dimen.task_thumbnail_top_margin);
    295             mRadius = res.getDimension(R.dimen.task_corner_radius);
    296         }
    297 
    298         @Override
    299         public void getOutline(View view, Outline outline) {
    300             outline.setRoundRect(0, mMarginTop, view.getWidth(),
    301                     view.getHeight(), mRadius);
    302         }
    303     }
    304 
    305     @Override
    306     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
    307         super.onInitializeAccessibilityNodeInfo(info);
    308 
    309         info.addAction(
    310                 new AccessibilityNodeInfo.AccessibilityAction(R.string.accessibility_close_task,
    311                         getContext().getText(R.string.accessibility_close_task)));
    312 
    313         final Context context = getContext();
    314         final BaseDraggingActivity activity = BaseDraggingActivity.fromContext(context);
    315         for (TaskSystemShortcut menuOption : TaskMenuView.MENU_OPTIONS) {
    316             OnClickListener onClickListener = menuOption.getOnClickListener(activity, this);
    317             if (onClickListener != null) {
    318                 info.addAction(new AccessibilityNodeInfo.AccessibilityAction(menuOption.labelResId,
    319                         context.getText(menuOption.labelResId)));
    320             }
    321         }
    322 
    323         final RecentsView recentsView = getRecentsView();
    324         final AccessibilityNodeInfo.CollectionItemInfo itemInfo =
    325                 AccessibilityNodeInfo.CollectionItemInfo.obtain(
    326                         0, 1, recentsView.getChildCount() - recentsView.indexOfChild(this) - 1, 1,
    327                         false);
    328         info.setCollectionItemInfo(itemInfo);
    329     }
    330 
    331     @Override
    332     public boolean performAccessibilityAction(int action, Bundle arguments) {
    333         if (action == R.string.accessibility_close_task) {
    334             getRecentsView().dismissTask(this, true /*animateTaskView*/,
    335                     true /*removeTask*/);
    336             return true;
    337         }
    338 
    339         for (TaskSystemShortcut menuOption : TaskMenuView.MENU_OPTIONS) {
    340             if (action == menuOption.labelResId) {
    341                 OnClickListener onClickListener = menuOption.getOnClickListener(
    342                         BaseDraggingActivity.fromContext(getContext()), this);
    343                 if (onClickListener != null) {
    344                     onClickListener.onClick(this);
    345                 }
    346                 return true;
    347             }
    348         }
    349 
    350         if (getRecentsView().performTaskAccessibilityActionExtra(action)) return true;
    351 
    352         return super.performAccessibilityAction(action, arguments);
    353     }
    354 
    355     private RecentsView getRecentsView() {
    356         return (RecentsView) getParent();
    357     }
    358 
    359     public void notifyTaskLaunchFailed(String tag) {
    360         String msg = "Failed to launch task";
    361         if (mTask != null) {
    362             msg += " (task=" + mTask.key.baseIntent + " userId=" + mTask.key.userId + ")";
    363         }
    364         Log.w(tag, msg);
    365         Toast.makeText(getContext(), R.string.activity_not_available, LENGTH_SHORT).show();
    366     }
    367 }
    368