Merge "Generalize notifications logic into a subclass of OverlayViewController" into rvc-dev

This commit is contained in:
Heemin Seog
2020-04-09 05:17:54 +00:00
committed by Android (Google) Code Review
10 changed files with 1111 additions and 457 deletions

View File

@@ -20,8 +20,6 @@
android:id="@+id/notification_view" android:id="@+id/notification_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:visibility="invisible"
android:layout_marginBottom="@dimen/navigation_bar_height"
android:background="@color/notification_shade_background_color"> android:background="@color/notification_shade_background_color">
<View <View

View File

@@ -18,4 +18,5 @@
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/notification_container" android:id="@+id/notification_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"/> android:layout_height="match_parent"
android:visibility="invisible"/>

View File

@@ -22,10 +22,12 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<!-- TODO(b/151617493): replace marginBottom with insets. -->
<ViewStub android:id="@+id/notification_panel_stub" <ViewStub android:id="@+id/notification_panel_stub"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout="@layout/notification_panel_container"/> android:layout="@layout/notification_panel_container"
android:layout_marginBottom="@dimen/navigation_bar_height"/>
<ViewStub android:id="@+id/fullscreen_user_switcher_stub" <ViewStub android:id="@+id/fullscreen_user_switcher_stub"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@@ -24,7 +24,7 @@ import com.android.systemui.statusbar.policy.DeviceProvisionedController;
*/ */
public interface CarDeviceProvisionedController extends DeviceProvisionedController { public interface CarDeviceProvisionedController extends DeviceProvisionedController {
/** /**
* Returns {@code true} then SUW is in progress for the given user. * Returns {@code true} when SUW is in progress for the given user.
*/ */
boolean isUserSetupInProgress(int user); boolean isUserSetupInProgress(int user);

View File

@@ -16,9 +16,6 @@
package com.android.systemui.car.notification; package com.android.systemui.car.notification;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.app.ActivityManager; import android.app.ActivityManager;
import android.car.Car; import android.car.Car;
import android.car.drivingstate.CarUxRestrictionsManager; import android.car.drivingstate.CarUxRestrictionsManager;
@@ -33,7 +30,6 @@ import android.view.LayoutInflater;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
@@ -54,7 +50,7 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.FlingAnimationUtils; import com.android.systemui.statusbar.FlingAnimationUtils;
import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.window.OverlayViewController; import com.android.systemui.window.OverlayPanelViewController;
import com.android.systemui.window.OverlayViewGlobalStateController; import com.android.systemui.window.OverlayViewGlobalStateController;
import javax.inject.Inject; import javax.inject.Inject;
@@ -62,39 +58,22 @@ import javax.inject.Singleton;
/** View controller for the notification panel. */ /** View controller for the notification panel. */
@Singleton @Singleton
public class NotificationPanelViewController extends OverlayViewController { public class NotificationPanelViewController extends OverlayPanelViewController {
// used to calculate how fast to open or close the window
private static final float DEFAULT_FLING_VELOCITY = 0;
// max time a fling animation takes
private static final float FLING_ANIMATION_MAX_TIME = 0.5f;
// acceleration rate for the fling animation
private static final float FLING_SPEED_UP_FACTOR = 0.6f;
private static final int SWIPE_DOWN_MIN_DISTANCE = 25;
private static final int SWIPE_MAX_OFF_PATH = 75;
private static final int SWIPE_THRESHOLD_VELOCITY = 200;
private static final boolean DEBUG = true; private static final boolean DEBUG = true;
private static final String TAG = "NotificationPanelViewController"; private static final String TAG = "NotificationPanelViewController";
private final Context mContext; private final Context mContext;
private final Resources mResources; private final Resources mResources;
private final CarServiceProvider mCarServiceProvider; private final CarServiceProvider mCarServiceProvider;
private final CarDeviceProvisionedController mCarDeviceProvisionedController;
private final IStatusBarService mBarService; private final IStatusBarService mBarService;
private final CommandQueue mCommandQueue; private final CommandQueue mCommandQueue;
private final NotificationDataManager mNotificationDataManager; private final NotificationDataManager mNotificationDataManager;
private final CarUxRestrictionManagerWrapper mCarUxRestrictionManagerWrapper; private final CarUxRestrictionManagerWrapper mCarUxRestrictionManagerWrapper;
private final CarNotificationListener mCarNotificationListener; private final CarNotificationListener mCarNotificationListener;
private final NotificationClickHandlerFactory mNotificationClickHandlerFactory; private final NotificationClickHandlerFactory mNotificationClickHandlerFactory;
private final FlingAnimationUtils mFlingAnimationUtils;
private final StatusBarStateController mStatusBarStateController; private final StatusBarStateController mStatusBarStateController;
private final int mSettleClosePercentage;
private float mOpeningVelocity = DEFAULT_FLING_VELOCITY;
private float mClosingVelocity = DEFAULT_FLING_VELOCITY;
private float mInitialBackgroundAlpha; private float mInitialBackgroundAlpha;
private float mBackgroundAlphaDiff; private float mBackgroundAlphaDiff;
@@ -108,13 +87,7 @@ public class NotificationPanelViewController extends OverlayViewController {
private float mFirstTouchDownOnGlassPane; private float mFirstTouchDownOnGlassPane;
private boolean mNotificationListAtBottomAtTimeOfTouch; private boolean mNotificationListAtBottomAtTimeOfTouch;
private boolean mIsSwipingVerticallyToClose; private boolean mIsSwipingVerticallyToClose;
private int mPercentageFromBottom;
private boolean mIsNotificationAnimating;
private boolean mIsNotificationCardSwiping; private boolean mIsNotificationCardSwiping;
private boolean mPanelExpanded = false;
private View.OnTouchListener mTopNavBarNotificationTouchListener;
private View.OnTouchListener mNavBarNotificationTouchListener;
private OnUnseenCountUpdateListener mUnseenCountUpdateListener; private OnUnseenCountUpdateListener mUnseenCountUpdateListener;
@@ -123,6 +96,7 @@ public class NotificationPanelViewController extends OverlayViewController {
Context context, Context context,
@Main Resources resources, @Main Resources resources,
OverlayViewGlobalStateController overlayViewGlobalStateController, OverlayViewGlobalStateController overlayViewGlobalStateController,
FlingAnimationUtils.Builder flingAnimationUtilsBuilder,
/* Other things */ /* Other things */
CarServiceProvider carServiceProvider, CarServiceProvider carServiceProvider,
@@ -135,26 +109,21 @@ public class NotificationPanelViewController extends OverlayViewController {
CarUxRestrictionManagerWrapper carUxRestrictionManagerWrapper, CarUxRestrictionManagerWrapper carUxRestrictionManagerWrapper,
CarNotificationListener carNotificationListener, CarNotificationListener carNotificationListener,
NotificationClickHandlerFactory notificationClickHandlerFactory, NotificationClickHandlerFactory notificationClickHandlerFactory,
FlingAnimationUtils.Builder flingAnimationUtilsBuilder,
/* Things that need to be replaced */ /* Things that need to be replaced */
StatusBarStateController statusBarStateController StatusBarStateController statusBarStateController
) { ) {
super(R.id.notification_panel_stub, overlayViewGlobalStateController); super(context, resources, R.id.notification_panel_stub, overlayViewGlobalStateController,
flingAnimationUtilsBuilder, carDeviceProvisionedController);
mContext = context; mContext = context;
mResources = resources; mResources = resources;
mCarServiceProvider = carServiceProvider; mCarServiceProvider = carServiceProvider;
mCarDeviceProvisionedController = carDeviceProvisionedController;
mBarService = barService; mBarService = barService;
mCommandQueue = commandQueue; mCommandQueue = commandQueue;
mNotificationDataManager = notificationDataManager; mNotificationDataManager = notificationDataManager;
mCarUxRestrictionManagerWrapper = carUxRestrictionManagerWrapper; mCarUxRestrictionManagerWrapper = carUxRestrictionManagerWrapper;
mCarNotificationListener = carNotificationListener; mCarNotificationListener = carNotificationListener;
mNotificationClickHandlerFactory = notificationClickHandlerFactory; mNotificationClickHandlerFactory = notificationClickHandlerFactory;
mFlingAnimationUtils = flingAnimationUtilsBuilder
.setMaxLengthSeconds(FLING_ANIMATION_MAX_TIME)
.setSpeedUpFactor(FLING_SPEED_UP_FACTOR)
.build();
mStatusBarStateController = statusBarStateController; mStatusBarStateController = statusBarStateController;
// Notification background setup. // Notification background setup.
@@ -175,60 +144,6 @@ public class NotificationPanelViewController extends OverlayViewController {
+ " percentage"); + " percentage");
} }
mBackgroundAlphaDiff = finalBackgroundAlpha - mInitialBackgroundAlpha; mBackgroundAlphaDiff = finalBackgroundAlpha - mInitialBackgroundAlpha;
// Notification Panel param setup
mSettleClosePercentage = mResources.getInteger(
R.integer.notification_settle_close_percentage);
// Attached to the top navigation bar (i.e. status bar) to detect pull down of the
// notification shade.
GestureDetector openGestureDetector = new GestureDetector(mContext,
new OpenNotificationGestureListener() {
@Override
protected void openNotification() {
animateExpandNotificationsPanel();
}
});
// Attached to the NavBars to close the notification shade
GestureDetector navBarCloseNotificationGestureDetector = new GestureDetector(mContext,
new NavBarCloseNotificationGestureListener() {
@Override
protected void close() {
if (mPanelExpanded) {
animateCollapsePanels();
}
}
});
mTopNavBarNotificationTouchListener = (v, event) -> {
if (!isInflated()) {
getOverlayViewGlobalStateController().inflateView(this);
}
if (!mCarDeviceProvisionedController.isCurrentUserFullySetup()) {
return true;
}
boolean consumed = openGestureDetector.onTouchEvent(event);
if (consumed) {
return true;
}
maybeCompleteAnimation(event);
return true;
};
mNavBarNotificationTouchListener =
(v, event) -> {
if (!isInflated()) {
return true;
}
boolean consumed = navBarCloseNotificationGestureDetector.onTouchEvent(event);
if (consumed) {
return true;
}
maybeCompleteAnimation(event);
return true;
};
} }
@Override @Override
@@ -252,14 +167,13 @@ public class NotificationPanelViewController extends OverlayViewController {
private void onNotificationViewInflated() { private void onNotificationViewInflated() {
// Find views. // Find views.
mNotificationView = getLayout().findViewById(R.id.notification_view); mNotificationView = getLayout().findViewById(R.id.notification_view);
View glassPane = mNotificationView.findViewById(R.id.glass_pane); setupHandleBar();
mHandleBar = mNotificationView.findViewById(R.id.handle_bar); setupNotificationPanel();
mNotificationList = mNotificationView.findViewById(R.id.notifications);
mNotificationClickHandlerFactory.registerClickListener((launchResult, alertEntry) -> { mNotificationClickHandlerFactory.registerClickListener((launchResult, alertEntry) -> {
if (launchResult == ActivityManager.START_TASK_TO_FRONT if (launchResult == ActivityManager.START_TASK_TO_FRONT
|| launchResult == ActivityManager.START_SUCCESS) { || launchResult == ActivityManager.START_SUCCESS) {
animateCollapsePanels(); animateCollapsePanel();
} }
}); });
@@ -269,39 +183,52 @@ public class NotificationPanelViewController extends OverlayViewController {
mNotificationDataManager.getUnseenNotificationCount()); mNotificationDataManager.getUnseenNotificationCount());
} }
}); });
mNotificationClickHandlerFactory.setNotificationDataManager(mNotificationDataManager); mNotificationClickHandlerFactory.setNotificationDataManager(mNotificationDataManager);
mNotificationView.setClickHandlerFactory(mNotificationClickHandlerFactory); mNotificationView.setClickHandlerFactory(mNotificationClickHandlerFactory);
mNotificationView.setNotificationDataManager(mNotificationDataManager); mNotificationView.setNotificationDataManager(mNotificationDataManager);
mNotificationList.addOnScrollListener(new RecyclerView.OnScrollListener() { mCarServiceProvider.addListener(car -> {
@Override CarUxRestrictionsManager carUxRestrictionsManager =
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { (CarUxRestrictionsManager)
super.onScrolled(recyclerView, dx, dy); car.getCarManager(Car.CAR_UX_RESTRICTION_SERVICE);
if (!mNotificationList.canScrollVertically(1)) { mCarUxRestrictionManagerWrapper.setCarUxRestrictionsManager(
mNotificationListAtBottom = true; carUxRestrictionsManager);
return;
}
mNotificationListAtBottom = false;
mIsSwipingVerticallyToClose = false;
mNotificationListAtBottomAtTimeOfTouch = false;
}
});
// Attached to the notification ui to detect close request of the notification shade. mNotificationViewController = new NotificationViewController(
mNotificationView,
PreprocessingManager.getInstance(mContext),
mCarNotificationListener,
mCarUxRestrictionManagerWrapper,
mNotificationDataManager);
mNotificationViewController.enable();
});
}
private void setupHandleBar() {
mHandleBar = mNotificationView.findViewById(R.id.handle_bar);
GestureDetector handleBarCloseNotificationGestureDetector = new GestureDetector(mContext,
new HandleBarCloseGestureListener());
mHandleBar.setOnTouchListener((v, event) -> {
handleBarCloseNotificationGestureDetector.onTouchEvent(event);
maybeCompleteAnimation(event);
return true;
});
}
private void setupNotificationPanel() {
View glassPane = mNotificationView.findViewById(R.id.glass_pane);
mNotificationList = mNotificationView.findViewById(R.id.notifications);
GestureDetector closeGestureDetector = new GestureDetector(mContext, GestureDetector closeGestureDetector = new GestureDetector(mContext,
new CloseNotificationGestureListener() { new CloseGestureListener() {
@Override @Override
protected void close() { protected void close() {
if (mPanelExpanded) { if (isPanelExpanded()) {
animateCollapsePanels(); animateCollapsePanel();
} }
} }
}); });
// Attached to the Handle bar to close the notification shade
GestureDetector handleBarCloseNotificationGestureDetector = new GestureDetector(mContext,
new HandleBarCloseNotificationGestureListener());
// The glass pane is used to view touch events before passed to the notification list. // The glass pane is used to view touch events before passed to the notification list.
// This allows us to initialize gesture listeners and detect when to close the notifications // This allows us to initialize gesture listeners and detect when to close the notifications
glassPane.setOnTouchListener((v, event) -> { glassPane.setOnTouchListener((v, event) -> {
@@ -320,6 +247,21 @@ public class NotificationPanelViewController extends OverlayViewController {
return false; return false;
}); });
mNotificationList.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
// Check if we can scroll vertically downwards.
if (!mNotificationList.canScrollVertically(/* direction= */ 1)) {
mNotificationListAtBottom = true;
return;
}
mNotificationListAtBottom = false;
mIsSwipingVerticallyToClose = false;
mNotificationListAtBottomAtTimeOfTouch = false;
}
});
mNotificationList.setOnTouchListener((v, event) -> { mNotificationList.setOnTouchListener((v, event) -> {
mIsNotificationCardSwiping = Math.abs(mFirstTouchDownOnGlassPane - event.getRawX()) mIsNotificationCardSwiping = Math.abs(mFirstTouchDownOnGlassPane - event.getRawX())
> SWIPE_MAX_OFF_PATH; > SWIPE_MAX_OFF_PATH;
@@ -341,19 +283,19 @@ public class NotificationPanelViewController extends OverlayViewController {
boolean handled = closeGestureDetector.onTouchEvent(event); boolean handled = closeGestureDetector.onTouchEvent(event);
boolean isTracking = mIsTracking; boolean isTracking = mIsTracking;
Rect rect = mNotificationView.getClipBounds(); Rect rect = getLayout().getClipBounds();
float clippedHeight = 0; float clippedHeight = 0;
if (rect != null) { if (rect != null) {
clippedHeight = rect.bottom; clippedHeight = rect.bottom;
} }
if (!handled && event.getActionMasked() == MotionEvent.ACTION_UP if (!handled && event.getActionMasked() == MotionEvent.ACTION_UP
&& mIsSwipingVerticallyToClose) { && mIsSwipingVerticallyToClose) {
if (mSettleClosePercentage < mPercentageFromBottom && isTracking) { if (getSettleClosePercentage() < getPercentageFromBottom() && isTracking) {
animateNotificationPanel(DEFAULT_FLING_VELOCITY, false); animatePanel(DEFAULT_FLING_VELOCITY, false);
} else if (clippedHeight != mNotificationView.getHeight() && isTracking) { } else if (clippedHeight != getLayout().getHeight() && isTracking) {
// this can be caused when user is at the end of the list and trying to // this can be caused when user is at the end of the list and trying to
// fling to top of the list by scrolling down. // fling to top of the list by scrolling down.
animateNotificationPanel(DEFAULT_FLING_VELOCITY, true); animatePanel(DEFAULT_FLING_VELOCITY, true);
} }
} }
@@ -365,28 +307,6 @@ public class NotificationPanelViewController extends OverlayViewController {
} }
return handled || isTracking; return handled || isTracking;
}); });
mCarServiceProvider.addListener(car -> {
CarUxRestrictionsManager carUxRestrictionsManager =
(CarUxRestrictionsManager)
car.getCarManager(Car.CAR_UX_RESTRICTION_SERVICE);
mCarUxRestrictionManagerWrapper.setCarUxRestrictionsManager(
carUxRestrictionsManager);
mNotificationViewController = new NotificationViewController(
mNotificationView,
PreprocessingManager.getInstance(mContext),
mCarNotificationListener,
mCarUxRestrictionManagerWrapper,
mNotificationDataManager);
mNotificationViewController.enable();
});
mHandleBar.setOnTouchListener((v, event) -> {
handleBarCloseNotificationGestureDetector.onTouchEvent(event);
maybeCompleteAnimation(event);
return true;
});
} }
/** Called when the car power state is changed to ON. */ /** Called when the car power state is changed to ON. */
@@ -397,139 +317,40 @@ public class NotificationPanelViewController extends OverlayViewController {
mNotificationDataManager.clearAll(); mNotificationDataManager.clearAll();
} }
View.OnTouchListener getTopNavBarNotificationTouchListener() {
return mTopNavBarNotificationTouchListener;
}
View.OnTouchListener getNavBarNotificationTouchListener() {
return mNavBarNotificationTouchListener;
}
private void maybeCompleteAnimation(MotionEvent event) {
if (event.getActionMasked() == MotionEvent.ACTION_UP
&& mNotificationView.getVisibility() == View.VISIBLE) {
if (mSettleClosePercentage < mPercentageFromBottom) {
animateNotificationPanel(DEFAULT_FLING_VELOCITY, false);
} else {
animateNotificationPanel(DEFAULT_FLING_VELOCITY, true);
}
}
}
/**
* Animates the notification shade from one position to other. This is used to either open or
* close the notification shade completely with a velocity. If the animation is to close the
* notification shade this method also makes the view invisible after animation ends.
*/
private void animateNotificationPanel(float velocity, boolean isClosing) {
float to = 0;
if (!isClosing) {
to = mNotificationView.getHeight();
}
Rect rect = mNotificationView.getClipBounds();
if (rect != null && rect.bottom != to) {
float from = rect.bottom;
animate(from, to, velocity, isClosing);
return;
}
// We will only be here if the shade is being opened programmatically or via button when
// height of the layout was not calculated.
ViewTreeObserver notificationTreeObserver = mNotificationView.getViewTreeObserver();
notificationTreeObserver.addOnGlobalLayoutListener(
new ViewTreeObserver.OnGlobalLayoutListener() {
@Override @Override
public void onGlobalLayout() { protected boolean shouldAnimateCollapsePanel() {
ViewTreeObserver obs = mNotificationView.getViewTreeObserver(); return true;
obs.removeOnGlobalLayoutListener(this);
float to = mNotificationView.getHeight();
animate(/* from= */ 0, to, velocity, isClosing);
}
});
} }
private void animateCollapsePanels() { @Override
if (!mPanelExpanded || mNotificationView.getVisibility() == View.INVISIBLE) { protected void onAnimateCollapsePanel() {
return; // No op.
}
getOverlayViewGlobalStateController().setWindowFocusable(false);
animateNotificationPanel(mClosingVelocity, true);
} }
private void animateExpandNotificationsPanel() { @Override
if (!mCommandQueue.panelsEnabled() protected boolean shouldAnimateExpandPanel() {
|| !mCarDeviceProvisionedController.isCurrentUserFullySetup()) { return mCommandQueue.panelsEnabled();
return;
} }
// scroll to top
@Override
protected void onAnimateExpandPanel() {
mNotificationList.scrollToPosition(0); mNotificationList.scrollToPosition(0);
setPanelVisible(true);
mNotificationView.setVisibility(View.VISIBLE);
animateNotificationPanel(mOpeningVelocity, false);
setPanelExpanded(true);
} }
private void animate(float from, float to, float velocity, boolean isClosing) {
if (mIsNotificationAnimating) {
return;
}
mIsNotificationAnimating = true;
mIsTracking = true;
ValueAnimator animator = ValueAnimator.ofFloat(from, to);
animator.addUpdateListener(
animation -> {
float animatedValue = (Float) animation.getAnimatedValue();
setNotificationViewClipBounds((int) animatedValue);
});
animator.addListener(new AnimatorListenerAdapter() {
@Override @Override
public void onAnimationEnd(Animator animation) { protected void onCollapseAnimationEnd() {
super.onAnimationEnd(animation);
mIsNotificationAnimating = false;
mIsTracking = false;
mOpeningVelocity = DEFAULT_FLING_VELOCITY;
mClosingVelocity = DEFAULT_FLING_VELOCITY;
if (isClosing) {
setPanelVisible(false);
mNotificationView.setVisibility(View.INVISIBLE);
mNotificationView.setClipBounds(null);
mNotificationViewController.onVisibilityChanged(false); mNotificationViewController.onVisibilityChanged(false);
// let the status bar know that the panel is closed }
setPanelExpanded(false);
} else { @Override
protected void onExpandAnimationEnd() {
mNotificationViewController.onVisibilityChanged(true); mNotificationViewController.onVisibilityChanged(true);
// let the status bar know that the panel is open
mNotificationView.setVisibleNotificationsAsSeen(); mNotificationView.setVisibleNotificationsAsSeen();
setPanelExpanded(true);
}
}
});
mFlingAnimationUtils.apply(animator, from, to, Math.abs(velocity));
animator.start();
} }
/** @Override
* Set the panel view to be visible. protected void onPanelExpanded(boolean expand) {
*/ super.onPanelExpanded(expand);
public void setPanelVisible(boolean visible) {
if (visible && !getOverlayViewGlobalStateController().isWindowVisible()) {
getOverlayViewGlobalStateController().setWindowVisible(true);
}
if (!visible && getOverlayViewGlobalStateController().isWindowVisible()) {
getOverlayViewGlobalStateController().setWindowVisible(false);
}
getLayout().setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
getOverlayViewGlobalStateController().setWindowFocusable(visible);
}
/**
* Set the panel state to expanded. This will expand or collapse the overlay window if
* necessary.
*/
public void setPanelExpanded(boolean expand) {
mPanelExpanded = expand;
if (expand && mStatusBarStateController.getState() != StatusBarState.KEYGUARD) { if (expand && mStatusBarStateController.getState() != StatusBarState.KEYGUARD) {
if (DEBUG) { if (DEBUG) {
@@ -550,19 +371,19 @@ public class NotificationPanelViewController extends OverlayViewController {
} }
} }
private void setNotificationViewClipBounds(int height) { @Override
if (height > mNotificationView.getHeight()) { protected void onOpenScrollStart() {
height = mNotificationView.getHeight(); mNotificationList.scrollToPosition(0);
} }
Rect clipBounds = new Rect();
clipBounds.set(0, 0, mNotificationView.getWidth(), height); @Override
// Sets the clip region on the notification list view. protected void onScroll(int height) {
mNotificationView.setClipBounds(clipBounds);
if (mHandleBar != null) { if (mHandleBar != null) {
ViewGroup.MarginLayoutParams lp = ViewGroup.MarginLayoutParams lp =
(ViewGroup.MarginLayoutParams) mHandleBar.getLayoutParams(); (ViewGroup.MarginLayoutParams) mHandleBar.getLayoutParams();
mHandleBar.setTranslationY(height - mHandleBar.getHeight() - lp.bottomMargin); mHandleBar.setTranslationY(height - mHandleBar.getHeight() - lp.bottomMargin);
} }
if (mNotificationView.getHeight() > 0) { if (mNotificationView.getHeight() > 0) {
Drawable background = mNotificationView.getBackground().mutate(); Drawable background = mNotificationView.getBackground().mutate();
background.setAlpha((int) (getBackgroundAlpha(height) * 255)); background.setAlpha((int) (getBackgroundAlpha(height) * 255));
@@ -570,6 +391,13 @@ public class NotificationPanelViewController extends OverlayViewController {
} }
} }
@Override
protected boolean shouldAllowClosingScroll() {
// Unless the notification list is at the bottom, the panel shouldn't be allowed to
// collapse on scroll.
return mNotificationListAtBottomAtTimeOfTouch;
}
/** /**
* Calculates the alpha value for the background based on how much of the notification * Calculates the alpha value for the background based on how much of the notification
* shade is visible to the user. When the notification shade is completely open then * shade is visible to the user. When the notification shade is completely open then
@@ -580,30 +408,6 @@ public class NotificationPanelViewController extends OverlayViewController {
+ ((float) height / mNotificationView.getHeight() * mBackgroundAlphaDiff); + ((float) height / mNotificationView.getHeight() * mBackgroundAlphaDiff);
} }
private void calculatePercentageFromBottom(float height) {
if (mNotificationView.getHeight() > 0) {
mPercentageFromBottom = (int) Math.abs(
height / mNotificationView.getHeight() * 100);
}
}
/** Toggles the visibility of the notification panel. */
public void toggle() {
if (!isInflated()) {
getOverlayViewGlobalStateController().inflateView(this);
}
if (mPanelExpanded) {
animateCollapsePanels();
} else {
animateExpandNotificationsPanel();
}
}
/** Returns {@code true} if the notification panel is expanded. */
public boolean isPanelExpanded() {
return mPanelExpanded;
}
/** Sets the unseen count listener. */ /** Sets the unseen count listener. */
public void setOnUnseenCountUpdateListener(OnUnseenCountUpdateListener listener) { public void setOnUnseenCountUpdateListener(OnUnseenCountUpdateListener listener) {
mUnseenCountUpdateListener = listener; mUnseenCountUpdateListener = listener;
@@ -618,155 +422,10 @@ public class NotificationPanelViewController extends OverlayViewController {
void onUnseenCountUpdate(int unseenNotificationCount); void onUnseenCountUpdate(int unseenNotificationCount);
} }
/**
* Only responsible for open hooks. Since once the panel opens it covers all elements
* there is no need to merge with close.
*/
private abstract class OpenNotificationGestureListener extends
GestureDetector.SimpleOnGestureListener {
@Override
public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanceX,
float distanceY) {
if (mNotificationView.getVisibility() == View.INVISIBLE) {
// when the on-scroll is called for the first time to open.
mNotificationList.scrollToPosition(0);
}
setPanelVisible(true);
mNotificationView.setVisibility(View.VISIBLE);
// clips the view for the notification shade when the user scrolls to open.
setNotificationViewClipBounds((int) event2.getRawY());
// Initially the scroll starts with height being zero. This checks protects from divide
// by zero error.
calculatePercentageFromBottom(event2.getRawY());
mIsTracking = true;
return true;
}
@Override
public boolean onFling(MotionEvent event1, MotionEvent event2,
float velocityX, float velocityY) {
if (velocityY > SWIPE_THRESHOLD_VELOCITY) {
mOpeningVelocity = velocityY;
openNotification();
return true;
}
animateNotificationPanel(DEFAULT_FLING_VELOCITY, true);
return false;
}
protected abstract void openNotification();
}
/**
* To be installed on the open panel notification panel
*/
private abstract class CloseNotificationGestureListener extends
GestureDetector.SimpleOnGestureListener {
@Override
public boolean onSingleTapUp(MotionEvent motionEvent) {
if (mPanelExpanded) {
animateNotificationPanel(DEFAULT_FLING_VELOCITY, true);
}
return true;
}
@Override
public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanceX,
float distanceY) {
// should not clip while scroll to the bottom of the list.
if (!mNotificationListAtBottomAtTimeOfTouch) {
return false;
}
float actualNotificationHeight =
mNotificationView.getHeight() - (event1.getRawY() - event2.getRawY());
if (actualNotificationHeight > mNotificationView.getHeight()) {
actualNotificationHeight = mNotificationView.getHeight();
}
if (mNotificationView.getHeight() > 0) {
mPercentageFromBottom = (int) Math.abs(
actualNotificationHeight / mNotificationView.getHeight() * 100);
boolean isUp = distanceY > 0;
// This check is to figure out if onScroll was called while swiping the card at
// bottom of the list. At that time we should not allow notification shade to
// close. We are also checking for the upwards swipe gesture here because it is
// possible if a user is closing the notification shade and while swiping starts
// to open again but does not fling. At that time we should allow the
// notification shade to close fully or else it would stuck in between.
if (Math.abs(mNotificationView.getHeight() - actualNotificationHeight)
> SWIPE_DOWN_MIN_DISTANCE && isUp) {
setNotificationViewClipBounds((int) actualNotificationHeight);
mIsTracking = true;
} else if (!isUp) {
setNotificationViewClipBounds((int) actualNotificationHeight);
}
}
// if we return true the items in RV won't be scrollable.
return false;
}
@Override
public boolean onFling(MotionEvent event1, MotionEvent event2,
float velocityX, float velocityY) {
// should not fling if the touch does not start when view is at the bottom of the list.
if (!mNotificationListAtBottomAtTimeOfTouch) {
return false;
}
if (Math.abs(event1.getX() - event2.getX()) > SWIPE_MAX_OFF_PATH
|| Math.abs(velocityY) < SWIPE_THRESHOLD_VELOCITY) {
// swipe was not vertical or was not fast enough
return false;
}
boolean isUp = velocityY < 0;
if (isUp) {
close();
return true;
} else {
// we should close the shade
animateNotificationPanel(velocityY, false);
}
return false;
}
protected abstract void close();
}
/**
* To be installed on the nav bars.
*/
private abstract class NavBarCloseNotificationGestureListener extends
CloseNotificationGestureListener {
@Override
public boolean onSingleTapUp(MotionEvent e) {
mClosingVelocity = DEFAULT_FLING_VELOCITY;
if (mPanelExpanded) {
close();
}
return super.onSingleTapUp(e);
}
@Override
public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanceX,
float distanceY) {
calculatePercentageFromBottom(event2.getRawY());
setNotificationViewClipBounds((int) event2.getRawY());
return true;
}
}
/** /**
* To be installed on the handle bar. * To be installed on the handle bar.
*/ */
private class HandleBarCloseNotificationGestureListener extends private class HandleBarCloseGestureListener extends
GestureDetector.SimpleOnGestureListener { GestureDetector.SimpleOnGestureListener {
@Override @Override
@@ -777,9 +436,8 @@ public class NotificationPanelViewController extends OverlayViewController {
// the handle bar we should calculate the height using the diff of event1 and event2. // the handle bar we should calculate the height using the diff of event1 and event2.
// This will help the notification shade to clip smoothly as the event2 value changes // This will help the notification shade to clip smoothly as the event2 value changes
// as event1 value will be fixed. // as event1 value will be fixed.
int clipHeight = int clipHeight = getLayout().getHeight() - (int) (event1.getRawY() - event2.getRawY());
mNotificationView.getHeight() - (int) (event1.getRawY() - event2.getRawY()); setViewClipBounds(clipHeight);
setNotificationViewClipBounds(clipHeight);
return true; return true;
} }
} }

