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.content.res.Resources;
28import android.graphics.Canvas;
29import android.graphics.Color;
30import android.graphics.Outline;
31import android.graphics.Paint;
32import android.graphics.PorterDuff;
33import android.graphics.PorterDuffColorFilter;
34import android.graphics.PorterDuffXfermode;
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.MotionEvent;
41import android.view.View;
42import android.view.ViewOutlineProvider;
43import android.widget.FrameLayout;
44import android.widget.ImageView;
45import android.widget.TextView;
46import com.android.systemui.R;
47import com.android.systemui.recents.Constants;
48import com.android.systemui.recents.RecentsConfiguration;
49import com.android.systemui.recents.misc.Utilities;
50import com.android.systemui.recents.model.Task;
51
52
53/* The task bar view */
54public class TaskViewHeader extends FrameLayout {
55
56    RecentsConfiguration mConfig;
57
58    // Header views
59    ImageView mDismissButton;
60    ImageView mApplicationIcon;
61    TextView mActivityDescription;
62
63    // Header drawables
64    boolean mCurrentPrimaryColorIsDark;
65    int mCurrentPrimaryColor;
66    int mBackgroundColor;
67    Drawable mLightDismissDrawable;
68    Drawable mDarkDismissDrawable;
69    RippleDrawable mBackground;
70    GradientDrawable mBackgroundColorDrawable;
71    AnimatorSet mFocusAnimator;
72    String mDismissContentDescription;
73
74    // Static highlight that we draw at the top of each view
75    static Paint sHighlightPaint;
76
77    // Header dim, which is only used when task view hardware layers are not used
78    Paint mDimLayerPaint = new Paint();
79    PorterDuffColorFilter mDimColorFilter = new PorterDuffColorFilter(0, PorterDuff.Mode.SRC_ATOP);
80
81    public TaskViewHeader(Context context) {
82        this(context, null);
83    }
84
85    public TaskViewHeader(Context context, AttributeSet attrs) {
86        this(context, attrs, 0);
87    }
88
89    public TaskViewHeader(Context context, AttributeSet attrs, int defStyleAttr) {
90        this(context, attrs, defStyleAttr, 0);
91    }
92
93    public TaskViewHeader(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
94        super(context, attrs, defStyleAttr, defStyleRes);
95        mConfig = RecentsConfiguration.getInstance();
96        setWillNotDraw(false);
97        setClipToOutline(true);
98        setOutlineProvider(new ViewOutlineProvider() {
99            @Override
100            public void getOutline(View view, Outline outline) {
101                outline.setRect(0, 0, getMeasuredWidth(), getMeasuredHeight());
102            }
103        });
104
105        // Load the dismiss resources
106        Resources res = context.getResources();
107        mLightDismissDrawable = res.getDrawable(R.drawable.recents_dismiss_light);
108        mDarkDismissDrawable = res.getDrawable(R.drawable.recents_dismiss_dark);
109        mDismissContentDescription =
110                res.getString(R.string.accessibility_recents_item_will_be_dismissed);
111
112        // Configure the highlight paint
113        if (sHighlightPaint == null) {
114            sHighlightPaint = new Paint();
115            sHighlightPaint.setStyle(Paint.Style.STROKE);
116            sHighlightPaint.setStrokeWidth(mConfig.taskViewHighlightPx);
117            sHighlightPaint.setColor(mConfig.taskBarViewHighlightColor);
118            sHighlightPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.ADD));
119            sHighlightPaint.setAntiAlias(true);
120        }
121    }
122
123    @Override
124    public boolean onTouchEvent(MotionEvent event) {
125        // We ignore taps on the task bar except on the filter and dismiss buttons
126        if (!Constants.DebugFlags.App.EnableTaskBarTouchEvents) return true;
127
128        return super.onTouchEvent(event);
129    }
130
131    @Override
132    protected void onFinishInflate() {
133        // Initialize the icon and description views
134        mApplicationIcon = (ImageView) findViewById(R.id.application_icon);
135        mActivityDescription = (TextView) findViewById(R.id.activity_description);
136        mDismissButton = (ImageView) findViewById(R.id.dismiss_task);
137
138        // Hide the backgrounds if they are ripple drawables
139        if (!Constants.DebugFlags.App.EnableTaskFiltering) {
140            if (mApplicationIcon.getBackground() instanceof RippleDrawable) {
141                mApplicationIcon.setBackground(null);
142            }
143        }
144
145        mBackgroundColorDrawable = (GradientDrawable) getContext().getDrawable(R.drawable
146                .recents_task_view_header_bg_color);
147        // Copy the ripple drawable since we are going to be manipulating it
148        mBackground = (RippleDrawable)
149                getContext().getDrawable(R.drawable.recents_task_view_header_bg);
150        mBackground = (RippleDrawable) mBackground.mutate().getConstantState().newDrawable();
151        mBackground.setColor(ColorStateList.valueOf(0));
152        mBackground.setDrawableByLayerId(mBackground.getId(0), mBackgroundColorDrawable);
153        setBackground(mBackground);
154    }
155
156    @Override
157    protected void onDraw(Canvas canvas) {
158        // Draw the highlight at the top edge (but put the bottom edge just out of view)
159        float offset = (float) Math.ceil(mConfig.taskViewHighlightPx / 2f);
160        float radius = mConfig.taskViewRoundedCornerRadiusPx;
161        int count = canvas.save(Canvas.CLIP_SAVE_FLAG);
162        canvas.clipRect(0, 0, getMeasuredWidth(), getMeasuredHeight());
163        canvas.drawRoundRect(-offset, 0f, (float) getMeasuredWidth() + offset,
164                getMeasuredHeight() + radius, radius, radius, sHighlightPaint);
165        canvas.restoreToCount(count);
166    }
167
168    @Override
169    public boolean hasOverlappingRendering() {
170        return false;
171    }
172
173    /**
174     * Sets the dim alpha, only used when we are not using hardware layers.
175     * (see RecentsConfiguration.useHardwareLayers)
176     */
177    void setDimAlpha(int alpha) {
178        mDimColorFilter.setColor(Color.argb(alpha, 0, 0, 0));
179        mDimLayerPaint.setColorFilter(mDimColorFilter);
180        setLayerType(LAYER_TYPE_HARDWARE, mDimLayerPaint);
181    }
182
183    /** Returns the secondary color for a primary color. */
184    int getSecondaryColor(int primaryColor, boolean useLightOverlayColor) {
185        int overlayColor = useLightOverlayColor ? Color.WHITE : Color.BLACK;
186        return Utilities.getColorWithOverlay(primaryColor, overlayColor, 0.8f);
187    }
188
189    /** Binds the bar view to the task */
190    public void rebindToTask(Task t) {
191        // If an activity icon is defined, then we use that as the primary icon to show in the bar,
192        // otherwise, we fall back to the application icon
193        if (t.activityIcon != null) {
194            mApplicationIcon.setImageDrawable(t.activityIcon);
195        } else if (t.applicationIcon != null) {
196            mApplicationIcon.setImageDrawable(t.applicationIcon);
197        }
198        mApplicationIcon.setContentDescription(t.activityLabel);
199        if (!mActivityDescription.getText().toString().equals(t.activityLabel)) {
200            mActivityDescription.setText(t.activityLabel);
201        }
202        // Try and apply the system ui tint
203        int existingBgColor = (getBackground() instanceof ColorDrawable) ?
204                ((ColorDrawable) getBackground()).getColor() : 0;
205        if (existingBgColor != t.colorPrimary) {
206            mBackgroundColorDrawable.setColor(t.colorPrimary);
207            mBackgroundColor = t.colorPrimary;
208        }
209        mCurrentPrimaryColor = t.colorPrimary;
210        mCurrentPrimaryColorIsDark = t.useLightOnPrimaryColor;
211        mActivityDescription.setTextColor(t.useLightOnPrimaryColor ?
212                mConfig.taskBarViewLightTextColor : mConfig.taskBarViewDarkTextColor);
213        mDismissButton.setImageDrawable(t.useLightOnPrimaryColor ?
214                mLightDismissDrawable : mDarkDismissDrawable);
215        mDismissButton.setContentDescription(String.format(mDismissContentDescription,
216                t.activityLabel));
217    }
218
219    /** Unbinds the bar view from the task */
220    void unbindFromTask() {
221        mApplicationIcon.setImageDrawable(null);
222    }
223
224    /** Animates this task bar dismiss button when launching a task. */
225    void startLaunchTaskDismissAnimation() {
226        if (mDismissButton.getVisibility() == View.VISIBLE) {
227            mDismissButton.animate().cancel();
228            mDismissButton.animate()
229                    .alpha(0f)
230                    .setStartDelay(0)
231                    .setInterpolator(mConfig.fastOutSlowInInterpolator)
232                    .setDuration(mConfig.taskViewExitToAppDuration)
233                    .withLayer()
234                    .start();
235        }
236    }
237
238    /** Animates this task bar if the user does not interact with the stack after a certain time. */
239    void startNoUserInteractionAnimation() {
240        if (mDismissButton.getVisibility() != View.VISIBLE) {
241            mDismissButton.setVisibility(View.VISIBLE);
242            mDismissButton.setAlpha(0f);
243            mDismissButton.animate()
244                    .alpha(1f)
245                    .setStartDelay(0)
246                    .setInterpolator(mConfig.fastOutLinearInInterpolator)
247                    .setDuration(mConfig.taskViewEnterFromAppDuration)
248                    .withLayer()
249                    .start();
250        }
251    }
252
253    /** Mark this task view that the user does has not interacted with the stack after a certain time. */
254    void setNoUserInteractionState() {
255        if (mDismissButton.getVisibility() != View.VISIBLE) {
256            mDismissButton.animate().cancel();
257            mDismissButton.setVisibility(View.VISIBLE);
258            mDismissButton.setAlpha(1f);
259        }
260    }
261
262    /** Resets the state tracking that the user has not interacted with the stack after a certain time. */
263    void resetNoUserInteractionState() {
264        mDismissButton.setVisibility(View.INVISIBLE);
265    }
266
267    @Override
268    protected int[] onCreateDrawableState(int extraSpace) {
269
270        // Don't forward our state to the drawable - we do it manually in onTaskViewFocusChanged.
271        // This is to prevent layer trashing when the view is pressed.
272        return new int[] {};
273    }
274
275    /** Notifies the associated TaskView has been focused. */
276    void onTaskViewFocusChanged(boolean focused, boolean animateFocusedState) {
277        // If we are not animating the visible state, just return
278        if (!animateFocusedState) return;
279
280        boolean isRunning = false;
281        if (mFocusAnimator != null) {
282            isRunning = mFocusAnimator.isRunning();
283            Utilities.cancelAnimationWithoutCallbacks(mFocusAnimator);
284        }
285
286        if (focused) {
287            int secondaryColor = getSecondaryColor(mCurrentPrimaryColor, mCurrentPrimaryColorIsDark);
288            int[][] states = new int[][] {
289                    new int[] { android.R.attr.state_enabled },
290                    new int[] { android.R.attr.state_pressed }
291            };
292            int[] newStates = new int[]{
293                    android.R.attr.state_enabled,
294                    android.R.attr.state_pressed
295            };
296            int[] colors = new int[] {
297                    secondaryColor,
298                    secondaryColor
299            };
300            mBackground.setColor(new ColorStateList(states, colors));
301            mBackground.setState(newStates);
302            // Pulse the background color
303            int currentColor = mBackgroundColor;
304            int lightPrimaryColor = getSecondaryColor(mCurrentPrimaryColor, mCurrentPrimaryColorIsDark);
305            ValueAnimator backgroundColor = ValueAnimator.ofObject(new ArgbEvaluator(),
306                    currentColor, lightPrimaryColor);
307            backgroundColor.addListener(new AnimatorListenerAdapter() {
308                @Override
309                public void onAnimationStart(Animator animation) {
310                    mBackground.setState(new int[]{});
311                }
312            });
313            backgroundColor.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
314                @Override
315                public void onAnimationUpdate(ValueAnimator animation) {
316                    int color = (int) animation.getAnimatedValue();
317                    mBackgroundColorDrawable.setColor(color);
318                    mBackgroundColor = color;
319                }
320            });
321            backgroundColor.setRepeatCount(ValueAnimator.INFINITE);
322            backgroundColor.setRepeatMode(ValueAnimator.REVERSE);
323            // Pulse the translation
324            ObjectAnimator translation = ObjectAnimator.ofFloat(this, "translationZ", 15f);
325            translation.setRepeatCount(ValueAnimator.INFINITE);
326            translation.setRepeatMode(ValueAnimator.REVERSE);
327
328            mFocusAnimator = new AnimatorSet();
329            mFocusAnimator.playTogether(backgroundColor, translation);
330            mFocusAnimator.setStartDelay(750);
331            mFocusAnimator.setDuration(750);
332            mFocusAnimator.start();
333        } else {
334            if (isRunning) {
335                // Restore the background color
336                int currentColor = mBackgroundColor;
337                ValueAnimator backgroundColor = ValueAnimator.ofObject(new ArgbEvaluator(),
338                        currentColor, mCurrentPrimaryColor);
339                backgroundColor.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
340                    @Override
341                    public void onAnimationUpdate(ValueAnimator animation) {
342                        int color = (int) animation.getAnimatedValue();
343                        mBackgroundColorDrawable.setColor(color);
344                        mBackgroundColor = color;
345                    }
346                });
347                // Restore the translation
348                ObjectAnimator translation = ObjectAnimator.ofFloat(this, "translationZ", 0f);
349
350                mFocusAnimator = new AnimatorSet();
351                mFocusAnimator.playTogether(backgroundColor, translation);
352                mFocusAnimator.setDuration(150);
353                mFocusAnimator.start();
354            } else {
355                mBackground.setState(new int[] {});
356                setTranslationZ(0f);
357            }
358        }
359    }
360}
361