TaskView.java revision b01ed681fe97ff5e98471c120ff9581a78db13c5
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.TimeInterpolator;
20import android.animation.ValueAnimator;
21import android.content.Context;
22import android.graphics.Canvas;
23import android.graphics.Outline;
24import android.graphics.Path;
25import android.graphics.Point;
26import android.graphics.Rect;
27import android.graphics.RectF;
28import android.util.AttributeSet;
29import android.view.MotionEvent;
30import android.view.View;
31import android.view.animation.AccelerateInterpolator;
32import android.widget.FrameLayout;
33import com.android.systemui.R;
34import com.android.systemui.recents.Constants;
35import com.android.systemui.recents.RecentsConfiguration;
36import com.android.systemui.recents.model.Task;
37
38
39/* A task view */
40public class TaskView extends FrameLayout implements Task.TaskCallbacks, View.OnClickListener,
41        View.OnLongClickListener {
42    /** The TaskView callbacks */
43    interface TaskViewCallbacks {
44        public void onTaskIconClicked(TaskView tv);
45        public void onTaskAppInfoClicked(TaskView tv);
46        public void onTaskFocused(TaskView tv);
47        public void onTaskDismissed(TaskView tv);
48
49        // public void onTaskViewReboundToTask(TaskView tv, Task t);
50    }
51
52    int mDim;
53    int mMaxDim;
54    TimeInterpolator mDimInterpolator = new AccelerateInterpolator();
55
56    Task mTask;
57    boolean mTaskDataLoaded;
58    boolean mIsFocused;
59    boolean mClipViewInStack;
60    Point mLastTouchDown = new Point();
61    Path mRoundedRectClipPath = new Path();
62
63    TaskThumbnailView mThumbnailView;
64    TaskBarView mBarView;
65    TaskViewCallbacks mCb;
66
67
68    public TaskView(Context context) {
69        this(context, null);
70    }
71
72    public TaskView(Context context, AttributeSet attrs) {
73        this(context, attrs, 0);
74    }
75
76    public TaskView(Context context, AttributeSet attrs, int defStyleAttr) {
77        this(context, attrs, defStyleAttr, 0);
78    }
79
80    public TaskView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
81        super(context, attrs, defStyleAttr, defStyleRes);
82        setWillNotDraw(false);
83    }
84
85    @Override
86    protected void onFinishInflate() {
87        RecentsConfiguration config = RecentsConfiguration.getInstance();
88        mMaxDim = config.taskStackMaxDim;
89
90        // By default, all views are clipped to other views in their stack
91        mClipViewInStack = true;
92
93        // Bind the views
94        mThumbnailView = (TaskThumbnailView) findViewById(R.id.task_view_thumbnail);
95        mBarView = (TaskBarView) findViewById(R.id.task_view_bar);
96
97        if (mTaskDataLoaded) {
98            onTaskDataLoaded(false);
99        }
100    }
101
102    @Override
103    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
104        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
105
106        // Update the rounded rect clip path
107        RecentsConfiguration config = RecentsConfiguration.getInstance();
108        float radius = config.taskViewRoundedCornerRadiusPx;
109        mRoundedRectClipPath.reset();
110        mRoundedRectClipPath.addRoundRect(new RectF(0, 0, getMeasuredWidth(), getMeasuredHeight()),
111                radius, radius, Path.Direction.CW);
112
113        // Update the outline
114        Outline o = new Outline();
115        o.setRoundRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), radius);
116        setOutline(o);
117    }
118
119    @Override
120    public boolean onInterceptTouchEvent(MotionEvent ev) {
121        switch (ev.getAction()) {
122            case MotionEvent.ACTION_DOWN:
123            case MotionEvent.ACTION_MOVE:
124                mLastTouchDown.set((int) ev.getX(), (int) ev.getY());
125                break;
126        }
127        return super.onInterceptTouchEvent(ev);
128    }
129
130    /** Set callback */
131    void setCallbacks(TaskViewCallbacks cb) {
132        mCb = cb;
133    }
134
135    /** Gets the task */
136    Task getTask() {
137        return mTask;
138    }
139
140    /** Synchronizes this view's properties with the task's transform */
141    void updateViewPropertiesToTaskTransform(TaskViewTransform animateFromTransform,
142                                             TaskViewTransform toTransform, int duration) {
143        RecentsConfiguration config = RecentsConfiguration.getInstance();
144        int minZ = config.taskViewTranslationZMinPx;
145        int incZ = config.taskViewTranslationZIncrementPx;
146
147        // Update the bar view
148        mBarView.updateViewPropertiesToTaskTransform(animateFromTransform, toTransform, duration);
149
150        // Update this task view
151        if (duration > 0) {
152            if (animateFromTransform != null) {
153                setTranslationY(animateFromTransform.translationY);
154                if (Constants.DebugFlags.App.EnableShadows) {
155                    setTranslationZ(Math.max(minZ, minZ + (animateFromTransform.t * incZ)));
156                }
157                setScaleX(animateFromTransform.scale);
158                setScaleY(animateFromTransform.scale);
159                setAlpha(animateFromTransform.alpha);
160            }
161            if (Constants.DebugFlags.App.EnableShadows) {
162                animate().translationZ(Math.max(minZ, minZ + (toTransform.t * incZ)));
163            }
164            animate().translationY(toTransform.translationY)
165                    .scaleX(toTransform.scale)
166                    .scaleY(toTransform.scale)
167                    .alpha(toTransform.alpha)
168                    .setDuration(duration)
169                    .setInterpolator(config.fastOutSlowInInterpolator)
170                    .withLayer()
171                    .setUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
172                        @Override
173                        public void onAnimationUpdate(ValueAnimator animation) {
174                            updateDimOverlayFromScale();
175                        }
176                    })
177                    .start();
178        } else {
179            setTranslationY(toTransform.translationY);
180            if (Constants.DebugFlags.App.EnableShadows) {
181                setTranslationZ(Math.max(minZ, minZ + (toTransform.t * incZ)));
182            }
183            setScaleX(toTransform.scale);
184            setScaleY(toTransform.scale);
185            setAlpha(toTransform.alpha);
186        }
187        updateDimOverlayFromScale();
188        invalidate();
189    }
190
191    /** Resets this view's properties */
192    void resetViewProperties() {
193        setTranslationX(0f);
194        setTranslationY(0f);
195        if (Constants.DebugFlags.App.EnableShadows) {
196            setTranslationZ(0f);
197        }
198        setScaleX(1f);
199        setScaleY(1f);
200        setAlpha(1f);
201        invalidate();
202    }
203
204    /**
205     * When we are un/filtering, this method will set up the transform that we are animating to,
206     * in order to hide the task.
207     */
208    void prepareTaskTransformForFilterTaskHidden(TaskViewTransform toTransform) {
209        // Fade the view out and slide it away
210        toTransform.alpha = 0f;
211        toTransform.translationY += 200;
212    }
213
214    /**
215     * When we are un/filtering, this method will setup the transform that we are animating from,
216     * in order to show the task.
217     */
218    void prepareTaskTransformForFilterTaskVisible(TaskViewTransform fromTransform) {
219        // Fade the view in
220        fromTransform.alpha = 0f;
221    }
222
223    /** Prepares this task view for the enter-recents animations.  This is called earlier in the
224     * first layout because the actual animation into recents may take a long time. */
225    public void prepareAnimateOnEnterRecents() {
226        mBarView.setVisibility(View.INVISIBLE);
227    }
228
229    /** Animates this task view as it enters recents */
230    public void animateOnEnterRecents() {
231        RecentsConfiguration config = RecentsConfiguration.getInstance();
232        mBarView.setVisibility(View.VISIBLE);
233        mBarView.setTranslationY(-mBarView.getMeasuredHeight());
234        mBarView.animate()
235                .translationY(0)
236                .setStartDelay(200)
237                .setInterpolator(config.fastOutSlowInInterpolator)
238                .setDuration(config.taskBarEnterAnimDuration)
239                .withLayer()
240                .start();
241    }
242
243    /** Animates this task view as it exits recents */
244    public void animateOnLeavingRecents(final Runnable r) {
245        RecentsConfiguration config = RecentsConfiguration.getInstance();
246        mBarView.animate()
247            .translationY(-mBarView.getMeasuredHeight())
248            .setStartDelay(0)
249            .setInterpolator(config.fastOutLinearInInterpolator)
250            .setDuration(config.taskBarExitAnimDuration)
251            .withLayer()
252            .withEndAction(new Runnable() {
253                @Override
254                public void run() {
255                    post(r);
256                }
257            })
258            .start();
259    }
260
261    /** Animates the deletion of this task view */
262    public void animateRemoval(final Runnable r) {
263        // Disabling clipping with the stack while the view is animating away
264        setClipViewInStack(false);
265
266        RecentsConfiguration config = RecentsConfiguration.getInstance();
267        animate().translationX(config.taskViewRemoveAnimTranslationXPx)
268            .alpha(0f)
269            .setStartDelay(0)
270            .setInterpolator(config.fastOutSlowInInterpolator)
271            .setDuration(config.taskViewRemoveAnimDuration)
272            .withLayer()
273            .withEndAction(new Runnable() {
274                @Override
275                public void run() {
276                    post(r);
277
278                    // Re-enable clipping with the stack (we will reuse this view)
279                    setClipViewInStack(false);
280                }
281            })
282            .start();
283    }
284
285    /** Returns the rect we want to clip (it may not be the full rect) */
286    Rect getClippingRect(Rect outRect) {
287        getHitRect(outRect);
288        // XXX: We should get the hit rect of the thumbnail view and intersect, but this is faster
289        outRect.right = outRect.left + mThumbnailView.getRight();
290        outRect.bottom = outRect.top + mThumbnailView.getBottom();
291        return outRect;
292    }
293
294    /** Enable the hw layers on this task view */
295    void enableHwLayers() {
296        mThumbnailView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
297    }
298
299    /** Disable the hw layers on this task view */
300    void disableHwLayers() {
301        mThumbnailView.setLayerType(View.LAYER_TYPE_NONE, null);
302    }
303
304    /**
305     * Returns whether this view should be clipped, or any views below should clip against this
306     * view.
307     */
308    boolean shouldClipViewInStack() {
309        return mClipViewInStack;
310    }
311
312    /** Sets whether this view should be clipped, or clipped against. */
313    void setClipViewInStack(boolean clip) {
314        if (clip != mClipViewInStack) {
315            mClipViewInStack = clip;
316            if (getParent() instanceof View) {
317                Rect r = new Rect();
318                getHitRect(r);
319                ((View) getParent()).invalidate(r);
320            }
321        }
322    }
323
324    /** Update the dim as a function of the scale of this view. */
325    void updateDimOverlayFromScale() {
326        float minScale = Constants.Values.TaskStackView.StackPeekMinScale;
327        float scaleRange = 1f - minScale;
328        float dim = (1f - getScaleX()) / scaleRange;
329        dim = mDimInterpolator.getInterpolation(Math.min(dim, 1f));
330        mDim = Math.max(0, Math.min(mMaxDim, (int) (dim * 255)));
331    }
332
333    @Override
334    public void draw(Canvas canvas) {
335        // Apply the rounded rect clip path on the whole view
336        canvas.clipPath(mRoundedRectClipPath);
337
338        super.draw(canvas);
339
340        // Apply the dim if necessary
341        if (mDim > 0) {
342            canvas.drawColor(mDim << 24);
343        }
344    }
345
346    /**
347     * Sets the focused task explicitly. We need a separate flag because requestFocus() won't happen
348     * if the view is not currently visible, or we are in touch state (where we still want to keep
349     * track of focus).
350     */
351    public void setFocusedTask() {
352        mIsFocused = true;
353        requestFocus();
354        invalidate();
355        mCb.onTaskFocused(this);
356    }
357
358    /**
359     * Updates the explicitly focused state when the view focus changes.
360     */
361    @Override
362    protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
363        super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
364        if (!gainFocus) {
365            mIsFocused = false;
366            invalidate();
367        }
368    }
369
370    /**
371     * Returns whether we have explicitly been focused.
372     */
373    public boolean isFocusedTask() {
374        return mIsFocused || isFocused();
375    }
376
377    /**** TaskCallbacks Implementation ****/
378
379    /** Binds this task view to the task */
380    public void onTaskBound(Task t) {
381        mTask = t;
382        mTask.setCallbacks(this);
383    }
384
385    @Override
386    public void onTaskDataLoaded(boolean reloadingTaskData) {
387        if (mThumbnailView != null && mBarView != null) {
388            // Bind each of the views to the new task data
389            mThumbnailView.rebindToTask(mTask, reloadingTaskData);
390            mBarView.rebindToTask(mTask, reloadingTaskData);
391            // Rebind any listeners
392            mBarView.mApplicationIcon.setOnClickListener(this);
393            mBarView.mDismissButton.setOnClickListener(this);
394            if (Constants.DebugFlags.App.EnableDevAppInfoOnLongPress) {
395                RecentsConfiguration config = RecentsConfiguration.getInstance();
396                if (config.developerOptionsEnabled) {
397                    mBarView.mApplicationIcon.setOnLongClickListener(this);
398                }
399            }
400        }
401        mTaskDataLoaded = true;
402    }
403
404    @Override
405    public void onTaskDataUnloaded() {
406        if (mThumbnailView != null && mBarView != null) {
407            // Unbind each of the views from the task data and remove the task callback
408            mTask.setCallbacks(null);
409            mThumbnailView.unbindFromTask();
410            mBarView.unbindFromTask();
411            // Unbind any listeners
412            mBarView.mApplicationIcon.setOnClickListener(null);
413            mBarView.mDismissButton.setOnClickListener(null);
414            if (Constants.DebugFlags.App.EnableDevAppInfoOnLongPress) {
415                mBarView.mApplicationIcon.setOnLongClickListener(null);
416            }
417        }
418        mTaskDataLoaded = false;
419    }
420
421    @Override
422    public void onClick(View v) {
423        if (v == mBarView.mApplicationIcon) {
424            mCb.onTaskIconClicked(this);
425        } else if (v == mBarView.mDismissButton) {
426            // Animate out the view and call the callback
427            final TaskView tv = this;
428            animateRemoval(new Runnable() {
429                @Override
430                public void run() {
431                    mCb.onTaskDismissed(tv);
432                }
433            });
434        }
435    }
436
437    @Override
438    public boolean onLongClick(View v) {
439        if (v == mBarView.mApplicationIcon) {
440            mCb.onTaskAppInfoClicked(this);
441            return true;
442        }
443        return false;
444    }
445}
446