TaskView.java revision 133ad44269e4b45e056793b579a7628aa4d91ccb
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.Animator;
20import android.animation.AnimatorSet;
21import android.animation.ObjectAnimator;
22import android.animation.ValueAnimator;
23import android.content.Context;
24import android.content.res.Resources;
25import android.graphics.Color;
26import android.graphics.Outline;
27import android.graphics.Paint;
28import android.graphics.Point;
29import android.graphics.PorterDuff;
30import android.graphics.PorterDuffColorFilter;
31import android.graphics.Rect;
32import android.util.AttributeSet;
33import android.util.FloatProperty;
34import android.util.IntProperty;
35import android.util.Property;
36import android.view.MotionEvent;
37import android.view.View;
38import android.view.ViewOutlineProvider;
39import android.view.animation.AccelerateInterpolator;
40
41import com.android.systemui.Interpolators;
42import com.android.systemui.R;
43import com.android.systemui.recents.Recents;
44import com.android.systemui.recents.RecentsActivity;
45import com.android.systemui.recents.RecentsConfiguration;
46import com.android.systemui.recents.events.EventBus;
47import com.android.systemui.recents.events.activity.LaunchTaskEvent;
48import com.android.systemui.recents.events.ui.DismissTaskViewEvent;
49import com.android.systemui.recents.events.ui.TaskViewDismissedEvent;
50import com.android.systemui.recents.events.ui.dragndrop.DragEndEvent;
51import com.android.systemui.recents.events.ui.dragndrop.DragStartEvent;
52import com.android.systemui.recents.misc.ReferenceCountedTrigger;
53import com.android.systemui.recents.misc.SystemServicesProxy;
54import com.android.systemui.recents.misc.Utilities;
55import com.android.systemui.recents.model.Task;
56import com.android.systemui.recents.model.TaskStack;
57
58import java.util.ArrayList;
59
60import static android.app.ActivityManager.StackId.INVALID_STACK_ID;
61
62/**
63 * A {@link TaskView} represents a fixed view of a task. Because the TaskView's layout is directed
64 * solely by the {@link TaskStackView}, we make it a fixed size layout which allows relayouts down
65 * the view hierarchy, but not upwards from any of its children (the TaskView will relayout itself
66 * with the previous bounds if any child requests layout).
67 */
68public class TaskView extends FixedSizeFrameLayout implements Task.TaskCallbacks,
69        TaskStackAnimationHelper.Callbacks, View.OnClickListener, View.OnLongClickListener {
70
71    /** The TaskView callbacks */
72    interface TaskViewCallbacks {
73        void onTaskViewClipStateChanged(TaskView tv);
74    }
75
76    /**
77     * The dim overlay is generally calculated from the task progress, but occasionally (like when
78     * launching) needs to be animated independently of the task progress.
79     */
80    public static final Property<TaskView, Integer> DIM =
81            new IntProperty<TaskView>("dim") {
82                @Override
83                public void setValue(TaskView tv, int dim) {
84                    tv.setDim(dim);
85                }
86
87                @Override
88                public Integer get(TaskView tv) {
89                    return tv.getDim();
90                }
91            };
92
93    public static final Property<TaskView, Float> TASK_PROGRESS =
94            new FloatProperty<TaskView>("taskProgress") {
95                @Override
96                public void setValue(TaskView tv, float p) {
97                    tv.setTaskProgress(p);
98                }
99
100                @Override
101                public Float get(TaskView tv) {
102                    return tv.getTaskProgress();
103                }
104            };
105
106    float mTaskProgress;
107    float mMaxDimScale;
108    int mDimAlpha;
109    AccelerateInterpolator mDimInterpolator = new AccelerateInterpolator(3f);
110    PorterDuffColorFilter mDimColorFilter = new PorterDuffColorFilter(0, PorterDuff.Mode.SRC_ATOP);
111    Paint mDimLayerPaint = new Paint();
112    float mActionButtonTranslationZ;
113
114    Task mTask;
115    boolean mTaskDataLoaded;
116    boolean mClipViewInStack = true;
117    AnimateableViewBounds mViewBounds;
118
119    private AnimatorSet mTransformAnimation;
120    private ArrayList<Animator> mTmpAnimators = new ArrayList<>();
121
122    View mContent;
123    TaskViewThumbnail mThumbnailView;
124    TaskViewHeader mHeaderView;
125    View mActionButtonView;
126    TaskViewCallbacks mCb;
127
128    Point mDownTouchPos = new Point();
129
130    public TaskView(Context context) {
131        this(context, null);
132    }
133
134    public TaskView(Context context, AttributeSet attrs) {
135        this(context, attrs, 0);
136    }
137
138    public TaskView(Context context, AttributeSet attrs, int defStyleAttr) {
139        this(context, attrs, defStyleAttr, 0);
140    }
141
142    public TaskView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
143        super(context, attrs, defStyleAttr, defStyleRes);
144        RecentsConfiguration config = Recents.getConfiguration();
145        Resources res = context.getResources();
146        mMaxDimScale = res.getInteger(R.integer.recents_max_task_stack_view_dim) / 255f;
147        mViewBounds = new AnimateableViewBounds(this, res.getDimensionPixelSize(
148                R.dimen.recents_task_view_rounded_corners_radius));
149        if (config.fakeShadows) {
150            setBackground(new FakeShadowDrawable(res, config));
151        }
152        setOutlineProvider(mViewBounds);
153        setOnLongClickListener(this);
154    }
155
156    /** Set callback */
157    void setCallbacks(TaskViewCallbacks cb) {
158        mCb = cb;
159    }
160
161    /** Resets this TaskView for reuse. */
162    void reset() {
163        resetViewProperties();
164        resetNoUserInteractionState();
165        setClipViewInStack(false);
166        setCallbacks(null);
167    }
168
169    /** Gets the task */
170    public Task getTask() {
171        return mTask;
172    }
173
174    /** Returns the view bounds. */
175    AnimateableViewBounds getViewBounds() {
176        return mViewBounds;
177    }
178
179    @Override
180    protected void onFinishInflate() {
181        // Bind the views
182        mContent = findViewById(R.id.task_view_content);
183        mHeaderView = (TaskViewHeader) findViewById(R.id.task_view_bar);
184        mThumbnailView = (TaskViewThumbnail) findViewById(R.id.task_view_thumbnail);
185        mActionButtonView = findViewById(R.id.lock_to_app_fab);
186        mActionButtonView.setOutlineProvider(new ViewOutlineProvider() {
187            @Override
188            public void getOutline(View view, Outline outline) {
189                // Set the outline to match the FAB background
190                outline.setOval(0, 0, mActionButtonView.getWidth(), mActionButtonView.getHeight());
191                outline.setAlpha(0.35f);
192            }
193        });
194        mActionButtonView.setOnClickListener(this);
195        mActionButtonTranslationZ = mActionButtonView.getTranslationZ();
196    }
197
198    @Override
199    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
200        super.onSizeChanged(w, h, oldw, oldh);
201        if (w > 0 && h > 0) {
202            mHeaderView.onTaskViewSizeChanged(w, h);
203            mThumbnailView.onTaskViewSizeChanged(w, h);
204        }
205    }
206
207    @Override
208    public boolean hasOverlappingRendering() {
209        return false;
210    }
211
212    @Override
213    public boolean onInterceptTouchEvent(MotionEvent ev) {
214        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
215            mDownTouchPos.set((int) (ev.getX() * getScaleX()), (int) (ev.getY() * getScaleY()));
216        }
217        return super.onInterceptTouchEvent(ev);
218    }
219
220
221    @Override
222    protected void measureContents(int width, int height) {
223        int widthWithoutPadding = width - mPaddingLeft - mPaddingRight;
224        int heightWithoutPadding = height - mPaddingTop - mPaddingBottom;
225
226        // Measure the content
227        mContent.measure(MeasureSpec.makeMeasureSpec(widthWithoutPadding, MeasureSpec.EXACTLY),
228                MeasureSpec.makeMeasureSpec(heightWithoutPadding, MeasureSpec.EXACTLY));
229
230        // Optimization: Prevent overdraw of the thumbnail under the header view
231        mThumbnailView.updateClipToTaskBar(mHeaderView);
232
233        setMeasuredDimension(width, height);
234    }
235
236    void updateViewPropertiesToTaskTransform(TaskViewTransform toTransform,
237            TaskViewAnimation toAnimation, ValueAnimator.AnimatorUpdateListener updateCallback) {
238        RecentsConfiguration config = Recents.getConfiguration();
239        Utilities.cancelAnimationWithoutCallbacks(mTransformAnimation);
240
241        // Compose the animations for the transform
242        mTmpAnimators.clear();
243        toTransform.applyToTaskView(this, mTmpAnimators, toAnimation, !config.fakeShadows);
244        if (toAnimation.isImmediate()) {
245            if (Float.compare(getTaskProgress(), toTransform.p) != 0) {
246                setTaskProgress(toTransform.p);
247            }
248            // Manually call back to the animator listener and update callback
249            if (toAnimation.listener != null) {
250                toAnimation.listener.onAnimationEnd(null);
251            }
252            if (updateCallback != null) {
253                updateCallback.onAnimationUpdate(null);
254            }
255        } else {
256            if (Float.compare(getTaskProgress(), toTransform.p) != 0) {
257                mTmpAnimators.add(ObjectAnimator.ofFloat(this, TASK_PROGRESS, getTaskProgress(),
258                        toTransform.p));
259            }
260            if (updateCallback != null) {
261                ValueAnimator updateCallbackAnim = ValueAnimator.ofInt(0, 1);
262                updateCallbackAnim.addUpdateListener(updateCallback);
263                mTmpAnimators.add(updateCallbackAnim);
264            }
265
266            // Create the animator
267            mTransformAnimation = toAnimation.createAnimator(mTmpAnimators);
268            mTransformAnimation.start();
269        }
270    }
271
272    /** Resets this view's properties */
273    void resetViewProperties() {
274        Utilities.cancelAnimationWithoutCallbacks(mTransformAnimation);
275        setDim(0);
276        setVisibility(View.VISIBLE);
277        getViewBounds().reset();
278        getHeaderView().reset();
279        TaskViewTransform.reset(this);
280
281        mActionButtonView.setScaleX(1f);
282        mActionButtonView.setScaleY(1f);
283        mActionButtonView.setAlpha(0f);
284        mActionButtonView.setTranslationZ(mActionButtonTranslationZ);
285    }
286
287    /**
288     * Cancels any current transform animations.
289     */
290    public void cancelTransformAnimation() {
291        Utilities.cancelAnimationWithoutCallbacks(mTransformAnimation);
292    }
293
294    /** Enables/disables handling touch on this task view. */
295    void setTouchEnabled(boolean enabled) {
296        setOnClickListener(enabled ? this : null);
297    }
298
299    /** Animates this task view if the user does not interact with the stack after a certain time. */
300    void startNoUserInteractionAnimation() {
301        mHeaderView.startNoUserInteractionAnimation();
302    }
303
304    /** Mark this task view that the user does has not interacted with the stack after a certain time. */
305    void setNoUserInteractionState() {
306        mHeaderView.setNoUserInteractionState();
307    }
308
309    /** Resets the state tracking that the user has not interacted with the stack after a certain time. */
310    void resetNoUserInteractionState() {
311        mHeaderView.resetNoUserInteractionState();
312    }
313
314    /** Dismisses this task. */
315    void dismissTask() {
316        // Animate out the view and call the callback
317        final TaskView tv = this;
318        DismissTaskViewEvent dismissEvent = new DismissTaskViewEvent(tv, mTask);
319        dismissEvent.addPostAnimationCallback(new Runnable() {
320            @Override
321            public void run() {
322                EventBus.getDefault().send(new TaskViewDismissedEvent(mTask, tv));
323            }
324        });
325        EventBus.getDefault().send(dismissEvent);
326    }
327
328    /**
329     * Returns whether this view should be clipped, or any views below should clip against this
330     * view.
331     */
332    boolean shouldClipViewInStack() {
333        // Never clip for freeform tasks or if invisible
334        if (mTask.isFreeformTask() || getVisibility() != View.VISIBLE) {
335            return false;
336        }
337        return mClipViewInStack;
338    }
339
340    /** Sets whether this view should be clipped, or clipped against. */
341    void setClipViewInStack(boolean clip) {
342        if (clip != mClipViewInStack) {
343            mClipViewInStack = clip;
344            if (mCb != null) {
345                mCb.onTaskViewClipStateChanged(this);
346            }
347        }
348    }
349
350    /** Sets the current task progress. */
351    public void setTaskProgress(float p) {
352        mTaskProgress = p;
353        mViewBounds.setAlpha(p);
354        updateDimFromTaskProgress();
355    }
356
357    public TaskViewHeader getHeaderView() {
358        return mHeaderView;
359    }
360
361    /** Returns the current task progress. */
362    public float getTaskProgress() {
363        return mTaskProgress;
364    }
365
366    /** Returns the current dim. */
367    public void setDim(int dim) {
368        RecentsConfiguration config = Recents.getConfiguration();
369
370        mDimAlpha = dim;
371        if (config.useHardwareLayers) {
372            // Defer setting hardware layers if we have not yet measured, or there is no dim to draw
373            if (getMeasuredWidth() > 0 && getMeasuredHeight() > 0) {
374                mDimColorFilter.setColor(Color.argb(mDimAlpha, 0, 0, 0));
375                mDimLayerPaint.setColorFilter(mDimColorFilter);
376                mContent.setLayerType(LAYER_TYPE_HARDWARE, mDimLayerPaint);
377            }
378        } else {
379            float dimAlpha = mDimAlpha / 255.0f;
380            mThumbnailView.setDimAlpha(dimAlpha);
381            mHeaderView.setDimAlpha(dimAlpha);
382        }
383    }
384
385    /** Returns the current dim. */
386    public int getDim() {
387        return mDimAlpha;
388    }
389
390    /** Animates the dim to the task progress. */
391    void animateDimToProgress(int duration, Animator.AnimatorListener animListener) {
392        // Animate the dim into view as well
393        int toDim = getDimFromTaskProgress();
394        if (toDim != getDim()) {
395            ObjectAnimator anim = ObjectAnimator.ofInt(this, DIM, getDim(), toDim);
396            anim.setDuration(duration);
397            if (animListener != null) {
398                anim.addListener(animListener);
399            }
400            anim.start();
401        } else {
402            animListener.onAnimationEnd(null);
403        }
404    }
405
406    /** Compute the dim as a function of the scale of this view. */
407    int getDimFromTaskProgress() {
408        float x = mTaskProgress < 0
409                ? 1f
410                : mDimInterpolator.getInterpolation(1f - mTaskProgress);
411        float dim = mMaxDimScale * x;
412        return (int) (dim * 255);
413    }
414
415    /** Update the dim as a function of the scale of this view. */
416    void updateDimFromTaskProgress() {
417        setDim(getDimFromTaskProgress());
418    }
419
420    /**
421     * Explicitly sets the focused state of this task.
422     */
423    public void setFocusedState(boolean isFocused, boolean requestViewFocus) {
424        SystemServicesProxy ssp = Recents.getSystemServices();
425        if (isFocused) {
426            if (requestViewFocus && !isFocused()) {
427                requestFocus();
428            }
429            if (requestViewFocus && !isAccessibilityFocused() && ssp.isTouchExplorationEnabled()) {
430                requestAccessibilityFocus();
431            }
432        } else {
433            if (isAccessibilityFocused() && ssp.isTouchExplorationEnabled()) {
434                clearAccessibilityFocus();
435            }
436        }
437    }
438
439    /**
440     * Shows the action button.
441     * @param fadeIn whether or not to animate the action button in.
442     * @param fadeInDuration the duration of the action button animation, only used if
443     *                       {@param fadeIn} is true.
444     */
445    public void showActionButton(boolean fadeIn, int fadeInDuration) {
446        mActionButtonView.setVisibility(View.VISIBLE);
447
448        if (fadeIn && mActionButtonView.getAlpha() < 1f) {
449            mActionButtonView.animate()
450                    .alpha(1f)
451                    .scaleX(1f)
452                    .scaleY(1f)
453                    .setDuration(fadeInDuration)
454                    .setInterpolator(Interpolators.ALPHA_IN)
455                    .start();
456        } else {
457            mActionButtonView.setScaleX(1f);
458            mActionButtonView.setScaleY(1f);
459            mActionButtonView.setAlpha(1f);
460            mActionButtonView.setTranslationZ(mActionButtonTranslationZ);
461        }
462    }
463
464    /**
465     * Immediately hides the action button.
466     *
467     * @param fadeOut whether or not to animate the action button out.
468     */
469    public void hideActionButton(boolean fadeOut, int fadeOutDuration, boolean scaleDown,
470            final Animator.AnimatorListener animListener) {
471        if (fadeOut && mActionButtonView.getAlpha() > 0f) {
472            if (scaleDown) {
473                float toScale = 0.9f;
474                mActionButtonView.animate()
475                        .scaleX(toScale)
476                        .scaleY(toScale);
477            }
478            mActionButtonView.animate()
479                    .alpha(0f)
480                    .setDuration(fadeOutDuration)
481                    .setInterpolator(Interpolators.ALPHA_OUT)
482                    .withEndAction(new Runnable() {
483                        @Override
484                        public void run() {
485                            if (animListener != null) {
486                                animListener.onAnimationEnd(null);
487                            }
488                            mActionButtonView.setVisibility(View.INVISIBLE);
489                        }
490                    })
491                    .start();
492        } else {
493            mActionButtonView.setAlpha(0f);
494            mActionButtonView.setVisibility(View.INVISIBLE);
495            if (animListener != null) {
496                animListener.onAnimationEnd(null);
497            }
498        }
499    }
500
501    /**** TaskStackAnimationHelper.Callbacks Implementation ****/
502
503    @Override
504    public void onPrepareLaunchTargetForEnterAnimation() {
505        // These values will be animated in when onStartLaunchTargetEnterAnimation() is called
506        setDim(0);
507        mActionButtonView.setAlpha(0f);
508    }
509
510    @Override
511    public void onStartLaunchTargetEnterAnimation(int duration, boolean screenPinningEnabled,
512            ReferenceCountedTrigger postAnimationTrigger) {
513        postAnimationTrigger.increment();
514        animateDimToProgress(duration, postAnimationTrigger.decrementOnAnimationEnd());
515
516        if (screenPinningEnabled) {
517            showActionButton(true /* fadeIn */, duration /* fadeInDuration */);
518        }
519    }
520
521    @Override
522    public void onStartLaunchTargetLaunchAnimation(int duration, boolean screenPinningRequested,
523            ReferenceCountedTrigger postAnimationTrigger) {
524        if (mDimAlpha > 0) {
525            ObjectAnimator anim = ObjectAnimator.ofInt(this, DIM, getDim(), 0);
526            anim.setDuration(duration);
527            anim.setInterpolator(Interpolators.ALPHA_OUT);
528            anim.start();
529        }
530
531        postAnimationTrigger.increment();
532        hideActionButton(true /* fadeOut */, duration,
533                !screenPinningRequested /* scaleDown */,
534                postAnimationTrigger.decrementOnAnimationEnd());
535    }
536
537    /**** TaskCallbacks Implementation ****/
538
539    public void onTaskBound(Task t) {
540        mTask = t;
541        mTask.addCallback(this);
542    }
543
544    @Override
545    public void onTaskDataLoaded(Task task) {
546        // Bind each of the views to the new task data
547        mThumbnailView.rebindToTask(mTask);
548        mHeaderView.rebindToTask(mTask);
549        mTaskDataLoaded = true;
550    }
551
552    @Override
553    public void onTaskDataUnloaded() {
554        // Unbind each of the views from the task data and remove the task callback
555        mTask.removeCallback(this);
556        mThumbnailView.unbindFromTask();
557        mHeaderView.unbindFromTask();
558        mTaskDataLoaded = false;
559    }
560
561    @Override
562    public void onTaskStackIdChanged() {
563        mHeaderView.rebindToTask(mTask);
564    }
565
566    /**** View.OnClickListener Implementation ****/
567
568    @Override
569     public void onClick(final View v) {
570        boolean screenPinningRequested = false;
571        if (v == mActionButtonView) {
572            // Reset the translation of the action button before we animate it out
573            mActionButtonView.setTranslationZ(0f);
574            screenPinningRequested = true;
575        }
576        EventBus.getDefault().send(new LaunchTaskEvent(this, mTask, null, INVALID_STACK_ID,
577                screenPinningRequested));
578    }
579
580    /**** View.OnLongClickListener Implementation ****/
581
582    @Override
583    public boolean onLongClick(View v) {
584        SystemServicesProxy ssp = Recents.getSystemServices();
585        // Since we are clipping the view to the bounds, manually do the hit test
586        Rect clipBounds = new Rect(mViewBounds.mClipBounds);
587        clipBounds.scale(getScaleX());
588        boolean inBounds = clipBounds.contains(mDownTouchPos.x, mDownTouchPos.y);
589        if (v == this && inBounds && !ssp.hasDockedTask()) {
590            // Start listening for drag events
591            setClipViewInStack(false);
592
593            mDownTouchPos.x += ((1f - getScaleX()) * getWidth()) / 2;
594            mDownTouchPos.y += ((1f - getScaleY()) * getHeight()) / 2;
595
596            EventBus.getDefault().register(this, RecentsActivity.EVENT_BUS_PRIORITY + 1);
597            EventBus.getDefault().send(new DragStartEvent(mTask, this, mDownTouchPos));
598            return true;
599        }
600        return false;
601    }
602
603    /**** Events ****/
604
605    public final void onBusEvent(DragEndEvent event) {
606        if (!(event.dropTarget instanceof TaskStack.DockState)) {
607            event.addPostAnimationCallback(new Runnable() {
608                @Override
609                public void run() {
610                    // Animate the drag view back from where it is, to the view location, then after
611                    // it returns, update the clip state
612                    setClipViewInStack(true);
613                }
614            });
615        }
616        EventBus.getDefault().unregister(this);
617    }
618}
619