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