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