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