View File

@@ -59,13 +59,13 @@ public class NotificationPanelViewMediator implements OverlayViewMediator,
@Override @Override
public void registerListeners() { public void registerListeners() {
mCarNavigationBarController.registerTopBarTouchListener( mCarNavigationBarController.registerTopBarTouchListener(
mNotificationPanelViewController.getTopNavBarNotificationTouchListener()); mNotificationPanelViewController.getDragOpenTouchListener());
mCarNavigationBarController.registerBottomBarTouchListener( mCarNavigationBarController.registerBottomBarTouchListener(
mNotificationPanelViewController.getNavBarNotificationTouchListener()); mNotificationPanelViewController.getDragCloseTouchListener());
mCarNavigationBarController.registerLeftBarTouchListener( mCarNavigationBarController.registerLeftBarTouchListener(
mNotificationPanelViewController.getNavBarNotificationTouchListener()); mNotificationPanelViewController.getDragCloseTouchListener());
mCarNavigationBarController.registerRightBarTouchListener( mCarNavigationBarController.registerRightBarTouchListener(
mNotificationPanelViewController.getNavBarNotificationTouchListener()); mNotificationPanelViewController.getDragCloseTouchListener());
mCarNavigationBarController.registerNotificationController( mCarNavigationBarController.registerNotificationController(
new CarNavigationBarController.NotificationsShadeController() { new CarNavigationBarController.NotificationsShadeController() {

View File

@@ -0,0 +1,563 @@
/*
* Copyright (C) 2020 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.systemui.window;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Rect;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewTreeObserver;
import androidx.annotation.CallSuper;
import com.android.systemui.R;
import com.android.systemui.car.CarDeviceProvisionedController;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.statusbar.FlingAnimationUtils;
/**
* The {@link OverlayPanelViewController} provides additional dragging animation capabilities to
* {@link OverlayViewController}.
*/
public abstract class OverlayPanelViewController extends OverlayViewController {
private static final boolean DEBUG = true;
private static final String TAG = "OverlayPanelViewController";
// used to calculate how fast to open or close the window
protected static final float DEFAULT_FLING_VELOCITY = 0;
// max time a fling animation takes
protected static final float FLING_ANIMATION_MAX_TIME = 0.5f;
// acceleration rate for the fling animation
protected static final float FLING_SPEED_UP_FACTOR = 0.6f;
protected static final int SWIPE_DOWN_MIN_DISTANCE = 25;
protected static final int SWIPE_MAX_OFF_PATH = 75;
protected static final int SWIPE_THRESHOLD_VELOCITY = 200;
private final FlingAnimationUtils mFlingAnimationUtils;
private final CarDeviceProvisionedController mCarDeviceProvisionedController;
private final View.OnTouchListener mDragOpenTouchListener;
private final View.OnTouchListener mDragCloseTouchListener;
private final int mSettleClosePercentage;
private int mPercentageFromBottom;
private boolean mPanelVisible;
private boolean mPanelExpanded;
private float mOpeningVelocity = DEFAULT_FLING_VELOCITY;
private float mClosingVelocity = DEFAULT_FLING_VELOCITY;
private boolean mIsAnimating;
private boolean mIsTracking;
public OverlayPanelViewController(
Context context,
@Main Resources resources,
int stubId,
OverlayViewGlobalStateController overlayViewGlobalStateController,
FlingAnimationUtils.Builder flingAnimationUtilsBuilder,
CarDeviceProvisionedController carDeviceProvisionedController
) {
super(stubId, overlayViewGlobalStateController);
mFlingAnimationUtils = flingAnimationUtilsBuilder
.setMaxLengthSeconds(FLING_ANIMATION_MAX_TIME)
.setSpeedUpFactor(FLING_SPEED_UP_FACTOR)
.build();
mCarDeviceProvisionedController = carDeviceProvisionedController;
mSettleClosePercentage = resources.getInteger(
R.integer.notification_settle_close_percentage);
// Attached to the top navigation bar (i.e. status bar) to detect pull down of the
// notification shade.
GestureDetector openGestureDetector = new GestureDetector(context,
new OpenGestureListener() {
@Override
protected void open() {
animateExpandPanel();
}
});
// Attached to the NavBars to close the notification shade
GestureDetector navBarCloseNotificationGestureDetector = new GestureDetector(context,
new SystemBarCloseGestureListener() {
@Override
protected void close() {
if (isPanelExpanded()) {
animateCollapsePanel();
}
}
});
mDragOpenTouchListener = (v, event) -> {
if (!mCarDeviceProvisionedController.isCurrentUserFullySetup()) {
return true;
}
if (!isInflated()) {
getOverlayViewGlobalStateController().inflateView(this);
}
boolean consumed = openGestureDetector.onTouchEvent(event);
if (consumed) {
return true;
}
maybeCompleteAnimation(event);
return true;
};
mDragCloseTouchListener = (v, event) -> {
if (!isInflated()) {
return true;
}
boolean consumed = navBarCloseNotificationGestureDetector.onTouchEvent(event);
if (consumed) {
return true;
}
maybeCompleteAnimation(event);
return true;
};
}
/** Toggles the visibility of the panel. */
public void toggle() {
if (!isInflated()) {
getOverlayViewGlobalStateController().inflateView(this);
}
if (isPanelExpanded()) {
animateCollapsePanel();
} else {
animateExpandPanel();
}
}
/* ***************************************************************************************** *
* Panel Animation
* ***************************************************************************************** */
/** Animates the closing of the panel. */
protected void animateCollapsePanel() {
if (!shouldAnimateCollapsePanel()) {
return;
}
if (!isPanelExpanded() || !isPanelVisible()) {
return;
}
onAnimateCollapsePanel();
getOverlayViewGlobalStateController().setWindowFocusable(false);
animatePanel(mClosingVelocity, /* isClosing= */ true);
}
/** Determines whether {@link #animateCollapsePanel()} should collapse the panel. */
protected abstract boolean shouldAnimateCollapsePanel();
/** Called when the panel is beginning to collapse. */
protected abstract void onAnimateCollapsePanel();
/** Animates the expansion of the panel. */
protected void animateExpandPanel() {
if (!shouldAnimateExpandPanel()) {
return;
}
if (!mCarDeviceProvisionedController.isCurrentUserFullySetup()) {
return;
}
onAnimateExpandPanel();
setPanelVisible(true);
animatePanel(mOpeningVelocity, /* isClosing= */ false);
setPanelExpanded(true);
}
/** Determines whether {@link #animateExpandPanel()}} should expand the panel. */
protected abstract boolean shouldAnimateExpandPanel();
/** Called when the panel is beginning to expand. */
protected abstract void onAnimateExpandPanel();
/**
* Depending on certain conditions, determines whether to fully expand or collapse the panel.
*/
protected void maybeCompleteAnimation(MotionEvent event) {
if (event.getActionMasked() == MotionEvent.ACTION_UP
&& isPanelVisible()) {
if (mSettleClosePercentage < mPercentageFromBottom) {
animatePanel(DEFAULT_FLING_VELOCITY, false);
} else {
animatePanel(DEFAULT_FLING_VELOCITY, true);
}
}
}
/**
* Animates the panel from one position to other. This is used to either open or
* close the panel completely with a velocity. If the animation is to close the
* panel this method also makes the view invisible after animation ends.
*/
protected void animatePanel(float velocity, boolean isClosing) {
float to = 0;
if (!isClosing) {
to = getLayout().getHeight();
}
Rect rect = getLayout().getClipBounds();
if (rect != null && rect.bottom != to) {
float from = rect.bottom;
animate(from, to, velocity, isClosing);
return;
}
// We will only be here if the shade is being opened programmatically or via button when
// height of the layout was not calculated.
ViewTreeObserver notificationTreeObserver = getLayout().getViewTreeObserver();
notificationTreeObserver.addOnGlobalLayoutListener(
new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
ViewTreeObserver obs = getLayout().getViewTreeObserver();
obs.removeOnGlobalLayoutListener(this);
float to = getLayout().getHeight();
animate(/* from= */ 0, to, velocity, isClosing);
}
});
}
private void animate(float from, float to, float velocity, boolean isClosing) {
if (mIsAnimating) {
return;
}
mIsAnimating = true;
mIsTracking = true;
ValueAnimator animator = ValueAnimator.ofFloat(from, to);
animator.addUpdateListener(
animation -> {
float animatedValue = (Float) animation.getAnimatedValue();
setViewClipBounds((int) animatedValue);
});
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
mIsAnimating = false;
mIsTracking = false;
mOpeningVelocity = DEFAULT_FLING_VELOCITY;
mClosingVelocity = DEFAULT_FLING_VELOCITY;
if (isClosing) {
setPanelVisible(false);
getLayout().setClipBounds(null);
onCollapseAnimationEnd();
setPanelExpanded(false);
} else {
onExpandAnimationEnd();
setPanelExpanded(true);
}
}
});
getFlingAnimationUtils().apply(animator, from, to, Math.abs(velocity));
animator.start();
}
/**
* Called in {@link Animator.AnimatorListener#onAnimationEnd(Animator)} when the panel is
* closing.
*/
protected abstract void onCollapseAnimationEnd();
/**
* Called in {@link Animator.AnimatorListener#onAnimationEnd(Animator)} when the panel is
* opening.
*/
protected abstract void onExpandAnimationEnd();
/* ***************************************************************************************** *
* Panel Visibility
* ***************************************************************************************** */
/** Set the panel view to be visible. */
protected final void setPanelVisible(boolean visible) {
mPanelVisible = visible;
onPanelVisible(visible);
}
/** Returns {@code true} if panel is visible. */
public final boolean isPanelVisible() {
return mPanelVisible;
}
/** Business logic run when panel visibility is set. */
@CallSuper
protected void onPanelVisible(boolean visible) {
if (DEBUG) {
Log.e(TAG, "onPanelVisible: " + visible);
}
if (visible && !getOverlayViewGlobalStateController().isWindowVisible()) {
getOverlayViewGlobalStateController().setWindowVisible(true);
}
if (!visible && getOverlayViewGlobalStateController().isWindowVisible()) {
getOverlayViewGlobalStateController().setWindowVisible(false);
}
getLayout().setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
getOverlayViewGlobalStateController().setWindowFocusable(visible);
}
/* ***************************************************************************************** *
* Panel Expansion
* ***************************************************************************************** */
/**
* Set the panel state to expanded. This will expand or collapse the overlay window if
* necessary.
*/
protected final void setPanelExpanded(boolean expand) {
mPanelExpanded = expand;
onPanelExpanded(expand);
}
/** Returns {@code true} if panel is expanded. */
public final boolean isPanelExpanded() {
return mPanelExpanded;
}
@CallSuper
protected void onPanelExpanded(boolean expand) {
if (DEBUG) {
Log.e(TAG, "onPanelExpanded: " + expand);
}
}
/* ***************************************************************************************** *
* Misc
* ***************************************************************************************** */
protected void calculatePercentageFromBottom(float height) {
if (getLayout().getHeight() > 0) {
mPercentageFromBottom = (int) Math.abs(
height / getLayout().getHeight() * 100);
}
}
protected void setViewClipBounds(int height) {
if (height > getLayout().getHeight()) {
height = getLayout().getHeight();
}
Rect clipBounds = new Rect();
clipBounds.set(0, 0, getLayout().getWidth(), height);
getLayout().setClipBounds(clipBounds);
onScroll(height);
}
/** Called while scrolling. */
protected abstract void onScroll(int height);
/* ***************************************************************************************** *
* Getters
* ***************************************************************************************** */
/** Returns the open touch listener. */
public final View.OnTouchListener getDragOpenTouchListener() {
return mDragOpenTouchListener;
}
/** Returns the close touch listener. */
public final View.OnTouchListener getDragCloseTouchListener() {
return mDragCloseTouchListener;
}
/** Gets the fling animation utils used for animating this panel. */
protected final FlingAnimationUtils getFlingAnimationUtils() {
return mFlingAnimationUtils;
}
/** Returns {@code true} if the panel is currently tracking. */
protected final boolean isTracking() {
return mIsTracking;
}
/** Returns {@code true} if the panel is currently animating. */
protected final boolean isAnimating() {
return mIsAnimating;
}
/** Returns the percentage of the panel that is open from the bottom. */
protected final int getPercentageFromBottom() {
return mPercentageFromBottom;
}
/** Returns the percentage at which we've determined whether to open or close the panel. */
protected final int getSettleClosePercentage() {
return mSettleClosePercentage;
}
/* ***************************************************************************************** *
* Gesture Listeners
* ***************************************************************************************** */
/** Called when the user is beginning to scroll down the panel. */
protected abstract void onOpenScrollStart();
/**
* Only responsible for open hooks. Since once the panel opens it covers all elements
* there is no need to merge with close.
*/
protected abstract class OpenGestureListener extends
GestureDetector.SimpleOnGestureListener {
@Override
public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanceX,
float distanceY) {
if (!isPanelVisible()) {
onOpenScrollStart();
}
setPanelVisible(true);
// clips the view for the notification shade when the user scrolls to open.
setViewClipBounds((int) event2.getRawY());
// Initially the scroll starts with height being zero. This checks protects from divide
// by zero error.
calculatePercentageFromBottom(event2.getRawY());
mIsTracking = true;
return true;
}
@Override
public boolean onFling(MotionEvent event1, MotionEvent event2,
float velocityX, float velocityY) {
if (velocityY > SWIPE_THRESHOLD_VELOCITY) {
mOpeningVelocity = velocityY;
open();
return true;
}
animatePanel(DEFAULT_FLING_VELOCITY, true);
return false;
}
protected abstract void open();
}
/** Determines whether the scroll event should allow closing of the panel. */
protected abstract boolean shouldAllowClosingScroll();
protected abstract class CloseGestureListener extends
GestureDetector.SimpleOnGestureListener {
@Override
public boolean onSingleTapUp(MotionEvent motionEvent) {
if (isPanelExpanded()) {
animatePanel(DEFAULT_FLING_VELOCITY, true);
}
return true;
}
@Override
public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanceX,
float distanceY) {
// should not clip while scroll to the bottom of the list.
if (!shouldAllowClosingScroll()) {
return false;
}
float actualNotificationHeight =
getLayout().getHeight() - (event1.getRawY() - event2.getRawY());
if (actualNotificationHeight > getLayout().getHeight()) {
actualNotificationHeight = getLayout().getHeight();
}
if (getLayout().getHeight() > 0) {
mPercentageFromBottom = (int) Math.abs(
actualNotificationHeight / getLayout().getHeight() * 100);
boolean isUp = distanceY > 0;
// This check is to figure out if onScroll was called while swiping the card at
// bottom of the list. At that time we should not allow notification shade to
// close. We are also checking for the upwards swipe gesture here because it is
// possible if a user is closing the notification shade and while swiping starts
// to open again but does not fling. At that time we should allow the
// notification shade to close fully or else it would stuck in between.
if (Math.abs(getLayout().getHeight() - actualNotificationHeight)
> SWIPE_DOWN_MIN_DISTANCE && isUp) {
setViewClipBounds((int) actualNotificationHeight);
mIsTracking = true;
} else if (!isUp) {
setViewClipBounds((int) actualNotificationHeight);
}
}
// if we return true the items in RV won't be scrollable.
return false;
}
@Override
public boolean onFling(MotionEvent event1, MotionEvent event2,
float velocityX, float velocityY) {
// should not fling if the touch does not start when view is at the bottom of the list.
if (!shouldAllowClosingScroll()) {
return false;
}
if (Math.abs(event1.getX() - event2.getX()) > SWIPE_MAX_OFF_PATH
|| Math.abs(velocityY) < SWIPE_THRESHOLD_VELOCITY) {
// swipe was not vertical or was not fast enough
return false;
}
boolean isUp = velocityY < 0;
if (isUp) {
close();
return true;
} else {
// we should close the shade
animatePanel(velocityY, false);
}
return false;
}
protected abstract void close();
}
protected abstract class SystemBarCloseGestureListener extends CloseGestureListener {
@Override
public boolean onSingleTapUp(MotionEvent e) {
mClosingVelocity = DEFAULT_FLING_VELOCITY;
if (isPanelExpanded()) {
close();
}
return super.onSingleTapUp(e);
}
@Override
public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanceX,
float distanceY) {
calculatePercentageFromBottom(event2.getRawY());
setViewClipBounds((int) event2.getRawY());
return true;
}
}
}

