NotificationMenuRow.java revision 5bec68fbea1a02d7debc2eeeaf01b9478de216e5
1/*
2 * Copyright (C) 2016 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.statusbar;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.ValueAnimator;
22import android.content.Context;
23import android.content.res.Resources;
24import android.graphics.drawable.Drawable;
25import android.util.AttributeSet;
26import android.util.Log;
27import android.view.LayoutInflater;
28import android.view.View;
29import android.widget.FrameLayout;
30
31import java.util.ArrayList;
32
33import com.android.systemui.Dependency;
34import com.android.systemui.Interpolators;
35import com.android.systemui.R;
36import com.android.systemui.plugins.PluginListener;
37import com.android.systemui.plugins.PluginManager;
38import com.android.systemui.plugins.statusbar.NotificationMenuRowProvider;
39import com.android.systemui.plugins.statusbar.NotificationMenuRowProvider.MenuItem;
40import com.android.systemui.plugins.statusbar.NotificationMenuRowProvider.OnMenuClickListener;
41
42public class NotificationMenuRow extends FrameLayout
43        implements PluginListener<NotificationMenuRowProvider>, View.OnClickListener {
44
45    private static final int ICON_ALPHA_ANIM_DURATION = 200;
46
47    private ExpandableNotificationRow mParent;
48    private OnMenuClickListener mListener;
49    private NotificationMenuRowProvider mMenuProvider;
50    private ArrayList<MenuItem> mMenuItems = new ArrayList<>();
51
52    private ValueAnimator mFadeAnimator;
53    private boolean mMenuFadedIn = false;
54    private boolean mAnimating = false;
55    private boolean mOnLeft = true;
56    private boolean mDismissing = false;
57    private boolean mSnapping = false;
58    private boolean mIconsPlaced = false;
59
60    private int[] mIconLocation = new int[2];
61    private int[] mParentLocation = new int[2];
62
63    private float mHorizSpaceForIcon;
64    private int mVertSpaceForIcons;
65
66    private int mIconPadding;
67    private int mIconTint;
68
69    private float mAlpha = 0f;
70
71    public NotificationMenuRow(Context context) {
72        this(context, null);
73    }
74
75    public NotificationMenuRow(Context context, AttributeSet attrs) {
76        this(context, attrs, 0);
77    }
78
79    public NotificationMenuRow(Context context, AttributeSet attrs, int defStyleAttr) {
80        this(context, attrs, defStyleAttr, 0);
81    }
82
83    public NotificationMenuRow(Context context, AttributeSet attrs, int defStyleAttr,
84            int defStyleRes) {
85        super(context, attrs);
86        mMenuItems.addAll(getDefaultNotificationMenuItems());
87    }
88
89    @Override
90    protected void onAttachedToWindow() {
91        super.onAttachedToWindow();
92        Dependency.get(PluginManager.class).addPluginListener(
93                this, NotificationMenuRowProvider.class, false /* Allow multiple */);
94    }
95
96    @Override
97    protected void onDetachedFromWindow() {
98        super.onDetachedFromWindow();
99        Dependency.get(PluginManager.class).removePluginListener(this);
100    }
101
102    @Override
103    protected void onFinishInflate() {
104        super.onFinishInflate();
105        final Resources res = getResources();
106        mHorizSpaceForIcon = res.getDimensionPixelSize(R.dimen.notification_menu_icon_size);
107        mVertSpaceForIcons = res.getDimensionPixelSize(R.dimen.notification_min_height);
108        mIconPadding = res.getDimensionPixelSize(R.dimen.notification_menu_icon_padding);
109        mIconTint = res.getColor(R.color.notification_gear_color);
110        updateMenu(false /* notify */);
111    }
112
113    public static MenuItem getLongpressMenuItem(Context context) {
114        Resources res = context.getResources();
115        Drawable settingsIcon = res.getDrawable(R.drawable.ic_settings);
116        String settingsDescription = res.getString(R.string.notification_menu_gear_description);
117        NotificationInfo settingsContent = (NotificationInfo) LayoutInflater.from(context).inflate(
118                R.layout.notification_info, null, false);
119        MenuItem settings = new MenuItem(settingsIcon, settingsDescription, settingsContent);
120        return settings;
121    }
122
123    public ArrayList<MenuItem> getDefaultNotificationMenuItems() {
124        ArrayList<MenuItem> items = new ArrayList<MenuItem>();
125        Resources res = getResources();
126
127        Drawable snoozeIcon = res.getDrawable(R.drawable.ic_snooze);
128        NotificationSnooze content = (NotificationSnooze) LayoutInflater.from(mContext)
129                .inflate(R.layout.notification_snooze, null, false);
130        String snoozeDescription = res.getString(R.string.notification_menu_snooze_description);
131        MenuItem snooze = new MenuItem(snoozeIcon, snoozeDescription, content);
132        items.add(snooze);
133
134        Drawable settingsIcon = res.getDrawable(R.drawable.ic_settings);
135        String settingsDescription = res.getString(R.string.notification_menu_gear_description);
136        NotificationInfo settingsContent = (NotificationInfo) LayoutInflater.from(mContext).inflate(
137                R.layout.notification_info, null, false);
138        MenuItem settings = new MenuItem(settingsIcon, settingsDescription, settingsContent);
139        items.add(settings);
140        return items;
141    }
142
143    private void updateMenu(boolean notify) {
144        removeAllViews();
145        mMenuItems.clear();
146        if (mMenuProvider != null) {
147            mMenuItems.addAll(mMenuProvider.getMenuItems(getContext()));
148        }
149        mMenuItems.addAll(getDefaultNotificationMenuItems());
150        for (int i = 0; i < mMenuItems.size(); i++) {
151            final View v = createMenuView(mMenuItems.get(i));
152            mMenuItems.get(i).menuView = v;
153        }
154        resetState(notify);
155    }
156
157    private View createMenuView(MenuItem item) {
158        AlphaOptimizedImageView iv = new AlphaOptimizedImageView(getContext());
159        addView(iv);
160        iv.setPadding(mIconPadding, mIconPadding, mIconPadding, mIconPadding);
161        iv.setImageDrawable(item.icon);
162        iv.setOnClickListener(this);
163        iv.setColorFilter(mIconTint);
164        iv.setAlpha(mAlpha);
165        FrameLayout.LayoutParams lp = (LayoutParams) iv.getLayoutParams();
166        lp.width = (int) mHorizSpaceForIcon;
167        lp.height = (int) mHorizSpaceForIcon;
168        return iv;
169    }
170
171    public void resetState(boolean notify) {
172        setMenuAlpha(0f);
173        mIconsPlaced = false;
174        mMenuFadedIn = false;
175        mAnimating = false;
176        mSnapping = false;
177        mDismissing = false;
178        setMenuLocation(mOnLeft ? 1 : -1 /* on left */);
179        if (mListener != null && notify) {
180            mListener.onMenuReset(mParent);
181        }
182    }
183
184    public void setMenuClickListener(OnMenuClickListener listener) {
185        mListener = listener;
186    }
187
188    public void setNotificationRowParent(ExpandableNotificationRow parent) {
189        mParent = parent;
190        setMenuLocation(mOnLeft ? 1 : -1);
191    }
192
193    public void setAppName(String appName) {
194        Resources res = getResources();
195        final int count = mMenuItems.size();
196        for (int i = 0; i < count; i++) {
197            MenuItem item = mMenuItems.get(i);
198            String description = String.format(
199                    res.getString(R.string.notification_menu_accessibility),
200                    appName, item.menuDescription);
201            item.menuView.setContentDescription(description);
202        }
203    }
204
205    public ExpandableNotificationRow getNotificationParent() {
206        return mParent;
207    }
208
209    public void setMenuAlpha(float alpha) {
210        mAlpha = alpha;
211        if (alpha == 0) {
212            mMenuFadedIn = false; // Can fade in again once it's gone.
213            setVisibility(View.INVISIBLE);
214        } else {
215            setVisibility(View.VISIBLE);
216        }
217        final int count = getChildCount();
218        for (int i = 0; i < count; i++) {
219            getChildAt(i).setAlpha(mAlpha);
220        }
221    }
222
223    /**
224     * Returns whether the menu is displayed on the left side of the view or not.
225     */
226    public boolean isMenuOnLeft() {
227        return mOnLeft;
228    }
229
230    /**
231     * Returns the horizontal space in pixels required to display the menu.
232     */
233    public float getSpaceForMenu() {
234        return mHorizSpaceForIcon * getChildCount();
235    }
236
237    /**
238     * Indicates whether the menu is visible at 1 alpha. Does not indicate if entire view is
239     * visible.
240     */
241    public boolean isVisible() {
242        return mAlpha > 0;
243    }
244
245    public void cancelFadeAnimator() {
246        if (mFadeAnimator != null) {
247            mFadeAnimator.cancel();
248        }
249    }
250
251    public void updateMenuAlpha(final float transX, final float size) {
252        if (mAnimating || !mMenuFadedIn) {
253            // Don't adjust when animating, or if the menu hasn't been shown yet.
254            return;
255        }
256
257        final float fadeThreshold = size * 0.3f;
258        final float absTrans = Math.abs(transX);
259        float desiredAlpha = 0;
260
261        if (absTrans == 0) {
262            desiredAlpha = 0;
263        } else if (absTrans <= fadeThreshold) {
264            desiredAlpha = 1;
265        } else {
266            desiredAlpha = 1 - ((absTrans - fadeThreshold) / (size - fadeThreshold));
267        }
268        setMenuAlpha(desiredAlpha);
269    }
270
271    public void fadeInMenu(final boolean fromLeft, final float transX,
272            final float notiThreshold) {
273        if (mDismissing || mAnimating) {
274            return;
275        }
276        if (isMenuLocationChange(transX)) {
277            setMenuAlpha(0f);
278        }
279        setMenuLocation((int) transX);
280        mFadeAnimator = ValueAnimator.ofFloat(mAlpha, 1);
281        mFadeAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
282            @Override
283            public void onAnimationUpdate(ValueAnimator animation) {
284                final float absTrans = Math.abs(transX);
285
286                boolean pastGear = (fromLeft && transX <= notiThreshold)
287                        || (!fromLeft && absTrans <= notiThreshold);
288                if (pastGear && !mMenuFadedIn) {
289                    setMenuAlpha((float) animation.getAnimatedValue());
290                }
291            }
292        });
293        mFadeAnimator.addListener(new AnimatorListenerAdapter() {
294            @Override
295            public void onAnimationStart(Animator animation) {
296                mAnimating = true;
297            }
298
299            @Override
300            public void onAnimationCancel(Animator animation) {
301                // TODO should animate back to 0f from current alpha
302                setMenuAlpha(0f);
303            }
304
305            @Override
306            public void onAnimationEnd(Animator animation) {
307                mAnimating = false;
308                mMenuFadedIn = mAlpha == 1;
309            }
310        });
311        mFadeAnimator.setInterpolator(Interpolators.ALPHA_IN);
312        mFadeAnimator.setDuration(ICON_ALPHA_ANIM_DURATION);
313        mFadeAnimator.start();
314    }
315
316    public void updateVerticalLocation() {
317        if (mParent == null || mMenuItems.size() == 0) {
318            return;
319        }
320        int parentHeight = mParent.getCollapsedHeight();
321        float translationY;
322        if (parentHeight < mVertSpaceForIcons) {
323            translationY = (parentHeight / 2) - (mHorizSpaceForIcon / 2);
324        } else {
325            translationY = (mVertSpaceForIcons - mHorizSpaceForIcon) / 2;
326        }
327        setTranslationY(translationY);
328    }
329
330    @Override
331    public void onRtlPropertiesChanged(int layoutDirection) {
332        mIconsPlaced = false;
333        setMenuLocation(mOnLeft ? 1 : -1);
334    }
335
336    public void setMenuLocation(int translation) {
337        boolean onLeft = translation > 0;
338        if ((mIconsPlaced && onLeft == mOnLeft) || mSnapping || mParent == null) {
339            // Do nothing
340            return;
341        }
342        final boolean isRtl = mParent.isLayoutRtl();
343        final int count = getChildCount();
344        final int width = getWidth();
345        for (int i = 0; i < count; i++) {
346            final View v = getChildAt(i);
347            final float left = isRtl
348                    ? -(width - mHorizSpaceForIcon * (i + 1))
349                    : i * mHorizSpaceForIcon;
350            final float right = isRtl
351                    ? -i * mHorizSpaceForIcon
352                    : width - (mHorizSpaceForIcon * (i + 1));
353            v.setTranslationX(onLeft ? left : right);
354        }
355        mOnLeft = onLeft;
356        mIconsPlaced = true;
357    }
358
359    public boolean isMenuLocationChange(float translation) {
360        boolean onLeft = translation > mIconPadding;
361        boolean onRight = translation < -mIconPadding;
362        if ((mOnLeft && onRight) || (!mOnLeft && onLeft)) {
363            return true;
364        }
365        return false;
366    }
367
368    public void setDismissing() {
369        mDismissing = true;
370    }
371
372    public void setSnapping(boolean snapping) {
373        mSnapping = snapping;
374    }
375
376    @Override
377    public void onClick(View v) {
378        if (mListener == null) {
379            // Nothing to do
380            return;
381        }
382        v.getLocationOnScreen(mIconLocation);
383        mParent.getLocationOnScreen(mParentLocation);
384        final int centerX = (int) (mHorizSpaceForIcon / 2);
385        final int centerY = (int) (v.getTranslationY() * 2 + v.getHeight()) / 2;
386        final int x = mIconLocation[0] - mParentLocation[0] + centerX;
387        final int y = mIconLocation[1] - mParentLocation[1] + centerY;
388        final int index = indexOfChild(v);
389        mListener.onMenuClicked(mParent, x, y, mMenuItems.get(index));
390    }
391
392    @Override
393    public void onPluginConnected(NotificationMenuRowProvider plugin, Context pluginContext) {
394        mMenuProvider = plugin;
395        updateMenu(false /* notify */);
396    }
397
398    @Override
399    public void onPluginDisconnected(NotificationMenuRowProvider plugin) {
400        mMenuProvider = null;
401        updateMenu(false /* notify */);
402    }
403}