/* * Copyright (C) 2021 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 static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity; import static com.android.launcher3.touch.SingleAxisSwipeDetector.HORIZONTAL; import android.animation.Animator; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.content.Context; import android.graphics.Rect; import android.util.AttributeSet; import android.util.FloatProperty; import android.view.MotionEvent; import android.view.View; import android.widget.FrameLayout; import com.android.launcher3.R; import com.android.launcher3.anim.AnimationSuccessListener; import com.android.launcher3.popup.PopupContainerWithArrow; import com.android.launcher3.touch.BaseSwipeDetector; import com.android.launcher3.touch.OverScroll; import com.android.launcher3.touch.SingleAxisSwipeDetector; import java.util.ArrayList; import java.util.Iterator; import java.util.List; /** * Class to manage the notification UI in a {@link PopupContainerWithArrow}. * * - Has two {@link NotificationMainView} that represent the top two notifications * - Handles dismissing a notification */ public class NotificationContainer extends FrameLayout implements SingleAxisSwipeDetector.Listener { private static final FloatProperty DRAG_TRANSLATION_X = new FloatProperty("notificationProgress") { @Override public void setValue(NotificationContainer view, float transX) { view.setDragTranslationX(transX); } @Override public Float get(NotificationContainer view) { return view.mDragTranslationX; } }; private static final Rect sTempRect = new Rect(); private final SingleAxisSwipeDetector mSwipeDetector; private final List mNotificationInfos = new ArrayList<>(); private boolean mIgnoreTouch = false; private final ObjectAnimator mContentTranslateAnimator; private float mDragTranslationX = 0; private final NotificationMainView mPrimaryView; private final NotificationMainView mSecondaryView; private PopupContainerWithArrow mPopupContainer; public NotificationContainer(Context context) { this(context, null, 0); } public NotificationContainer(Context context, AttributeSet attrs) { this(context, attrs, 0); } public NotificationContainer(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mSwipeDetector = new SingleAxisSwipeDetector(getContext(), this, HORIZONTAL); mSwipeDetector.setDetectableScrollConditions(SingleAxisSwipeDetector.DIRECTION_BOTH, false); mContentTranslateAnimator = ObjectAnimator.ofFloat(this, DRAG_TRANSLATION_X, 0); mPrimaryView = (NotificationMainView) View.inflate(getContext(), R.layout.notification_content, null); mSecondaryView = (NotificationMainView) View.inflate(getContext(), R.layout.notification_content, null); mSecondaryView.setAlpha(0); addView(mSecondaryView); addView(mPrimaryView); } public void setPopupView(PopupContainerWithArrow popupView) { mPopupContainer = popupView; } /** * Returns true if we should intercept the swipe. */ public boolean onInterceptSwipeEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { sTempRect.set(getLeft(), getTop(), getRight(), getBottom()); mIgnoreTouch = !sTempRect.contains((int) ev.getX(), (int) ev.getY()); if (!mIgnoreTouch) { mPopupContainer.getParent().requestDisallowInterceptTouchEvent(true); } } if (mIgnoreTouch) { return false; } if (mPrimaryView.getNotificationInfo() == null) { // The notification hasn't been populated yet. return false; } mSwipeDetector.onTouchEvent(ev); return mSwipeDetector.isDraggingOrSettling(); } /** * Returns true when we should handle the swipe. */ public boolean onSwipeEvent(MotionEvent ev) { if (mIgnoreTouch) { return false; } if (mPrimaryView.getNotificationInfo() == null) { // The notification hasn't been populated yet. return false; } return mSwipeDetector.onTouchEvent(ev); } /** * Applies the list of @param notificationInfos to this container. */ public void applyNotificationInfos(final List notificationInfos) { mNotificationInfos.clear(); if (notificationInfos.isEmpty()) { mPrimaryView.applyNotificationInfo(null); mSecondaryView.applyNotificationInfo(null); return; } mNotificationInfos.addAll(notificationInfos); NotificationInfo mainNotification = notificationInfos.get(0); mPrimaryView.applyNotificationInfo(mainNotification); mSecondaryView.applyNotificationInfo(notificationInfos.size() > 1 ? notificationInfos.get(1) : null); } /** * Trims the notifications. * @param notificationKeys List of all valid notification keys. */ public void trimNotifications(final List notificationKeys) { Iterator iterator = mNotificationInfos.iterator(); while (iterator.hasNext()) { if (!notificationKeys.contains(iterator.next().notificationKey)) { iterator.remove(); } } NotificationInfo primaryInfo = mNotificationInfos.size() > 0 ? mNotificationInfos.get(0) : null; NotificationInfo secondaryInfo = mNotificationInfos.size() > 1 ? mNotificationInfos.get(1) : null; mPrimaryView.applyNotificationInfo(primaryInfo); mSecondaryView.applyNotificationInfo(secondaryInfo); mPrimaryView.onPrimaryDrag(0); mSecondaryView.onSecondaryDrag(0); } private void setDragTranslationX(float translationX) { mDragTranslationX = translationX; float progress = translationX / getWidth(); mPrimaryView.onPrimaryDrag(progress); if (mSecondaryView.getNotificationInfo() == null) { mSecondaryView.setAlpha(0f); } else { mSecondaryView.onSecondaryDrag(progress); } } // SingleAxisSwipeDetector.Listener's @Override public void onDragStart(boolean start, float startDisplacement) { mPopupContainer.showArrow(false); } @Override public boolean onDrag(float displacement) { if (!mPrimaryView.canChildBeDismissed()) { displacement = OverScroll.dampedScroll(displacement, getWidth()); } float progress = displacement / getWidth(); mPrimaryView.onPrimaryDrag(progress); if (mSecondaryView.getNotificationInfo() == null) { mSecondaryView.setAlpha(0f); } else { mSecondaryView.onSecondaryDrag(progress); } mContentTranslateAnimator.cancel(); return true; } @Override public void onDragEnd(float velocity) { final boolean willExit; final float endTranslation; final float startTranslation = mPrimaryView.getTranslationX(); final float width = getWidth(); if (!mPrimaryView.canChildBeDismissed()) { willExit = false; endTranslation = 0; } else if (mSwipeDetector.isFling(velocity)) { willExit = true; endTranslation = velocity < 0 ? -width : width; } else if (Math.abs(startTranslation) > width / 2f) { willExit = true; endTranslation = (startTranslation < 0 ? -width : width); } else { willExit = false; endTranslation = 0; } long duration = BaseSwipeDetector.calculateDuration(velocity, (endTranslation - startTranslation) / width); mContentTranslateAnimator.removeAllListeners(); mContentTranslateAnimator.setDuration(duration) .setInterpolator(scrollInterpolatorForVelocity(velocity)); mContentTranslateAnimator.setFloatValues(startTranslation, endTranslation); NotificationMainView current = mPrimaryView; mContentTranslateAnimator.addListener(new AnimationSuccessListener() { @Override public void onAnimationSuccess(Animator animator) { mSwipeDetector.finishedScrolling(); if (willExit) { current.onChildDismissed(); } mPopupContainer.showArrow(true); } }); mContentTranslateAnimator.start(); } /** * Animates the background color to a new color. * @param color The color to change to. * @param animatorSetOut The AnimatorSet where we add the color animator to. */ public void updateBackgroundColor(int color, AnimatorSet animatorSetOut) { mPrimaryView.updateBackgroundColor(color, animatorSetOut); mSecondaryView.updateBackgroundColor(color, animatorSetOut); } /** * Updates the header with a new @param notificationCount. */ public void updateHeader(int notificationCount) { mPrimaryView.updateHeader(notificationCount); mSecondaryView.updateHeader(notificationCount - 1); } }