TaskViewHeader.java revision 296278a0679375b8c43962a9e3c9bb4e8ab201e7
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.content.Context;
20import android.content.res.ColorStateList;
21import android.graphics.Canvas;
22import android.graphics.Color;
23import android.graphics.Paint;
24import android.graphics.PorterDuff;
25import android.graphics.PorterDuffXfermode;
26import android.graphics.Rect;
27import android.graphics.drawable.ColorDrawable;
28import android.graphics.drawable.Drawable;
29import android.graphics.drawable.GradientDrawable;
30import android.graphics.drawable.RippleDrawable;
31import android.util.AttributeSet;
32import android.view.View;
33import android.view.animation.AnimationUtils;
34import android.view.animation.Interpolator;
35import android.widget.FrameLayout;
36import android.widget.ImageView;
37import android.widget.TextView;
38import com.android.internal.logging.MetricsLogger;
39import com.android.systemui.R;
40import com.android.systemui.recents.Constants;
41import com.android.systemui.recents.Recents;
42import com.android.systemui.recents.events.EventBus;
43import com.android.systemui.recents.events.activity.LaunchTaskEvent;
44import com.android.systemui.recents.events.ui.ShowApplicationInfoEvent;
45import com.android.systemui.recents.misc.SystemServicesProxy;
46import com.android.systemui.recents.misc.Utilities;
47import com.android.systemui.recents.model.Task;
48
49import static android.app.ActivityManager.StackId.FREEFORM_WORKSPACE_STACK_ID;
50import static android.app.ActivityManager.StackId.FULLSCREEN_WORKSPACE_STACK_ID;
51import static android.app.ActivityManager.StackId.INVALID_STACK_ID;
52
53
54/* The task bar view */
55public class TaskViewHeader extends FrameLayout
56        implements View.OnClickListener, View.OnLongClickListener {
57
58    Task mTask;
59
60    // Header views
61    ImageView mMoveTaskButton;
62    ImageView mDismissButton;
63    ImageView mIconView;
64    TextView mTitleView;
65    int mMoveTaskTargetStackId = INVALID_STACK_ID;
66
67    // Header drawables
68    Rect mTaskViewRect = new Rect();
69    int mCornerRadius;
70    int mHighlightHeight;
71    Drawable mLightDismissDrawable;
72    Drawable mDarkDismissDrawable;
73    RippleDrawable mBackground;
74    GradientDrawable mBackgroundColorDrawable;
75    String mDismissContentDescription;
76
77    // Static highlight that we draw at the top of each view
78    static Paint sHighlightPaint;
79
80    // Header dim, which is only used when task view hardware layers are not used
81    Paint mDimLayerPaint = new Paint();
82
83    Interpolator mFastOutSlowInInterpolator;
84    Interpolator mFastOutLinearInInterpolator;
85
86    public TaskViewHeader(Context context) {
87        this(context, null);
88    }
89
90    public TaskViewHeader(Context context, AttributeSet attrs) {
91        this(context, attrs, 0);
92    }
93
94    public TaskViewHeader(Context context, AttributeSet attrs, int defStyleAttr) {
95        this(context, attrs, defStyleAttr, 0);
96    }
97
98    public TaskViewHeader(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
99        super(context, attrs, defStyleAttr, defStyleRes);
100        setWillNotDraw(false);
101
102        // Load the dismiss resources
103        mDimLayerPaint.setColor(Color.argb(0, 0, 0, 0));
104        mLightDismissDrawable = context.getDrawable(R.drawable.recents_dismiss_light);
105        mDarkDismissDrawable = context.getDrawable(R.drawable.recents_dismiss_dark);
106        mDismissContentDescription =
107                context.getString(R.string.accessibility_recents_item_will_be_dismissed);
108        mCornerRadius = getResources().getDimensionPixelSize(
109                R.dimen.recents_task_view_rounded_corners_radius);
110        mHighlightHeight = getResources().getDimensionPixelSize(
111                R.dimen.recents_task_view_highlight);
112        mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(context,
113                com.android.internal.R.interpolator.fast_out_slow_in);
114        mFastOutLinearInInterpolator = AnimationUtils.loadInterpolator(context,
115                com.android.internal.R.interpolator.fast_out_linear_in);
116
117        // Configure the highlight paint
118        if (sHighlightPaint == null) {
119            sHighlightPaint = new Paint();
120            sHighlightPaint.setStyle(Paint.Style.STROKE);
121            sHighlightPaint.setStrokeWidth(mHighlightHeight);
122            sHighlightPaint.setColor(context.getColor(R.color.recents_task_bar_highlight_color));
123            sHighlightPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.ADD));
124            sHighlightPaint.setAntiAlias(true);
125        }
126    }
127
128    @Override
129    protected void onFinishInflate() {
130        // Initialize the icon and description views
131        mIconView = (ImageView) findViewById(R.id.icon);
132        mIconView.setOnLongClickListener(this);
133        mTitleView = (TextView) findViewById(R.id.title);
134        mDismissButton = (ImageView) findViewById(R.id.dismiss_task);
135        mDismissButton.setOnClickListener(this);
136        mMoveTaskButton = (ImageView) findViewById(R.id.move_task);
137
138        // Hide the backgrounds if they are ripple drawables
139        if (mIconView.getBackground() instanceof RippleDrawable) {
140            mIconView.setBackground(null);
141        }
142
143        mBackgroundColorDrawable = (GradientDrawable) getContext().getDrawable(
144                R.drawable.recents_task_view_header_bg_color);
145        // Copy the ripple drawable since we are going to be manipulating it
146        mBackground = (RippleDrawable)
147                getContext().getDrawable(R.drawable.recents_task_view_header_bg);
148        mBackground = (RippleDrawable) mBackground.mutate().getConstantState().newDrawable();
149        mBackground.setColor(ColorStateList.valueOf(0));
150        mBackground.setDrawableByLayerId(mBackground.getId(0), mBackgroundColorDrawable);
151        setBackground(mBackground);
152    }
153
154    /**
155     * Called when the task view frame changes, allowing us to move the contents of the header
156     * to match the frame changes.
157     */
158    public void onTaskViewSizeChanged(int width, int height) {
159        mTaskViewRect.set(0, 0, width, height);
160        boolean updateMoveTaskButton = mMoveTaskButton.getVisibility() != View.GONE;
161        int appIconWidth = mIconView.getMeasuredWidth();
162        int activityDescWidth = mTitleView.getMeasuredWidth();
163        int dismissIconWidth = mDismissButton.getMeasuredWidth();
164        int moveTaskIconWidth = mMoveTaskButton.getVisibility() == View.VISIBLE
165                ? mMoveTaskButton.getMeasuredWidth()
166                : 0;
167
168        // Priority-wise, we show the activity icon first, the dismiss icon if there is room, the
169        // move-task icon if there is room, and then finally, the activity label if there is room
170        if (width < (appIconWidth + dismissIconWidth)) {
171            mTitleView.setVisibility(View.INVISIBLE);
172            if (updateMoveTaskButton) {
173                mMoveTaskButton.setVisibility(View.INVISIBLE);
174            }
175            mDismissButton.setVisibility(View.INVISIBLE);
176        } else if (width < (appIconWidth + dismissIconWidth + moveTaskIconWidth)) {
177            mTitleView.setVisibility(View.INVISIBLE);
178            if (updateMoveTaskButton) {
179                mMoveTaskButton.setVisibility(View.INVISIBLE);
180            }
181            mDismissButton.setVisibility(View.VISIBLE);
182        } else if (width < (appIconWidth + dismissIconWidth + moveTaskIconWidth +
183                activityDescWidth)) {
184            mTitleView.setVisibility(View.INVISIBLE);
185            if (updateMoveTaskButton) {
186                mMoveTaskButton.setVisibility(View.VISIBLE);
187            }
188            mDismissButton.setVisibility(View.VISIBLE);
189        } else {
190            mTitleView.setVisibility(View.VISIBLE);
191            if (updateMoveTaskButton) {
192                mMoveTaskButton.setVisibility(View.VISIBLE);
193            }
194            mDismissButton.setVisibility(View.VISIBLE);
195        }
196        if (updateMoveTaskButton) {
197            mMoveTaskButton.setTranslationX(width - getMeasuredWidth());
198        }
199        mDismissButton.setTranslationX(width - getMeasuredWidth());
200        invalidate();
201    }
202
203    @Override
204    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
205        onTaskViewSizeChanged(mTaskViewRect.width(), mTaskViewRect.height());
206    }
207
208    @Override
209    protected void onDraw(Canvas canvas) {
210        // Draw the highlight at the top edge (but put the bottom edge just out of view)
211        float offset = (float) Math.ceil(mHighlightHeight / 2f);
212        float radius = mCornerRadius;
213        int count = canvas.save(Canvas.CLIP_SAVE_FLAG);
214        canvas.clipRect(0, 0, mTaskViewRect.width(), getMeasuredHeight());
215        canvas.drawRoundRect(-offset, 0f, (float) mTaskViewRect.width() + offset,
216                getMeasuredHeight() + radius, radius, radius, sHighlightPaint);
217        canvas.restoreToCount(count);
218    }
219
220    /**
221     * Sets the dim alpha, only used when we are not using hardware layers.
222     * (see RecentsConfiguration.useHardwareLayers)
223     */
224    void setDimAlpha(int alpha) {
225        mDimLayerPaint.setColor(Color.argb(alpha, 0, 0, 0));
226        invalidate();
227    }
228
229    /** Binds the bar view to the task */
230    public void rebindToTask(Task t) {
231        SystemServicesProxy ssp = Recents.getSystemServices();
232        mTask = t;
233
234        // If an activity icon is defined, then we use that as the primary icon to show in the bar,
235        // otherwise, we fall back to the application icon
236        if (t.icon != null) {
237            mIconView.setImageDrawable(t.icon);
238        }
239        if (!mTitleView.getText().toString().equals(t.title)) {
240            mTitleView.setText(t.title);
241        }
242        mTitleView.setContentDescription(t.contentDescription);
243
244        // Try and apply the system ui tint
245        int existingBgColor = (getBackground() instanceof ColorDrawable) ?
246                ((ColorDrawable) getBackground()).getColor() : 0;
247        if (existingBgColor != t.colorPrimary) {
248            mBackgroundColorDrawable.setColor(t.colorPrimary);
249        }
250
251        int taskBarViewLightTextColor = getResources().getColor(
252                R.color.recents_task_bar_light_text_color);
253        int taskBarViewDarkTextColor = getResources().getColor(
254                R.color.recents_task_bar_dark_text_color);
255        mTitleView.setTextColor(t.useLightOnPrimaryColor ?
256                taskBarViewLightTextColor : taskBarViewDarkTextColor);
257        mDismissButton.setImageDrawable(t.useLightOnPrimaryColor ?
258                mLightDismissDrawable : mDarkDismissDrawable);
259        mDismissButton.setContentDescription(String.format(mDismissContentDescription,
260                t.contentDescription));
261
262        // When freeform workspaces are enabled, then update the move-task button depending on the
263        // current task
264        if (ssp.hasFreeformWorkspaceSupport()) {
265            if (t.isFreeformTask()) {
266                mMoveTaskTargetStackId = FULLSCREEN_WORKSPACE_STACK_ID;
267                mMoveTaskButton.setImageResource(t.useLightOnPrimaryColor
268                        ? R.drawable.recents_move_task_fullscreen_light
269                        : R.drawable.recents_move_task_fullscreen_dark);
270            } else {
271                mMoveTaskTargetStackId = FREEFORM_WORKSPACE_STACK_ID;
272                mMoveTaskButton.setImageResource(t.useLightOnPrimaryColor
273                        ? R.drawable.recents_move_task_freeform_light
274                        : R.drawable.recents_move_task_freeform_dark);
275            }
276            mMoveTaskButton.setVisibility(View.VISIBLE);
277            mMoveTaskButton.setOnClickListener(this);
278        }
279
280        // In accessibility, a single click on the focused app info button will show it
281        if (ssp.isTouchExplorationEnabled()) {
282            mIconView.setOnClickListener(this);
283        }
284    }
285
286    /** Unbinds the bar view from the task */
287    void unbindFromTask() {
288        mTask = null;
289        mIconView.setImageDrawable(null);
290        mIconView.setOnClickListener(null);
291        mMoveTaskButton.setOnClickListener(null);
292    }
293
294    /** Animates this task bar dismiss button when launching a task. */
295    void startLaunchTaskDismissAnimation(final Runnable postAnimationRunanble) {
296        if (mDismissButton.getVisibility() == View.VISIBLE) {
297            int taskViewExitToAppDuration = mContext.getResources().getInteger(
298                    R.integer.recents_task_exit_to_app_duration);
299            mDismissButton.animate().cancel();
300            mDismissButton.animate()
301                    .alpha(0f)
302                    .setStartDelay(0)
303                    .setInterpolator(mFastOutSlowInInterpolator)
304                    .setDuration(taskViewExitToAppDuration)
305                    .withEndAction(postAnimationRunanble)
306                    .start();
307        }
308    }
309
310    /** Animates this task bar if the user does not interact with the stack after a certain time. */
311    void startNoUserInteractionAnimation() {
312        if (mDismissButton.getVisibility() != View.VISIBLE) {
313            mDismissButton.setVisibility(View.VISIBLE);
314            mDismissButton.setAlpha(0f);
315            mDismissButton.animate()
316                    .alpha(1f)
317                    .setStartDelay(0)
318                    .setInterpolator(mFastOutLinearInInterpolator)
319                    .setDuration(getResources().getInteger(
320                            R.integer.recents_task_enter_from_app_duration))
321                    .start();
322        }
323    }
324
325    /** Mark this task view that the user does has not interacted with the stack after a certain time. */
326    void setNoUserInteractionState() {
327        if (mDismissButton.getVisibility() != View.VISIBLE) {
328            mDismissButton.animate().cancel();
329            mDismissButton.setVisibility(View.VISIBLE);
330            mDismissButton.setAlpha(1f);
331        }
332    }
333
334    /** Resets the state tracking that the user has not interacted with the stack after a certain time. */
335    void resetNoUserInteractionState() {
336        mDismissButton.setVisibility(View.INVISIBLE);
337    }
338
339    @Override
340    protected int[] onCreateDrawableState(int extraSpace) {
341
342        // Don't forward our state to the drawable - we do it manually in onTaskViewFocusChanged.
343        // This is to prevent layer trashing when the view is pressed.
344        return new int[] {};
345    }
346
347    @Override
348    protected void dispatchDraw(Canvas canvas) {
349        super.dispatchDraw(canvas);
350
351        // Draw the dim layer with the rounded corners
352        canvas.drawRoundRect(0, 0, mTaskViewRect.width(), getHeight(),
353                mCornerRadius, mCornerRadius, mDimLayerPaint);
354    }
355
356    @Override
357    public void onClick(View v) {
358        if (v == mIconView) {
359            // In accessibility, a single click on the focused app info button will show it
360            EventBus.getDefault().send(new ShowApplicationInfoEvent(mTask));
361        } else if (v == mDismissButton) {
362            TaskView tv = Utilities.findParent(this, TaskView.class);
363            tv.dismissTask();
364
365            // Keep track of deletions by the dismiss button
366            MetricsLogger.histogram(getContext(), "overview_task_dismissed_source",
367                    Constants.Metrics.DismissSourceHeaderButton);
368        } else if (v == mMoveTaskButton) {
369            TaskView tv = Utilities.findParent(this, TaskView.class);
370            Rect bounds = mMoveTaskTargetStackId == FREEFORM_WORKSPACE_STACK_ID
371                    ? new Rect(mTaskViewRect)
372                    : new Rect();
373            EventBus.getDefault().send(new LaunchTaskEvent(tv, mTask, bounds,
374                    mMoveTaskTargetStackId, false));
375        }
376    }
377
378    @Override
379    public boolean onLongClick(View v) {
380        if (v == mIconView) {
381            EventBus.getDefault().send(new ShowApplicationInfoEvent(mTask));
382            return true;
383        }
384        return false;
385    }
386}
387