TaskView.java revision 81e0c8491f22c64300182c652ac2add96888dd2e
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        // Defer setting hardware layers if we have not yet measured, or there is no dim to draw
641        if (getMeasuredWidth() > 0 && getMeasuredHeight() > 0) {
642            if (mDimAnimator != null) {
643                mDimAnimator.removeAllListeners();
644                mDimAnimator.cancel();
645            }
646
647            int inverse = 255 - mDim;
648            mDimColorFilter.setColor(Color.argb(0xFF, inverse, inverse, inverse));
649            mLayerPaint.setColorFilter(mDimColorFilter);
650            setLayerType(LAYER_TYPE_HARDWARE, mLayerPaint);
651        }
652    }
653
654    /** Returns the current dim. */
655    public int getDim() {
656        return mDim;
657    }
658
659    /** Animates the dim to the task progress. */
660    void animateDimToProgress(int delay, int duration, Animator.AnimatorListener postAnimRunnable) {
661        // Animate the dim into view as well
662        int toDim = getDimFromTaskProgress();
663        if (toDim != getDim()) {
664            ObjectAnimator anim = ObjectAnimator.ofInt(TaskView.this, "dim", toDim);
665            anim.setStartDelay(delay);
666            anim.setDuration(duration);
667            if (postAnimRunnable != null) {
668                anim.addListener(postAnimRunnable);
669            }
670            anim.start();
671        }
672    }
673
674    /** Compute the dim as a function of the scale of this view. */
675    int getDimFromTaskProgress() {
676        float dim = mMaxDimScale * mDimInterpolator.getInterpolation(1f - mTaskProgress);
677        return (int) (dim * 255);
678    }
679
680    /** Update the dim as a function of the scale of this view. */
681    void updateDimFromTaskProgress() {
682        setDim(getDimFromTaskProgress());
683    }
684
685    /**** View focus state ****/
686
687    /**
688     * Sets the focused task explicitly. We need a separate flag because requestFocus() won't happen
689     * if the view is not currently visible, or we are in touch state (where we still want to keep
690     * track of focus).
691     */
692    public void setFocusedTask() {
693        mIsFocused = true;
694        if (mFocusAnimationsEnabled) {
695            // Focus the header bar
696            mHeaderView.onTaskViewFocusChanged(true);
697        }
698        // Update the thumbnail alpha with the focus
699        mThumbnailView.onFocusChanged(true);
700        // Call the callback
701        mCb.onTaskViewFocusChanged(this, true);
702        // Workaround, we don't always want it focusable in touch mode, but we want the first task
703        // to be focused after the enter-recents animation, which can be triggered from either touch
704        // or keyboard
705        setFocusableInTouchMode(true);
706        requestFocus();
707        setFocusableInTouchMode(false);
708        invalidate();
709    }
710
711    /**
712     * Unsets the focused task explicitly.
713     */
714    void unsetFocusedTask() {
715        mIsFocused = false;
716        if (mFocusAnimationsEnabled) {
717            // Un-focus the header bar
718            mHeaderView.onTaskViewFocusChanged(false);
719        }
720
721        // Update the thumbnail alpha with the focus
722        mThumbnailView.onFocusChanged(false);
723        // Call the callback
724        mCb.onTaskViewFocusChanged(this, false);
725        invalidate();
726    }
727
728    /**
729     * Updates the explicitly focused state when the view focus changes.
730     */
731    @Override
732    protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
733        super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
734        if (!gainFocus) {
735            unsetFocusedTask();
736        }
737    }
738
739    /**
740     * Returns whether we have explicitly been focused.
741     */
742    public boolean isFocusedTask() {
743        return mIsFocused || isFocused();
744    }
745
746    /** Enables all focus animations. */
747    void enableFocusAnimations() {
748        boolean wasFocusAnimationsEnabled = mFocusAnimationsEnabled;
749        mFocusAnimationsEnabled = true;
750        if (mIsFocused && !wasFocusAnimationsEnabled) {
751            // Re-notify the header if we were focused and animations were not previously enabled
752            mHeaderView.onTaskViewFocusChanged(true);
753        }
754    }
755
756    /**** TaskCallbacks Implementation ****/
757
758    /** Binds this task view to the task */
759    public void onTaskBound(Task t) {
760        mTask = t;
761        mTask.setCallbacks(this);
762        if (getMeasuredWidth() == 0) {
763            // If we haven't yet measured, we should just set the footer height with any animation
764            animateFooterVisibility(t.lockToThisTask, 0);
765        } else {
766            animateFooterVisibility(t.lockToThisTask, mConfig.taskViewLockToAppLongAnimDuration);
767        }
768        // Hide the action button if lock to app is disabled
769        if (!t.lockToTaskEnabled && mActionButtonView.getVisibility() != View.GONE) {
770            mActionButtonView.setVisibility(View.GONE);
771        }
772    }
773
774    @Override
775    public void onTaskDataLoaded() {
776        if (mThumbnailView != null && mHeaderView != null) {
777            // Bind each of the views to the new task data
778            if (mIsFullScreenView) {
779                mThumbnailView.bindToScreenshot(AlternateRecentsComponent.getLastScreenshot());
780            } else {
781                mThumbnailView.rebindToTask(mTask);
782            }
783            mHeaderView.rebindToTask(mTask);
784            // Rebind any listeners
785            mHeaderView.mApplicationIcon.setOnClickListener(this);
786            mHeaderView.mDismissButton.setOnClickListener(this);
787            if (mFooterView != null) {
788                mFooterView.setOnClickListener(this);
789            }
790            mActionButtonView.setOnClickListener(this);
791            if (Constants.DebugFlags.App.EnableDevAppInfoOnLongPress) {
792                if (mConfig.developerOptionsEnabled) {
793                    mHeaderView.mApplicationIcon.setOnLongClickListener(this);
794                }
795            }
796        }
797        mTaskDataLoaded = true;
798    }
799
800    @Override
801    public void onTaskDataUnloaded() {
802        if (mThumbnailView != null && mHeaderView != null) {
803            // Unbind each of the views from the task data and remove the task callback
804            mTask.setCallbacks(null);
805            mThumbnailView.unbindFromTask();
806            mHeaderView.unbindFromTask();
807            // Unbind any listeners
808            mHeaderView.mApplicationIcon.setOnClickListener(null);
809            mHeaderView.mDismissButton.setOnClickListener(null);
810            if (mFooterView != null) {
811                mFooterView.setOnClickListener(null);
812            }
813            mActionButtonView.setOnClickListener(null);
814            if (Constants.DebugFlags.App.EnableDevAppInfoOnLongPress) {
815                mHeaderView.mApplicationIcon.setOnLongClickListener(null);
816            }
817        }
818        mTaskDataLoaded = false;
819    }
820
821    /** Enables/disables handling touch on this task view. */
822    void setTouchEnabled(boolean enabled) {
823        setOnClickListener(enabled ? this : null);
824    }
825
826    /**** TaskViewFooter.TaskFooterViewCallbacks ****/
827
828    @Override
829    public void onTaskFooterHeightChanged(int height, int maxHeight) {
830        if (mIsFullScreenView) {
831            // Disable the bottom outline clip when fullscreen
832            mViewBounds.setOutlineClipBottom(0);
833        } else {
834            // Update the bottom clip in our outline provider
835            mViewBounds.setOutlineClipBottom(maxHeight - height);
836        }
837    }
838
839    /**** View.OnClickListener Implementation ****/
840
841    @Override
842     public void onClick(final View v) {
843        final TaskView tv = this;
844        final boolean delayViewClick = (v != this) && (v != mActionButtonView);
845        if (delayViewClick) {
846            // We purposely post the handler delayed to allow for the touch feedback to draw
847            postDelayed(new Runnable() {
848                @Override
849                public void run() {
850                    if (Constants.DebugFlags.App.EnableTaskFiltering && v == mHeaderView.mApplicationIcon) {
851                        mCb.onTaskViewAppIconClicked(tv);
852                    } else if (v == mHeaderView.mDismissButton) {
853                        dismissTask();
854                    }
855                }
856            }, 125);
857        } else {
858            if (v == mActionButtonView) {
859                // Reset the translation of the action button before we animate it out
860                mActionButtonView.setTranslationZ(0f);
861            }
862            mCb.onTaskViewClicked(tv, tv.getTask(),
863                    (v == mFooterView || v == mActionButtonView));
864        }
865    }
866
867    /**** View.OnLongClickListener Implementation ****/
868
869    @Override
870    public boolean onLongClick(View v) {
871        if (v == mHeaderView.mApplicationIcon) {
872            mCb.onTaskViewAppInfoClicked(this);
873            return true;
874        }
875        return false;
876    }
877}
878