1 /*
2  * Copyright (C) 2017 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 
17 package com.android.launcher3.notification;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.animation.AnimatorSet;
22 import android.animation.ObjectAnimator;
23 import android.content.Context;
24 import android.content.res.Resources;
25 import android.graphics.Rect;
26 import android.util.AttributeSet;
27 import android.view.Gravity;
28 import android.view.View;
29 import android.widget.FrameLayout;
30 import android.widget.LinearLayout;
31 
32 import com.android.launcher3.R;
33 import com.android.launcher3.Utilities;
34 import com.android.launcher3.anim.PropertyListBuilder;
35 import com.android.launcher3.anim.PropertyResetListener;
36 import com.android.launcher3.util.Themes;
37 
38 import java.util.ArrayList;
39 import java.util.Iterator;
40 import java.util.List;
41 
42 /**
43  * A {@link FrameLayout} that contains only icons of notifications.
44  * If there are more than {@link #MAX_FOOTER_NOTIFICATIONS} icons, we add a "..." overflow.
45  */
46 public class NotificationFooterLayout extends FrameLayout {
47 
48     public interface IconAnimationEndListener {
onIconAnimationEnd(NotificationInfo animatedNotification)49         void onIconAnimationEnd(NotificationInfo animatedNotification);
50     }
51 
52     private static final int MAX_FOOTER_NOTIFICATIONS = 5;
53 
54     private static final Rect sTempRect = new Rect();
55 
56     private final List<NotificationInfo> mNotifications = new ArrayList<>();
57     private final List<NotificationInfo> mOverflowNotifications = new ArrayList<>();
58     private final boolean mRtl;
59     private final int mBackgroundColor;
60 
61     FrameLayout.LayoutParams mIconLayoutParams;
62     private View mOverflowEllipsis;
63     private LinearLayout mIconRow;
64     private NotificationItemView mContainer;
65 
NotificationFooterLayout(Context context)66     public NotificationFooterLayout(Context context) {
67         this(context, null, 0);
68     }
69 
NotificationFooterLayout(Context context, AttributeSet attrs)70     public NotificationFooterLayout(Context context, AttributeSet attrs) {
71         this(context, attrs, 0);
72     }
73 
NotificationFooterLayout(Context context, AttributeSet attrs, int defStyle)74     public NotificationFooterLayout(Context context, AttributeSet attrs, int defStyle) {
75         super(context, attrs, defStyle);
76 
77         Resources res = getResources();
78         mRtl = Utilities.isRtl(res);
79 
80         int iconSize = res.getDimensionPixelSize(R.dimen.notification_footer_icon_size);
81         mIconLayoutParams = new LayoutParams(iconSize, iconSize);
82         mIconLayoutParams.gravity = Gravity.CENTER_VERTICAL;
83         setWidth((int) res.getDimension(R.dimen.bg_popup_item_width));
84         mBackgroundColor = Themes.getAttrColor(context, R.attr.popupColorPrimary);
85     }
86 
87 
88     /**
89      * Compute margin start for each icon such that the icons between the first one and the ellipsis
90      * are evenly spaced out.
91      */
setWidth(int width)92     public void setWidth(int width) {
93         if (getLayoutParams() != null) {
94             getLayoutParams().width = width;
95         }
96         Resources res = getResources();
97         int iconSize = res.getDimensionPixelSize(R.dimen.notification_footer_icon_size);
98 
99         int paddingEnd = res.getDimensionPixelSize(R.dimen.notification_footer_icon_row_padding);
100         int ellipsisSpace = res.getDimensionPixelSize(R.dimen.horizontal_ellipsis_offset)
101                 + res.getDimensionPixelSize(R.dimen.horizontal_ellipsis_size);
102         int availableIconRowSpace = width - paddingEnd - ellipsisSpace
103                 - iconSize * MAX_FOOTER_NOTIFICATIONS;
104         mIconLayoutParams.setMarginStart(availableIconRowSpace / MAX_FOOTER_NOTIFICATIONS);
105     }
106 
107     @Override
onFinishInflate()108     protected void onFinishInflate() {
109         super.onFinishInflate();
110         mOverflowEllipsis = findViewById(R.id.overflow);
111         mIconRow = findViewById(R.id.icon_row);
112     }
113 
setContainer(NotificationItemView container)114     void setContainer(NotificationItemView container) {
115         mContainer = container;
116     }
117 
118     /**
119      * Keep track of the NotificationInfo, and then update the UI when
120      * {@link #commitNotificationInfos()} is called.
121      */
addNotificationInfo(final NotificationInfo notificationInfo)122     public void addNotificationInfo(final NotificationInfo notificationInfo) {
123         if (mNotifications.size() < MAX_FOOTER_NOTIFICATIONS) {
124             mNotifications.add(notificationInfo);
125         } else {
126             mOverflowNotifications.add(notificationInfo);
127         }
128     }
129 
130     /**
131      * Adds icons and potentially overflow text for all of the NotificationInfo's
132      * added using {@link #addNotificationInfo(NotificationInfo)}.
133      */
commitNotificationInfos()134     public void commitNotificationInfos() {
135         mIconRow.removeAllViews();
136 
137         for (int i = 0; i < mNotifications.size(); i++) {
138             NotificationInfo info = mNotifications.get(i);
139             addNotificationIconForInfo(info);
140         }
141         updateOverflowEllipsisVisibility();
142     }
143 
updateOverflowEllipsisVisibility()144     private void updateOverflowEllipsisVisibility() {
145         mOverflowEllipsis.setVisibility(mOverflowNotifications.isEmpty() ? GONE : VISIBLE);
146     }
147 
148     /**
149      * Creates an icon for the given NotificationInfo, and adds it to the icon row.
150      * @return the icon view that was added
151      */
addNotificationIconForInfo(NotificationInfo info)152     private View addNotificationIconForInfo(NotificationInfo info) {
153         View icon = new View(getContext());
154         icon.setBackground(info.getIconForBackground(getContext(), mBackgroundColor));
155         icon.setOnClickListener(info);
156         icon.setTag(info);
157         icon.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
158         mIconRow.addView(icon, 0, mIconLayoutParams);
159         return icon;
160     }
161 
animateFirstNotificationTo(Rect toBounds, final IconAnimationEndListener callback)162     public void animateFirstNotificationTo(Rect toBounds,
163             final IconAnimationEndListener callback) {
164         AnimatorSet animation = new AnimatorSet();
165         final View firstNotification = mIconRow.getChildAt(mIconRow.getChildCount() - 1);
166 
167         Rect fromBounds = sTempRect;
168         firstNotification.getGlobalVisibleRect(fromBounds);
169         float scale = (float) toBounds.height() / fromBounds.height();
170         Animator moveAndScaleIcon = new PropertyListBuilder().scale(scale)
171                 .translationY(toBounds.top - fromBounds.top
172                         + (fromBounds.height() * scale - fromBounds.height()) / 2)
173                 .build(firstNotification);
174         moveAndScaleIcon.addListener(new AnimatorListenerAdapter() {
175             @Override
176             public void onAnimationEnd(Animator animation) {
177                 callback.onIconAnimationEnd((NotificationInfo) firstNotification.getTag());
178                 removeViewFromIconRow(firstNotification);
179             }
180         });
181         animation.play(moveAndScaleIcon);
182 
183         // Shift all notifications (not the overflow) over to fill the gap.
184         int gapWidth = mIconLayoutParams.width + mIconLayoutParams.getMarginStart();
185         if (mRtl) {
186             gapWidth = -gapWidth;
187         }
188         if (!mOverflowNotifications.isEmpty()) {
189             NotificationInfo notification = mOverflowNotifications.remove(0);
190             mNotifications.add(notification);
191             View iconFromOverflow = addNotificationIconForInfo(notification);
192             animation.play(ObjectAnimator.ofFloat(iconFromOverflow, ALPHA, 0, 1));
193         }
194         int numIcons = mIconRow.getChildCount() - 1; // All children besides the one leaving.
195         // We have to reset the translation X to 0 when the new main notification
196         // is removed from the footer.
197         PropertyResetListener<View, Float> propertyResetListener
198                 = new PropertyResetListener<>(TRANSLATION_X, 0f);
199         for (int i = 0; i < numIcons; i++) {
200             final View child = mIconRow.getChildAt(i);
201             Animator shiftChild = ObjectAnimator.ofFloat(child, TRANSLATION_X, gapWidth);
202             shiftChild.addListener(propertyResetListener);
203             animation.play(shiftChild);
204         }
205         animation.start();
206     }
207 
removeViewFromIconRow(View child)208     private void removeViewFromIconRow(View child) {
209         mIconRow.removeView(child);
210         mNotifications.remove(child.getTag());
211         updateOverflowEllipsisVisibility();
212         if (mIconRow.getChildCount() == 0) {
213             // There are no more icons in the footer, so hide it.
214             if (mContainer != null) {
215                 mContainer.removeFooter();
216             }
217         }
218     }
219 
trimNotifications(List<String> notifications)220     public void trimNotifications(List<String> notifications) {
221         if (!isAttachedToWindow() || mIconRow.getChildCount() == 0) {
222             return;
223         }
224         Iterator<NotificationInfo> overflowIterator = mOverflowNotifications.iterator();
225         while (overflowIterator.hasNext()) {
226             if (!notifications.contains(overflowIterator.next().notificationKey)) {
227                 overflowIterator.remove();
228             }
229         }
230         for (int i = mIconRow.getChildCount() - 1; i >= 0; i--) {
231             View child = mIconRow.getChildAt(i);
232             NotificationInfo childInfo = (NotificationInfo) child.getTag();
233             if (!notifications.contains(childInfo.notificationKey)) {
234                 removeViewFromIconRow(child);
235             }
236         }
237     }
238 }
239