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