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