| /* |
| * Copyright (C) 2017 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.launcher3.notification; |
| |
| import android.animation.Animator; |
| import android.animation.AnimatorListenerAdapter; |
| import android.animation.AnimatorSet; |
| import android.animation.ObjectAnimator; |
| import android.content.Context; |
| import android.content.res.Resources; |
| import android.graphics.Rect; |
| import android.util.AttributeSet; |
| import android.view.Gravity; |
| import android.view.View; |
| import android.widget.FrameLayout; |
| import android.widget.LinearLayout; |
| |
| import com.android.launcher3.LauncherAnimUtils; |
| import com.android.launcher3.R; |
| import com.android.launcher3.Utilities; |
| import com.android.launcher3.anim.PropertyListBuilder; |
| import com.android.launcher3.anim.PropertyResetListener; |
| import com.android.launcher3.util.Themes; |
| |
| import java.util.ArrayList; |
| import java.util.Iterator; |
| import java.util.List; |
| |
| /** |
| * A {@link FrameLayout} that contains only icons of notifications. |
| * If there are more than {@link #MAX_FOOTER_NOTIFICATIONS} icons, we add a "..." overflow. |
| */ |
| public class NotificationFooterLayout extends FrameLayout { |
| |
| public interface IconAnimationEndListener { |
| void onIconAnimationEnd(NotificationInfo animatedNotification); |
| } |
| |
| private static final int MAX_FOOTER_NOTIFICATIONS = 5; |
| |
| private static final Rect sTempRect = new Rect(); |
| |
| private final List<NotificationInfo> mNotifications = new ArrayList<>(); |
| private final List<NotificationInfo> mOverflowNotifications = new ArrayList<>(); |
| private final boolean mRtl; |
| private final int mBackgroundColor; |
| |
| FrameLayout.LayoutParams mIconLayoutParams; |
| private View mOverflowEllipsis; |
| private LinearLayout mIconRow; |
| private NotificationItemView mContainer; |
| |
| public NotificationFooterLayout(Context context) { |
| this(context, null, 0); |
| } |
| |
| public NotificationFooterLayout(Context context, AttributeSet attrs) { |
| this(context, attrs, 0); |
| } |
| |
| public NotificationFooterLayout(Context context, AttributeSet attrs, int defStyle) { |
| super(context, attrs, defStyle); |
| |
| Resources res = getResources(); |
| mRtl = Utilities.isRtl(res); |
| |
| int iconSize = res.getDimensionPixelSize(R.dimen.notification_footer_icon_size); |
| mIconLayoutParams = new LayoutParams(iconSize, iconSize); |
| mIconLayoutParams.gravity = Gravity.CENTER_VERTICAL; |
| // Compute margin start for each icon such that the icons between the first one |
| // and the ellipsis are evenly spaced out. |
| int paddingEnd = res.getDimensionPixelSize(R.dimen.notification_footer_icon_row_padding); |
| int ellipsisSpace = res.getDimensionPixelSize(R.dimen.horizontal_ellipsis_offset) |
| + res.getDimensionPixelSize(R.dimen.horizontal_ellipsis_size); |
| int footerWidth = res.getDimensionPixelSize(R.dimen.bg_popup_item_width); |
| int availableIconRowSpace = footerWidth - paddingEnd - ellipsisSpace |
| - iconSize * MAX_FOOTER_NOTIFICATIONS; |
| mIconLayoutParams.setMarginStart(availableIconRowSpace / MAX_FOOTER_NOTIFICATIONS); |
| |
| mBackgroundColor = Themes.getAttrColor(context, R.attr.popupColorPrimary); |
| } |
| |
| @Override |
| protected void onFinishInflate() { |
| super.onFinishInflate(); |
| mOverflowEllipsis = findViewById(R.id.overflow); |
| mIconRow = findViewById(R.id.icon_row); |
| } |
| |
| void setContainer(NotificationItemView container) { |
| mContainer = container; |
| } |
| |
| /** |
| * Keep track of the NotificationInfo, and then update the UI when |
| * {@link #commitNotificationInfos()} is called. |
| */ |
| public void addNotificationInfo(final NotificationInfo notificationInfo) { |
| if (mNotifications.size() < MAX_FOOTER_NOTIFICATIONS) { |
| mNotifications.add(notificationInfo); |
| } else { |
| mOverflowNotifications.add(notificationInfo); |
| } |
| } |
| |
| /** |
| * Adds icons and potentially overflow text for all of the NotificationInfo's |
| * added using {@link #addNotificationInfo(NotificationInfo)}. |
| */ |
| public void commitNotificationInfos() { |
| mIconRow.removeAllViews(); |
| |
| for (int i = 0; i < mNotifications.size(); i++) { |
| NotificationInfo info = mNotifications.get(i); |
| addNotificationIconForInfo(info); |
| } |
| updateOverflowEllipsisVisibility(); |
| } |
| |
| private void updateOverflowEllipsisVisibility() { |
| mOverflowEllipsis.setVisibility(mOverflowNotifications.isEmpty() ? GONE : VISIBLE); |
| } |
| |
| /** |
| * Creates an icon for the given NotificationInfo, and adds it to the icon row. |
| * @return the icon view that was added |
| */ |
| private View addNotificationIconForInfo(NotificationInfo info) { |
| View icon = new View(getContext()); |
| icon.setBackground(info.getIconForBackground(getContext(), mBackgroundColor)); |
| icon.setOnClickListener(info); |
| icon.setTag(info); |
| icon.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); |
| mIconRow.addView(icon, 0, mIconLayoutParams); |
| return icon; |
| } |
| |
| public void animateFirstNotificationTo(Rect toBounds, |
| final IconAnimationEndListener callback) { |
| AnimatorSet animation = LauncherAnimUtils.createAnimatorSet(); |
| final View firstNotification = mIconRow.getChildAt(mIconRow.getChildCount() - 1); |
| |
| Rect fromBounds = sTempRect; |
| firstNotification.getGlobalVisibleRect(fromBounds); |
| float scale = (float) toBounds.height() / fromBounds.height(); |
| Animator moveAndScaleIcon = LauncherAnimUtils.ofPropertyValuesHolder(firstNotification, |
| new PropertyListBuilder().scale(scale).translationY(toBounds.top - fromBounds.top |
| + (fromBounds.height() * scale - fromBounds.height()) / 2).build()); |
| moveAndScaleIcon.addListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| callback.onIconAnimationEnd((NotificationInfo) firstNotification.getTag()); |
| removeViewFromIconRow(firstNotification); |
| } |
| }); |
| animation.play(moveAndScaleIcon); |
| |
| // Shift all notifications (not the overflow) over to fill the gap. |
| int gapWidth = mIconLayoutParams.width + mIconLayoutParams.getMarginStart(); |
| if (mRtl) { |
| gapWidth = -gapWidth; |
| } |
| if (!mOverflowNotifications.isEmpty()) { |
| NotificationInfo notification = mOverflowNotifications.remove(0); |
| mNotifications.add(notification); |
| View iconFromOverflow = addNotificationIconForInfo(notification); |
| animation.play(ObjectAnimator.ofFloat(iconFromOverflow, ALPHA, 0, 1)); |
| } |
| int numIcons = mIconRow.getChildCount() - 1; // All children besides the one leaving. |
| // We have to reset the translation X to 0 when the new main notification |
| // is removed from the footer. |
| PropertyResetListener<View, Float> propertyResetListener |
| = new PropertyResetListener<>(TRANSLATION_X, 0f); |
| for (int i = 0; i < numIcons; i++) { |
| final View child = mIconRow.getChildAt(i); |
| Animator shiftChild = ObjectAnimator.ofFloat(child, TRANSLATION_X, gapWidth); |
| shiftChild.addListener(propertyResetListener); |
| animation.play(shiftChild); |
| } |
| animation.start(); |
| } |
| |
| private void removeViewFromIconRow(View child) { |
| mIconRow.removeView(child); |
| mNotifications.remove(child.getTag()); |
| updateOverflowEllipsisVisibility(); |
| if (mIconRow.getChildCount() == 0) { |
| // There are no more icons in the footer, so hide it. |
| if (mContainer != null) { |
| mContainer.removeFooter(); |
| } |
| } |
| } |
| |
| public void trimNotifications(List<String> notifications) { |
| if (!isAttachedToWindow() || mIconRow.getChildCount() == 0) { |
| return; |
| } |
| Iterator<NotificationInfo> overflowIterator = mOverflowNotifications.iterator(); |
| while (overflowIterator.hasNext()) { |
| if (!notifications.contains(overflowIterator.next().notificationKey)) { |
| overflowIterator.remove(); |
| } |
| } |
| for (int i = mIconRow.getChildCount() - 1; i >= 0; i--) { |
| View child = mIconRow.getChildAt(i); |
| NotificationInfo childInfo = (NotificationInfo) child.getTag(); |
| if (!notifications.contains(childInfo.notificationKey)) { |
| removeViewFromIconRow(child); |
| } |
| } |
| } |
| } |