Home | History | Annotate | Download | only in notification
      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