View File

@@ -20,13 +20,14 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import android.os.Handler; import android.os.Handler;
import android.test.suitebuilder.annotation.SmallTest;
import android.testing.AndroidTestingRunner; import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper; import android.testing.TestableLooper;
import android.testing.TestableResources; import android.testing.TestableResources;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.WindowManager; import android.view.WindowManager;
import androidx.test.filters.SmallTest;
import com.android.systemui.R; import com.android.systemui.R;
import com.android.systemui.SysuiTestCase; import com.android.systemui.SysuiTestCase;
import com.android.systemui.car.CarDeviceProvisionedController; import com.android.systemui.car.CarDeviceProvisionedController;

View File

@@ -0,0 +1,431 @@
/*
* Copyright (C) 2020 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.systemui.window;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.anyFloat;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.Context;
import android.content.res.Resources;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.car.CarDeviceProvisionedController;
import com.android.systemui.statusbar.FlingAnimationUtils;
import com.android.systemui.tests.R;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.ArrayList;
import java.util.List;
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
@SmallTest
public class OverlayPanelViewControllerTest extends SysuiTestCase {
private TestOverlayPanelViewController mOverlayPanelViewController;
private ViewGroup mBaseLayout;
@Mock
private OverlayViewGlobalStateController mOverlayViewGlobalStateController;
@Mock
private FlingAnimationUtils.Builder mFlingAnimationUtilsBuilder;
@Mock
private FlingAnimationUtils mFlingAnimationUtils;
@Mock
private CarDeviceProvisionedController mCarDeviceProvisionedController;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mBaseLayout = (ViewGroup) LayoutInflater.from(mContext).inflate(
R.layout.overlay_view_controller_test, /* root= */ null);
when(mFlingAnimationUtilsBuilder.setMaxLengthSeconds(anyFloat())).thenReturn(
mFlingAnimationUtilsBuilder);
when(mFlingAnimationUtilsBuilder.setSpeedUpFactor(anyFloat())).thenReturn(
mFlingAnimationUtilsBuilder);
when(mFlingAnimationUtilsBuilder.build()).thenReturn(mFlingAnimationUtils);
mOverlayPanelViewController = new TestOverlayPanelViewController(
getContext(),
getContext().getOrCreateTestableResources().getResources(),
R.id.overlay_view_controller_stub,
mOverlayViewGlobalStateController,
mFlingAnimationUtilsBuilder,
mCarDeviceProvisionedController);
}
@Test
public void toggle_notInflated_inflates() {
assertThat(mOverlayPanelViewController.isInflated()).isFalse();
mOverlayPanelViewController.toggle();
verify(mOverlayViewGlobalStateController).inflateView(mOverlayPanelViewController);
}
@Test
public void toggle_inflated_doesNotInflate() {
mOverlayPanelViewController.inflate(mBaseLayout);
assertThat(mOverlayPanelViewController.isInflated()).isTrue();
mOverlayPanelViewController.toggle();
verify(mOverlayViewGlobalStateController, never()).inflateView(mOverlayPanelViewController);
}
@Test
public void toggle_notExpanded_panelExpands() {
mOverlayPanelViewController.inflate(mBaseLayout);
mOverlayPanelViewController.setPanelExpanded(false);
mOverlayPanelViewController.toggle();
assertThat(mOverlayPanelViewController.mAnimateExpandPanelCalled).isTrue();
}
@Test
public void toggle_expanded_panelCollapses() {
mOverlayPanelViewController.inflate(mBaseLayout);
mOverlayPanelViewController.setPanelExpanded(true);
mOverlayPanelViewController.toggle();
assertThat(mOverlayPanelViewController.mAnimateCollapsePanelCalled).isTrue();
}
@Test
public void animateCollapsePanel_shouldNotAnimateCollapsePanel_doesNotCollapse() {
mOverlayPanelViewController.inflate(mBaseLayout);
mOverlayPanelViewController.setShouldAnimateCollapsePanel(false);
mOverlayPanelViewController.animateCollapsePanel();
assertThat(mOverlayPanelViewController.mAnimateCollapsePanelCalled).isTrue();
assertThat(mOverlayPanelViewController.mOnAnimateCollapsePanelCalled).isFalse();
}
@Test
public void animateCollapsePanel_isNotExpanded_doesNotCollapse() {
mOverlayPanelViewController.inflate(mBaseLayout);
mOverlayPanelViewController.setShouldAnimateCollapsePanel(true);
mOverlayPanelViewController.setPanelExpanded(false);
mOverlayPanelViewController.animateCollapsePanel();
assertThat(mOverlayPanelViewController.mAnimateCollapsePanelCalled).isTrue();
assertThat(mOverlayPanelViewController.mOnAnimateCollapsePanelCalled).isFalse();
}
@Test
public void animateCollapsePanel_isNotVisible_doesNotCollapse() {
mOverlayPanelViewController.inflate(mBaseLayout);
mOverlayPanelViewController.setShouldAnimateCollapsePanel(true);
mOverlayPanelViewController.setPanelExpanded(true);
mOverlayPanelViewController.setPanelVisible(false);
mOverlayPanelViewController.animateCollapsePanel();
assertThat(mOverlayPanelViewController.mAnimateCollapsePanelCalled).isTrue();
assertThat(mOverlayPanelViewController.mOnAnimateCollapsePanelCalled).isFalse();
}
@Test
public void animateCollapsePanel_collapses() {
mOverlayPanelViewController.inflate(mBaseLayout);
mOverlayPanelViewController.setShouldAnimateCollapsePanel(true);
mOverlayPanelViewController.setPanelExpanded(true);
mOverlayPanelViewController.setPanelVisible(true);
mOverlayPanelViewController.animateCollapsePanel();
assertThat(mOverlayPanelViewController.mOnAnimateCollapsePanelCalled).isTrue();
}
@Test
public void animateCollapsePanel_removesWindowFocus() {
mOverlayPanelViewController.inflate(mBaseLayout);
mOverlayPanelViewController.setShouldAnimateCollapsePanel(true);
mOverlayPanelViewController.setPanelExpanded(true);
mOverlayPanelViewController.setPanelVisible(true);
mOverlayPanelViewController.animateCollapsePanel();
verify(mOverlayViewGlobalStateController).setWindowFocusable(false);
}
@Test
public void animateExpandPanel_shouldNotAnimateExpandPanel_doesNotExpand() {
mOverlayPanelViewController.inflate(mBaseLayout);
mOverlayPanelViewController.setShouldAnimateExpandPanel(false);
mOverlayPanelViewController.animateExpandPanel();
assertThat(mOverlayPanelViewController.mAnimateExpandPanelCalled).isTrue();
assertThat(mOverlayPanelViewController.mOnAnimateExpandPanelCalled).isFalse();
}
@Test
public void animateExpandPanel_userNotSetup_doesNotExpand() {
mOverlayPanelViewController.inflate(mBaseLayout);
mOverlayPanelViewController.setShouldAnimateExpandPanel(true);
when(mCarDeviceProvisionedController.isCurrentUserFullySetup()).thenReturn(false);
mOverlayPanelViewController.animateExpandPanel();
assertThat(mOverlayPanelViewController.mAnimateExpandPanelCalled).isTrue();
assertThat(mOverlayPanelViewController.mOnAnimateExpandPanelCalled).isFalse();
}
@Test
public void animateExpandPanel_expands() {
mOverlayPanelViewController.inflate(mBaseLayout);
mOverlayPanelViewController.setShouldAnimateExpandPanel(true);
when(mCarDeviceProvisionedController.isCurrentUserFullySetup()).thenReturn(true);
mOverlayPanelViewController.animateExpandPanel();
assertThat(mOverlayPanelViewController.mOnAnimateExpandPanelCalled).isTrue();
}
@Test
public void animateExpandPanel_setsPanelVisible() {
mOverlayPanelViewController.inflate(mBaseLayout);
mOverlayPanelViewController.setShouldAnimateExpandPanel(true);
when(mCarDeviceProvisionedController.isCurrentUserFullySetup()).thenReturn(true);
mOverlayPanelViewController.animateExpandPanel();
assertThat(mOverlayPanelViewController.isPanelVisible()).isTrue();
}
@Test
public void animateExpandPanel_setsPanelExpanded() {
mOverlayPanelViewController.inflate(mBaseLayout);
mOverlayPanelViewController.setShouldAnimateExpandPanel(true);
when(mCarDeviceProvisionedController.isCurrentUserFullySetup()).thenReturn(true);
mOverlayPanelViewController.animateExpandPanel();
assertThat(mOverlayPanelViewController.isPanelExpanded()).isTrue();
}
@Test
public void setPanelVisible_setTrue_windowNotVisible_setsWindowVisible() {
mOverlayPanelViewController.inflate(mBaseLayout);
when(mOverlayViewGlobalStateController.isWindowVisible()).thenReturn(false);
mOverlayPanelViewController.setPanelVisible(true);
verify(mOverlayViewGlobalStateController).setWindowVisible(true);
}
@Test
public void setPanelVisible_setTrue_windowVisible_doesNotSetWindowVisible() {
mOverlayPanelViewController.inflate(mBaseLayout);
when(mOverlayViewGlobalStateController.isWindowVisible()).thenReturn(true);
mOverlayPanelViewController.setPanelVisible(true);
verify(mOverlayViewGlobalStateController, never()).setWindowVisible(true);
}
@Test
public void setPanelVisible_setTrue_setLayoutVisible() {
mOverlayPanelViewController.inflate(mBaseLayout);
mOverlayPanelViewController.getLayout().setVisibility(View.INVISIBLE);
mOverlayPanelViewController.setPanelVisible(true);
assertThat(mOverlayPanelViewController.getLayout().getVisibility()).isEqualTo(View.VISIBLE);
}
@Test
public void setPanelVisible_setTrue_setWindowFocusable() {
mOverlayPanelViewController.inflate(mBaseLayout);
mOverlayPanelViewController.setPanelVisible(true);
verify(mOverlayViewGlobalStateController).setWindowFocusable(true);
}
@Test
public void setPanelVisible_setFalse_windowVisible_setsWindowNotVisible() {
mOverlayPanelViewController.inflate(mBaseLayout);
when(mOverlayViewGlobalStateController.isWindowVisible()).thenReturn(true);
mOverlayPanelViewController.setPanelVisible(false);
verify(mOverlayViewGlobalStateController).setWindowVisible(false);
}
@Test
public void setPanelVisible_setFalse_windowNotVisible_doesNotSetWindowNotVisible() {
mOverlayPanelViewController.inflate(mBaseLayout);
when(mOverlayViewGlobalStateController.isWindowVisible()).thenReturn(false);
mOverlayPanelViewController.setPanelVisible(false);
verify(mOverlayViewGlobalStateController, never()).setWindowVisible(false);
}
@Test
public void setPanelVisible_setFalse_setLayoutInvisible() {
mOverlayPanelViewController.inflate(mBaseLayout);
mOverlayPanelViewController.getLayout().setVisibility(View.VISIBLE);
mOverlayPanelViewController.setPanelVisible(false);
assertThat(mOverlayPanelViewController.getLayout().getVisibility()).isEqualTo(
View.INVISIBLE);
}
@Test
public void setPanelVisible_setFalse_setWindowNotFocusable() {
mOverlayPanelViewController.inflate(mBaseLayout);
mOverlayPanelViewController.setPanelVisible(false);
verify(mOverlayViewGlobalStateController).setWindowFocusable(false);
}
@Test
public void dragOpenTouchListener_isNotInflated_inflatesView() {
when(mCarDeviceProvisionedController.isCurrentUserFullySetup()).thenReturn(true);
assertThat(mOverlayPanelViewController.isInflated()).isFalse();
mOverlayPanelViewController.getDragOpenTouchListener().onTouch(/* v= */ null,
MotionEvent.obtain(/* downTime= */ 200, /* eventTime= */ 300,
MotionEvent.ACTION_MOVE, /* x= */ 0, /* y= */ 0, /* metaState= */ 0));
verify(mOverlayViewGlobalStateController).inflateView(mOverlayPanelViewController);
}
private static class TestOverlayPanelViewController extends OverlayPanelViewController {
private boolean mShouldAnimateCollapsePanel;
private boolean mShouldAnimateExpandPanel;
private boolean mShouldAllowClosingScroll;
boolean mOnAnimateCollapsePanelCalled;
boolean mAnimateCollapsePanelCalled;
boolean mOnAnimateExpandPanelCalled;
boolean mAnimateExpandPanelCalled;
boolean mOnCollapseAnimationEndCalled;
boolean mOnExpandAnimationEndCalled;
boolean mOnOpenScrollStartEnd;
List<Integer> mOnScrollHeights;
TestOverlayPanelViewController(
Context context,
Resources resources,
int stubId,
OverlayViewGlobalStateController overlayViewGlobalStateController,
FlingAnimationUtils.Builder flingAnimationUtilsBuilder,
CarDeviceProvisionedController carDeviceProvisionedController) {
super(context, resources, stubId, overlayViewGlobalStateController,
flingAnimationUtilsBuilder,
carDeviceProvisionedController);
mOnScrollHeights = new ArrayList<>();
}
public void setShouldAnimateCollapsePanel(boolean shouldAnimate) {
mShouldAnimateCollapsePanel = shouldAnimate;
}
@Override
protected boolean shouldAnimateCollapsePanel() {
return mShouldAnimateCollapsePanel;
}
@Override
protected void animateCollapsePanel() {
super.animateCollapsePanel();
mAnimateCollapsePanelCalled = true;
}
@Override
protected void onAnimateCollapsePanel() {
mOnAnimateCollapsePanelCalled = true;
}
public void setShouldAnimateExpandPanel(boolean shouldAnimate) {
mShouldAnimateExpandPanel = shouldAnimate;
}
@Override
protected boolean shouldAnimateExpandPanel() {
return mShouldAnimateExpandPanel;
}
@Override
protected void animateExpandPanel() {
super.animateExpandPanel();
mAnimateExpandPanelCalled = true;
}
@Override
protected void onAnimateExpandPanel() {
mOnAnimateExpandPanelCalled = true;
}
@Override
protected void onCollapseAnimationEnd() {
mOnCollapseAnimationEndCalled = true;
}
@Override
protected void onExpandAnimationEnd() {
mOnExpandAnimationEndCalled = true;
}
@Override
protected void onScroll(int height) {
mOnScrollHeights.add(height);
}
@Override
protected void onOpenScrollStart() {
mOnOpenScrollStartEnd = true;
}
public void setShouldAllowClosingScroll(boolean shouldAllow) {
mShouldAllowClosingScroll = shouldAllow;
}
@Override
protected boolean shouldAllowClosingScroll() {
return mShouldAllowClosingScroll;
}
}
}

