TaskView.java revision c9567c07dd4953f5c5e3821d30525e50ed8b9b29
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(false);
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(0)
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    }
252
253    /**
254     * When we are un/filtering, this method will setup the transform that we are animating from,
255     * in order to show the task.
256     */
257    void prepareTaskTransformForFilterTaskVisible(TaskViewTransform fromTransform) {
258        // Fade the view in
259        fromTransform.alpha = 0f;
260    }
261
262    /** Prepares this task view for the enter-recents animations.  This is called earlier in the
263     * first layout because the actual animation into recents may take a long time. */
264    public void prepareEnterRecentsAnimation(boolean isTaskViewFrontMost, int offsetY, int offscreenY,
265                                             Rect taskRect) {
266        if (mConfig.launchedFromAppWithScreenshot) {
267            if (isTaskViewFrontMost) {
268                // Hide the task view as we are going to animate the full screenshot into view
269                // and then replace it with this view once we are done
270                setVisibility(View.INVISIBLE);
271                // Also hide the front most task bar view so we can animate it in
272                mBarView.prepareEnterRecentsAnimation();
273            } else {
274                // Top align the task views
275                setTranslationY(offsetY);
276                setScaleX(1f);
277                setScaleY(1f);
278            }
279
280        } else if (mConfig.launchedFromAppWithThumbnail) {
281            if (isTaskViewFrontMost) {
282                // Hide the front most task bar view so we can animate it in
283                mBarView.prepareEnterRecentsAnimation();
284                // Set the dim to 0 so we can animate it in
285                setDim(0);
286            }
287
288        } else if (mConfig.launchedFromHome) {
289            // Move the task view off screen (below) so we can animate it in
290            setTranslationY(offscreenY);
291            setTranslationZ(0);
292            setScaleX(1f);
293            setScaleY(1f);
294        }
295    }
296
297    /** Animates this task view as it enters recents */
298    public void startEnterRecentsAnimation(ViewAnimation.TaskViewEnterContext ctx) {
299        TaskViewTransform transform = ctx.transform;
300
301        if (mConfig.launchedFromAppWithScreenshot) {
302            if (ctx.isFrontMost) {
303                // Animate the full screenshot down first, before swapping with this task view
304                ctx.fullScreenshot.animateOnEnterRecents(ctx, new Runnable() {
305                    @Override
306                    public void run() {
307                        // Animate the task bar of the first task view
308                        mBarView.startEnterRecentsAnimation(0, mEnableThumbnailClip);
309                        setVisibility(View.VISIBLE);
310                    }
311                });
312            } else {
313                // Animate the tasks down behind the full screenshot
314                animate()
315                        .scaleX(transform.scale)
316                        .scaleY(transform.scale)
317                        .translationY(transform.translationY)
318                        .setStartDelay(0)
319                        .setUpdateListener(null)
320                        .setInterpolator(mConfig.linearOutSlowInInterpolator)
321                        .setDuration(475)
322                        .withLayer()
323                        .withEndAction(mEnableThumbnailClip)
324                        .start();
325            }
326
327        } else if (mConfig.launchedFromAppWithThumbnail) {
328            if (ctx.isFrontMost) {
329                // Animate the task bar of the first task view
330                mBarView.startEnterRecentsAnimation(mConfig.taskBarEnterAnimDelay, mEnableThumbnailClip);
331
332                // Animate the dim into view as well
333                ObjectAnimator anim = ObjectAnimator.ofInt(this, "dim", getDimOverlayFromScale());
334                anim.setStartDelay(mConfig.taskBarEnterAnimDelay);
335                anim.setDuration(mConfig.taskBarEnterAnimDuration);
336                anim.setInterpolator(mConfig.fastOutLinearInInterpolator);
337                anim.start();
338            } else {
339                mEnableThumbnailClip.run();
340            }
341
342        } else if (mConfig.launchedFromHome) {
343            // Animate the tasks up
344            int frontIndex = (ctx.stackViewCount - ctx.stackViewIndex - 1);
345            int delay = mConfig.taskBarEnterAnimDelay +
346                    frontIndex * mConfig.taskViewEnterFromHomeDelay;
347            animate()
348                    .scaleX(transform.scale)
349                    .scaleY(transform.scale)
350                    .translationY(transform.translationY)
351                    .translationZ(transform.translationZ)
352                    .setStartDelay(delay)
353                    .setUpdateListener(null)
354                    .setInterpolator(mConfig.quintOutInterpolator)
355                    .setDuration(mConfig.taskViewEnterFromHomeDuration)
356                    .withLayer()
357                    .withEndAction(mEnableThumbnailClip)
358                    .start();
359        }
360    }
361
362    /** Animates this task view as it leaves recents by pressing home. */
363    public void startExitToHomeAnimation(ViewAnimation.TaskViewExitContext ctx) {
364        animate()
365                .translationY(ctx.offscreenTranslationY)
366                .setStartDelay(0)
367                .setUpdateListener(null)
368                .setInterpolator(mConfig.fastOutLinearInInterpolator)
369                .setDuration(mConfig.taskViewExitToHomeDuration)
370                .withLayer()
371                .withEndAction(ctx.postAnimationTrigger.decrementAsRunnable())
372                .start();
373        ctx.postAnimationTrigger.increment();
374    }
375
376    /** Animates this task view as it exits recents */
377    public void startLaunchTaskAnimation(final Runnable r, boolean isLaunchingTask) {
378        if (isLaunchingTask) {
379            // Disable the thumbnail clip and animate the bar out
380            mBarView.startLaunchTaskAnimation(mDisableThumbnailClip, r);
381
382            // Animate the dim
383            if (mDim > 0) {
384                ObjectAnimator anim = ObjectAnimator.ofInt(this, "dim", 0);
385                anim.setDuration(mConfig.taskBarExitAnimDuration);
386                anim.setInterpolator(mConfig.fastOutLinearInInterpolator);
387                anim.start();
388            }
389        } else {
390            // Hide the dismiss button
391            mBarView.startLaunchTaskDismissAnimation();
392        }
393    }
394
395    /** Animates the deletion of this task view */
396    public void startDeleteTaskAnimation(final Runnable r) {
397        // Disabling clipping with the stack while the view is animating away
398        setClipViewInStack(false);
399
400        animate().translationX(mConfig.taskViewRemoveAnimTranslationXPx)
401            .alpha(0f)
402            .setStartDelay(0)
403            .setUpdateListener(null)
404            .setInterpolator(mConfig.fastOutSlowInInterpolator)
405            .setDuration(mConfig.taskViewRemoveAnimDuration)
406            .withLayer()
407            .withEndAction(new Runnable() {
408                @Override
409                public void run() {
410                    // We just throw this into a runnable because starting a view property
411                    // animation using layers can cause inconsisten results if we try and
412                    // update the layers while the animation is running.  In some cases,
413                    // the runnabled passed in may start an animation which also uses layers
414                    // so we defer all this by posting this.
415                    r.run();
416
417                    // Re-enable clipping with the stack (we will reuse this view)
418                    setClipViewInStack(true);
419                }
420            })
421            .start();
422    }
423
424    /** Animates this task view if the user does not interact with the stack after a certain time. */
425    public void startNoUserInteractionAnimation() {
426        mBarView.startNoUserInteractionAnimation();
427    }
428
429    /** Mark this task view that the user does has not interacted with the stack after a certain time. */
430    public void setNoUserInteractionState() {
431        mBarView.setNoUserInteractionState();
432    }
433
434    /** Returns the rect we want to clip (it may not be the full rect) */
435    Rect getClippingRect(Rect outRect) {
436        getHitRect(outRect);
437        // XXX: We should get the hit rect of the thumbnail view and intersect, but this is faster
438        outRect.right = outRect.left + mThumbnailView.getRight();
439        outRect.bottom = outRect.top + mThumbnailView.getBottom();
440        return outRect;
441    }
442
443    /** Enable the hw layers on this task view */
444    void enableHwLayers() {
445        mThumbnailView.setLayerType(View.LAYER_TYPE_HARDWARE, mLayerPaint);
446        mBarView.enableHwLayers();
447    }
448
449    /** Disable the hw layers on this task view */
450    void disableHwLayers() {
451        mThumbnailView.setLayerType(View.LAYER_TYPE_NONE, mLayerPaint);
452        mBarView.disableHwLayers();
453    }
454
455    /**
456     * Returns whether this view should be clipped, or any views below should clip against this
457     * view.
458     */
459    boolean shouldClipViewInStack() {
460        return mClipViewInStack && (getVisibility() == View.VISIBLE);
461    }
462
463    /** Sets whether this view should be clipped, or clipped against. */
464    void setClipViewInStack(boolean clip) {
465        if (clip != mClipViewInStack) {
466            mClipViewInStack = clip;
467            if (getParent() instanceof View) {
468                getHitRect(mTmpRect);
469                ((View) getParent()).invalidate(mTmpRect);
470            }
471        }
472    }
473
474    /** Returns the current dim. */
475    public void setDim(int dim) {
476        mDim = dim;
477        postInvalidateOnAnimation();
478    }
479
480    /** Returns the current dim. */
481    public int getDim() {
482        return mDim;
483    }
484
485    /** Compute the dim as a function of the scale of this view. */
486    int getDimOverlayFromScale() {
487        float minScale = Constants.Values.TaskStackView.StackPeekMinScale;
488        float scaleRange = 1f - minScale;
489        float dim = (1f - getScaleX()) / scaleRange;
490        dim = mDimInterpolator.getInterpolation(Math.min(dim, 1f));
491        return Math.max(0, Math.min(mMaxDim, (int) (dim * 255)));
492    }
493
494    /** Update the dim as a function of the scale of this view. */
495    void updateDimOverlayFromScale() {
496        setDim(getDimOverlayFromScale());
497    }
498
499    @Override
500    public void draw(Canvas canvas) {
501        int restoreCount = canvas.save(Canvas.CLIP_SAVE_FLAG | Canvas.CLIP_TO_LAYER_SAVE_FLAG);
502        // Apply the rounded rect clip path on the whole view
503        canvas.clipPath(mRoundedRectClipPath);
504        super.draw(canvas);
505        canvas.restoreToCount(restoreCount);
506
507        // Apply the dim if necessary
508        if (mDim > 0) {
509            canvas.drawColor(mDim << 24);
510        }
511    }
512
513    /**
514     * Sets the focused task explicitly. We need a separate flag because requestFocus() won't happen
515     * if the view is not currently visible, or we are in touch state (where we still want to keep
516     * track of focus).
517     */
518    public void setFocusedTask() {
519        mIsFocused = true;
520        requestFocus();
521        invalidate();
522        mCb.onTaskFocused(this);
523    }
524
525    /**
526     * Updates the explicitly focused state when the view focus changes.
527     */
528    @Override
529    protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
530        super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
531        if (!gainFocus) {
532            mIsFocused = false;
533            invalidate();
534        }
535    }
536
537    /**
538     * Returns whether we have explicitly been focused.
539     */
540    public boolean isFocusedTask() {
541        return mIsFocused || isFocused();
542    }
543
544    /**** TaskCallbacks Implementation ****/
545
546    /** Binds this task view to the task */
547    public void onTaskBound(Task t) {
548        mTask = t;
549        mTask.setCallbacks(this);
550    }
551
552    @Override
553    public void onTaskDataLoaded(boolean reloadingTaskData) {
554        if (mThumbnailView != null && mBarView != null) {
555            // Bind each of the views to the new task data
556            mThumbnailView.rebindToTask(mTask, reloadingTaskData);
557            mBarView.rebindToTask(mTask, reloadingTaskData);
558            // Rebind any listeners
559            mBarView.mApplicationIcon.setOnClickListener(this);
560            mBarView.mDismissButton.setOnClickListener(this);
561            if (Constants.DebugFlags.App.EnableDevAppInfoOnLongPress) {
562                if (mConfig.developerOptionsEnabled) {
563                    mBarView.mApplicationIcon.setOnLongClickListener(this);
564                }
565            }
566        }
567        mTaskDataLoaded = true;
568    }
569
570    @Override
571    public void onTaskDataUnloaded() {
572        if (mThumbnailView != null && mBarView != null) {
573            // Unbind each of the views from the task data and remove the task callback
574            mTask.setCallbacks(null);
575            mThumbnailView.unbindFromTask();
576            mBarView.unbindFromTask();
577            // Unbind any listeners
578            mBarView.mApplicationIcon.setOnClickListener(null);
579            mBarView.mDismissButton.setOnClickListener(null);
580            if (Constants.DebugFlags.App.EnableDevAppInfoOnLongPress) {
581                mBarView.mApplicationIcon.setOnLongClickListener(null);
582            }
583        }
584        mTaskDataLoaded = false;
585    }
586
587    @Override
588    public void onClick(View v) {
589        if (v == mBarView.mApplicationIcon) {
590            mCb.onTaskIconClicked(this);
591        } else if (v == mBarView.mDismissButton) {
592            // Animate out the view and call the callback
593            final TaskView tv = this;
594            startDeleteTaskAnimation(new Runnable() {
595                @Override
596                public void run() {
597                    mCb.onTaskDismissed(tv);
598                }
599            });
600        }
601    }
602
603    @Override
604    public boolean onLongClick(View v) {
605        if (v == mBarView.mApplicationIcon) {
606            mCb.onTaskAppInfoClicked(this);
607            return true;
608        }
609        return false;
610    }
611}
612