• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 static com.android.launcher3.Utilities.mapToRange;
20 import static com.android.launcher3.anim.Interpolators.LINEAR;
21 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NOTIFICATION_DISMISSED;
22 
23 import android.animation.AnimatorSet;
24 import android.animation.ValueAnimator;
25 import android.annotation.TargetApi;
26 import android.content.Context;
27 import android.graphics.Outline;
28 import android.graphics.Rect;
29 import android.graphics.drawable.GradientDrawable;
30 import android.os.Build;
31 import android.text.TextUtils;
32 import android.util.AttributeSet;
33 import android.view.View;
34 import android.view.ViewGroup;
35 import android.view.ViewOutlineProvider;
36 import android.widget.LinearLayout;
37 import android.widget.TextView;
38 
39 import androidx.annotation.Nullable;
40 
41 import com.android.launcher3.R;
42 import com.android.launcher3.Utilities;
43 import com.android.launcher3.model.data.ItemInfo;
44 import com.android.launcher3.popup.PopupDataProvider;
45 import com.android.launcher3.util.Themes;
46 import com.android.launcher3.views.ActivityContext;
47 
48 /**
49  * A {@link android.widget.FrameLayout} that contains a single notification,
50  * e.g. icon + title + text.
51  */
52 @TargetApi(Build.VERSION_CODES.N)
53 public class NotificationMainView extends LinearLayout {
54 
55     // This is used only to track the notification view, so that it can be properly logged.
56     public static final ItemInfo NOTIFICATION_ITEM_INFO = new ItemInfo();
57 
58     // Value when the primary notification main view will be gone (zero alpha).
59     private static final float PRIMARY_GONE_PROGRESS = 0.7f;
60     private static final float PRIMARY_MIN_PROGRESS = 0.40f;
61     private static final float PRIMARY_MAX_PROGRESS = 0.60f;
62     private static final float SECONDARY_MIN_PROGRESS = 0.30f;
63     private static final float SECONDARY_MAX_PROGRESS = 0.50f;
64     private static final float SECONDARY_CONTENT_MAX_PROGRESS = 0.6f;
65 
66     private NotificationInfo mNotificationInfo;
67     private int mBackgroundColor;
68     private TextView mTitleView;
69     private TextView mTextView;
70     private View mIconView;
71 
72     private View mHeader;
73     private View mMainView;
74 
75     private TextView mHeaderCount;
76     private final Rect mOutline = new Rect();
77 
78     // Space between notifications during swipe
79     private final int mNotificationSpace;
80     private final int mMaxTransX;
81     private final int mMaxElevation;
82 
83     private final GradientDrawable mBackground;
84 
NotificationMainView(Context context)85     public NotificationMainView(Context context) {
86         this(context, null, 0);
87     }
88 
NotificationMainView(Context context, AttributeSet attrs)89     public NotificationMainView(Context context, AttributeSet attrs) {
90         this(context, attrs, 0);
91     }
92 
NotificationMainView(Context context, AttributeSet attrs, int defStyle)93     public NotificationMainView(Context context, AttributeSet attrs, int defStyle) {
94         this(context, attrs, defStyle, 0);
95     }
96 
NotificationMainView(Context context, AttributeSet attrs, int defStyle, int defStylRes)97     public NotificationMainView(Context context, AttributeSet attrs, int defStyle, int defStylRes) {
98         super(context, attrs, defStyle, defStylRes);
99 
100         float outlineRadius = Themes.getDialogCornerRadius(context);
101 
102         mBackground = new GradientDrawable();
103         mBackground.setColor(Themes.getAttrColor(context, R.attr.popupColorPrimary));
104         mBackground.setCornerRadius(outlineRadius);
105         setBackground(mBackground);
106 
107         mMaxElevation = getResources().getDimensionPixelSize(R.dimen.deep_shortcuts_elevation);
108         setElevation(mMaxElevation);
109 
110         mMaxTransX = getResources().getDimensionPixelSize(R.dimen.notification_max_trans);
111         mNotificationSpace = getResources().getDimensionPixelSize(R.dimen.notification_space);
112 
113         setClipToOutline(true);
114         setOutlineProvider(new ViewOutlineProvider() {
115             @Override
116             public void getOutline(View view, Outline outline) {
117                 outline.setRoundRect(mOutline, outlineRadius);
118             }
119         });
120     }
121 
122     /**
123      * Updates the header text.
124      * @param notificationCount The number of notifications.
125      */
updateHeader(int notificationCount)126     public void updateHeader(int notificationCount) {
127         final String text;
128         final int visibility;
129         if (notificationCount <= 1) {
130             text = "";
131             visibility = View.INVISIBLE;
132         } else {
133             text = String.valueOf(notificationCount);
134             visibility = View.VISIBLE;
135 
136         }
137         mHeaderCount.setText(text);
138         mHeaderCount.setVisibility(visibility);
139     }
140 
141     @Override
onFinishInflate()142     protected void onFinishInflate() {
143         super.onFinishInflate();
144 
145         ViewGroup textAndBackground = findViewById(R.id.text_and_background);
146         mTitleView = textAndBackground.findViewById(R.id.title);
147         mTextView = textAndBackground.findViewById(R.id.text);
148         mIconView = findViewById(R.id.popup_item_icon);
149         mHeaderCount = findViewById(R.id.notification_count);
150 
151         mHeader = findViewById(R.id.header);
152         mMainView = findViewById(R.id.main_view);
153     }
154 
155     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)156     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
157         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
158         mOutline.set(0, 0, getWidth(), getHeight());
159         invalidateOutline();
160     }
161 
updateBackgroundColor(int color)162     private void updateBackgroundColor(int color) {
163         mBackgroundColor = color;
164         mBackground.setColor(color);
165         if (mNotificationInfo != null) {
166             mIconView.setBackground(mNotificationInfo.getIconForBackground(getContext(),
167                     mBackgroundColor));
168         }
169     }
170 
171     /**
172      * Animates the background color to a new color.
173      * @param color The color to change to.
174      * @param animatorSetOut The AnimatorSet where we add the color animator to.
175      */
updateBackgroundColor(int color, AnimatorSet animatorSetOut)176     public void updateBackgroundColor(int color, AnimatorSet animatorSetOut) {
177         int oldColor = mBackgroundColor;
178         ValueAnimator colors = ValueAnimator.ofArgb(oldColor, color);
179         colors.addUpdateListener(valueAnimator -> {
180             int newColor = (int) valueAnimator.getAnimatedValue();
181             updateBackgroundColor(newColor);
182         });
183         animatorSetOut.play(colors);
184     }
185 
186     /**
187      * Sets the content of this view, animating it after a new icon shifts up if necessary.
188      */
applyNotificationInfo(NotificationInfo notificationInfo)189     public void applyNotificationInfo(NotificationInfo notificationInfo) {
190         mNotificationInfo = notificationInfo;
191         if (notificationInfo == null) {
192             return;
193         }
194         NotificationListener listener = NotificationListener.getInstanceIfConnected();
195         if (listener != null) {
196             listener.setNotificationsShown(new String[] {mNotificationInfo.notificationKey});
197         }
198         CharSequence title = mNotificationInfo.title;
199         CharSequence text = mNotificationInfo.text;
200         if (!TextUtils.isEmpty(title) && !TextUtils.isEmpty(text)) {
201             mTitleView.setText(title.toString());
202             mTextView.setText(text.toString());
203         } else {
204             mTitleView.setMaxLines(2);
205             mTitleView.setText(TextUtils.isEmpty(title) ? text.toString() : title.toString());
206             mTextView.setVisibility(GONE);
207         }
208         mIconView.setBackground(mNotificationInfo.getIconForBackground(getContext(),
209                 mBackgroundColor));
210         if (mNotificationInfo.intent != null) {
211             setOnClickListener(mNotificationInfo);
212         }
213 
214         // Add a stub ItemInfo so that logging populates the correct container and item types
215         // instead of DEFAULT_CONTAINERTYPE and DEFAULT_ITEMTYPE, respectively.
216         setTag(NOTIFICATION_ITEM_INFO);
217     }
218 
219     /**
220      * Sets the alpha of only the child views.
221      */
setContentAlpha(float alpha)222     public void setContentAlpha(float alpha) {
223         mHeader.setAlpha(alpha);
224         mMainView.setAlpha(alpha);
225     }
226 
227     /**
228      * Sets the translation of only the child views.
229      */
setContentTranslationX(float transX)230     public void setContentTranslationX(float transX) {
231         mHeader.setTranslationX(transX);
232         mMainView.setTranslationX(transX);
233     }
234 
235     /**
236      * Updates the alpha, content alpha, and elevation of this view.
237      *
238      * @param progress Range from [0, 1] or [-1, 0]
239      *                 When 0: Full alpha
240      *                 When 1/-1: zero alpha
241      */
onPrimaryDrag(float progress)242     public void onPrimaryDrag(float progress) {
243         float absProgress = Math.abs(progress);
244         final int width = getWidth();
245 
246         float min = PRIMARY_MIN_PROGRESS;
247         float max = PRIMARY_MAX_PROGRESS;
248 
249         if (absProgress < min) {
250             setAlpha(1f);
251             setContentAlpha(1);
252             setElevation(mMaxElevation);
253         } else if (absProgress < max) {
254             setAlpha(1f);
255             setContentAlpha(mapToRange(absProgress, min, max, 1f, 0f, LINEAR));
256             setElevation(Utilities.mapToRange(absProgress, min, max, mMaxElevation, 0, LINEAR));
257         } else {
258             setAlpha(mapToRange(absProgress, max, PRIMARY_GONE_PROGRESS, 1f, 0f, LINEAR));
259             setContentAlpha(0f);
260             setElevation(0f);
261         }
262 
263         setTranslationX(width * progress);
264     }
265 
266     /**
267      * Updates the alpha, content alpha, elevation, and clipping of this view.
268      * @param progress Range from [0, 1] or [-1, 0]
269       *                 When 0: Smallest clipping, zero alpha
270       *                 When 1/-1: Full clip, full alpha
271      */
onSecondaryDrag(float progress)272     public void onSecondaryDrag(float progress) {
273         final float absProgress = Math.abs(progress);
274 
275         float min = SECONDARY_MIN_PROGRESS;
276         float max = SECONDARY_MAX_PROGRESS;
277         float contentMax = SECONDARY_CONTENT_MAX_PROGRESS;
278 
279         if (absProgress < min) {
280             setAlpha(0f);
281             setContentAlpha(0);
282             setElevation(0f);
283         } else if (absProgress < max) {
284             setAlpha(mapToRange(absProgress, min, max, 0, 1f, LINEAR));
285             setContentAlpha(0f);
286             setElevation(0f);
287         } else {
288             setAlpha(1f);
289             setContentAlpha(absProgress > contentMax
290                     ? 1f
291                     : mapToRange(absProgress, max, contentMax, 0, 1f, LINEAR));
292             setElevation(Utilities.mapToRange(absProgress, max, 1, 0, mMaxElevation, LINEAR));
293         }
294 
295         final int width = getWidth();
296         int crop = (int) (width * absProgress);
297         int space = (int) (absProgress > PRIMARY_GONE_PROGRESS
298                 ? mapToRange(absProgress, PRIMARY_GONE_PROGRESS, 1f, mNotificationSpace, 0, LINEAR)
299                 : mNotificationSpace);
300         if (progress < 0) {
301             mOutline.left = Math.max(0, getWidth() - crop + space);
302             mOutline.right = getWidth();
303         } else {
304             mOutline.right = Math.min(getWidth(), crop - space);
305             mOutline.left = 0;
306         }
307 
308         float contentTransX = mMaxTransX * (1f - absProgress);
309         setContentTranslationX(progress < 0
310                 ? contentTransX
311                 : -contentTransX);
312         invalidateOutline();
313     }
314 
315     public @Nullable NotificationInfo getNotificationInfo() {
316         return mNotificationInfo;
317     }
318 
319     public boolean canChildBeDismissed() {
320         return mNotificationInfo != null && mNotificationInfo.dismissable;
321     }
322 
323     public void onChildDismissed() {
324         ActivityContext activityContext = ActivityContext.lookupContext(getContext());
325         PopupDataProvider popupDataProvider = activityContext.getPopupDataProvider();
326         if (popupDataProvider == null) {
327             return;
328         }
329         popupDataProvider.cancelNotification(mNotificationInfo.notificationKey);
330         activityContext.getStatsLogManager().logger().log(LAUNCHER_NOTIFICATION_DISMISSED);
331     }
332 }
333