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