TaskView.java revision 65c851e6e9e08656744b6f66d3da188e3283b17d
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            if (Float.compare(getTaskProgress(), toTransform.p) != 0) {
261                setTaskProgress(toTransform.p);
262            }
263            // Manually call back to the animator listener and update callback
264            if (toAnimation.listener != null) {
265                toAnimation.listener.onAnimationEnd(null);
266            }
267            if (updateCallback != null) {
268                updateCallback.onAnimationUpdate(null);
269            }
270        } else {
271            if (Float.compare(getTaskProgress(), toTransform.p) != 0) {
272                mTmpAnimators.add(ObjectAnimator.ofFloat(this, TASK_PROGRESS, getTaskProgress(),
273                        toTransform.p));
274            }
275            if (updateCallback != null) {
276                ValueAnimator updateCallbackAnim = ValueAnimator.ofInt(0, 1);
277                updateCallbackAnim.addUpdateListener(updateCallback);
278                mTmpAnimators.add(updateCallbackAnim);
279            }
280
281            // Create the animator
282            mTransformAnimation = toAnimation.createAnimator(mTmpAnimators);
283            mTransformAnimation.start();
284        }
285    }
286
287    /** Resets this view's properties */
288    void resetViewProperties() {
289        Utilities.cancelAnimationWithoutCallbacks(mTransformAnimation);
290        setDim(0);
291        setVisibility(View.VISIBLE);
292        getViewBounds().reset();
293        TaskViewTransform.reset(this);
294
295        mActionButtonView.setScaleX(1f);
296        mActionButtonView.setScaleY(1f);
297        mActionButtonView.setAlpha(0f);
298        mActionButtonView.setTranslationZ(mActionButtonTranslationZ);
299    }
300
301    /**
302     * Cancels any current transform animations.
303     */
304    public void cancelTransformAnimation() {
305        Utilities.cancelAnimationWithoutCallbacks(mTransformAnimation);
306    }
307
308    /** Enables/disables handling touch on this task view. */
309    void setTouchEnabled(boolean enabled) {
310        setOnClickListener(enabled ? this : null);
311    }
312
313    /** Animates this task view if the user does not interact with the stack after a certain time. */
314    void startNoUserInteractionAnimation() {
315        mHeaderView.startNoUserInteractionAnimation();
316    }
317
318    /** Mark this task view that the user does has not interacted with the stack after a certain time. */
319    void setNoUserInteractionState() {
320        mHeaderView.setNoUserInteractionState();
321    }
322
323    /** Resets the state tracking that the user has not interacted with the stack after a certain time. */
324    void resetNoUserInteractionState() {
325        mHeaderView.resetNoUserInteractionState();
326    }
327
328    /** Dismisses this task. */
329    void dismissTask() {
330        // Animate out the view and call the callback
331        final TaskView tv = this;
332        DismissTaskViewEvent dismissEvent = new DismissTaskViewEvent(tv, mTask);
333        dismissEvent.addPostAnimationCallback(new Runnable() {
334            @Override
335            public void run() {
336                EventBus.getDefault().send(new TaskViewDismissedEvent(mTask, tv));
337            }
338        });
339        EventBus.getDefault().send(dismissEvent);
340    }
341
342    /**
343     * Returns whether this view should be clipped, or any views below should clip against this
344     * view.
345     */
346    boolean shouldClipViewInStack() {
347        // Never clip for freeform tasks or if invisible
348        if (mTask.isFreeformTask() || getVisibility() != View.VISIBLE) {
349            return false;
350        }
351        return mClipViewInStack;
352    }
353
354    /** Sets whether this view should be clipped, or clipped against. */
355    void setClipViewInStack(boolean clip) {
356        if (clip != mClipViewInStack) {
357            mClipViewInStack = clip;
358            if (mCb != null) {
359                mCb.onTaskViewClipStateChanged(this);
360            }
361        }
362    }
363
364    /** Sets the current task progress. */
365    public void setTaskProgress(float p) {
366        mTaskProgress = p;
367        mViewBounds.setAlpha(p);
368        updateDimFromTaskProgress();
369    }
370
371    public TaskViewHeader getHeaderView() {
372        return mHeaderView;
373    }
374
375    /** Returns the current task progress. */
376    public float getTaskProgress() {
377        return mTaskProgress;
378    }
379
380    /** Returns the current dim. */
381    public void setDim(int dim) {
382        RecentsConfiguration config = Recents.getConfiguration();
383
384        mDimAlpha = dim;
385        if (config.useHardwareLayers) {
386            // Defer setting hardware layers if we have not yet measured, or there is no dim to draw
387            if (getMeasuredWidth() > 0 && getMeasuredHeight() > 0) {
388                mDimColorFilter.setColor(Color.argb(mDimAlpha, 0, 0, 0));
389                mDimLayerPaint.setColorFilter(mDimColorFilter);
390                mContent.setLayerType(LAYER_TYPE_HARDWARE, mDimLayerPaint);
391            }
392        } else {
393            float dimAlpha = mDimAlpha / 255.0f;
394            mThumbnailView.setDimAlpha(dimAlpha);
395            mHeaderView.setDimAlpha(dimAlpha);
396        }
397    }
398
399    /** Returns the current dim. */
400    public int getDim() {
401        return mDimAlpha;
402    }
403
404    /** Animates the dim to the task progress. */
405    void animateDimToProgress(int duration, Animator.AnimatorListener animListener) {
406        // Animate the dim into view as well
407        int toDim = getDimFromTaskProgress();
408        if (toDim != getDim()) {
409            ObjectAnimator anim = ObjectAnimator.ofInt(this, DIM, getDim(), toDim);
410            anim.setDuration(duration);
411            if (animListener != null) {
412                anim.addListener(animListener);
413            }
414            anim.start();
415        } else {
416            animListener.onAnimationEnd(null);
417        }
418    }
419
420    /** Compute the dim as a function of the scale of this view. */
421    int getDimFromTaskProgress() {
422        float x = mTaskProgress < 0
423                ? 1f
424                : mDimInterpolator.getInterpolation(1f - mTaskProgress);
425        float dim = mMaxDimScale * x;
426        return (int) (dim * 255);
427    }
428
429    /** Update the dim as a function of the scale of this view. */
430    void updateDimFromTaskProgress() {
431        setDim(getDimFromTaskProgress());
432    }
433
434    /**
435     * Explicitly sets the focused state of this task.
436     */
437    public void setFocusedState(boolean isFocused, boolean requestViewFocus) {
438        SystemServicesProxy ssp = Recents.getSystemServices();
439        if (isFocused) {
440            if (requestViewFocus && !isFocused()) {
441                requestFocus();
442            }
443            if (requestViewFocus && !isAccessibilityFocused() && ssp.isTouchExplorationEnabled()) {
444                requestAccessibilityFocus();
445            }
446        } else {
447            if (isAccessibilityFocused() && ssp.isTouchExplorationEnabled()) {
448                clearAccessibilityFocus();
449            }
450        }
451    }
452
453    /**
454     * Shows the action button.
455     * @param fadeIn whether or not to animate the action button in.
456     * @param fadeInDuration the duration of the action button animation, only used if
457     *                       {@param fadeIn} is true.
458     */
459    public void showActionButton(boolean fadeIn, int fadeInDuration) {
460        mActionButtonView.setVisibility(View.VISIBLE);
461
462        if (fadeIn && mActionButtonView.getAlpha() < 1f) {
463            mActionButtonView.animate()
464                    .alpha(1f)
465                    .scaleX(1f)
466                    .scaleY(1f)
467                    .setDuration(fadeInDuration)
468                    .setInterpolator(PhoneStatusBar.ALPHA_IN)
469                    .start();
470        } else {
471            mActionButtonView.setScaleX(1f);
472            mActionButtonView.setScaleY(1f);
473            mActionButtonView.setAlpha(1f);
474            mActionButtonView.setTranslationZ(mActionButtonTranslationZ);
475        }
476    }
477
478    /**
479     * Immediately hides the action button.
480     *
481     * @param fadeOut whether or not to animate the action button out.
482     */
483    public void hideActionButton(boolean fadeOut, int fadeOutDuration, boolean scaleDown,
484            final Animator.AnimatorListener animListener) {
485        if (fadeOut && mActionButtonView.getAlpha() > 0f) {
486            if (scaleDown) {
487                float toScale = 0.9f;
488                mActionButtonView.animate()
489                        .scaleX(toScale)
490                        .scaleY(toScale);
491            }
492            mActionButtonView.animate()
493                    .alpha(0f)
494                    .setDuration(fadeOutDuration)
495                    .setInterpolator(PhoneStatusBar.ALPHA_OUT)
496                    .withEndAction(new Runnable() {
497                        @Override
498                        public void run() {
499                            if (animListener != null) {
500                                animListener.onAnimationEnd(null);
501                            }
502                            mActionButtonView.setVisibility(View.INVISIBLE);
503                        }
504                    })
505                    .start();
506        } else {
507            mActionButtonView.setAlpha(0f);
508            mActionButtonView.setVisibility(View.INVISIBLE);
509            if (animListener != null) {
510                animListener.onAnimationEnd(null);
511            }
512        }
513    }
514
515    /**** TaskStackAnimationHelper.Callbacks Implementation ****/
516
517    @Override
518    public void onPrepareLaunchTargetForEnterAnimation() {
519        // These values will be animated in when onStartLaunchTargetEnterAnimation() is called
520        setDim(0);
521        mActionButtonView.setAlpha(0f);
522    }
523
524    @Override
525    public void onStartLaunchTargetEnterAnimation(int duration, boolean screenPinningEnabled,
526            ReferenceCountedTrigger postAnimationTrigger) {
527        postAnimationTrigger.increment();
528        animateDimToProgress(duration, postAnimationTrigger.decrementOnAnimationEnd());
529
530        if (screenPinningEnabled) {
531            showActionButton(true /* fadeIn */, duration /* fadeInDuration */);
532        }
533    }
534
535    @Override
536    public void onStartLaunchTargetLaunchAnimation(int duration, boolean screenPinningRequested,
537            ReferenceCountedTrigger postAnimationTrigger) {
538        if (mDimAlpha > 0) {
539            ObjectAnimator anim = ObjectAnimator.ofInt(this, DIM, getDim(), 0);
540            anim.setDuration(duration);
541            anim.setInterpolator(PhoneStatusBar.ALPHA_OUT);
542            anim.start();
543        }
544
545        postAnimationTrigger.increment();
546        hideActionButton(true /* fadeOut */, duration,
547                !screenPinningRequested /* scaleDown */,
548                postAnimationTrigger.decrementOnAnimationEnd());
549    }
550
551    /**** TaskCallbacks Implementation ****/
552
553    public void onTaskBound(Task t) {
554        mTask = t;
555        mTask.addCallback(this);
556    }
557
558    @Override
559    public void onTaskDataLoaded(Task task) {
560        // Bind each of the views to the new task data
561        mThumbnailView.rebindToTask(mTask);
562        mHeaderView.rebindToTask(mTask);
563        mTaskDataLoaded = true;
564    }
565
566    @Override
567    public void onTaskDataUnloaded() {
568        // Unbind each of the views from the task data and remove the task callback
569        mTask.removeCallback(this);
570        mThumbnailView.unbindFromTask();
571        mHeaderView.unbindFromTask();
572        mTaskDataLoaded = false;
573    }
574
575    @Override
576    public void onTaskStackIdChanged() {
577        mHeaderView.rebindToTask(mTask);
578    }
579
580    /**** View.OnClickListener Implementation ****/
581
582    @Override
583     public void onClick(final View v) {
584        boolean screenPinningRequested = false;
585        if (v == mActionButtonView) {
586            // Reset the translation of the action button before we animate it out
587            mActionButtonView.setTranslationZ(0f);
588            screenPinningRequested = true;
589        }
590        EventBus.getDefault().send(new LaunchTaskEvent(this, mTask, null, INVALID_STACK_ID,
591                screenPinningRequested));
592    }
593
594    /**** View.OnLongClickListener Implementation ****/
595
596    @Override
597    public boolean onLongClick(View v) {
598        SystemServicesProxy ssp = Recents.getSystemServices();
599        // Since we are clipping the view to the bounds, manually do the hit test
600        Rect clipBounds = new Rect(mViewBounds.mClipBounds);
601        clipBounds.scale(getScaleX());
602        boolean inBounds = clipBounds.contains(mDownTouchPos.x, mDownTouchPos.y);
603        if (v == this && inBounds && !ssp.hasDockedTask()) {
604            // Start listening for drag events
605            setClipViewInStack(false);
606
607            mDownTouchPos.x += ((1f - getScaleX()) * getWidth()) / 2;
608            mDownTouchPos.y += ((1f - getScaleY()) * getHeight()) / 2;
609
610            EventBus.getDefault().register(this, RecentsActivity.EVENT_BUS_PRIORITY + 1);
611            EventBus.getDefault().send(new DragStartEvent(mTask, this, mDownTouchPos));
612            return true;
613        }
614        return false;
615    }
616
617    /**** Events ****/
618
619    public final void onBusEvent(DragEndEvent event) {
620        if (!(event.dropTarget instanceof TaskStack.DockState)) {
621            event.addPostAnimationCallback(new Runnable() {
622                @Override
623                public void run() {
624                    // Animate the drag view back from where it is, to the view location, then after
625                    // it returns, update the clip state
626                    setClipViewInStack(true);
627                }
628            });
629        }
630        EventBus.getDefault().unregister(this);
631    }
632}
633