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