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.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; 33 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; 41 42 import java.util.ArrayList; 43 import java.util.Iterator; 44 import java.util.List; 45 46 /** 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. 49 */ 50 public class NotificationFooterLayout extends FrameLayout { 51 52 public interface IconAnimationEndListener { 53 void onIconAnimationEnd(NotificationInfo animatedNotification); 54 } 55 56 private static final int MAX_FOOTER_NOTIFICATIONS = 5; 57 58 private static final Rect sTempRect = new Rect(); 59 60 private final List<NotificationInfo> mNotifications = new ArrayList<>(); 61 private final List<NotificationInfo> mOverflowNotifications = new ArrayList<>(); 62 private final boolean mRtl; 63 64 FrameLayout.LayoutParams mIconLayoutParams; 65 private View mOverflowEllipsis; 66 private LinearLayout mIconRow; 67 private int mBackgroundColor; 68 69 public NotificationFooterLayout(Context context) { 70 this(context, null, 0); 71 } 72 73 public NotificationFooterLayout(Context context, AttributeSet attrs) { 74 this(context, attrs, 0); 75 } 76 77 public NotificationFooterLayout(Context context, AttributeSet attrs, int defStyle) { 78 super(context, attrs, defStyle); 79 80 Resources res = getResources(); 81 mRtl = Utilities.isRtl(res); 82 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); 95 } 96 97 @Override 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(); 103 } 104 105 /** 106 * Keep track of the NotificationInfo, and then update the UI when 107 * {@link #commitNotificationInfos()} is called. 108 */ 109 public void addNotificationInfo(final NotificationInfo notificationInfo) { 110 if (mNotifications.size() < MAX_FOOTER_NOTIFICATIONS) { 111 mNotifications.add(notificationInfo); 112 } else { 113 mOverflowNotifications.add(notificationInfo); 114 } 115 } 116 117 /** 118 * Adds icons and potentially overflow text for all of the NotificationInfo's 119 * added using {@link #addNotificationInfo(NotificationInfo)}. 120 */ 121 public void commitNotificationInfos() { 122 mIconRow.removeAllViews(); 123 124 for (int i = 0; i < mNotifications.size(); i++) { 125 NotificationInfo info = mNotifications.get(i); 126 addNotificationIconForInfo(info); 127 } 128 updateOverflowEllipsisVisibility(); 129 } 130 131 private void updateOverflowEllipsisVisibility() { 132 mOverflowEllipsis.setVisibility(mOverflowNotifications.isEmpty() ? GONE : VISIBLE); 133 } 134 135 /** 136 * Creates an icon for the given NotificationInfo, and adds it to the icon row. 137 * @return the icon view that was added 138 */ 139 private View addNotificationIconForInfo(NotificationInfo info) { 140 View icon = new View(getContext()); 141 icon.setBackground(info.getIconForBackground(getContext(), mBackgroundColor)); 142 icon.setOnClickListener(info); 143 icon.setTag(info); 144 icon.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); 145 mIconRow.addView(icon, 0, mIconLayoutParams); 146 return icon; 147 } 148 149 public void animateFirstNotificationTo(Rect toBounds, 150 final IconAnimationEndListener callback) { 151 AnimatorSet animation = LauncherAnimUtils.createAnimatorSet(); 152 final View firstNotification = mIconRow.getChildAt(mIconRow.getChildCount() - 1); 153 154 Rect fromBounds = sTempRect; 155 firstNotification.getGlobalVisibleRect(fromBounds); 156 float scale = (float) toBounds.height() / fromBounds.height(); 157 Animator moveAndScaleIcon = LauncherAnimUtils.ofPropertyValuesHolder(firstNotification, 158 new PropertyListBuilder().scale(scale).translationY(toBounds.top - fromBounds.top 159 + (fromBounds.height() * scale - fromBounds.height()) / 2).build()); 160 moveAndScaleIcon.addListener(new AnimatorListenerAdapter() { 161 @Override 162 public void onAnimationEnd(Animator animation) { 163 callback.onIconAnimationEnd((NotificationInfo) firstNotification.getTag()); 164 removeViewFromIconRow(firstNotification); 165 } 166 }); 167 animation.play(moveAndScaleIcon); 168 169 // Shift all notifications (not the overflow) over to fill the gap. 170 int gapWidth = mIconLayoutParams.width + mIconLayoutParams.getMarginStart(); 171 if (mRtl) { 172 gapWidth = -gapWidth; 173 } 174 if (!mOverflowNotifications.isEmpty()) { 175 NotificationInfo notification = mOverflowNotifications.remove(0); 176 mNotifications.add(notification); 177 View iconFromOverflow = addNotificationIconForInfo(notification); 178 animation.play(ObjectAnimator.ofFloat(iconFromOverflow, ALPHA, 0, 1)); 179 } 180 int numIcons = mIconRow.getChildCount() - 1; // All children besides the one leaving. 181 // We have to reset the translation X to 0 when the new main notification 182 // is removed from the footer. 183 PropertyResetListener<View, Float> propertyResetListener 184 = new PropertyResetListener<>(TRANSLATION_X, 0f); 185 for (int i = 0; i < numIcons; i++) { 186 final View child = mIconRow.getChildAt(i); 187 Animator shiftChild = ObjectAnimator.ofFloat(child, TRANSLATION_X, gapWidth); 188 shiftChild.addListener(propertyResetListener); 189 animation.play(shiftChild); 190 } 191 animation.start(); 192 } 193 194 private void removeViewFromIconRow(View child) { 195 mIconRow.removeView(child); 196 mNotifications.remove((NotificationInfo) child.getTag()); 197 updateOverflowEllipsisVisibility(); 198 if (mIconRow.getChildCount() == 0) { 199 // There are no more icons in the footer, so hide it. 200 PopupContainerWithArrow popup = PopupContainerWithArrow.getOpen( 201 Launcher.getLauncher(getContext())); 202 if (popup != null) { 203 Animator collapseFooter = popup.reduceNotificationViewHeight(getHeight(), 204 getResources().getInteger(R.integer.config_removeNotificationViewDuration)); 205 collapseFooter.addListener(new AnimatorListenerAdapter() { 206 @Override 207 public void onAnimationEnd(Animator animation) { 208 ((ViewGroup) getParent()).removeView(NotificationFooterLayout.this); 209 } 210 }); 211 collapseFooter.start(); 212 } 213 } 214 } 215 216 public void trimNotifications(List<String> notifications) { 217 if (!isAttachedToWindow() || mIconRow.getChildCount() == 0) { 218 return; 219 } 220 Iterator<NotificationInfo> overflowIterator = mOverflowNotifications.iterator(); 221 while (overflowIterator.hasNext()) { 222 if (!notifications.contains(overflowIterator.next().notificationKey)) { 223 overflowIterator.remove(); 224 } 225 } 226 for (int i = mIconRow.getChildCount() - 1; i >= 0; i--) { 227 View child = mIconRow.getChildAt(i); 228 NotificationInfo childInfo = (NotificationInfo) child.getTag(); 229 if (!notifications.contains(childInfo.notificationKey)) { 230 removeViewFromIconRow(child); 231 } 232 } 233 } 234 } 235