1 /* 2 * Copyright (C) 2021 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 package com.android.launcher3.notification; 17 18 import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity; 19 import static com.android.launcher3.touch.SingleAxisSwipeDetector.HORIZONTAL; 20 21 import android.animation.Animator; 22 import android.animation.AnimatorSet; 23 import android.animation.ObjectAnimator; 24 import android.content.Context; 25 import android.graphics.Rect; 26 import android.util.AttributeSet; 27 import android.util.FloatProperty; 28 import android.view.MotionEvent; 29 import android.view.View; 30 import android.widget.FrameLayout; 31 32 import com.android.launcher3.R; 33 import com.android.launcher3.anim.AnimationSuccessListener; 34 import com.android.launcher3.popup.PopupContainerWithArrow; 35 import com.android.launcher3.touch.BaseSwipeDetector; 36 import com.android.launcher3.touch.OverScroll; 37 import com.android.launcher3.touch.SingleAxisSwipeDetector; 38 39 import java.util.ArrayList; 40 import java.util.Iterator; 41 import java.util.List; 42 43 /** 44 * Class to manage the notification UI in a {@link PopupContainerWithArrow}. 45 * 46 * - Has two {@link NotificationMainView} that represent the top two notifications 47 * - Handles dismissing a notification 48 */ 49 public class NotificationContainer extends FrameLayout implements SingleAxisSwipeDetector.Listener { 50 51 private static final FloatProperty<NotificationContainer> DRAG_TRANSLATION_X = 52 new FloatProperty<NotificationContainer>("notificationProgress") { 53 @Override 54 public void setValue(NotificationContainer view, float transX) { 55 view.setDragTranslationX(transX); 56 } 57 58 @Override 59 public Float get(NotificationContainer view) { 60 return view.mDragTranslationX; 61 } 62 }; 63 64 private static final Rect sTempRect = new Rect(); 65 66 private final SingleAxisSwipeDetector mSwipeDetector; 67 private final List<NotificationInfo> mNotificationInfos = new ArrayList<>(); 68 private boolean mIgnoreTouch = false; 69 70 private final ObjectAnimator mContentTranslateAnimator; 71 private float mDragTranslationX = 0; 72 73 private final NotificationMainView mPrimaryView; 74 private final NotificationMainView mSecondaryView; 75 private PopupContainerWithArrow mPopupContainer; 76 NotificationContainer(Context context)77 public NotificationContainer(Context context) { 78 this(context, null, 0); 79 } 80 NotificationContainer(Context context, AttributeSet attrs)81 public NotificationContainer(Context context, AttributeSet attrs) { 82 this(context, attrs, 0); 83 } 84 NotificationContainer(Context context, AttributeSet attrs, int defStyleAttr)85 public NotificationContainer(Context context, AttributeSet attrs, int defStyleAttr) { 86 super(context, attrs, defStyleAttr); 87 mSwipeDetector = new SingleAxisSwipeDetector(getContext(), this, HORIZONTAL); 88 mSwipeDetector.setDetectableScrollConditions(SingleAxisSwipeDetector.DIRECTION_BOTH, false); 89 mContentTranslateAnimator = ObjectAnimator.ofFloat(this, DRAG_TRANSLATION_X, 0); 90 91 mPrimaryView = (NotificationMainView) View.inflate(getContext(), 92 R.layout.notification_content, null); 93 mSecondaryView = (NotificationMainView) View.inflate(getContext(), 94 R.layout.notification_content, null); 95 mSecondaryView.setAlpha(0); 96 97 addView(mSecondaryView); 98 addView(mPrimaryView); 99 100 } 101 setPopupView(PopupContainerWithArrow popupView)102 public void setPopupView(PopupContainerWithArrow popupView) { 103 mPopupContainer = popupView; 104 } 105 106 /** 107 * Returns true if we should intercept the swipe. 108 */ onInterceptSwipeEvent(MotionEvent ev)109 public boolean onInterceptSwipeEvent(MotionEvent ev) { 110 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 111 sTempRect.set(getLeft(), getTop(), getRight(), getBottom()); 112 mIgnoreTouch = !sTempRect.contains((int) ev.getX(), (int) ev.getY()); 113 if (!mIgnoreTouch) { 114 mPopupContainer.getParent().requestDisallowInterceptTouchEvent(true); 115 } 116 } 117 if (mIgnoreTouch) { 118 return false; 119 } 120 if (mPrimaryView.getNotificationInfo() == null) { 121 // The notification hasn't been populated yet. 122 return false; 123 } 124 125 mSwipeDetector.onTouchEvent(ev); 126 return mSwipeDetector.isDraggingOrSettling(); 127 } 128 129 /** 130 * Returns true when we should handle the swipe. 131 */ onSwipeEvent(MotionEvent ev)132 public boolean onSwipeEvent(MotionEvent ev) { 133 if (mIgnoreTouch) { 134 return false; 135 } 136 if (mPrimaryView.getNotificationInfo() == null) { 137 // The notification hasn't been populated yet. 138 return false; 139 } 140 return mSwipeDetector.onTouchEvent(ev); 141 } 142 143 /** 144 * Applies the list of @param notificationInfos to this container. 145 */ applyNotificationInfos(final List<NotificationInfo> notificationInfos)146 public void applyNotificationInfos(final List<NotificationInfo> notificationInfos) { 147 mNotificationInfos.clear(); 148 if (notificationInfos.isEmpty()) { 149 mPrimaryView.applyNotificationInfo(null); 150 mSecondaryView.applyNotificationInfo(null); 151 return; 152 } 153 mNotificationInfos.addAll(notificationInfos); 154 155 NotificationInfo mainNotification = notificationInfos.get(0); 156 mPrimaryView.applyNotificationInfo(mainNotification); 157 mSecondaryView.applyNotificationInfo(notificationInfos.size() > 1 158 ? notificationInfos.get(1) 159 : null); 160 } 161 162 /** 163 * Trims the notifications. 164 * @param notificationKeys List of all valid notification keys. 165 */ trimNotifications(final List<String> notificationKeys)166 public void trimNotifications(final List<String> notificationKeys) { 167 Iterator<NotificationInfo> iterator = mNotificationInfos.iterator(); 168 while (iterator.hasNext()) { 169 if (!notificationKeys.contains(iterator.next().notificationKey)) { 170 iterator.remove(); 171 } 172 } 173 174 NotificationInfo primaryInfo = mNotificationInfos.size() > 0 175 ? mNotificationInfos.get(0) 176 : null; 177 NotificationInfo secondaryInfo = mNotificationInfos.size() > 1 178 ? mNotificationInfos.get(1) 179 : null; 180 181 mPrimaryView.applyNotificationInfo(primaryInfo); 182 mSecondaryView.applyNotificationInfo(secondaryInfo); 183 184 mPrimaryView.onPrimaryDrag(0); 185 mSecondaryView.onSecondaryDrag(0); 186 } 187 setDragTranslationX(float translationX)188 private void setDragTranslationX(float translationX) { 189 mDragTranslationX = translationX; 190 191 float progress = translationX / getWidth(); 192 mPrimaryView.onPrimaryDrag(progress); 193 if (mSecondaryView.getNotificationInfo() == null) { 194 mSecondaryView.setAlpha(0f); 195 } else { 196 mSecondaryView.onSecondaryDrag(progress); 197 } 198 } 199 200 // SingleAxisSwipeDetector.Listener's 201 @Override onDragStart(boolean start, float startDisplacement)202 public void onDragStart(boolean start, float startDisplacement) { 203 mPopupContainer.showArrow(false); 204 } 205 206 @Override onDrag(float displacement)207 public boolean onDrag(float displacement) { 208 if (!mPrimaryView.canChildBeDismissed()) { 209 displacement = OverScroll.dampedScroll(displacement, getWidth()); 210 } 211 212 float progress = displacement / getWidth(); 213 mPrimaryView.onPrimaryDrag(progress); 214 if (mSecondaryView.getNotificationInfo() == null) { 215 mSecondaryView.setAlpha(0f); 216 } else { 217 mSecondaryView.onSecondaryDrag(progress); 218 } 219 mContentTranslateAnimator.cancel(); 220 return true; 221 } 222 223 @Override onDragEnd(float velocity)224 public void onDragEnd(float velocity) { 225 final boolean willExit; 226 final float endTranslation; 227 final float startTranslation = mPrimaryView.getTranslationX(); 228 final float width = getWidth(); 229 230 if (!mPrimaryView.canChildBeDismissed()) { 231 willExit = false; 232 endTranslation = 0; 233 } else if (mSwipeDetector.isFling(velocity)) { 234 willExit = true; 235 endTranslation = velocity < 0 ? -width : width; 236 } else if (Math.abs(startTranslation) > width / 2f) { 237 willExit = true; 238 endTranslation = (startTranslation < 0 ? -width : width); 239 } else { 240 willExit = false; 241 endTranslation = 0; 242 } 243 244 long duration = BaseSwipeDetector.calculateDuration(velocity, 245 (endTranslation - startTranslation) / width); 246 247 mContentTranslateAnimator.removeAllListeners(); 248 mContentTranslateAnimator.setDuration(duration) 249 .setInterpolator(scrollInterpolatorForVelocity(velocity)); 250 mContentTranslateAnimator.setFloatValues(startTranslation, endTranslation); 251 252 NotificationMainView current = mPrimaryView; 253 mContentTranslateAnimator.addListener(new AnimationSuccessListener() { 254 @Override 255 public void onAnimationSuccess(Animator animator) { 256 mSwipeDetector.finishedScrolling(); 257 if (willExit) { 258 current.onChildDismissed(); 259 } 260 mPopupContainer.showArrow(true); 261 } 262 }); 263 mContentTranslateAnimator.start(); 264 } 265 266 /** 267 * Animates the background color to a new color. 268 * @param color The color to change to. 269 * @param animatorSetOut The AnimatorSet where we add the color animator to. 270 */ updateBackgroundColor(int color, AnimatorSet animatorSetOut)271 public void updateBackgroundColor(int color, AnimatorSet animatorSetOut) { 272 mPrimaryView.updateBackgroundColor(color, animatorSetOut); 273 mSecondaryView.updateBackgroundColor(color, animatorSetOut); 274 } 275 276 /** 277 * Updates the header with a new @param notificationCount. 278 */ updateHeader(int notificationCount)279 public void updateHeader(int notificationCount) { 280 mPrimaryView.updateHeader(notificationCount); 281 mSecondaryView.updateHeader(notificationCount - 1); 282 } 283 } 284