View File

@@ -43,7 +43,7 @@ import org.mockito.MockitoAnnotations;
@TestableLooper.RunWithLooper @TestableLooper.RunWithLooper
@SmallTest @SmallTest
public class OverlayViewControllerTest extends SysuiTestCase { public class OverlayViewControllerTest extends SysuiTestCase {
private MockOverlayViewController mOverlayViewController; private TestOverlayViewController mOverlayViewController;
private ViewGroup mBaseLayout; private ViewGroup mBaseLayout;
@Mock @Mock
@@ -56,7 +56,7 @@ public class OverlayViewControllerTest extends SysuiTestCase {
public void setUp() { public void setUp() {
MockitoAnnotations.initMocks(/* testClass= */ this); MockitoAnnotations.initMocks(/* testClass= */ this);
mOverlayViewController = new MockOverlayViewController(R.id.overlay_view_controller_stub, mOverlayViewController = new TestOverlayViewController(R.id.overlay_view_controller_stub,
mOverlayViewGlobalStateController); mOverlayViewGlobalStateController);
mBaseLayout = (ViewGroup) LayoutInflater.from(mContext).inflate( mBaseLayout = (ViewGroup) LayoutInflater.from(mContext).inflate(
@@ -130,12 +130,12 @@ public class OverlayViewControllerTest extends SysuiTestCase {
assertThat(mOverlayViewController.mHideInternalCalled).isFalse(); assertThat(mOverlayViewController.mHideInternalCalled).isFalse();
} }
private static class MockOverlayViewController extends OverlayViewController { private static class TestOverlayViewController extends OverlayViewController {
boolean mOnFinishInflateCalled = false; boolean mOnFinishInflateCalled = false;
boolean mShowInternalCalled = false; boolean mShowInternalCalled = false;
boolean mHideInternalCalled = false; boolean mHideInternalCalled = false;
MockOverlayViewController(int stubId, TestOverlayViewController(int stubId,
OverlayViewGlobalStateController overlayViewGlobalStateController) { OverlayViewGlobalStateController overlayViewGlobalStateController) {
super(stubId, overlayViewGlobalStateController); super(stubId, overlayViewGlobalStateController);
} }