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