2 * Copyright (C) 2017 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.launcher3.notification;
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.graphics.drawable.ColorDrawable;
27 import android.util.AttributeSet;
28 import android.view.Gravity;
29 import android.view.View;
30 import android.view.ViewGroup;
31 import android.widget.FrameLayout;
32 import android.widget.LinearLayout;
34 import com.android.launcher3.Launcher;
35 import com.android.launcher3.LauncherAnimUtils;
36 import com.android.launcher3.R;
37 import com.android.launcher3.Utilities;
38 import com.android.launcher3.anim.PropertyListBuilder;
39 import com.android.launcher3.anim.PropertyResetListener;
40 import com.android.launcher3.popup.PopupContainerWithArrow;
42 import java.util.ArrayList;
43 import java.util.Iterator;
44 import java.util.List;
47 * A {@link FrameLayout} that contains only icons of notifications.
48 * If there are more than {@link #MAX_FOOTER_NOTIFICATIONS} icons, we add a "..." overflow.
50 public class NotificationFooterLayout extends FrameLayout {
52 public interface IconAnimationEndListener {
53 void onIconAnimationEnd(NotificationInfo animatedNotification);
56 private static final int MAX_FOOTER_NOTIFICATIONS = 5;
58 private static final Rect sTempRect = new Rect();
60 private final List<NotificationInfo> mNotifications = new ArrayList<>();
61 private final List<NotificationInfo> mOverflowNotifications = new ArrayList<>();
62 private final boolean mRtl;
64 FrameLayout.LayoutParams mIconLayoutParams;
65 private View mOverflowEllipsis;
66 private LinearLayout mIconRow;
67 private int mBackgroundColor;
69 public NotificationFooterLayout(Context context) {
70 this(context, null, 0);
73 public NotificationFooterLayout(Context context, AttributeSet attrs) {
74 this(context, attrs, 0);
77 public NotificationFooterLayout(Context context, AttributeSet attrs, int defStyle) {
78 super(context, attrs, defStyle);
80 Resources res = getResources();
81 mRtl = Utilities.isRtl(res);
83 int iconSize = res.getDimensionPixelSize(R.dimen.notification_footer_icon_size);
84 mIconLayoutParams = new LayoutParams(iconSize, iconSize);
85 mIconLayoutParams.gravity = Gravity.CENTER_VERTICAL;
86 // Compute margin start for each icon such that the icons between the first one
87 // and the ellipsis are evenly spaced out.
88 int paddingEnd = res.getDimensionPixelSize(R.dimen.notification_footer_icon_row_padding);
89 int ellipsisSpace = res.getDimensionPixelSize(R.dimen.horizontal_ellipsis_offset)
90 + res.getDimensionPixelSize(R.dimen.horizontal_ellipsis_size);
91 int footerWidth = res.getDimensionPixelSize(R.dimen.bg_popup_item_width);
92 int availableIconRowSpace = footerWidth - paddingEnd - ellipsisSpace
93 - iconSize * MAX_FOOTER_NOTIFICATIONS;
94 mIconLayoutParams.setMarginStart(availableIconRowSpace / MAX_FOOTER_NOTIFICATIONS);
98 protected void onFinishInflate() {
99 super.onFinishInflate();
100 mOverflowEllipsis = findViewById(R.id.overflow);
101 mIconRow = (LinearLayout) findViewById(R.id.icon_row);
102 mBackgroundColor = ((ColorDrawable) getBackground()).getColor();
106 * Keep track of the NotificationInfo, and then update the UI when
107 * {@link #commitNotificationInfos()} is called.
109 public void addNotificationInfo(final NotificationInfo notificationInfo) {
110 if (mNotifications.size() < MAX_FOOTER_NOTIFICATIONS) {
111 mNotifications.add(notificationInfo);
113 mOverflowNotifications.add(notificationInfo);
118 * Adds icons and potentially overflow text for all of the NotificationInfo's
119 * added using {@link #addNotificationInfo(NotificationInfo)}.
121 public void commitNotificationInfos() {
122 mIconRow.removeAllViews();
124 for (int i = 0; i < mNotifications.size(); i++) {
125 NotificationInfo info = mNotifications.get(i);
126 addNotificationIconForInfo(info);
128 updateOverflowEllipsisVisibility();
131 private void updateOverflowEllipsisVisibility() {
132 mOverflowEllipsis.setVisibility(mOverflowNotifications.isEmpty() ? GONE : VISIBLE);
136 * Creates an icon for the given NotificationInfo, and adds it to the icon row.
137 * @return the icon view that was added
139 private View addNotificationIconForInfo(NotificationInfo info) {
140 View icon = new View(getContext());
141 icon.setBackground(info.getIconForBackground(getContext(), mBackgroundColor));
142 icon.setOnClickListener(info);
144 mIconRow.addView(icon, 0, mIconLayoutParams);
148 public void animateFirstNotificationTo(Rect toBounds,
149 final IconAnimationEndListener callback) {
150 AnimatorSet animation = LauncherAnimUtils.createAnimatorSet();
151 final View firstNotification = mIconRow.getChildAt(mIconRow.getChildCount() - 1);
153 Rect fromBounds = sTempRect;
154 firstNotification.getGlobalVisibleRect(fromBounds);
155 float scale = (float) toBounds.height() / fromBounds.height();
156 Animator moveAndScaleIcon = LauncherAnimUtils.ofPropertyValuesHolder(firstNotification,
157 new PropertyListBuilder().scale(scale).translationY(toBounds.top - fromBounds.top
158 + (fromBounds.height() * scale - fromBounds.height()) / 2).build());
159 moveAndScaleIcon.addListener(new AnimatorListenerAdapter() {
161 public void onAnimationEnd(Animator animation) {
162 callback.onIconAnimationEnd((NotificationInfo) firstNotification.getTag());
163 removeViewFromIconRow(firstNotification);
166 animation.play(moveAndScaleIcon);
168 // Shift all notifications (not the overflow) over to fill the gap.
169 int gapWidth = mIconLayoutParams.width + mIconLayoutParams.getMarginStart();
171 gapWidth = -gapWidth;
173 if (!mOverflowNotifications.isEmpty()) {
174 NotificationInfo notification = mOverflowNotifications.remove(0);
175 mNotifications.add(notification);
176 View iconFromOverflow = addNotificationIconForInfo(notification);
177 animation.play(ObjectAnimator.ofFloat(iconFromOverflow, ALPHA, 0, 1));
179 int numIcons = mIconRow.getChildCount() - 1; // All children besides the one leaving.
180 // We have to reset the translation X to 0 when the new main notification
181 // is removed from the footer.
182 PropertyResetListener<View, Float> propertyResetListener
183 = new PropertyResetListener<>(TRANSLATION_X, 0f);
184 for (int i = 0; i < numIcons; i++) {
185 final View child = mIconRow.getChildAt(i);
186 Animator shiftChild = ObjectAnimator.ofFloat(child, TRANSLATION_X, gapWidth);
187 shiftChild.addListener(propertyResetListener);
188 animation.play(shiftChild);
193 private void removeViewFromIconRow(View child) {
194 mIconRow.removeView(child);
195 mNotifications.remove((NotificationInfo) child.getTag());
196 updateOverflowEllipsisVisibility();
197 if (mIconRow.getChildCount() == 0) {
198 // There are no more icons in the footer, so hide it.
199 PopupContainerWithArrow popup = PopupContainerWithArrow.getOpen(
200 Launcher.getLauncher(getContext()));
202 Animator collapseFooter = popup.reduceNotificationViewHeight(getHeight(),
203 getResources().getInteger(R.integer.config_removeNotificationViewDuration));
204 collapseFooter.addListener(new AnimatorListenerAdapter() {
206 public void onAnimationEnd(Animator animation) {
207 ((ViewGroup) getParent()).removeView(NotificationFooterLayout.this);
210 collapseFooter.start();
215 public void trimNotifications(List<String> notifications) {
216 if (!isAttachedToWindow() || mIconRow.getChildCount() == 0) {
219 Iterator<NotificationInfo> overflowIterator = mOverflowNotifications.iterator();
220 while (overflowIterator.hasNext()) {
221 if (!notifications.contains(overflowIterator.next().notificationKey)) {
222 overflowIterator.remove();
225 for (int i = mIconRow.getChildCount() - 1; i >= 0; i--) {
226 View child = mIconRow.getChildAt(i);
227 NotificationInfo childInfo = (NotificationInfo) child.getTag();
228 if (!notifications.contains(childInfo.notificationKey)) {
229 removeViewFromIconRow(child);