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