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