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         // Compute margin start for each icon such that the icons between the first one
84         // and the ellipsis are evenly spaced out.
85         int paddingEnd = res.getDimensionPixelSize(R.dimen.notification_footer_icon_row_padding);
86         int ellipsisSpace = res.getDimensionPixelSize(R.dimen.horizontal_ellipsis_offset)
87                 + res.getDimensionPixelSize(R.dimen.horizontal_ellipsis_size);
88         int footerWidth = res.getDimensionPixelSize(R.dimen.bg_popup_item_width);
89         int availableIconRowSpace = footerWidth - paddingEnd - ellipsisSpace
90                 - iconSize * MAX_FOOTER_NOTIFICATIONS;
91         mIconLayoutParams.setMarginStart(availableIconRowSpace / MAX_FOOTER_NOTIFICATIONS);
92 
93         mBackgroundColor = Themes.getAttrColor(context, R.attr.popupColorPrimary);
94     }
95 
96     @Override
onFinishInflate()97     protected void onFinishInflate() {
98         super.onFinishInflate();
99         mOverflowEllipsis = findViewById(R.id.overflow);
100         mIconRow = findViewById(R.id.icon_row);
101     }
102 
setContainer(NotificationItemView container)103     void setContainer(NotificationItemView container) {
104         mContainer = container;
105     }
106 
107     /**
108      * Keep track of the NotificationInfo, and then update the UI when
109      * {@link #commitNotificationInfos()} is called.
110      */
addNotificationInfo(final NotificationInfo notificationInfo)111     public void addNotificationInfo(final NotificationInfo notificationInfo) {
112         if (mNotifications.size() < MAX_FOOTER_NOTIFICATIONS) {
113             mNotifications.add(notificationInfo);
114         } else {
115             mOverflowNotifications.add(notificationInfo);
116         }
117     }
118 
119     /**
120      * Adds icons and potentially overflow text for all of the NotificationInfo's
121      * added using {@link #addNotificationInfo(NotificationInfo)}.
122      */
commitNotificationInfos()123     public void commitNotificationInfos() {
124         mIconRow.removeAllViews();
125 
126         for (int i = 0; i < mNotifications.size(); i++) {
127             NotificationInfo info = mNotifications.get(i);
128             addNotificationIconForInfo(info);
129         }
130         updateOverflowEllipsisVisibility();
131     }
132 
updateOverflowEllipsisVisibility()133     private void updateOverflowEllipsisVisibility() {
134         mOverflowEllipsis.setVisibility(mOverflowNotifications.isEmpty() ? GONE : VISIBLE);
135     }
136 
137     /**
138      * Creates an icon for the given NotificationInfo, and adds it to the icon row.
139      * @return the icon view that was added
140      */
addNotificationIconForInfo(NotificationInfo info)141     private View addNotificationIconForInfo(NotificationInfo info) {
142         View icon = new View(getContext());
143         icon.setBackground(info.getIconForBackground(getContext(), mBackgroundColor));
144         icon.setOnClickListener(info);
145         icon.setTag(info);
146         icon.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
147         mIconRow.addView(icon, 0, mIconLayoutParams);
148         return icon;
149     }
150 
animateFirstNotificationTo(Rect toBounds, final IconAnimationEndListener callback)151     public void animateFirstNotificationTo(Rect toBounds,
152             final IconAnimationEndListener callback) {
153         AnimatorSet animation = new AnimatorSet();
154         final View firstNotification = mIconRow.getChildAt(mIconRow.getChildCount() - 1);
155 
156         Rect fromBounds = sTempRect;
157         firstNotification.getGlobalVisibleRect(fromBounds);
158         float scale = (float) toBounds.height() / fromBounds.height();
159         Animator moveAndScaleIcon = new PropertyListBuilder().scale(scale)
160                 .translationY(toBounds.top - fromBounds.top
161                         + (fromBounds.height() * scale - fromBounds.height()) / 2)
162                 .build(firstNotification);
163         moveAndScaleIcon.addListener(new AnimatorListenerAdapter() {
164             @Override
165             public void onAnimationEnd(Animator animation) {
166                 callback.onIconAnimationEnd((NotificationInfo) firstNotification.getTag());
167                 removeViewFromIconRow(firstNotification);
168             }
169         });
170         animation.play(moveAndScaleIcon);
171 
172         // Shift all notifications (not the overflow) over to fill the gap.
173         int gapWidth = mIconLayoutParams.width + mIconLayoutParams.getMarginStart();
174         if (mRtl) {
175             gapWidth = -gapWidth;
176         }
177         if (!mOverflowNotifications.isEmpty()) {
178             NotificationInfo notification = mOverflowNotifications.remove(0);
179             mNotifications.add(notification);
180             View iconFromOverflow = addNotificationIconForInfo(notification);
181             animation.play(ObjectAnimator.ofFloat(iconFromOverflow, ALPHA, 0, 1));
182         }
183         int numIcons = mIconRow.getChildCount() - 1; // All children besides the one leaving.
184         // We have to reset the translation X to 0 when the new main notification
185         // is removed from the footer.
186         PropertyResetListener<View, Float> propertyResetListener
187                 = new PropertyResetListener<>(TRANSLATION_X, 0f);
188         for (int i = 0; i < numIcons; i++) {
189             final View child = mIconRow.getChildAt(i);
190             Animator shiftChild = ObjectAnimator.ofFloat(child, TRANSLATION_X, gapWidth);
191             shiftChild.addListener(propertyResetListener);
192             animation.play(shiftChild);
193         }
194         animation.start();
195     }
196 
removeViewFromIconRow(View child)197     private void removeViewFromIconRow(View child) {
198         mIconRow.removeView(child);
199         mNotifications.remove(child.getTag());
200         updateOverflowEllipsisVisibility();
201         if (mIconRow.getChildCount() == 0) {
202             // There are no more icons in the footer, so hide it.
203             if (mContainer != null) {
204                 mContainer.removeFooter();
205             }
206         }
207     }
208 
trimNotifications(List<String> notifications)209     public void trimNotifications(List<String> notifications) {
210         if (!isAttachedToWindow() || mIconRow.getChildCount() == 0) {
211             return;
212         }
213         Iterator<NotificationInfo> overflowIterator = mOverflowNotifications.iterator();
214         while (overflowIterator.hasNext()) {
215             if (!notifications.contains(overflowIterator.next().notificationKey)) {
216                 overflowIterator.remove();
217             }
218         }
219         for (int i = mIconRow.getChildCount() - 1; i >= 0; i--) {
220             View child = mIconRow.getChildAt(i);
221             NotificationInfo childInfo = (NotificationInfo) child.getTag();
222             if (!notifications.contains(childInfo.notificationKey)) {
223                 removeViewFromIconRow(child);
224             }
225         }
226     }
227 }
228