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