diff options
8 files changed, 428 insertions, 92 deletions
diff --git a/packages/CarSystemUI/res/values/config.xml b/packages/CarSystemUI/res/values/config.xml index eb1d9d0dd602..728d57d54edb 100644 --- a/packages/CarSystemUI/res/values/config.xml +++ b/packages/CarSystemUI/res/values/config.xml @@ -72,11 +72,21 @@ <!-- Car System UI's OverlayViewsMediator--> <string-array name="config_carSystemUIOverlayViewsMediators" translatable="false"> - <item>com.android.systemui.car.notification.NotificationPanelViewMediator</item> + <item>@string/config_notificationPanelViewMediator</item> <item>com.android.systemui.car.keyguard.CarKeyguardViewMediator</item> <item>com.android.systemui.car.userswitcher.FullscreenUserSwitcherViewMediator</item> </string-array> + <!-- + Car SystemUI's notification mediator. Replace with other notification mediators to have + the notification panel show from another system bar. The system bar should be enabled to + use the mediator with that system bar. + Example: config_enableBottomNavigationBar=true + config_notificationPanelViewMediator= + com.android.systemui.car.notification.BottomNotificationPanelViewMediator --> + <string name="config_notificationPanelViewMediator" translatable="false"> + com.android.systemui.car.notification.TopNotificationPanelViewMediator</string> + <!-- List of package names that are allowed sources of app installation. --> <string-array name="config_allowedAppInstallSources" translatable="false"> <item>com.android.vending</item> diff --git a/packages/CarSystemUI/src/com/android/systemui/car/notification/BottomNotificationPanelViewMediator.java b/packages/CarSystemUI/src/com/android/systemui/car/notification/BottomNotificationPanelViewMediator.java new file mode 100644 index 000000000000..6d140cae5442 --- /dev/null +++ b/packages/CarSystemUI/src/com/android/systemui/car/notification/BottomNotificationPanelViewMediator.java @@ -0,0 +1,59 @@ +/* + * 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.car.notification; + +import com.android.systemui.car.CarDeviceProvisionedController; +import com.android.systemui.car.navigationbar.CarNavigationBarController; +import com.android.systemui.car.window.OverlayPanelViewController; +import com.android.systemui.statusbar.policy.ConfigurationController; + +import javax.inject.Inject; +import javax.inject.Singleton; + +/** + * Implementation of NotificationPanelViewMediator that sets the notification panel to be opened + * from the top navigation bar. + */ +@Singleton +public class BottomNotificationPanelViewMediator extends NotificationPanelViewMediator { + + @Inject + public BottomNotificationPanelViewMediator( + CarNavigationBarController carNavigationBarController, + NotificationPanelViewController notificationPanelViewController, + + PowerManagerHelper powerManagerHelper, + + CarDeviceProvisionedController carDeviceProvisionedController, + ConfigurationController configurationController + ) { + super(carNavigationBarController, + notificationPanelViewController, + powerManagerHelper, + carDeviceProvisionedController, + configurationController); + notificationPanelViewController.setOverlayDirection( + OverlayPanelViewController.OVERLAY_FROM_BOTTOM_BAR); + } + + @Override + public void registerListeners() { + super.registerListeners(); + getCarNavigationBarController().registerBottomBarTouchListener( + getNotificationPanelViewController().getDragOpenTouchListener()); + } +} diff --git a/packages/CarSystemUI/src/com/android/systemui/car/notification/NotificationPanelViewController.java b/packages/CarSystemUI/src/com/android/systemui/car/notification/NotificationPanelViewController.java index a17a0e9d2920..cb9539ad5b1d 100644 --- a/packages/CarSystemUI/src/com/android/systemui/car/notification/NotificationPanelViewController.java +++ b/packages/CarSystemUI/src/com/android/systemui/car/notification/NotificationPanelViewController.java @@ -83,9 +83,9 @@ public class NotificationPanelViewController extends OverlayPanelViewController private NotificationViewController mNotificationViewController; private boolean mIsTracking; - private boolean mNotificationListAtBottom; + private boolean mNotificationListAtEnd; private float mFirstTouchDownOnGlassPane; - private boolean mNotificationListAtBottomAtTimeOfTouch; + private boolean mNotificationListAtEndAtTimeOfTouch; private boolean mIsSwipingVerticallyToClose; private boolean mIsNotificationCardSwiping; @@ -233,11 +233,11 @@ public class NotificationPanelViewController extends OverlayPanelViewController // This allows us to initialize gesture listeners and detect when to close the notifications glassPane.setOnTouchListener((v, event) -> { if (event.getActionMasked() == MotionEvent.ACTION_UP) { - mNotificationListAtBottomAtTimeOfTouch = false; + mNotificationListAtEndAtTimeOfTouch = false; } if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { mFirstTouchDownOnGlassPane = event.getRawX(); - mNotificationListAtBottomAtTimeOfTouch = mNotificationListAtBottom; + mNotificationListAtEndAtTimeOfTouch = mNotificationListAtEnd; // Reset the tracker when there is a touch down on the glass pane. mIsTracking = false; // Pass the down event to gesture detector so that it knows where the touch event @@ -251,34 +251,34 @@ public class NotificationPanelViewController extends OverlayPanelViewController @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; + // Check if we can scroll vertically in the animation direction. + if (!mNotificationList.canScrollVertically(mAnimateDirection)) { + mNotificationListAtEnd = true; return; } - mNotificationListAtBottom = false; + mNotificationListAtEnd = false; mIsSwipingVerticallyToClose = false; - mNotificationListAtBottomAtTimeOfTouch = false; + mNotificationListAtEndAtTimeOfTouch = false; } }); mNotificationList.setOnTouchListener((v, event) -> { mIsNotificationCardSwiping = Math.abs(mFirstTouchDownOnGlassPane - event.getRawX()) > SWIPE_MAX_OFF_PATH; - if (mNotificationListAtBottomAtTimeOfTouch && mNotificationListAtBottom) { + if (mNotificationListAtEndAtTimeOfTouch && mNotificationListAtEnd) { // We need to save the state here as if notification card is swiping we will - // change the mNotificationListAtBottomAtTimeOfTouch. This is to protect + // change the mNotificationListAtEndAtTimeOfTouch. This is to protect // closing the notification shade while the notification card is being swiped. mIsSwipingVerticallyToClose = true; } // If the card is swiping we should not allow the notification shade to close. - // Hence setting mNotificationListAtBottomAtTimeOfTouch to false will stop that + // Hence setting mNotificationListAtEndAtTimeOfTouch to false will stop that // for us. We are also checking for mIsTracking because while swiping the // notification shade to close if the user goes a bit horizontal while swiping // upwards then also this should close. if (mIsNotificationCardSwiping && !mIsTracking) { - mNotificationListAtBottomAtTimeOfTouch = false; + mNotificationListAtEndAtTimeOfTouch = false; } boolean handled = closeGestureDetector.onTouchEvent(event); @@ -290,7 +290,7 @@ public class NotificationPanelViewController extends OverlayPanelViewController } if (!handled && event.getActionMasked() == MotionEvent.ACTION_UP && mIsSwipingVerticallyToClose) { - if (getSettleClosePercentage() < getPercentageFromBottom() && isTracking) { + if (getSettleClosePercentage() < getPercentageFromEndingEdge() && isTracking) { animatePanel(DEFAULT_FLING_VELOCITY, false); } else if (clippedHeight != getLayout().getHeight() && isTracking) { // this can be caused when user is at the end of the list and trying to @@ -299,11 +299,11 @@ public class NotificationPanelViewController extends OverlayPanelViewController } } - // Updating the mNotificationListAtBottomAtTimeOfTouch state has to be done after + // Updating the mNotificationListAtEndAtTimeOfTouch state has to be done after // the event has been passed to the closeGestureDetector above, such that the // closeGestureDetector sees the up event before the state has changed. if (event.getActionMasked() == MotionEvent.ACTION_UP) { - mNotificationListAtBottomAtTimeOfTouch = false; + mNotificationListAtEndAtTimeOfTouch = false; } return handled || isTracking; }); @@ -377,25 +377,31 @@ public class NotificationPanelViewController extends OverlayPanelViewController } @Override - protected void onScroll(int height) { + protected void onScroll(int y) { if (mHandleBar != null) { ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) mHandleBar.getLayoutParams(); - mHandleBar.setTranslationY(height - mHandleBar.getHeight() - lp.bottomMargin); + // Adjust handlebar to new pointer position, and a little more depending on the + // animate direction so the bar can be seen fully. + if (mAnimateDirection > 0) { + mHandleBar.setTranslationY(y - mHandleBar.getHeight() - lp.bottomMargin); + } else { + mHandleBar.setTranslationY(y + mHandleBar.getHeight() + lp.topMargin); + } } if (mNotificationView.getHeight() > 0) { Drawable background = mNotificationView.getBackground().mutate(); - background.setAlpha((int) (getBackgroundAlpha(height) * 255)); + background.setAlpha((int) (getBackgroundAlpha(y) * 255)); mNotificationView.setBackground(background); } } @Override protected boolean shouldAllowClosingScroll() { - // Unless the notification list is at the bottom, the panel shouldn't be allowed to + // Unless the notification list is at the end, the panel shouldn't be allowed to // collapse on scroll. - return mNotificationListAtBottomAtTimeOfTouch; + return mNotificationListAtEndAtTimeOfTouch; } /** @@ -403,9 +409,11 @@ public class NotificationPanelViewController extends OverlayPanelViewController * shade is visible to the user. When the notification shade is completely open then * alpha value will be 1. */ - private float getBackgroundAlpha(int height) { - return mInitialBackgroundAlpha - + ((float) height / mNotificationView.getHeight() * mBackgroundAlphaDiff); + private float getBackgroundAlpha(int y) { + float fractionCovered = + ((float) (mAnimateDirection > 0 ? y : mNotificationView.getHeight() - y)) + / mNotificationView.getHeight(); + return mInitialBackgroundAlpha + fractionCovered * mBackgroundAlphaDiff; } /** Sets the unseen count listener. */ @@ -431,13 +439,18 @@ public class NotificationPanelViewController extends OverlayPanelViewController @Override public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanceX, float distanceY) { - calculatePercentageFromBottom(event2.getRawY()); - // To prevent the jump in the clip bounds while closing the notification shade using + calculatePercentageFromEndingEdge(event2.getRawY()); + // To prevent the jump in the clip bounds while closing the notification panel using // 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 // as event1 value will be fixed. - int clipHeight = getLayout().getHeight() - (int) (event1.getRawY() - event2.getRawY()); - setViewClipBounds(clipHeight); + float diff = mAnimateDirection * (event1.getRawY() - event2.getRawY()); + float y = mAnimateDirection > 0 + ? getLayout().getHeight() - diff + : diff; + // Ensure the position is within the overlay panel. + y = Math.max(0, Math.min(y, getLayout().getHeight())); + setViewClipBounds((int) y); return true; } } diff --git a/packages/CarSystemUI/src/com/android/systemui/car/notification/NotificationPanelViewMediator.java b/packages/CarSystemUI/src/com/android/systemui/car/notification/NotificationPanelViewMediator.java index 24a84d940577..8f52638afdf1 100644 --- a/packages/CarSystemUI/src/com/android/systemui/car/notification/NotificationPanelViewMediator.java +++ b/packages/CarSystemUI/src/com/android/systemui/car/notification/NotificationPanelViewMediator.java @@ -19,17 +19,15 @@ package com.android.systemui.car.notification; import android.car.hardware.power.CarPowerManager; import android.content.res.Configuration; +import androidx.annotation.CallSuper; + import com.android.systemui.car.CarDeviceProvisionedController; import com.android.systemui.car.navigationbar.CarNavigationBarController; import com.android.systemui.car.window.OverlayViewMediator; import com.android.systemui.statusbar.policy.ConfigurationController; -import javax.inject.Inject; -import javax.inject.Singleton; - /** The view mediator which attaches the view controller to other elements of the system ui. */ -@Singleton -public class NotificationPanelViewMediator implements OverlayViewMediator, +public abstract class NotificationPanelViewMediator implements OverlayViewMediator, ConfigurationController.ConfigurationListener { private final CarNavigationBarController mCarNavigationBarController; @@ -38,7 +36,6 @@ public class NotificationPanelViewMediator implements OverlayViewMediator, private final CarDeviceProvisionedController mCarDeviceProvisionedController; private final ConfigurationController mConfigurationController; - @Inject public NotificationPanelViewMediator( CarNavigationBarController carNavigationBarController, NotificationPanelViewController notificationPanelViewController, @@ -56,9 +53,10 @@ public class NotificationPanelViewMediator implements OverlayViewMediator, } @Override + @CallSuper public void registerListeners() { mCarNavigationBarController.registerTopBarTouchListener( - mNotificationPanelViewController.getDragOpenTouchListener()); + mNotificationPanelViewController.getDragCloseTouchListener()); mCarNavigationBarController.registerBottomBarTouchListener( mNotificationPanelViewController.getDragCloseTouchListener()); mCarNavigationBarController.registerLeftBarTouchListener( @@ -128,4 +126,12 @@ public class NotificationPanelViewMediator implements OverlayViewMediator, mNotificationPanelViewController.reinflate(); registerListeners(); } + + protected final CarNavigationBarController getCarNavigationBarController() { + return mCarNavigationBarController; + } + + protected final NotificationPanelViewController getNotificationPanelViewController() { + return mNotificationPanelViewController; + } } diff --git a/packages/CarSystemUI/src/com/android/systemui/car/notification/TopNotificationPanelViewMediator.java b/packages/CarSystemUI/src/com/android/systemui/car/notification/TopNotificationPanelViewMediator.java new file mode 100644 index 000000000000..09a462185dac --- /dev/null +++ b/packages/CarSystemUI/src/com/android/systemui/car/notification/TopNotificationPanelViewMediator.java @@ -0,0 +1,59 @@ +/* + * 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.car.notification; + +import com.android.systemui.car.CarDeviceProvisionedController; +import com.android.systemui.car.navigationbar.CarNavigationBarController; +import com.android.systemui.car.window.OverlayPanelViewController; +import com.android.systemui.statusbar.policy.ConfigurationController; + +import javax.inject.Inject; +import javax.inject.Singleton; + +/** + * Implementation of NotificationPanelViewMediator that sets the notification panel to be opened + * from the top navigation bar. + */ +@Singleton +public class TopNotificationPanelViewMediator extends NotificationPanelViewMediator { + + @Inject + public TopNotificationPanelViewMediator( + CarNavigationBarController carNavigationBarController, + NotificationPanelViewController notificationPanelViewController, + + PowerManagerHelper powerManagerHelper, + + CarDeviceProvisionedController carDeviceProvisionedController, + ConfigurationController configurationController + ) { + super(carNavigationBarController, + notificationPanelViewController, + powerManagerHelper, + carDeviceProvisionedController, + configurationController); + notificationPanelViewController.setOverlayDirection( + OverlayPanelViewController.OVERLAY_FROM_TOP_BAR); + } + + @Override + public void registerListeners() { + super.registerListeners(); + getCarNavigationBarController().registerBottomBarTouchListener( + getNotificationPanelViewController().getDragOpenTouchListener()); + } +} diff --git a/packages/CarSystemUI/src/com/android/systemui/car/window/OverlayPanelViewController.java b/packages/CarSystemUI/src/com/android/systemui/car/window/OverlayPanelViewController.java index 90892d5c53e4..0fe985684543 100644 --- a/packages/CarSystemUI/src/com/android/systemui/car/window/OverlayPanelViewController.java +++ b/packages/CarSystemUI/src/com/android/systemui/car/window/OverlayPanelViewController.java @@ -19,6 +19,7 @@ package com.android.systemui.car.window; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; +import android.annotation.IntDef; import android.content.Context; import android.content.res.Resources; import android.graphics.Rect; @@ -35,13 +36,36 @@ import com.android.systemui.car.CarDeviceProvisionedController; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.statusbar.FlingAnimationUtils; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + /** * The {@link OverlayPanelViewController} provides additional dragging animation capabilities to * {@link OverlayViewController}. */ public abstract class OverlayPanelViewController extends OverlayViewController { - private static final boolean DEBUG = true; + /** @hide */ + @IntDef(flag = true, prefix = { "OVERLAY_" }, value = { + OVERLAY_FROM_TOP_BAR, + OVERLAY_FROM_BOTTOM_BAR + }) + @Retention(RetentionPolicy.SOURCE) + public @interface OverlayDirection {} + + /** + * Indicates that the overlay panel should be opened from the top bar and expanded by dragging + * towards the bottom bar. + */ + public static final int OVERLAY_FROM_TOP_BAR = 0; + + /** + * Indicates that the overlay panel should be opened from the bottom bar and expanded by + * dragging towards the top bar. + */ + public static final int OVERLAY_FROM_BOTTOM_BAR = 1; + + private static final boolean DEBUG = false; private static final String TAG = "OverlayPanelViewController"; // used to calculate how fast to open or close the window @@ -54,14 +78,18 @@ public abstract class OverlayPanelViewController extends OverlayViewController { 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 static final int POSITIVE_DIRECTION = 1; + private static final int NEGATIVE_DIRECTION = -1; private final FlingAnimationUtils mFlingAnimationUtils; private final CarDeviceProvisionedController mCarDeviceProvisionedController; private final View.OnTouchListener mDragOpenTouchListener; private final View.OnTouchListener mDragCloseTouchListener; + protected int mAnimateDirection = POSITIVE_DIRECTION; + private final int mSettleClosePercentage; - private int mPercentageFromBottom; + private int mPercentageFromEndingEdge; private boolean mPanelVisible; private boolean mPanelExpanded; @@ -91,8 +119,7 @@ public abstract class OverlayPanelViewController extends OverlayViewController { 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. + // Attached to a navigation bar to open the overlay panel GestureDetector openGestureDetector = new GestureDetector(context, new OpenGestureListener() { @Override @@ -101,8 +128,8 @@ public abstract class OverlayPanelViewController extends OverlayViewController { } }); - // Attached to the NavBars to close the notification shade - GestureDetector navBarCloseNotificationGestureDetector = new GestureDetector(context, + // Attached to the other navigation bars to close the overlay panel + GestureDetector closeGestureDetector = new GestureDetector(context, new SystemBarCloseGestureListener() { @Override protected void close() { @@ -132,7 +159,7 @@ public abstract class OverlayPanelViewController extends OverlayViewController { if (!isInflated()) { return true; } - boolean consumed = navBarCloseNotificationGestureDetector.onTouchEvent(event); + boolean consumed = closeGestureDetector.onTouchEvent(event); if (consumed) { return true; } @@ -141,6 +168,17 @@ public abstract class OverlayPanelViewController extends OverlayViewController { }; } + /** Sets the overlay panel animation direction along the x or y axis. */ + public void setOverlayDirection(@OverlayDirection int direction) { + if (direction == OVERLAY_FROM_TOP_BAR) { + mAnimateDirection = POSITIVE_DIRECTION; + } else if (direction == OVERLAY_FROM_BOTTOM_BAR) { + mAnimateDirection = NEGATIVE_DIRECTION; + } else { + throw new IllegalArgumentException("Direction not supported"); + } + } + /** Toggles the visibility of the panel. */ public void toggle() { if (!isInflated()) { @@ -207,7 +245,7 @@ public abstract class OverlayPanelViewController extends OverlayViewController { protected void maybeCompleteAnimation(MotionEvent event) { if (event.getActionMasked() == MotionEvent.ACTION_UP && isPanelVisible()) { - if (mSettleClosePercentage < mPercentageFromBottom) { + if (mSettleClosePercentage < mPercentageFromEndingEdge) { animatePanel(DEFAULT_FLING_VELOCITY, false); } else { animatePanel(DEFAULT_FLING_VELOCITY, true); @@ -221,16 +259,15 @@ public abstract class OverlayPanelViewController extends OverlayViewController { * 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(); - } + float to = getEndPosition(isClosing); Rect rect = getLayout().getClipBounds(); - if (rect != null && rect.bottom != to) { - float from = rect.bottom; - animate(from, to, velocity, isClosing); - return; + if (rect != null) { + float from = getCurrentStartPosition(rect); + if (from != to) { + animate(from, to, velocity, isClosing); + return; + } } // We will only be here if the shade is being opened programmatically or via button when @@ -242,12 +279,32 @@ public abstract class OverlayPanelViewController extends OverlayViewController { public void onGlobalLayout() { ViewTreeObserver obs = getLayout().getViewTreeObserver(); obs.removeOnGlobalLayoutListener(this); - float to = getLayout().getHeight(); - animate(/* from= */ 0, to, velocity, isClosing); + animate( + getDefaultStartPosition(), + getEndPosition(/* isClosing= */ false), + velocity, + isClosing + ); } }); } + /* Returns the start position if the user has not started swiping. */ + private int getDefaultStartPosition() { + return mAnimateDirection > 0 ? 0 : getLayout().getHeight(); + } + + /** Returns the start position if we are in the middle of swiping. */ + private int getCurrentStartPosition(Rect clipBounds) { + return mAnimateDirection > 0 ? clipBounds.bottom : clipBounds.top; + } + + private int getEndPosition(boolean isClosing) { + return (mAnimateDirection > 0 && !isClosing) || (mAnimateDirection == -1 && isClosing) + ? getLayout().getHeight() + : 0; + } + private void animate(float from, float to, float velocity, boolean isClosing) { if (mIsAnimating) { return; @@ -356,25 +413,44 @@ public abstract class OverlayPanelViewController extends OverlayViewController { * Misc * ***************************************************************************************** */ - protected void calculatePercentageFromBottom(float height) { + /** + * Given the position of the pointer dragging the panel, return the percentage of its closeness + * to the ending edge. + */ + protected void calculatePercentageFromEndingEdge(float y) { if (getLayout().getHeight() > 0) { - mPercentageFromBottom = (int) Math.abs( - height / getLayout().getHeight() * 100); + float height = getVisiblePanelHeight(y); + mPercentageFromEndingEdge = (int) Math.abs(height / getLayout().getHeight() * 100); } } - protected void setViewClipBounds(int height) { - if (height > getLayout().getHeight()) { - height = getLayout().getHeight(); - } + private float getVisiblePanelHeight(float y) { + return mAnimateDirection > 0 ? y : getLayout().getHeight() - y; + } + + /** Sets the boundaries of the overlay panel that can be seen based on pointer position. */ + protected void setViewClipBounds(int y) { + // Bound the pointer position to be within the overlay panel. + y = Math.max(0, Math.min(y, getLayout().getHeight())); Rect clipBounds = new Rect(); - clipBounds.set(0, 0, getLayout().getWidth(), height); + int top, bottom; + if (mAnimateDirection > 0) { + top = 0; + bottom = y; + } else { + top = y; + bottom = getLayout().getHeight(); + } + clipBounds.set(0, top, getLayout().getWidth(), bottom); getLayout().setClipBounds(clipBounds); - onScroll(height); + onScroll(y); } - /** Called while scrolling. */ - protected abstract void onScroll(int height); + /** + * Called while scrolling, this passes the position of the clip boundary that is currently + * changing. + */ + protected abstract void onScroll(int y); /* ***************************************************************************************** * * Getters @@ -406,8 +482,8 @@ public abstract class OverlayPanelViewController extends OverlayViewController { } /** Returns the percentage of the panel that is open from the bottom. */ - protected final int getPercentageFromBottom() { - return mPercentageFromBottom; + protected final int getPercentageFromEndingEdge() { + return mPercentageFromEndingEdge; } /** Returns the percentage at which we've determined whether to open or close the panel. */ @@ -443,7 +519,7 @@ public abstract class OverlayPanelViewController extends OverlayViewController { // Initially the scroll starts with height being zero. This checks protects from divide // by zero error. - calculatePercentageFromBottom(event2.getRawY()); + calculatePercentageFromEndingEdge(event2.getRawY()); mIsTracking = true; return true; @@ -453,7 +529,7 @@ public abstract class OverlayPanelViewController extends OverlayViewController { @Override public boolean onFling(MotionEvent event1, MotionEvent event2, float velocityX, float velocityY) { - if (velocityY > SWIPE_THRESHOLD_VELOCITY) { + if (mAnimateDirection * velocityY > SWIPE_THRESHOLD_VELOCITY) { mOpeningVelocity = velocityY; open(); return true; @@ -483,19 +559,14 @@ public abstract class OverlayPanelViewController extends OverlayViewController { @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(); - } + float y = getYPositionOfPanelEndingEdge(event1, event2); if (getLayout().getHeight() > 0) { - mPercentageFromBottom = (int) Math.abs( - actualNotificationHeight / getLayout().getHeight() * 100); - boolean isUp = distanceY > 0; + mPercentageFromEndingEdge = (int) Math.abs( + y / getLayout().getHeight() * 100); + boolean isInClosingDirection = mAnimateDirection * 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 @@ -503,23 +574,37 @@ public abstract class OverlayPanelViewController extends OverlayViewController { // 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); + if (Math.abs(getLayout().getHeight() - y) + > SWIPE_DOWN_MIN_DISTANCE && isInClosingDirection) { + setViewClipBounds((int) y); mIsTracking = true; - } else if (!isUp) { - setViewClipBounds((int) actualNotificationHeight); + } else if (!isInClosingDirection) { + setViewClipBounds((int) y); } } // if we return true the items in RV won't be scrollable. return false; } + /** + * To prevent the jump in the clip bounds while closing the panel we should calculate the y + * position using the diff of event1 and event2. This will help the panel clip smoothly as + * the event2 value changes while event1 value will be fixed. + * @param event1 MotionEvent that contains the position of where the event2 started. + * @param event2 MotionEvent that contains the position of where the user has scrolled to + * on the screen. + */ + private float getYPositionOfPanelEndingEdge(MotionEvent event1, MotionEvent event2) { + float diff = mAnimateDirection * (event1.getRawY() - event2.getRawY()); + float y = mAnimateDirection > 0 ? getLayout().getHeight() - diff : diff; + y = Math.max(0, Math.min(y, getLayout().getHeight())); + return y; + } @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. + // should not fling if the touch does not start when view is at the end of the list. if (!shouldAllowClosingScroll()) { return false; } @@ -528,8 +613,8 @@ public abstract class OverlayPanelViewController extends OverlayViewController { // swipe was not vertical or was not fast enough return false; } - boolean isUp = velocityY < 0; - if (isUp) { + boolean isInClosingDirection = mAnimateDirection * velocityY < 0; + if (isInClosingDirection) { close(); return true; } else { @@ -555,7 +640,7 @@ public abstract class OverlayPanelViewController extends OverlayViewController { @Override public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanceX, float distanceY) { - calculatePercentageFromBottom(event2.getRawY()); + calculatePercentageFromEndingEdge(event2.getRawY()); setViewClipBounds((int) event2.getRawY()); return true; } diff --git a/packages/CarSystemUI/src/com/android/systemui/car/window/OverlayWindowModule.java b/packages/CarSystemUI/src/com/android/systemui/car/window/OverlayWindowModule.java index c46b287ade5a..e1918ceeaea4 100644 --- a/packages/CarSystemUI/src/com/android/systemui/car/window/OverlayWindowModule.java +++ b/packages/CarSystemUI/src/com/android/systemui/car/window/OverlayWindowModule.java @@ -17,7 +17,8 @@ package com.android.systemui.car.window; import com.android.systemui.car.keyguard.CarKeyguardViewMediator; -import com.android.systemui.car.notification.NotificationPanelViewMediator; +import com.android.systemui.car.notification.BottomNotificationPanelViewMediator; +import com.android.systemui.car.notification.TopNotificationPanelViewMediator; import com.android.systemui.car.userswitcher.FullscreenUserSwitcherViewMediator; import dagger.Binds; @@ -31,12 +32,19 @@ import dagger.multibindings.IntoMap; @Module public abstract class OverlayWindowModule { - /** Injects NotificationPanelViewMediator. */ + /** Injects TopNotificationPanelViewMediator. */ @Binds @IntoMap - @ClassKey(NotificationPanelViewMediator.class) - public abstract OverlayViewMediator bindNotificationPanelViewMediator( - NotificationPanelViewMediator notificationPanelViewMediator); + @ClassKey(TopNotificationPanelViewMediator.class) + public abstract OverlayViewMediator bindTopNotificationPanelViewMediator( + TopNotificationPanelViewMediator topNotificationPanelViewMediator); + + /** Injects BottomNotificationPanelViewMediator. */ + @Binds + @IntoMap + @ClassKey(BottomNotificationPanelViewMediator.class) + public abstract OverlayViewMediator bindBottomNotificationPanelViewMediator( + BottomNotificationPanelViewMediator bottomNotificationPanelViewMediator); /** Inject into CarKeyguardViewMediator. */ @Binds diff --git a/packages/CarSystemUI/tests/src/com/android/systemui/car/window/OverlayPanelViewControllerTest.java b/packages/CarSystemUI/tests/src/com/android/systemui/car/window/OverlayPanelViewControllerTest.java index 70f1d25fe2a4..8d705a8cca1f 100644 --- a/packages/CarSystemUI/tests/src/com/android/systemui/car/window/OverlayPanelViewControllerTest.java +++ b/packages/CarSystemUI/tests/src/com/android/systemui/car/window/OverlayPanelViewControllerTest.java @@ -18,13 +18,16 @@ package com.android.systemui.car.window; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; 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.animation.Animator; import android.content.Context; import android.content.res.Resources; +import android.graphics.Rect; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.LayoutInflater; @@ -42,6 +45,7 @@ import com.android.systemui.tests.R; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -173,6 +177,51 @@ public class OverlayPanelViewControllerTest extends SysuiTestCase { } @Test + public void animateCollapsePanel_withOverlayFromTopBar_collapsesTowardsTopBar() { + mOverlayPanelViewController.inflate(mBaseLayout); + // Mock a panel that has layout size 50 and where the panel is opened. + int size = 50; + mockPanelWithSize(size); + mOverlayPanelViewController.getLayout().setClipBounds( + new Rect(0, 0, size, size)); + mOverlayPanelViewController.setShouldAnimateCollapsePanel(true); + mOverlayPanelViewController.setPanelExpanded(true); + mOverlayPanelViewController.setPanelVisible(true); + mOverlayPanelViewController.setOverlayDirection( + OverlayPanelViewController.OVERLAY_FROM_TOP_BAR); + + mOverlayPanelViewController.animateCollapsePanel(); + + ArgumentCaptor<Float> endValueCaptor = ArgumentCaptor.forClass(Float.class); + verify(mFlingAnimationUtils).apply( + any(Animator.class), anyFloat(), endValueCaptor.capture(), anyFloat()); + assertThat(endValueCaptor.getValue().intValue()).isEqualTo(0); + } + + @Test + public void animateCollapsePanel_withOverlayFromBottomBar_collapsesTowardsBottomBar() { + mOverlayPanelViewController.inflate(mBaseLayout); + // Mock a panel that has layout size 50 and where the panel is opened. + int size = 50; + mockPanelWithSize(size); + mOverlayPanelViewController.getLayout().setClipBounds( + new Rect(0, 0, size, size)); + mOverlayPanelViewController.setShouldAnimateCollapsePanel(true); + mOverlayPanelViewController.setPanelExpanded(true); + mOverlayPanelViewController.setPanelVisible(true); + mOverlayPanelViewController.setOverlayDirection( + OverlayPanelViewController.OVERLAY_FROM_BOTTOM_BAR); + + mOverlayPanelViewController.animateCollapsePanel(); + + ArgumentCaptor<Float> endValueCaptor = ArgumentCaptor.forClass(Float.class); + verify(mFlingAnimationUtils).apply( + any(Animator.class), anyFloat(), endValueCaptor.capture(), anyFloat()); + assertThat(endValueCaptor.getValue().intValue()).isEqualTo( + mOverlayPanelViewController.getLayout().getHeight()); + } + + @Test public void animateCollapsePanel_removesWindowFocus() { mOverlayPanelViewController.inflate(mBaseLayout); mOverlayPanelViewController.setShouldAnimateCollapsePanel(true); @@ -219,6 +268,49 @@ public class OverlayPanelViewControllerTest extends SysuiTestCase { } @Test + public void animateExpandPanel_withOverlayFromTopBar_expandsToBottom() { + mOverlayPanelViewController.inflate(mBaseLayout); + // Mock a panel that has layout size 50 and where the panel is not opened. + int size = 50; + mockPanelWithSize(size); + mOverlayPanelViewController.getLayout().setClipBounds( + new Rect(0, 0, size, 0)); + mOverlayPanelViewController.setShouldAnimateExpandPanel(true); + when(mCarDeviceProvisionedController.isCurrentUserFullySetup()).thenReturn(true); + mOverlayPanelViewController.setOverlayDirection( + OverlayPanelViewController.OVERLAY_FROM_TOP_BAR); + + mOverlayPanelViewController.animateExpandPanel(); + + ArgumentCaptor<Float> endValueCaptor = ArgumentCaptor.forClass(Float.class); + verify(mFlingAnimationUtils).apply( + any(Animator.class), anyFloat(), endValueCaptor.capture(), anyFloat()); + assertThat(endValueCaptor.getValue().intValue()).isEqualTo( + mOverlayPanelViewController.getLayout().getHeight()); + } + + @Test + public void animateExpandPanel_withOverlayFromBottomBar_expandsToTop() { + mOverlayPanelViewController.inflate(mBaseLayout); + // Mock a panel that has layout size 50 and where the panel is not opened. + int size = 50; + mockPanelWithSize(size); + mOverlayPanelViewController.getLayout().setClipBounds( + new Rect(0, size, size, size)); + mOverlayPanelViewController.setShouldAnimateExpandPanel(true); + when(mCarDeviceProvisionedController.isCurrentUserFullySetup()).thenReturn(true); + mOverlayPanelViewController.setOverlayDirection( + OverlayPanelViewController.OVERLAY_FROM_BOTTOM_BAR); + + mOverlayPanelViewController.animateExpandPanel(); + + ArgumentCaptor<Float> endValueCaptor = ArgumentCaptor.forClass(Float.class); + verify(mFlingAnimationUtils).apply( + any(Animator.class), anyFloat(), endValueCaptor.capture(), anyFloat()); + assertThat(endValueCaptor.getValue().intValue()).isEqualTo(0); + } + + @Test public void animateExpandPanel_setsPanelVisible() { mOverlayPanelViewController.inflate(mBaseLayout); mOverlayPanelViewController.setShouldAnimateExpandPanel(true); @@ -330,6 +422,10 @@ public class OverlayPanelViewControllerTest extends SysuiTestCase { verify(mOverlayViewGlobalStateController).inflateView(mOverlayPanelViewController); } + private void mockPanelWithSize(int size) { + mOverlayPanelViewController.getLayout().setLeftTopRightBottom(0, 0, size, size); + } + private static class TestOverlayPanelViewController extends OverlayPanelViewController { private boolean mShouldAnimateCollapsePanel; |