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