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