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