TaskView.java revision 85cfec811e35025dbde54f4dc09fe0e1337c36b8
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.AnimatorListenerAdapter;
21import android.animation.ObjectAnimator;
22import android.animation.ValueAnimator;
23import android.content.Context;
24import android.graphics.Canvas;
25import android.graphics.Outline;
26import android.graphics.Paint;
27import android.graphics.Rect;
28import android.util.AttributeSet;
29import android.view.View;
30import android.view.ViewOutlineProvider;
31import android.view.ViewPropertyAnimator;
32import android.view.animation.AccelerateInterpolator;
33import android.widget.FrameLayout;
34import com.android.systemui.R;
35import com.android.systemui.recents.misc.Console;
36import com.android.systemui.recents.Constants;
37import com.android.systemui.recents.RecentsConfiguration;
38import com.android.systemui.recents.model.Task;
39import com.android.systemui.recents.model.TaskStack;
40
41
42/* A task view */
43public class TaskView extends FrameLayout implements Task.TaskCallbacks, View.OnClickListener,
44        View.OnLongClickListener {
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    }
53
54    RecentsConfiguration mConfig;
55
56    int mFooterHeight;
57    int mMaxFooterHeight;
58    ObjectAnimator mFooterAnimator;
59
60    int mDim;
61    int mMaxDim;
62    AccelerateInterpolator mDimInterpolator = new AccelerateInterpolator();
63
64    Task mTask;
65    boolean mTaskDataLoaded;
66    boolean mIsFocused;
67    boolean mIsStub;
68    boolean mClipViewInStack;
69    int mClipFromBottom;
70    Paint mLayerPaint = new Paint();
71
72    TaskThumbnailView mThumbnailView;
73    TaskBarView mBarView;
74    View mLockToAppButtonView;
75    TaskViewCallbacks mCb;
76
77    // Optimizations
78    ValueAnimator.AnimatorUpdateListener mUpdateDimListener =
79            new ValueAnimator.AnimatorUpdateListener() {
80                @Override
81                public void onAnimationUpdate(ValueAnimator animation) {
82                    updateDimOverlayFromScale();
83                }
84            };
85    Runnable mEnableThumbnailClip = new Runnable() {
86        @Override
87        public void run() {
88            mThumbnailView.updateTaskBarClip(mBarView);
89        }
90    };
91    Runnable mDisableThumbnailClip = new Runnable() {
92        @Override
93        public void run() {
94            mThumbnailView.disableClipTaskBarView();
95        }
96    };
97
98
99    public TaskView(Context context) {
100        this(context, null);
101    }
102
103    public TaskView(Context context, AttributeSet attrs) {
104        this(context, attrs, 0);
105    }
106
107    public TaskView(Context context, AttributeSet attrs, int defStyleAttr) {
108        this(context, attrs, defStyleAttr, 0);
109    }
110
111    public TaskView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
112        super(context, attrs, defStyleAttr, defStyleRes);
113        mConfig = RecentsConfiguration.getInstance();
114        mMaxFooterHeight = mConfig.taskViewLockToAppButtonHeight;
115        setWillNotDraw(false);
116        setClipToOutline(true);
117        setDim(getDim());
118        setFooterHeight(getFooterHeight());
119        setOutlineProvider(new ViewOutlineProvider() {
120            @Override
121            public boolean getOutline(View view, Outline outline) {
122                // The current height is measured with the footer, so account for the footer height
123                // and the current clip (in the stack)
124                int height = getMeasuredHeight() - mClipFromBottom - mMaxFooterHeight + mFooterHeight;
125                outline.setRoundRect(0, 0, getWidth(), height,
126                        mConfig.taskViewRoundedCornerRadiusPx);
127                return true;
128            }
129        });
130    }
131
132    @Override
133    protected void onFinishInflate() {
134        mMaxDim = mConfig.taskStackMaxDim;
135
136        // By default, all views are clipped to other views in their stack
137        mClipViewInStack = true;
138
139        // Bind the views
140        mBarView = (TaskBarView) findViewById(R.id.task_view_bar);
141        mThumbnailView = (TaskThumbnailView) findViewById(R.id.task_view_thumbnail);
142        mLockToAppButtonView = findViewById(R.id.lock_to_app);
143
144        if (mTaskDataLoaded) {
145            onTaskDataLoaded();
146        }
147    }
148
149    @Override
150    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
151        int width = MeasureSpec.getSize(widthMeasureSpec);
152        int height = MeasureSpec.getSize(heightMeasureSpec);
153
154        // Measure the bar view, thumbnail, and lock-to-app buttons
155        mBarView.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
156                MeasureSpec.makeMeasureSpec(mConfig.taskBarHeight, MeasureSpec.EXACTLY));
157        mLockToAppButtonView.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
158                MeasureSpec.makeMeasureSpec(mConfig.taskViewLockToAppButtonHeight,
159                        MeasureSpec.EXACTLY));
160        // Measure the thumbnail height to be the same as the width
161        mThumbnailView.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
162                MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY));
163        setMeasuredDimension(width, height);
164    }
165
166    /** Set callback */
167    void setCallbacks(TaskViewCallbacks cb) {
168        mCb = cb;
169    }
170
171    /** Gets the task */
172    Task getTask() {
173        return mTask;
174    }
175
176    /** Synchronizes this view's properties with the task's transform */
177    void updateViewPropertiesToTaskTransform(TaskViewTransform toTransform, int duration) {
178        // Update the bar view
179        mBarView.updateViewPropertiesToTaskTransform(toTransform, duration);
180
181        // Check to see if any properties have changed, and update the task view
182        if (duration > 0) {
183            ViewPropertyAnimator anim = animate();
184            boolean useLayers = false;
185
186            // Animate to the final state
187            if (toTransform.hasTranslationYChangedFrom(getTranslationY())) {
188                anim.translationY(toTransform.translationY);
189            }
190            if (Constants.DebugFlags.App.EnableShadows &&
191                    toTransform.hasTranslationZChangedFrom(getTranslationZ())) {
192                anim.translationZ(toTransform.translationZ);
193            }
194            if (toTransform.hasScaleChangedFrom(getScaleX())) {
195                anim.scaleX(toTransform.scale)
196                    .scaleY(toTransform.scale)
197                    .setUpdateListener(mUpdateDimListener);
198                useLayers = true;
199            }
200            if (toTransform.hasAlphaChangedFrom(getAlpha())) {
201                // Use layers if we animate alpha
202                anim.alpha(toTransform.alpha);
203                useLayers = true;
204            }
205            if (useLayers) {
206                anim.withLayer();
207            }
208            anim.setStartDelay(toTransform.startDelay)
209                .setDuration(duration)
210                .setInterpolator(mConfig.fastOutSlowInInterpolator)
211                .start();
212        } else {
213            // Set the changed properties
214            if (toTransform.hasTranslationYChangedFrom(getTranslationY())) {
215                setTranslationY(toTransform.translationY);
216            }
217            if (Constants.DebugFlags.App.EnableShadows &&
218                    toTransform.hasTranslationZChangedFrom(getTranslationZ())) {
219                setTranslationZ(toTransform.translationZ);
220            }
221            if (toTransform.hasScaleChangedFrom(getScaleX())) {
222                setScaleX(toTransform.scale);
223                setScaleY(toTransform.scale);
224                updateDimOverlayFromScale();
225            }
226            if (toTransform.hasAlphaChangedFrom(getAlpha())) {
227                setAlpha(toTransform.alpha);
228            }
229        }
230    }
231
232    /** Resets this view's properties */
233    void resetViewProperties() {
234        setTranslationX(0f);
235        setTranslationY(0f);
236        if (Constants.DebugFlags.App.EnableShadows) {
237            setTranslationZ(0f);
238        }
239        setScaleX(1f);
240        setScaleY(1f);
241        setAlpha(1f);
242        setDim(0);
243        invalidate();
244    }
245
246    /**
247     * When we are un/filtering, this method will set up the transform that we are animating to,
248     * in order to hide the task.
249     */
250    void prepareTaskTransformForFilterTaskHidden(TaskViewTransform toTransform) {
251        // Fade the view out and slide it away
252        toTransform.alpha = 0f;
253        toTransform.translationY += 200;
254        toTransform.translationZ = 0;
255    }
256
257    /**
258     * When we are un/filtering, this method will setup the transform that we are animating from,
259     * in order to show the task.
260     */
261    void prepareTaskTransformForFilterTaskVisible(TaskViewTransform fromTransform) {
262        // Fade the view in
263        fromTransform.alpha = 0f;
264    }
265
266    /** Prepares this task view for the enter-recents animations.  This is called earlier in the
267     * first layout because the actual animation into recents may take a long time. */
268    public void prepareEnterRecentsAnimation(boolean isTaskViewLaunchTargetTask, int offsetY,
269                                             int offscreenY) {
270        if (mConfig.launchedFromAppWithScreenshot) {
271            if (isTaskViewLaunchTargetTask) {
272                // Hide the task view as we are going to animate the full screenshot into view
273                // and then replace it with this view once we are done
274                setVisibility(View.INVISIBLE);
275                // Also hide the front most task bar view so we can animate it in
276                mBarView.prepareEnterRecentsAnimation();
277            } else {
278                // Top align the task views
279                setTranslationY(offsetY);
280                setScaleX(1f);
281                setScaleY(1f);
282            }
283
284        } else if (mConfig.launchedFromAppWithThumbnail) {
285            if (isTaskViewLaunchTargetTask) {
286                // Hide the front most task bar view so we can animate it in
287                mBarView.prepareEnterRecentsAnimation();
288                // Set the dim to 0 so we can animate it in
289                setDim(0);
290            }
291
292        } else if (mConfig.launchedFromHome) {
293            // Move the task view off screen (below) so we can animate it in
294            setTranslationY(offscreenY);
295            if (Constants.DebugFlags.App.EnableShadows) {
296                setTranslationZ(0);
297            }
298            setScaleX(1f);
299            setScaleY(1f);
300        }
301    }
302
303    /** Animates this task view as it enters recents */
304    public void startEnterRecentsAnimation(final ViewAnimation.TaskViewEnterContext ctx) {
305        TaskViewTransform transform = ctx.currentTaskTransform;
306
307        if (mConfig.launchedFromAppWithScreenshot) {
308            if (ctx.isCurrentTaskLaunchTarget) {
309                // Animate the full screenshot down first, before swapping with this task view
310                ctx.fullScreenshotView.animateOnEnterRecents(ctx, new Runnable() {
311                    @Override
312                    public void run() {
313                        // Animate the task bar of the first task view
314                        mBarView.startEnterRecentsAnimation(0, mEnableThumbnailClip);
315                        setVisibility(View.VISIBLE);
316                        // Animate the footer into view
317                        animateFooterVisibility(true, mConfig.taskBarEnterAnimDuration, 0);
318                        // Decrement the post animation trigger
319                        ctx.postAnimationTrigger.decrement();
320                    }
321                });
322            } else {
323                // Animate the tasks down behind the full screenshot
324                animate()
325                        .scaleX(transform.scale)
326                        .scaleY(transform.scale)
327                        .translationY(transform.translationY)
328                        .setStartDelay(0)
329                        .setUpdateListener(null)
330                        .setInterpolator(mConfig.linearOutSlowInInterpolator)
331                        .setDuration(475)
332                        .withLayer()
333                        .withEndAction(new Runnable() {
334                            @Override
335                            public void run() {
336                                mEnableThumbnailClip.run();
337                                // Decrement the post animation trigger
338                                ctx.postAnimationTrigger.decrement();
339                            }
340                        })
341                        .start();
342            }
343            ctx.postAnimationTrigger.increment();
344
345        } else if (mConfig.launchedFromAppWithThumbnail) {
346            if (ctx.isCurrentTaskLaunchTarget) {
347                // Animate the task bar of the first task view
348                mBarView.startEnterRecentsAnimation(mConfig.taskBarEnterAnimDelay, mEnableThumbnailClip);
349
350                // Animate the dim into view as well
351                ObjectAnimator anim = ObjectAnimator.ofInt(this, "dim", getDimOverlayFromScale());
352                anim.setStartDelay(mConfig.taskBarEnterAnimDelay);
353                anim.setDuration(mConfig.taskBarEnterAnimDuration);
354                anim.setInterpolator(mConfig.fastOutLinearInInterpolator);
355                anim.addListener(new AnimatorListenerAdapter() {
356                    @Override
357                    public void onAnimationEnd(Animator animation) {
358                        // Decrement the post animation trigger
359                        ctx.postAnimationTrigger.decrement();
360                    }
361                });
362                anim.start();
363                ctx.postAnimationTrigger.increment();
364
365                // Animate the footer into view
366                animateFooterVisibility(true, mConfig.taskBarEnterAnimDuration,
367                        mConfig.taskBarEnterAnimDelay);
368            } else {
369                mEnableThumbnailClip.run();
370            }
371
372        } else if (mConfig.launchedFromHome) {
373            // Animate the tasks up
374            int frontIndex = (ctx.currentStackViewCount - ctx.currentStackViewIndex - 1);
375            int delay = mConfig.taskBarEnterAnimDelay +
376                    frontIndex * mConfig.taskViewEnterFromHomeDelay;
377            if (Constants.DebugFlags.App.EnableShadows) {
378                animate().translationZ(transform.translationZ);
379            }
380            animate()
381                    .scaleX(transform.scale)
382                    .scaleY(transform.scale)
383                    .translationY(transform.translationY)
384                    .setStartDelay(delay)
385                    .setUpdateListener(null)
386                    .setInterpolator(mConfig.quintOutInterpolator)
387                    .setDuration(mConfig.taskViewEnterFromHomeDuration)
388                    .withLayer()
389                    .withEndAction(new Runnable() {
390                        @Override
391                        public void run() {
392                            mEnableThumbnailClip.run();
393                            // Decrement the post animation trigger
394                            ctx.postAnimationTrigger.decrement();
395                        }
396                    })
397                    .start();
398            ctx.postAnimationTrigger.increment();
399
400            // Animate the footer into view
401            animateFooterVisibility(true, mConfig.taskViewEnterFromHomeDuration,
402                    mConfig.taskBarEnterAnimDelay);
403        } else {
404            // Otherwise, just enable the thumbnail clip
405            mEnableThumbnailClip.run();
406
407            // Animate the footer into view
408            animateFooterVisibility(true, 0, 0);
409        }
410    }
411
412    /** Animates this task view as it leaves recents by pressing home. */
413    public void startExitToHomeAnimation(ViewAnimation.TaskViewExitContext ctx) {
414        animate()
415                .translationY(ctx.offscreenTranslationY)
416                .setStartDelay(0)
417                .setUpdateListener(null)
418                .setInterpolator(mConfig.fastOutLinearInInterpolator)
419                .setDuration(mConfig.taskViewExitToHomeDuration)
420                .withLayer()
421                .withEndAction(ctx.postAnimationTrigger.decrementAsRunnable())
422                .start();
423        ctx.postAnimationTrigger.increment();
424    }
425
426    /** Animates this task view as it exits recents */
427    public void startLaunchTaskAnimation(final Runnable r, boolean isLaunchingTask) {
428        if (isLaunchingTask) {
429            // Disable the thumbnail clip and animate the bar out
430            mBarView.startLaunchTaskAnimation(mDisableThumbnailClip, r);
431
432            // Animate the dim
433            if (mDim > 0) {
434                ObjectAnimator anim = ObjectAnimator.ofInt(this, "dim", 0);
435                anim.setDuration(mConfig.taskBarExitAnimDuration);
436                anim.setInterpolator(mConfig.fastOutLinearInInterpolator);
437                anim.start();
438            }
439        } else {
440            // Hide the dismiss button
441            mBarView.startLaunchTaskDismissAnimation();
442        }
443    }
444
445    /** Animates the deletion of this task view */
446    public void startDeleteTaskAnimation(final Runnable r) {
447        // Disabling clipping with the stack while the view is animating away
448        setClipViewInStack(false);
449
450        animate().translationX(mConfig.taskViewRemoveAnimTranslationXPx)
451            .alpha(0f)
452            .setStartDelay(0)
453            .setUpdateListener(null)
454            .setInterpolator(mConfig.fastOutSlowInInterpolator)
455            .setDuration(mConfig.taskViewRemoveAnimDuration)
456            .withLayer()
457            .withEndAction(new Runnable() {
458                @Override
459                public void run() {
460                    // We just throw this into a runnable because starting a view property
461                    // animation using layers can cause inconsisten results if we try and
462                    // update the layers while the animation is running.  In some cases,
463                    // the runnabled passed in may start an animation which also uses layers
464                    // so we defer all this by posting this.
465                    r.run();
466
467                    // Re-enable clipping with the stack (we will reuse this view)
468                    setClipViewInStack(true);
469                }
470            })
471            .start();
472    }
473
474    /** Animates this task view if the user does not interact with the stack after a certain time. */
475    public void startNoUserInteractionAnimation() {
476        mBarView.startNoUserInteractionAnimation();
477    }
478
479    /** Mark this task view that the user does has not interacted with the stack after a certain time. */
480    public void setNoUserInteractionState() {
481        mBarView.setNoUserInteractionState();
482    }
483
484    /** Enable the hw layers on this task view */
485    void enableHwLayers() {
486        mThumbnailView.setLayerType(View.LAYER_TYPE_HARDWARE, mLayerPaint);
487        mBarView.enableHwLayers();
488        mLockToAppButtonView.setLayerType(View.LAYER_TYPE_HARDWARE, mLayerPaint);
489    }
490
491    /** Disable the hw layers on this task view */
492    void disableHwLayers() {
493        mThumbnailView.setLayerType(View.LAYER_TYPE_NONE, mLayerPaint);
494        mBarView.disableHwLayers();
495        mLockToAppButtonView.setLayerType(View.LAYER_TYPE_NONE, mLayerPaint);
496    }
497
498    /** Sets the stubbed state of this task view.
499    void setStubState(boolean isStub) {
500        if (!mIsStub && isStub) {
501            // This is now a stub task view, so clip to the bar height, hide the thumbnail
502            setClipBounds(new Rect(0, 0, getMeasuredWidth(), mBarView.getMeasuredHeight()));
503            mThumbnailView.setVisibility(View.INVISIBLE);
504            // Temporary
505            mBarView.mActivityDescription.setText("Stub");
506        } else if (mIsStub && !isStub) {
507            setClipBounds(null);
508            mThumbnailView.setVisibility(View.VISIBLE);
509        }
510        mIsStub = isStub;
511    } */
512
513    /**
514     * Returns whether this view should be clipped, or any views below should clip against this
515     * view.
516     */
517    boolean shouldClipViewInStack() {
518        return mClipViewInStack && (getVisibility() == View.VISIBLE);
519    }
520
521    /** Sets whether this view should be clipped, or clipped against. */
522    void setClipViewInStack(boolean clip) {
523        if (clip != mClipViewInStack) {
524            mClipViewInStack = clip;
525            mCb.onTaskViewClipStateChanged(this);
526        }
527    }
528
529    void setClipFromBottom(int clipFromBottom) {
530        clipFromBottom = Math.max(0, Math.min(getMeasuredHeight(), clipFromBottom));
531        if (mClipFromBottom != clipFromBottom) {
532            mClipFromBottom = clipFromBottom;
533            invalidateOutline();
534        }
535    }
536
537    /** Sets the footer height. */
538    public void setFooterHeight(int footerHeight) {
539        if (footerHeight != mFooterHeight) {
540            mFooterHeight = footerHeight;
541            invalidateOutline();
542            invalidate(0, getMeasuredHeight() - mMaxFooterHeight, getMeasuredWidth(),
543                    getMeasuredHeight());
544        }
545    }
546
547    /** Gets the footer height. */
548    public int getFooterHeight() {
549        return mFooterHeight;
550    }
551
552    /** Animates the footer into and out of view. */
553    public void animateFooterVisibility(boolean visible, int duration, int delay) {
554        if (!mTask.canLockToTask) return;
555        if (mMaxFooterHeight <= 0) return;
556
557        if (mFooterAnimator != null) {
558            mFooterAnimator.removeAllListeners();
559            mFooterAnimator.cancel();
560        }
561        int height = visible ? mMaxFooterHeight : 0;
562        if (visible && mLockToAppButtonView.getVisibility() != View.VISIBLE) {
563            if (duration > 0) {
564                setFooterHeight(0);
565            } else {
566                setFooterHeight(mMaxFooterHeight);
567            }
568            mLockToAppButtonView.setVisibility(View.VISIBLE);
569        }
570        if (duration > 0) {
571            mFooterAnimator = ObjectAnimator.ofInt(this, "footerHeight", height);
572            mFooterAnimator.setDuration(duration);
573            mFooterAnimator.setInterpolator(mConfig.fastOutSlowInInterpolator);
574            if (!visible) {
575                mFooterAnimator.addListener(new AnimatorListenerAdapter() {
576                    @Override
577                    public void onAnimationEnd(Animator animation) {
578                        mLockToAppButtonView.setVisibility(View.INVISIBLE);
579                    }
580                });
581            }
582            mFooterAnimator.start();
583        } else {
584            if (!visible) {
585                mLockToAppButtonView.setVisibility(View.INVISIBLE);
586            }
587        }
588    }
589
590    /** Returns the current dim. */
591    public void setDim(int dim) {
592        mDim = dim;
593        postInvalidateOnAnimation();
594    }
595
596    /** Returns the current dim. */
597    public int getDim() {
598        return mDim;
599    }
600
601    /** Compute the dim as a function of the scale of this view. */
602    int getDimOverlayFromScale() {
603        float minScale = TaskStackViewLayoutAlgorithm.StackPeekMinScale;
604        float scaleRange = 1f - minScale;
605        float dim = (1f - getScaleX()) / scaleRange;
606        dim = mDimInterpolator.getInterpolation(Math.min(dim, 1f));
607        return Math.max(0, Math.min(mMaxDim, (int) (dim * 255)));
608    }
609
610    /** Update the dim as a function of the scale of this view. */
611    void updateDimOverlayFromScale() {
612        setDim(getDimOverlayFromScale());
613    }
614
615    @Override
616    public void draw(Canvas canvas) {
617        super.draw(canvas);
618
619        // Apply the dim if necessary
620        if (mDim > 0) {
621            canvas.drawColor(mDim << 24);
622        }
623    }
624
625    @Override
626    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
627        if (mIsStub && (child == mThumbnailView)) {
628            // Skip the thumbnail view if we are in stub mode
629            return false;
630        }
631        return super.drawChild(canvas, child, drawingTime);
632    }
633
634    /**
635     * Sets the focused task explicitly. We need a separate flag because requestFocus() won't happen
636     * if the view is not currently visible, or we are in touch state (where we still want to keep
637     * track of focus).
638     */
639    public void setFocusedTask() {
640        mIsFocused = true;
641        // Workaround, we don't always want it focusable in touch mode, but we want the first task
642        // to be focused after the enter-recents animation, which can be triggered from either touch
643        // or keyboard
644        setFocusableInTouchMode(true);
645        requestFocus();
646        setFocusableInTouchMode(false);
647        invalidate();
648    }
649
650    /**
651     * Updates the explicitly focused state when the view focus changes.
652     */
653    @Override
654    protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
655        super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
656        if (!gainFocus) {
657            mIsFocused = false;
658            invalidate();
659        }
660    }
661
662    /**
663     * Returns whether we have explicitly been focused.
664     */
665    public boolean isFocusedTask() {
666        return mIsFocused || isFocused();
667    }
668
669    /**** TaskCallbacks Implementation ****/
670
671    /** Binds this task view to the task */
672    public void onTaskBound(Task t) {
673        mTask = t;
674        mTask.setCallbacks(this);
675        if (getMeasuredWidth() == 0) {
676            // If we haven't yet measured, we should just set the footer height with any animation
677            animateFooterVisibility(t.canLockToTask, 0, 0);
678        } else {
679            animateFooterVisibility(t.canLockToTask, mConfig.taskViewLockToAppLongAnimDuration, 0);
680        }
681    }
682
683    @Override
684    public void onTaskDataLoaded() {
685        if (mThumbnailView != null && mBarView != null) {
686            // Bind each of the views to the new task data
687            mThumbnailView.rebindToTask(mTask);
688            mBarView.rebindToTask(mTask);
689            // Rebind any listeners
690            if (Constants.DebugFlags.App.EnableTaskFiltering) {
691                mBarView.mApplicationIcon.setOnClickListener(this);
692            }
693            mBarView.mDismissButton.setOnClickListener(this);
694            mLockToAppButtonView.setOnClickListener(this);
695            if (Constants.DebugFlags.App.EnableDevAppInfoOnLongPress) {
696                if (mConfig.developerOptionsEnabled) {
697                    mBarView.mApplicationIcon.setOnLongClickListener(this);
698                }
699            }
700        }
701        mTaskDataLoaded = true;
702    }
703
704    @Override
705    public void onTaskDataUnloaded() {
706        if (mThumbnailView != null && mBarView != null) {
707            // Unbind each of the views from the task data and remove the task callback
708            mTask.setCallbacks(null);
709            mThumbnailView.unbindFromTask();
710            mBarView.unbindFromTask();
711            // Unbind any listeners
712            if (Constants.DebugFlags.App.EnableTaskFiltering) {
713                mBarView.mApplicationIcon.setOnClickListener(null);
714            }
715            mBarView.mDismissButton.setOnClickListener(null);
716            mLockToAppButtonView.setOnClickListener(null);
717            if (Constants.DebugFlags.App.EnableDevAppInfoOnLongPress) {
718                mBarView.mApplicationIcon.setOnLongClickListener(null);
719            }
720        }
721        mTaskDataLoaded = false;
722    }
723
724    /** Enables/disables handling touch on this task view. */
725    void setTouchEnabled(boolean enabled) {
726        setOnClickListener(enabled ? this : null);
727    }
728
729    @Override
730    public void onClick(final View v) {
731        // We purposely post the handler delayed to allow for the touch feedback to draw
732        final TaskView tv = this;
733        postDelayed(new Runnable() {
734            @Override
735            public void run() {
736                if (v == mBarView.mApplicationIcon) {
737                    mCb.onTaskViewAppIconClicked(tv);
738                } else if (v == mBarView.mDismissButton) {
739                    // Animate out the view and call the callback
740                    startDeleteTaskAnimation(new Runnable() {
741                        @Override
742                        public void run() {
743                            mCb.onTaskViewDismissed(tv);
744                        }
745                    });
746                    // Hide the footer
747                    tv.animateFooterVisibility(false, mConfig.taskViewRemoveAnimDuration, 0);
748                } else if (v == tv || v == mLockToAppButtonView) {
749                    mCb.onTaskViewClicked(tv, tv.getTask(), (v == mLockToAppButtonView));
750                }
751            }
752        }, 125);
753    }
754
755    @Override
756    public boolean onLongClick(View v) {
757        if (v == mBarView.mApplicationIcon) {
758            mCb.onTaskViewAppInfoClicked(this);
759            return true;
760        }
761        return false;
762    }
763}
764