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