Home | History | Annotate | Download | only in views
      1 /*
      2  * Copyright (C) 2014 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.systemui.recents.views;
     18 
     19 import android.animation.Animator;
     20 import android.animation.ObjectAnimator;
     21 import android.animation.ValueAnimator;
     22 import android.content.Context;
     23 import android.graphics.*;
     24 import android.util.AttributeSet;
     25 import android.view.View;
     26 import android.view.ViewOutlineProvider;
     27 import android.view.animation.AccelerateInterpolator;
     28 import android.widget.FrameLayout;
     29 import com.android.systemui.R;
     30 import com.android.systemui.recents.Constants;
     31 import com.android.systemui.recents.RecentsConfiguration;
     32 import com.android.systemui.recents.misc.Utilities;
     33 import com.android.systemui.recents.model.Task;
     34 import com.android.systemui.statusbar.phone.PhoneStatusBar;
     35 
     36 /* A task view */
     37 public class TaskView extends FrameLayout implements Task.TaskCallbacks,
     38         View.OnClickListener, View.OnLongClickListener {
     39 
     40     /** The TaskView callbacks */
     41     interface TaskViewCallbacks {
     42         public void onTaskViewAppIconClicked(TaskView tv);
     43         public void onTaskViewAppInfoClicked(TaskView tv);
     44         public void onTaskViewClicked(TaskView tv, Task task, boolean lockToTask);
     45         public void onTaskViewDismissed(TaskView tv);
     46         public void onTaskViewClipStateChanged(TaskView tv);
     47         public void onTaskViewFocusChanged(TaskView tv, boolean focused);
     48     }
     49 
     50     RecentsConfiguration mConfig;
     51 
     52     float mTaskProgress;
     53     ObjectAnimator mTaskProgressAnimator;
     54     float mMaxDimScale;
     55     int mDimAlpha;
     56     AccelerateInterpolator mDimInterpolator = new AccelerateInterpolator(1f);
     57     PorterDuffColorFilter mDimColorFilter = new PorterDuffColorFilter(0, PorterDuff.Mode.SRC_ATOP);
     58     Paint mDimLayerPaint = new Paint();
     59     float mActionButtonTranslationZ;
     60 
     61     Task mTask;
     62     boolean mTaskDataLoaded;
     63     boolean mIsFocused;
     64     boolean mFocusAnimationsEnabled;
     65     boolean mClipViewInStack;
     66     AnimateableViewBounds mViewBounds;
     67 
     68     View mContent;
     69     TaskViewThumbnail mThumbnailView;
     70     TaskViewHeader mHeaderView;
     71     View mActionButtonView;
     72     TaskViewCallbacks mCb;
     73 
     74     // Optimizations
     75     ValueAnimator.AnimatorUpdateListener mUpdateDimListener =
     76             new ValueAnimator.AnimatorUpdateListener() {
     77                 @Override
     78                 public void onAnimationUpdate(ValueAnimator animation) {
     79                     setTaskProgress((Float) animation.getAnimatedValue());
     80                 }
     81             };
     82 
     83 
     84     public TaskView(Context context) {
     85         this(context, null);
     86     }
     87 
     88     public TaskView(Context context, AttributeSet attrs) {
     89         this(context, attrs, 0);
     90     }
     91 
     92     public TaskView(Context context, AttributeSet attrs, int defStyleAttr) {
     93         this(context, attrs, defStyleAttr, 0);
     94     }
     95 
     96     public TaskView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
     97         super(context, attrs, defStyleAttr, defStyleRes);
     98         mConfig = RecentsConfiguration.getInstance();
     99         mMaxDimScale = mConfig.taskStackMaxDim / 255f;
    100         mClipViewInStack = true;
    101         mViewBounds = new AnimateableViewBounds(this, mConfig.taskViewRoundedCornerRadiusPx);
    102         setTaskProgress(getTaskProgress());
    103         setDim(getDim());
    104         if (mConfig.fakeShadows) {
    105             setBackground(new FakeShadowDrawable(context.getResources(), mConfig));
    106         }
    107         setOutlineProvider(mViewBounds);
    108     }
    109 
    110     /** Set callback */
    111     void setCallbacks(TaskViewCallbacks cb) {
    112         mCb = cb;
    113     }
    114 
    115     /** Resets this TaskView for reuse. */
    116     void reset() {
    117         resetViewProperties();
    118         resetNoUserInteractionState();
    119         setClipViewInStack(false);
    120         setCallbacks(null);
    121     }
    122 
    123     /** Gets the task */
    124     Task getTask() {
    125         return mTask;
    126     }
    127 
    128     /** Returns the view bounds. */
    129     AnimateableViewBounds getViewBounds() {
    130         return mViewBounds;
    131     }
    132 
    133     @Override
    134     protected void onFinishInflate() {
    135         // Bind the views
    136         mContent = findViewById(R.id.task_view_content);
    137         mHeaderView = (TaskViewHeader) findViewById(R.id.task_view_bar);
    138         mThumbnailView = (TaskViewThumbnail) findViewById(R.id.task_view_thumbnail);
    139         mThumbnailView.updateClipToTaskBar(mHeaderView);
    140         mActionButtonView = findViewById(R.id.lock_to_app_fab);
    141         mActionButtonView.setOutlineProvider(new ViewOutlineProvider() {
    142             @Override
    143             public void getOutline(View view, Outline outline) {
    144                 // Set the outline to match the FAB background
    145                 outline.setOval(0, 0, mActionButtonView.getWidth(), mActionButtonView.getHeight());
    146             }
    147         });
    148         mActionButtonTranslationZ = mActionButtonView.getTranslationZ();
    149     }
    150 
    151     @Override
    152     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    153         int width = MeasureSpec.getSize(widthMeasureSpec);
    154         int height = MeasureSpec.getSize(heightMeasureSpec);
    155 
    156         int widthWithoutPadding = width - mPaddingLeft - mPaddingRight;
    157         int heightWithoutPadding = height - mPaddingTop - mPaddingBottom;
    158 
    159         // Measure the content
    160         mContent.measure(MeasureSpec.makeMeasureSpec(widthWithoutPadding, MeasureSpec.EXACTLY),
    161                 MeasureSpec.makeMeasureSpec(widthWithoutPadding, MeasureSpec.EXACTLY));
    162 
    163         // Measure the bar view, and action button
    164         mHeaderView.measure(MeasureSpec.makeMeasureSpec(widthWithoutPadding, MeasureSpec.EXACTLY),
    165                 MeasureSpec.makeMeasureSpec(mConfig.taskBarHeight, MeasureSpec.EXACTLY));
    166         mActionButtonView.measure(
    167                 MeasureSpec.makeMeasureSpec(widthWithoutPadding, MeasureSpec.AT_MOST),
    168                 MeasureSpec.makeMeasureSpec(heightWithoutPadding, MeasureSpec.AT_MOST));
    169         // Measure the thumbnail to be square
    170         mThumbnailView.measure(
    171                 MeasureSpec.makeMeasureSpec(widthWithoutPadding, MeasureSpec.EXACTLY),
    172                 MeasureSpec.makeMeasureSpec(widthWithoutPadding, MeasureSpec.EXACTLY));
    173         setMeasuredDimension(width, height);
    174         invalidateOutline();
    175     }
    176 
    177     /** Synchronizes this view's properties with the task's transform */
    178     void updateViewPropertiesToTaskTransform(TaskViewTransform toTransform, int duration) {
    179         updateViewPropertiesToTaskTransform(toTransform, duration, null);
    180     }
    181 
    182     void updateViewPropertiesToTaskTransform(TaskViewTransform toTransform, int duration,
    183                                              ValueAnimator.AnimatorUpdateListener updateCallback) {
    184         // Apply the transform
    185         toTransform.applyToTaskView(this, duration, mConfig.fastOutSlowInInterpolator, false,
    186                 !mConfig.fakeShadows, updateCallback);
    187 
    188         // Update the task progress
    189         Utilities.cancelAnimationWithoutCallbacks(mTaskProgressAnimator);
    190         if (duration <= 0) {
    191             setTaskProgress(toTransform.p);
    192         } else {
    193             mTaskProgressAnimator = ObjectAnimator.ofFloat(this, "taskProgress", toTransform.p);
    194             mTaskProgressAnimator.setDuration(duration);
    195             mTaskProgressAnimator.addUpdateListener(mUpdateDimListener);
    196             mTaskProgressAnimator.start();
    197         }
    198     }
    199 
    200     /** Resets this view's properties */
    201     void resetViewProperties() {
    202         setDim(0);
    203         setLayerType(View.LAYER_TYPE_NONE, null);
    204         TaskViewTransform.reset(this);
    205         if (mActionButtonView != null) {
    206             mActionButtonView.setScaleX(1f);
    207             mActionButtonView.setScaleY(1f);
    208             mActionButtonView.setAlpha(1f);
    209             mActionButtonView.setTranslationZ(mActionButtonTranslationZ);
    210         }
    211     }
    212 
    213     /**
    214      * When we are un/filtering, this method will set up the transform that we are animating to,
    215      * in order to hide the task.
    216      */
    217     void prepareTaskTransformForFilterTaskHidden(TaskViewTransform toTransform) {
    218         // Fade the view out and slide it away
    219         toTransform.alpha = 0f;
    220         toTransform.translationY += 200;
    221         toTransform.translationZ = 0;
    222     }
    223 
    224     /**
    225      * When we are un/filtering, this method will setup the transform that we are animating from,
    226      * in order to show the task.
    227      */
    228     void prepareTaskTransformForFilterTaskVisible(TaskViewTransform fromTransform) {
    229         // Fade the view in
    230         fromTransform.alpha = 0f;
    231     }
    232 
    233     /** Prepares this task view for the enter-recents animations.  This is called earlier in the
    234      * first layout because the actual animation into recents may take a long time. */
    235     void prepareEnterRecentsAnimation(boolean isTaskViewLaunchTargetTask,
    236                                              boolean occludesLaunchTarget, int offscreenY) {
    237         int initialDim = getDim();
    238         if (mConfig.launchedHasConfigurationChanged) {
    239             // Just load the views as-is
    240         } else if (mConfig.launchedFromAppWithThumbnail) {
    241             if (isTaskViewLaunchTargetTask) {
    242                 // Set the dim to 0 so we can animate it in
    243                 initialDim = 0;
    244                 // Hide the action button
    245                 mActionButtonView.setAlpha(0f);
    246             } else if (occludesLaunchTarget) {
    247                 // Move the task view off screen (below) so we can animate it in
    248                 setTranslationY(offscreenY);
    249             }
    250 
    251         } else if (mConfig.launchedFromHome) {
    252             // Move the task view off screen (below) so we can animate it in
    253             setTranslationY(offscreenY);
    254             setTranslationZ(0);
    255             setScaleX(1f);
    256             setScaleY(1f);
    257         }
    258         // Apply the current dim
    259         setDim(initialDim);
    260         // Prepare the thumbnail view alpha
    261         mThumbnailView.prepareEnterRecentsAnimation(isTaskViewLaunchTargetTask);
    262     }
    263 
    264     /** Animates this task view as it enters recents */
    265     void startEnterRecentsAnimation(final ViewAnimation.TaskViewEnterContext ctx) {
    266         final TaskViewTransform transform = ctx.currentTaskTransform;
    267         int startDelay = 0;
    268 
    269         if (mConfig.launchedFromAppWithThumbnail) {
    270             if (mTask.isLaunchTarget) {
    271                 // Animate the dim/overlay
    272                 if (Constants.DebugFlags.App.EnableThumbnailAlphaOnFrontmost) {
    273                     // Animate the thumbnail alpha before the dim animation (to prevent updating the
    274                     // hardware layer)
    275                     mThumbnailView.startEnterRecentsAnimation(mConfig.transitionEnterFromAppDelay,
    276                             new Runnable() {
    277                                 @Override
    278                                 public void run() {
    279                                     animateDimToProgress(0, mConfig.taskViewEnterFromAppDuration,
    280                                             ctx.postAnimationTrigger.decrementOnAnimationEnd());
    281                                 }
    282                             });
    283                 } else {
    284                     // Immediately start the dim animation
    285                     animateDimToProgress(mConfig.transitionEnterFromAppDelay,
    286                             mConfig.taskViewEnterFromAppDuration,
    287                             ctx.postAnimationTrigger.decrementOnAnimationEnd());
    288                 }
    289                 ctx.postAnimationTrigger.increment();
    290 
    291                 // Animate the action button in
    292                 fadeInActionButton(mConfig.transitionEnterFromAppDelay,
    293                         mConfig.taskViewEnterFromAppDuration);
    294             } else {
    295                 // Animate the task up if it was occluding the launch target
    296                 if (ctx.currentTaskOccludesLaunchTarget) {
    297                     setTranslationY(transform.translationY + mConfig.taskViewAffiliateGroupEnterOffsetPx);
    298                     setAlpha(0f);
    299                     animate().alpha(1f)
    300                             .translationY(transform.translationY)
    301                             .setStartDelay(mConfig.transitionEnterFromAppDelay)
    302                             .setUpdateListener(null)
    303                             .setInterpolator(mConfig.fastOutSlowInInterpolator)
    304                             .setDuration(mConfig.taskViewEnterFromHomeDuration)
    305                             .withEndAction(new Runnable() {
    306                                 @Override
    307                                 public void run() {
    308                                     // Decrement the post animation trigger
    309                                     ctx.postAnimationTrigger.decrement();
    310                                 }
    311                             })
    312                             .start();
    313                     ctx.postAnimationTrigger.increment();
    314                 }
    315             }
    316             startDelay = mConfig.transitionEnterFromAppDelay;
    317 
    318         } else if (mConfig.launchedFromHome) {
    319             // Animate the tasks up
    320             int frontIndex = (ctx.currentStackViewCount - ctx.currentStackViewIndex - 1);
    321             int delay = mConfig.transitionEnterFromHomeDelay +
    322                     frontIndex * mConfig.taskViewEnterFromHomeStaggerDelay;
    323 
    324             setScaleX(transform.scale);
    325             setScaleY(transform.scale);
    326             if (!mConfig.fakeShadows) {
    327                 animate().translationZ(transform.translationZ);
    328             }
    329             animate()
    330                     .translationY(transform.translationY)
    331                     .setStartDelay(delay)
    332                     .setUpdateListener(ctx.updateListener)
    333                     .setInterpolator(mConfig.quintOutInterpolator)
    334                     .setDuration(mConfig.taskViewEnterFromHomeDuration +
    335                             frontIndex * mConfig.taskViewEnterFromHomeStaggerDelay)
    336                     .withEndAction(new Runnable() {
    337                         @Override
    338                         public void run() {
    339                             // Decrement the post animation trigger
    340                             ctx.postAnimationTrigger.decrement();
    341                         }
    342                     })
    343                     .start();
    344             ctx.postAnimationTrigger.increment();
    345             startDelay = delay;
    346         }
    347 
    348         // Enable the focus animations from this point onwards so that they aren't affected by the
    349         // window transitions
    350         postDelayed(new Runnable() {
    351             @Override
    352             public void run() {
    353                 enableFocusAnimations();
    354             }
    355         }, startDelay);
    356     }
    357 
    358     public void fadeInActionButton(int delay, int duration) {
    359         // Hide the action button
    360         mActionButtonView.setAlpha(0f);
    361 
    362         // Animate the action button in
    363         mActionButtonView.animate().alpha(1f)
    364                 .setStartDelay(delay)
    365                 .setDuration(duration)
    366                 .setInterpolator(PhoneStatusBar.ALPHA_IN)
    367                 .withLayer()
    368                 .start();
    369     }
    370 
    371     /** Animates this task view as it leaves recents by pressing home. */
    372     void startExitToHomeAnimation(ViewAnimation.TaskViewExitContext ctx) {
    373         animate()
    374                 .translationY(ctx.offscreenTranslationY)
    375                 .setStartDelay(0)
    376                 .setUpdateListener(null)
    377                 .setInterpolator(mConfig.fastOutLinearInInterpolator)
    378                 .setDuration(mConfig.taskViewExitToHomeDuration)
    379                 .withEndAction(ctx.postAnimationTrigger.decrementAsRunnable())
    380                 .start();
    381         ctx.postAnimationTrigger.increment();
    382     }
    383 
    384     /** Animates this task view as it exits recents */
    385     void startLaunchTaskAnimation(final Runnable postAnimRunnable, boolean isLaunchingTask,
    386             boolean occludesLaunchTarget, boolean lockToTask) {
    387         if (isLaunchingTask) {
    388             // Animate the thumbnail alpha back into full opacity for the window animation out
    389             mThumbnailView.startLaunchTaskAnimation(postAnimRunnable);
    390 
    391             // Animate the dim
    392             if (mDimAlpha > 0) {
    393                 ObjectAnimator anim = ObjectAnimator.ofInt(this, "dim", 0);
    394                 anim.setDuration(mConfig.taskViewExitToAppDuration);
    395                 anim.setInterpolator(mConfig.fastOutLinearInInterpolator);
    396                 anim.start();
    397             }
    398 
    399             // Animate the action button away
    400             if (!lockToTask) {
    401                 float toScale = 0.9f;
    402                 mActionButtonView.animate()
    403                         .scaleX(toScale)
    404                         .scaleY(toScale);
    405             }
    406             mActionButtonView.animate()
    407                     .alpha(0f)
    408                     .setStartDelay(0)
    409                     .setDuration(mConfig.taskViewExitToAppDuration)
    410                     .setInterpolator(mConfig.fastOutLinearInInterpolator)
    411                     .withLayer()
    412                     .start();
    413         } else {
    414             // Hide the dismiss button
    415             mHeaderView.startLaunchTaskDismissAnimation();
    416             // If this is another view in the task grouping and is in front of the launch task,
    417             // animate it away first
    418             if (occludesLaunchTarget) {
    419                 animate().alpha(0f)
    420                     .translationY(getTranslationY() + mConfig.taskViewAffiliateGroupEnterOffsetPx)
    421                     .setStartDelay(0)
    422                     .setUpdateListener(null)
    423                     .setInterpolator(mConfig.fastOutLinearInInterpolator)
    424                     .setDuration(mConfig.taskViewExitToAppDuration)
    425                     .start();
    426             }
    427         }
    428     }
    429 
    430     /** Animates the deletion of this task view */
    431     void startDeleteTaskAnimation(final Runnable r) {
    432         // Disabling clipping with the stack while the view is animating away
    433         setClipViewInStack(false);
    434 
    435         animate().translationX(mConfig.taskViewRemoveAnimTranslationXPx)
    436             .alpha(0f)
    437             .setStartDelay(0)
    438             .setUpdateListener(null)
    439             .setInterpolator(mConfig.fastOutSlowInInterpolator)
    440             .setDuration(mConfig.taskViewRemoveAnimDuration)
    441             .withEndAction(new Runnable() {
    442                 @Override
    443                 public void run() {
    444                     // We just throw this into a runnable because starting a view property
    445                     // animation using layers can cause inconsisten results if we try and
    446                     // update the layers while the animation is running.  In some cases,
    447                     // the runnabled passed in may start an animation which also uses layers
    448                     // so we defer all this by posting this.
    449                     r.run();
    450 
    451                     // Re-enable clipping with the stack (we will reuse this view)
    452                     setClipViewInStack(true);
    453                 }
    454             })
    455             .start();
    456     }
    457 
    458     /** Animates this task view if the user does not interact with the stack after a certain time. */
    459     void startNoUserInteractionAnimation() {
    460         mHeaderView.startNoUserInteractionAnimation();
    461     }
    462 
    463     /** Mark this task view that the user does has not interacted with the stack after a certain time. */
    464     void setNoUserInteractionState() {
    465         mHeaderView.setNoUserInteractionState();
    466     }
    467 
    468     /** Resets the state tracking that the user has not interacted with the stack after a certain time. */
    469     void resetNoUserInteractionState() {
    470         mHeaderView.resetNoUserInteractionState();
    471     }
    472 
    473     /** Dismisses this task. */
    474     void dismissTask() {
    475         // Animate out the view and call the callback
    476         final TaskView tv = this;
    477         startDeleteTaskAnimation(new Runnable() {
    478             @Override
    479             public void run() {
    480                 if (mCb != null) {
    481                     mCb.onTaskViewDismissed(tv);
    482                 }
    483             }
    484         });
    485     }
    486 
    487     /**
    488      * Returns whether this view should be clipped, or any views below should clip against this
    489      * view.
    490      */
    491     boolean shouldClipViewInStack() {
    492         return mClipViewInStack && (getVisibility() == View.VISIBLE);
    493     }
    494 
    495     /** Sets whether this view should be clipped, or clipped against. */
    496     void setClipViewInStack(boolean clip) {
    497         if (clip != mClipViewInStack) {
    498             mClipViewInStack = clip;
    499             if (mCb != null) {
    500                 mCb.onTaskViewClipStateChanged(this);
    501             }
    502         }
    503     }
    504 
    505     /** Sets the current task progress. */
    506     public void setTaskProgress(float p) {
    507         mTaskProgress = p;
    508         mViewBounds.setAlpha(p);
    509         updateDimFromTaskProgress();
    510     }
    511 
    512     /** Returns the current task progress. */
    513     public float getTaskProgress() {
    514         return mTaskProgress;
    515     }
    516 
    517     /** Returns the current dim. */
    518     public void setDim(int dim) {
    519         mDimAlpha = dim;
    520         if (mConfig.useHardwareLayers) {
    521             // Defer setting hardware layers if we have not yet measured, or there is no dim to draw
    522             if (getMeasuredWidth() > 0 && getMeasuredHeight() > 0) {
    523                 mDimColorFilter.setColor(Color.argb(mDimAlpha, 0, 0, 0));
    524                 mDimLayerPaint.setColorFilter(mDimColorFilter);
    525                 mContent.setLayerType(LAYER_TYPE_HARDWARE, mDimLayerPaint);
    526             }
    527         } else {
    528             float dimAlpha = mDimAlpha / 255.0f;
    529             if (mThumbnailView != null) {
    530                 mThumbnailView.setDimAlpha(dimAlpha);
    531             }
    532             if (mHeaderView != null) {
    533                 mHeaderView.setDimAlpha(dim);
    534             }
    535         }
    536     }
    537 
    538     /** Returns the current dim. */
    539     public int getDim() {
    540         return mDimAlpha;
    541     }
    542 
    543     /** Animates the dim to the task progress. */
    544     void animateDimToProgress(int delay, int duration, Animator.AnimatorListener postAnimRunnable) {
    545         // Animate the dim into view as well
    546         int toDim = getDimFromTaskProgress();
    547         if (toDim != getDim()) {
    548             ObjectAnimator anim = ObjectAnimator.ofInt(TaskView.this, "dim", toDim);
    549             anim.setStartDelay(delay);
    550             anim.setDuration(duration);
    551             if (postAnimRunnable != null) {
    552                 anim.addListener(postAnimRunnable);
    553             }
    554             anim.start();
    555         }
    556     }
    557 
    558     /** Compute the dim as a function of the scale of this view. */
    559     int getDimFromTaskProgress() {
    560         float dim = mMaxDimScale * mDimInterpolator.getInterpolation(1f - mTaskProgress);
    561         return (int) (dim * 255);
    562     }
    563 
    564     /** Update the dim as a function of the scale of this view. */
    565     void updateDimFromTaskProgress() {
    566         setDim(getDimFromTaskProgress());
    567     }
    568 
    569     /**** View focus state ****/
    570 
    571     /**
    572      * Sets the focused task explicitly. We need a separate flag because requestFocus() won't happen
    573      * if the view is not currently visible, or we are in touch state (where we still want to keep
    574      * track of focus).
    575      */
    576     public void setFocusedTask(boolean animateFocusedState) {
    577         mIsFocused = true;
    578         if (mFocusAnimationsEnabled) {
    579             // Focus the header bar
    580             mHeaderView.onTaskViewFocusChanged(true, animateFocusedState);
    581         }
    582         // Update the thumbnail alpha with the focus
    583         mThumbnailView.onFocusChanged(true);
    584         // Call the callback
    585         if (mCb != null) {
    586             mCb.onTaskViewFocusChanged(this, true);
    587         }
    588         // Workaround, we don't always want it focusable in touch mode, but we want the first task
    589         // to be focused after the enter-recents animation, which can be triggered from either touch
    590         // or keyboard
    591         setFocusableInTouchMode(true);
    592         requestFocus();
    593         setFocusableInTouchMode(false);
    594         invalidate();
    595     }
    596 
    597     /**
    598      * Unsets the focused task explicitly.
    599      */
    600     void unsetFocusedTask() {
    601         mIsFocused = false;
    602         if (mFocusAnimationsEnabled) {
    603             // Un-focus the header bar
    604             mHeaderView.onTaskViewFocusChanged(false, true);
    605         }
    606 
    607         // Update the thumbnail alpha with the focus
    608         mThumbnailView.onFocusChanged(false);
    609         // Call the callback
    610         if (mCb != null) {
    611             mCb.onTaskViewFocusChanged(this, false);
    612         }
    613         invalidate();
    614     }
    615 
    616     /**
    617      * Updates the explicitly focused state when the view focus changes.
    618      */
    619     @Override
    620     protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
    621         super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
    622         if (!gainFocus) {
    623             unsetFocusedTask();
    624         }
    625     }
    626 
    627     /**
    628      * Returns whether we have explicitly been focused.
    629      */
    630     public boolean isFocusedTask() {
    631         return mIsFocused || isFocused();
    632     }
    633 
    634     /** Enables all focus animations. */
    635     void enableFocusAnimations() {
    636         boolean wasFocusAnimationsEnabled = mFocusAnimationsEnabled;
    637         mFocusAnimationsEnabled = true;
    638         if (mIsFocused && !wasFocusAnimationsEnabled) {
    639             // Re-notify the header if we were focused and animations were not previously enabled
    640             mHeaderView.onTaskViewFocusChanged(true, true);
    641         }
    642     }
    643 
    644     /**** TaskCallbacks Implementation ****/
    645 
    646     /** Binds this task view to the task */
    647     public void onTaskBound(Task t) {
    648         mTask = t;
    649         mTask.setCallbacks(this);
    650 
    651         // Hide the action button if lock to app is disabled for this view
    652         int lockButtonVisibility = (!t.lockToTaskEnabled || !t.lockToThisTask) ? GONE : VISIBLE;
    653         if (mActionButtonView.getVisibility() != lockButtonVisibility) {
    654             mActionButtonView.setVisibility(lockButtonVisibility);
    655             requestLayout();
    656         }
    657     }
    658 
    659     @Override
    660     public void onTaskDataLoaded() {
    661         if (mThumbnailView != null && mHeaderView != null) {
    662             // Bind each of the views to the new task data
    663             mThumbnailView.rebindToTask(mTask);
    664             mHeaderView.rebindToTask(mTask);
    665             // Rebind any listeners
    666             mHeaderView.mApplicationIcon.setOnClickListener(this);
    667             mHeaderView.mDismissButton.setOnClickListener(this);
    668             mActionButtonView.setOnClickListener(this);
    669             if (Constants.DebugFlags.App.EnableDevAppInfoOnLongPress) {
    670                 if (mConfig.developerOptionsEnabled) {
    671                     mHeaderView.mApplicationIcon.setOnLongClickListener(this);
    672                 }
    673             }
    674         }
    675         mTaskDataLoaded = true;
    676     }
    677 
    678     @Override
    679     public void onTaskDataUnloaded() {
    680         if (mThumbnailView != null && mHeaderView != null) {
    681             // Unbind each of the views from the task data and remove the task callback
    682             mTask.setCallbacks(null);
    683             mThumbnailView.unbindFromTask();
    684             mHeaderView.unbindFromTask();
    685             // Unbind any listeners
    686             mHeaderView.mApplicationIcon.setOnClickListener(null);
    687             mHeaderView.mDismissButton.setOnClickListener(null);
    688             mActionButtonView.setOnClickListener(null);
    689             if (Constants.DebugFlags.App.EnableDevAppInfoOnLongPress) {
    690                 mHeaderView.mApplicationIcon.setOnLongClickListener(null);
    691             }
    692         }
    693         mTaskDataLoaded = false;
    694     }
    695 
    696     /** Enables/disables handling touch on this task view. */
    697     void setTouchEnabled(boolean enabled) {
    698         setOnClickListener(enabled ? this : null);
    699     }
    700 
    701     /**** View.OnClickListener Implementation ****/
    702 
    703     @Override
    704      public void onClick(final View v) {
    705         final TaskView tv = this;
    706         final boolean delayViewClick = (v != this) && (v != mActionButtonView);
    707         if (delayViewClick) {
    708             // We purposely post the handler delayed to allow for the touch feedback to draw
    709             postDelayed(new Runnable() {
    710                 @Override
    711                 public void run() {
    712                     if (Constants.DebugFlags.App.EnableTaskFiltering && v == mHeaderView.mApplicationIcon) {
    713                         if (mCb != null) {
    714                             mCb.onTaskViewAppIconClicked(tv);
    715                         }
    716                     } else if (v == mHeaderView.mDismissButton) {
    717                         dismissTask();
    718                     }
    719                 }
    720             }, 125);
    721         } else {
    722             if (v == mActionButtonView) {
    723                 // Reset the translation of the action button before we animate it out
    724                 mActionButtonView.setTranslationZ(0f);
    725             }
    726             if (mCb != null) {
    727                 mCb.onTaskViewClicked(tv, tv.getTask(), (v == mActionButtonView));
    728             }
    729         }
    730     }
    731 
    732     /**** View.OnLongClickListener Implementation ****/
    733 
    734     @Override
    735     public boolean onLongClick(View v) {
    736         if (v == mHeaderView.mApplicationIcon) {
    737             if (mCb != null) {
    738                 mCb.onTaskViewAppInfoClicked(this);
    739                 return true;
    740             }
    741         }
    742         return false;
    743     }
    744 }
    745