TaskViewHeader.java revision 2536c7ed446203ea12b38cf05a88e603f8d1b768
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.ArgbEvaluator;
23import android.animation.ObjectAnimator;
24import android.animation.ValueAnimator;
25import android.content.Context;
26import android.content.res.ColorStateList;
27import android.graphics.Canvas;
28import android.graphics.Color;
29import android.graphics.Outline;
30import android.graphics.Paint;
31import android.graphics.PorterDuff;
32import android.graphics.PorterDuffColorFilter;
33import android.graphics.PorterDuffXfermode;
34import android.graphics.Rect;
35import android.graphics.drawable.ColorDrawable;
36import android.graphics.drawable.Drawable;
37import android.graphics.drawable.GradientDrawable;
38import android.graphics.drawable.RippleDrawable;
39import android.util.AttributeSet;
40import android.view.View;
41import android.view.ViewOutlineProvider;
42import android.view.accessibility.AccessibilityManager;
43import android.view.animation.AnimationUtils;
44import android.view.animation.Interpolator;
45import android.widget.FrameLayout;
46import android.widget.ImageView;
47import android.widget.TextView;
48import com.android.internal.logging.MetricsLogger;
49import com.android.systemui.R;
50import com.android.systemui.recents.Constants;
51import com.android.systemui.recents.RecentsConfiguration;
52import com.android.systemui.recents.events.EventBus;
53import com.android.systemui.recents.events.ui.ResizeTaskEvent;
54import com.android.systemui.recents.events.ui.ShowApplicationInfoEvent;
55import com.android.systemui.recents.misc.SystemServicesProxy;
56import com.android.systemui.recents.misc.Utilities;
57import com.android.systemui.recents.model.RecentsTaskLoader;
58import com.android.systemui.recents.model.Task;
59
60
61/* The task bar view */
62public class TaskViewHeader extends FrameLayout
63        implements View.OnClickListener, View.OnLongClickListener {
64
65    RecentsConfiguration mConfig;
66    private SystemServicesProxy mSsp;
67    Task mTask;
68
69    // Header views
70    ImageView mMoveTaskButton;
71    ImageView mDismissButton;
72    ImageView mApplicationIcon;
73    TextView mActivityDescription;
74
75    // Header drawables
76    boolean mCurrentPrimaryColorIsDark;
77    int mCurrentPrimaryColor;
78    int mBackgroundColor;
79    int mCornerRadius;
80    int mHighlightHeight;
81    Drawable mLightDismissDrawable;
82    Drawable mDarkDismissDrawable;
83    RippleDrawable mBackground;
84    GradientDrawable mBackgroundColorDrawable;
85    AnimatorSet mFocusAnimator;
86    String mDismissContentDescription;
87
88    // Static highlight that we draw at the top of each view
89    static Paint sHighlightPaint;
90
91    // Header dim, which is only used when task view hardware layers are not used
92    Paint mDimLayerPaint = new Paint();
93    PorterDuffColorFilter mDimColorFilter = new PorterDuffColorFilter(0, PorterDuff.Mode.SRC_ATOP);
94
95    Interpolator mFastOutSlowInInterpolator;
96    Interpolator mFastOutLinearInInterpolator;
97
98    boolean mLayersDisabled;
99
100    public TaskViewHeader(Context context) {
101        this(context, null);
102    }
103
104    public TaskViewHeader(Context context, AttributeSet attrs) {
105        this(context, attrs, 0);
106    }
107
108    public TaskViewHeader(Context context, AttributeSet attrs, int defStyleAttr) {
109        this(context, attrs, defStyleAttr, 0);
110    }
111
112    public TaskViewHeader(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
113        super(context, attrs, defStyleAttr, defStyleRes);
114        mConfig = RecentsConfiguration.getInstance();
115        mSsp = RecentsTaskLoader.getInstance().getSystemServicesProxy();
116        setWillNotDraw(false);
117        setClipToOutline(true);
118        setOutlineProvider(new ViewOutlineProvider() {
119            @Override
120            public void getOutline(View view, Outline outline) {
121                outline.setRect(0, 0, getMeasuredWidth(), getMeasuredHeight());
122            }
123        });
124
125        // Load the dismiss resources
126        mLightDismissDrawable = context.getDrawable(R.drawable.recents_dismiss_light);
127        mDarkDismissDrawable = context.getDrawable(R.drawable.recents_dismiss_dark);
128        mDismissContentDescription =
129                context.getString(R.string.accessibility_recents_item_will_be_dismissed);
130        mCornerRadius = getResources().getDimensionPixelSize(
131                R.dimen.recents_task_view_rounded_corners_radius);
132        mHighlightHeight = getResources().getDimensionPixelSize(
133                R.dimen.recents_task_view_highlight);
134        mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(context,
135                com.android.internal.R.interpolator.fast_out_slow_in);
136        mFastOutLinearInInterpolator = AnimationUtils.loadInterpolator(context,
137                com.android.internal.R.interpolator.fast_out_linear_in);
138
139        // Configure the highlight paint
140        if (sHighlightPaint == null) {
141            sHighlightPaint = new Paint();
142            sHighlightPaint.setStyle(Paint.Style.STROKE);
143            sHighlightPaint.setStrokeWidth(mHighlightHeight);
144            sHighlightPaint.setColor(context.getColor(R.color.recents_task_bar_highlight_color));
145            sHighlightPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.ADD));
146            sHighlightPaint.setAntiAlias(true);
147        }
148    }
149
150    @Override
151    protected void onFinishInflate() {
152        // Initialize the icon and description views
153        mApplicationIcon = (ImageView) findViewById(R.id.application_icon);
154        mApplicationIcon.setOnLongClickListener(this);
155        mActivityDescription = (TextView) findViewById(R.id.activity_description);
156        mDismissButton = (ImageView) findViewById(R.id.dismiss_task);
157        mDismissButton.setOnClickListener(this);
158        mMoveTaskButton = (ImageView) findViewById(R.id.move_task);
159
160        // Hide the backgrounds if they are ripple drawables
161        if (mApplicationIcon.getBackground() instanceof RippleDrawable) {
162            mApplicationIcon.setBackground(null);
163        }
164
165        mBackgroundColorDrawable = (GradientDrawable) getContext().getDrawable(R.drawable
166                .recents_task_view_header_bg_color);
167        // Copy the ripple drawable since we are going to be manipulating it
168        mBackground = (RippleDrawable)
169                getContext().getDrawable(R.drawable.recents_task_view_header_bg);
170        mBackground = (RippleDrawable) mBackground.mutate().getConstantState().newDrawable();
171        mBackground.setColor(ColorStateList.valueOf(0));
172        mBackground.setDrawableByLayerId(mBackground.getId(0), mBackgroundColorDrawable);
173        setBackground(mBackground);
174    }
175
176    @Override
177    protected void onDraw(Canvas canvas) {
178        // Draw the highlight at the top edge (but put the bottom edge just out of view)
179        float offset = (float) Math.ceil(mHighlightHeight / 2f);
180        float radius = mCornerRadius;
181        int count = canvas.save(Canvas.CLIP_SAVE_FLAG);
182        canvas.clipRect(0, 0, getMeasuredWidth(), getMeasuredHeight());
183        canvas.drawRoundRect(-offset, 0f, (float) getMeasuredWidth() + offset,
184                getMeasuredHeight() + radius, radius, radius, sHighlightPaint);
185        canvas.restoreToCount(count);
186    }
187
188    @Override
189    public boolean hasOverlappingRendering() {
190        return false;
191    }
192
193    /**
194     * Sets the dim alpha, only used when we are not using hardware layers.
195     * (see RecentsConfiguration.useHardwareLayers)
196     */
197    void setDimAlpha(int alpha) {
198        mDimColorFilter.setColor(Color.argb(alpha, 0, 0, 0));
199        mDimLayerPaint.setColorFilter(mDimColorFilter);
200        if (!mLayersDisabled) {
201            setLayerType(LAYER_TYPE_HARDWARE, mDimLayerPaint);
202        }
203    }
204
205    /** Returns the secondary color for a primary color. */
206    int getSecondaryColor(int primaryColor, boolean useLightOverlayColor) {
207        int overlayColor = useLightOverlayColor ? Color.WHITE : Color.BLACK;
208        return Utilities.getColorWithOverlay(primaryColor, overlayColor, 0.8f);
209    }
210
211    /** Binds the bar view to the task */
212    public void rebindToTask(Task t) {
213        mTask = t;
214
215        // If an activity icon is defined, then we use that as the primary icon to show in the bar,
216        // otherwise, we fall back to the application icon
217        if (t.activityIcon != null) {
218            mApplicationIcon.setImageDrawable(t.activityIcon);
219        } else if (t.applicationIcon != null) {
220            mApplicationIcon.setImageDrawable(t.applicationIcon);
221        }
222        if (!mActivityDescription.getText().toString().equals(t.activityLabel)) {
223            mActivityDescription.setText(t.activityLabel);
224        }
225        mActivityDescription.setContentDescription(t.contentDescription);
226
227        // Try and apply the system ui tint
228        int existingBgColor = (getBackground() instanceof ColorDrawable) ?
229                ((ColorDrawable) getBackground()).getColor() : 0;
230        if (existingBgColor != t.colorPrimary) {
231            mBackgroundColorDrawable.setColor(t.colorPrimary);
232            mBackgroundColor = t.colorPrimary;
233        }
234
235        int taskBarViewLightTextColor = getResources().getColor(
236                R.color.recents_task_bar_light_text_color);
237        int taskBarViewDarkTextColor = getResources().getColor(
238                R.color.recents_task_bar_dark_text_color);
239        mCurrentPrimaryColor = t.colorPrimary;
240        mCurrentPrimaryColorIsDark = t.useLightOnPrimaryColor;
241        mActivityDescription.setTextColor(t.useLightOnPrimaryColor ?
242                taskBarViewLightTextColor : taskBarViewDarkTextColor);
243        mDismissButton.setImageDrawable(t.useLightOnPrimaryColor ?
244                mLightDismissDrawable : mDarkDismissDrawable);
245        mDismissButton.setContentDescription(String.format(mDismissContentDescription,
246                t.contentDescription));
247        mMoveTaskButton.setVisibility((mConfig.multiWindowEnabled) ? View.VISIBLE : View.INVISIBLE);
248        if (mConfig.multiWindowEnabled) {
249            updateResizeTaskBarIcon(t);
250            mMoveTaskButton.setOnClickListener(this);
251        }
252
253        // In accessibility, a single click on the focused app info button will show it
254        AccessibilityManager am = (AccessibilityManager) getContext().
255                getSystemService(Context.ACCESSIBILITY_SERVICE);
256        if (am != null && am.isEnabled()) {
257            mApplicationIcon.setOnClickListener(this);
258        }
259    }
260
261    /** Unbinds the bar view from the task */
262    void unbindFromTask() {
263        mTask = null;
264        mApplicationIcon.setImageDrawable(null);
265        mApplicationIcon.setOnClickListener(null);
266
267        if (mConfig.multiWindowEnabled) {
268            mMoveTaskButton.setOnClickListener(null);
269        }
270    }
271
272    /** Updates the resize task bar button. */
273    void updateResizeTaskBarIcon(Task t) {
274        Rect display = mSsp.getWindowRect();
275        Rect taskRect = mSsp.getTaskBounds(t.key.id);
276        int resId = R.drawable.star;
277        if (display.equals(taskRect) || taskRect.isEmpty()) {
278            resId = R.drawable.vector_drawable_place_fullscreen;
279        } else {
280            boolean top = display.top == taskRect.top;
281            boolean bottom = display.bottom == taskRect.bottom;
282            boolean left = display.left == taskRect.left;
283            boolean right = display.right == taskRect.right;
284            if (top && bottom && left) {
285                resId = R.drawable.vector_drawable_place_left;
286            } else if (top && bottom && right) {
287                resId = R.drawable.vector_drawable_place_right;
288            } else if (top && left && right) {
289                resId = R.drawable.vector_drawable_place_top;
290            } else if (bottom && left && right) {
291                resId = R.drawable.vector_drawable_place_bottom;
292            } else if (top && right) {
293                resId = R.drawable.vector_drawable_place_top_right;
294            } else if (top && left) {
295                resId = R.drawable.vector_drawable_place_top_left;
296            } else if (bottom && right) {
297                resId = R.drawable.vector_drawable_place_bottom_right;
298            } else if (bottom && left) {
299                resId = R.drawable.vector_drawable_place_bottom_left;
300            }
301        }
302        mMoveTaskButton.setImageResource(resId);
303    }
304
305    /** Animates this task bar dismiss button when launching a task. */
306    void startLaunchTaskDismissAnimation() {
307        if (mDismissButton.getVisibility() == View.VISIBLE) {
308            int taskViewExitToAppDuration = mContext.getResources().getInteger(
309                    R.integer.recents_task_exit_to_app_duration);
310            mDismissButton.animate().cancel();
311            mDismissButton.animate()
312                    .alpha(0f)
313                    .setStartDelay(0)
314                    .setInterpolator(mFastOutSlowInInterpolator)
315                    .setDuration(taskViewExitToAppDuration)
316                    .start();
317        }
318    }
319
320    /** Animates this task bar if the user does not interact with the stack after a certain time. */
321    void startNoUserInteractionAnimation() {
322        if (mDismissButton.getVisibility() != View.VISIBLE) {
323            mDismissButton.setVisibility(View.VISIBLE);
324            mDismissButton.setAlpha(0f);
325            mDismissButton.animate()
326                    .alpha(1f)
327                    .setStartDelay(0)
328                    .setInterpolator(mFastOutLinearInInterpolator)
329                    .setDuration(getResources().getInteger(
330                            R.integer.recents_task_enter_from_app_duration))
331                    .start();
332        }
333    }
334
335    /** Mark this task view that the user does has not interacted with the stack after a certain time. */
336    void setNoUserInteractionState() {
337        if (mDismissButton.getVisibility() != View.VISIBLE) {
338            mDismissButton.animate().cancel();
339            mDismissButton.setVisibility(View.VISIBLE);
340            mDismissButton.setAlpha(1f);
341        }
342    }
343
344    /** Resets the state tracking that the user has not interacted with the stack after a certain time. */
345    void resetNoUserInteractionState() {
346        mDismissButton.setVisibility(View.INVISIBLE);
347    }
348
349    @Override
350    protected int[] onCreateDrawableState(int extraSpace) {
351
352        // Don't forward our state to the drawable - we do it manually in onTaskViewFocusChanged.
353        // This is to prevent layer trashing when the view is pressed.
354        return new int[] {};
355    }
356
357    @Override
358    protected void dispatchDraw(Canvas canvas) {
359        super.dispatchDraw(canvas);
360        if (mLayersDisabled) {
361            mLayersDisabled = false;
362            postOnAnimation(new Runnable() {
363                @Override
364                public void run() {
365                    mLayersDisabled = false;
366                    setLayerType(LAYER_TYPE_HARDWARE, mDimLayerPaint);
367                }
368            });
369        }
370    }
371
372    public void disableLayersForOneFrame() {
373        mLayersDisabled = true;
374
375        // Disable layer for a frame so we can draw our first frame faster.
376        setLayerType(LAYER_TYPE_NONE, null);
377    }
378
379    /** Notifies the associated TaskView has been focused. */
380    void onTaskViewFocusChanged(boolean focused, boolean animateFocusedState) {
381        // If we are not animating the visible state, just return
382        if (!animateFocusedState) return;
383
384        boolean isRunning = false;
385        if (mFocusAnimator != null) {
386            isRunning = mFocusAnimator.isRunning();
387            Utilities.cancelAnimationWithoutCallbacks(mFocusAnimator);
388        }
389
390        if (focused) {
391            int currentColor = mBackgroundColor;
392            int secondaryColor = getSecondaryColor(mCurrentPrimaryColor, mCurrentPrimaryColorIsDark);
393            int[][] states = new int[][] {
394                    new int[] {},
395                    new int[] { android.R.attr.state_enabled },
396                    new int[] { android.R.attr.state_pressed }
397            };
398            int[] newStates = new int[]{
399                    0,
400                    android.R.attr.state_enabled,
401                    android.R.attr.state_pressed
402            };
403            int[] colors = new int[] {
404                    currentColor,
405                    secondaryColor,
406                    secondaryColor
407            };
408            mBackground.setColor(new ColorStateList(states, colors));
409            mBackground.setState(newStates);
410            // Pulse the background color
411            int lightPrimaryColor = getSecondaryColor(mCurrentPrimaryColor, mCurrentPrimaryColorIsDark);
412            ValueAnimator backgroundColor = ValueAnimator.ofObject(new ArgbEvaluator(),
413                    currentColor, lightPrimaryColor);
414            backgroundColor.addListener(new AnimatorListenerAdapter() {
415                @Override
416                public void onAnimationStart(Animator animation) {
417                    mBackground.setState(new int[]{});
418                }
419            });
420            backgroundColor.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
421                @Override
422                public void onAnimationUpdate(ValueAnimator animation) {
423                    int color = (int) animation.getAnimatedValue();
424                    mBackgroundColorDrawable.setColor(color);
425                    mBackgroundColor = color;
426                }
427            });
428            backgroundColor.setRepeatCount(ValueAnimator.INFINITE);
429            backgroundColor.setRepeatMode(ValueAnimator.REVERSE);
430            // Pulse the translation
431            ObjectAnimator translation = ObjectAnimator.ofFloat(this, "translationZ", 15f);
432            translation.setRepeatCount(ValueAnimator.INFINITE);
433            translation.setRepeatMode(ValueAnimator.REVERSE);
434
435            mFocusAnimator = new AnimatorSet();
436            mFocusAnimator.playTogether(backgroundColor, translation);
437            mFocusAnimator.setStartDelay(150);
438            mFocusAnimator.setDuration(750);
439            mFocusAnimator.start();
440        } else {
441            if (isRunning) {
442                // Restore the background color
443                int currentColor = mBackgroundColor;
444                ValueAnimator backgroundColor = ValueAnimator.ofObject(new ArgbEvaluator(),
445                        currentColor, mCurrentPrimaryColor);
446                backgroundColor.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
447                    @Override
448                    public void onAnimationUpdate(ValueAnimator animation) {
449                        int color = (int) animation.getAnimatedValue();
450                        mBackgroundColorDrawable.setColor(color);
451                        mBackgroundColor = color;
452                    }
453                });
454                // Restore the translation
455                ObjectAnimator translation = ObjectAnimator.ofFloat(this, "translationZ", 0f);
456
457                mFocusAnimator = new AnimatorSet();
458                mFocusAnimator.playTogether(backgroundColor, translation);
459                mFocusAnimator.setDuration(150);
460                mFocusAnimator.start();
461            } else {
462                mBackground.setState(new int[] {});
463                setTranslationZ(0f);
464            }
465        }
466    }
467
468    @Override
469    public void onClick(View v) {
470        if (v == mApplicationIcon) {
471            // In accessibility, a single click on the focused app info button will show it
472            EventBus.getDefault().send(new ShowApplicationInfoEvent(mTask));
473        } else if (v == mDismissButton) {
474            TaskView tv = (TaskView) getParent().getParent();
475            tv.dismissTask();
476
477            // Keep track of deletions by the dismiss button
478            MetricsLogger.histogram(getContext(), "overview_task_dismissed_source",
479                    Constants.Metrics.DismissSourceHeaderButton);
480        } else if (v == mMoveTaskButton) {
481            EventBus.getDefault().send(new ResizeTaskEvent(mTask));
482        }
483    }
484
485    @Override
486    public boolean onLongClick(View v) {
487        if (v == mApplicationIcon) {
488            EventBus.getDefault().send(new ShowApplicationInfoEvent(mTask));
489            return true;
490        }
491        return false;
492    }
493}
494