Add all the needed touch listeners and gesture detectors to allow for car

Test: on car product build

Change-Id: Icc44cae87033383535d6a0ab13203afb14cf83e0
(cherry picked from commit 3f55c3502e120bacea38162ccbff94e462b009ed)
This commit is contained in:
Brad Stenning
2019-01-29 11:24:11 -08:00
parent 0b409e8cc0
commit c622f1d6b1
9 changed files with 386 additions and 35 deletions

View File

@@ -22,6 +22,7 @@
android:layout_height="@dimen/status_bar_height">
<include layout="@layout/car_top_navigation_bar"
android:id="@+id/qs_car_top_bar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"

View File

@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2018 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.
-->
<com.android.car.notification.CarNotificationView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/notification_view"
android:layout_width="match_parent"
android:layout_height="match_parent">
<View
android:id="@+id/glass_pane"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:translationZ="2dp"
/>
<androidx.car.widget.PagedListView
android:id="@+id/notifications"
android:layout_width="0dp"
android:layout_height="0dp"
android:orientation="vertical"
android:theme="@style/PagedListTheme"
app:gutter="none"
app:itemSpacing="@dimen/item_spacing"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:scrollBarEnabled="false"
app:showPagedListViewDivider="false"/>
</com.android.car.notification.CarNotificationView>

View File

@@ -37,8 +37,8 @@
<color name="status_bar_background_color">#33000000</color>
<drawable name="system_bar_background">@color/status_bar_background_color</drawable>
<!-- The scrim color for the background of the notifications shade. -->
<color name="scrim_behind_color">#172026</color>
<!-- The background color of the notification shade -->
<color name="notification_shade_background_color">#99000000</color>
<!-- The color of the dividing line between grouped notifications. -->
<color name="notification_divider_color">@*android:color/notification_action_list</color>

View File

@@ -34,6 +34,7 @@
<string-array name="config_systemUIServiceComponents" translatable="false">
<item>com.android.systemui.Dependency$DependencyCreator</item>
<item>com.android.systemui.util.NotificationChannels</item>
<item>com.android.systemui.notifications.NotificationsUI</item>
<item>com.android.systemui.statusbar.CommandQueue$CommandQueueStart</item>
<item>com.android.systemui.keyguard.KeyguardViewMediator</item>
<item>com.android.systemui.recents.Recents</item>
@@ -52,6 +53,5 @@
<item>com.android.systemui.globalactions.GlobalActionsComponent</item>
<item>com.android.systemui.ScreenDecorations</item>
<item>com.android.systemui.SliceBroadcastRelayHandler</item>
<item>com.android.systemui.notifications.NotificationsUI</item>
</string-array>
</resources>

View File

@@ -24,4 +24,12 @@
<!-- If the number is negative, the feature is disabled.
If it's zero, we switch to guest immediately as we start driving. -->
<integer name="driving_on_keyguard_timeout_ms">30000</integer>
<!--Percentage of the screen height, from the bottom, that a notification panel being
partially closed at will result in it remaining open if released-->
<integer name="notification_settle_open_percentage">20</integer>
<!--Percentage of the screen height, from the bottom, that a notification panel being peeked
at will result in remaining closed the panel if released-->
<integer name="notification_settle_close_percentage">80</integer>
</resources>

View File

@@ -21,4 +21,7 @@
<!--This Theme contains attributes required for components from the car support lib -->
<style name="PagedListTheme" parent="Theme.CarSupportWrapper.NoActionBar">
</style>
<style name="Theme.Notification" parent="Theme.DeviceDefault.NoActionBar.Notification">
</style>
</resources>

View File

@@ -16,6 +16,10 @@
package com.android.systemui.notifications;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.app.ActivityManager;
import android.car.Car;
import android.car.CarNotConnectedException;
@@ -23,29 +27,48 @@ import android.car.drivingstate.CarUxRestrictionsManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.ServiceConnection;
import android.content.res.Configuration;
import android.graphics.PixelFormat;
import android.os.IBinder;
import android.os.ServiceManager;
import android.util.Log;
import android.view.ContextThemeWrapper;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.WindowManager;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.android.car.notification.CarNotificationListener;
import com.android.car.notification.CarNotificationView;
import com.android.car.notification.CarUxRestrictionManagerWrapper;
import com.android.car.notification.NotificationClickHandlerFactory;
import com.android.car.notification.NotificationViewController;
import com.android.car.notification.PreprocessingManager;
import com.android.internal.statusbar.IStatusBarService;
import com.android.systemui.Dependency;
import com.android.systemui.R;
import com.android.systemui.SystemUI;
import com.android.systemui.statusbar.FlingAnimationUtils;
import com.android.systemui.statusbar.policy.ConfigurationController;
/**
* Standalone SystemUI for displaying Notifications that have been designed to be used in the car
*/
public class NotificationsUI extends SystemUI {
public class NotificationsUI extends SystemUI
implements ConfigurationController.ConfigurationListener {
private static final String TAG = "NotificationsUI";
// 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 CarNotificationListener mCarNotificationListener;
private CarUxRestrictionsManager mCarUxRestrictionsManager;
private NotificationClickHandlerFactory mClickHandlerFactory;
@@ -53,8 +76,20 @@ public class NotificationsUI extends SystemUI {
private ViewGroup mCarNotificationWindow;
private NotificationViewController mNotificationViewController;
private boolean mIsShowing;
private boolean mIsTracking;
private boolean mNotificationListAtBottom;
private boolean mNotificationListAtBottomAtTimeOfTouch;
private CarUxRestrictionManagerWrapper mCarUxRestrictionManagerWrapper =
new CarUxRestrictionManagerWrapper();
// Used in the Notification panel touch listener
private GestureDetector mGestureDetector;
// Used in scrollable content of the notifications
private GestureDetector mScrollUpDetector;
private View mContent;
private View.OnTouchListener mOnTouchListener;
private FlingAnimationUtils mFlingAnimationUtils;
private static int sSettleOpenPercentage;
private static int sSettleClosePercentage;
/**
* Inits the window that hosts the notifications and establishes the connections
@@ -62,32 +97,41 @@ public class NotificationsUI extends SystemUI {
*/
@Override
public void start() {
sSettleOpenPercentage = mContext.getResources().getInteger(
R.integer.notification_settle_open_percentage);
sSettleClosePercentage = mContext.getResources().getInteger(
R.integer.notification_settle_close_percentage);
WindowManager windowManager =
(WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
mFlingAnimationUtils = new FlingAnimationUtils(mContext,
FLING_ANIMATION_MAX_TIME, FLING_SPEED_UP_FACTOR);
mCarNotificationListener = new CarNotificationListener();
// create a notification click handler that closes the notification ui if the an activity
// is launched successfully
mClickHandlerFactory = new NotificationClickHandlerFactory(
IStatusBarService.Stub.asInterface(
ServiceManager.getService(Context.STATUS_BAR_SERVICE)),
launchResult -> {
if (launchResult == ActivityManager.START_TASK_TO_FRONT
|| launchResult == ActivityManager.START_SUCCESS) {
closeCarNotifications();
closeCarNotifications(DEFAULT_FLING_VELOCITY);
}
});
mCarNotificationListener.registerAsSystemService(mContext, mCarUxRestrictionManagerWrapper,
mClickHandlerFactory);
mCar = Car.createCar(mContext, mCarConnectionListener);
mCar.connect();
mCarNotificationWindow = (ViewGroup) View.inflate(mContext,
NotificationGestureListener gestureListener = new NotificationGestureListener();
mGestureDetector = new GestureDetector(mContext, gestureListener);
mScrollUpDetector = new GestureDetector(mContext, new ScrollUpDetector());
mOnTouchListener = new NotificationPanelTouchListener();
mCarNotificationWindow = (ViewGroup) View.inflate(new ContextThemeWrapper(mContext,
R.style.Theme_Notification),
R.layout.navigation_bar_window, null);
View.inflate(mContext,
com.android.car.notification.R.layout.notification_center_activity,
mCarNotificationWindow);
mCarNotificationWindow.findViewById(
com.android.car.notification.R.id.exit_button_container)
.setOnClickListener(v -> toggleShowingCarNotifications());
mCarNotificationWindow
.setBackgroundColor(mContext.getColor(R.color.notification_shade_background_color));
inflateNotificationContent();
WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT,
@@ -101,15 +145,222 @@ public class NotificationsUI extends SystemUI {
// start in the hidden state
mCarNotificationWindow.setVisibility(View.GONE);
windowManager.addView(mCarNotificationWindow, layoutParams);
// Add this object to the SystemUI component registry such that the status bar
// can get a reference to it.
putComponent(NotificationsUI.class, this);
Dependency.get(ConfigurationController.class).addCallback(this);
}
@SuppressLint("ClickableViewAccessibility")
private void inflateNotificationContent() {
if (mNotificationViewController != null) {
mNotificationViewController.disable();
}
mCarNotificationWindow.removeAllViews();
mContent = View.inflate(new ContextThemeWrapper(mContext,
com.android.car.notification.R.style.Theme_Notification),
R.layout.notification_center_activity,
mCarNotificationWindow);
// set the click handler such that we can dismiss the UI when a notification is clicked
CarNotificationView noteView = mCarNotificationWindow.findViewById(R.id.notification_view);
noteView.setClickHandlerFactory(mClickHandlerFactory);
mContent.setOnTouchListener(mOnTouchListener);
// set initial translation after size is calculated
mContent.getViewTreeObserver().addOnGlobalLayoutListener(
new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
mContent.getViewTreeObserver().removeOnGlobalLayoutListener(this);
if (!mIsShowing && !mIsTracking) {
mContent.setTranslationY(mContent.getHeight() * -1);
}
}
});
RecyclerView notificationList = mCarNotificationWindow
.findViewById(com.android.car.notification.R.id.recycler_view);
// register a scroll listener so we can figure out if we are at the bottom of the
// list of notifications
notificationList.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (!notificationList.canScrollVertically(1)) {
mNotificationListAtBottom = true;
return;
}
mNotificationListAtBottom = false;
mNotificationListAtBottomAtTimeOfTouch = false;
}
});
// add a touch listener such that when the user scrolls up and they are at the bottom
// of the list we can start the closing of the view.
notificationList.setOnTouchListener(new NotificationListTouchListener());
// There's a view installed at a higher z-order such that we can intercept the ACTION_DOWN
// to set the initial click state.
mCarNotificationWindow.findViewById(R.id.glass_pane).setOnTouchListener((v, event) -> {
if (event.getActionMasked() == MotionEvent.ACTION_UP) {
mNotificationListAtBottomAtTimeOfTouch = false;
}
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
mNotificationListAtBottomAtTimeOfTouch = mNotificationListAtBottom;
// register the down event with the gesture detectors so then know where the down
// started. This is needed because at this point we don't know which listener
// is going to handle scroll and fling events.
mGestureDetector.onTouchEvent(event);
mScrollUpDetector.onTouchEvent(event);
}
return false;
});
mNotificationViewController = new NotificationViewController(
mCarNotificationWindow
.findViewById(com.android.car.notification.R.id.notification_view),
PreprocessingManager.getInstance(mContext),
mCarNotificationListener,
mCarUxRestrictionManagerWrapper
);
// Add to the SystemUI component registry
putComponent(NotificationsUI.class, this);
mCarUxRestrictionManagerWrapper);
mNotificationViewController.enable();
}
// allows for day night switch
@Override
public void onConfigChanged(Configuration newConfig) {
inflateNotificationContent();
}
public View.OnTouchListener getDragDownListener() {
return mOnTouchListener;
}
/**
* This listener is attached to the notification list UI to intercept gestures if the user
* is scrolling up when the notification list is at the bottom
*/
private class ScrollUpDetector extends GestureDetector.SimpleOnGestureListener {
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
return distanceY > 0;
}
}
private class NotificationListTouchListener implements View.OnTouchListener {
@Override
public boolean onTouch(View v, MotionEvent event) {
// reset mNotificationListAtBottomAtTimeOfTouch here since the "glass pane" will not
// get the up event
if (event.getActionMasked() == MotionEvent.ACTION_UP) {
mNotificationListAtBottomAtTimeOfTouch = false;
}
boolean wasScrolledUp = mScrollUpDetector.onTouchEvent(event);
if (mIsTracking
|| (mNotificationListAtBottomAtTimeOfTouch && mNotificationListAtBottom
&& wasScrolledUp)) {
mOnTouchListener.onTouch(v, event);
// touch event should not be propagated further
return true;
}
return false;
}
}
/**
* Touch listener installed on the notification panel. It is also used by the Nav and StatusBar
*/
private class NotificationPanelTouchListener implements View.OnTouchListener {
@Override
public boolean onTouch(View v, MotionEvent event) {
boolean consumed = mGestureDetector.onTouchEvent(event);
if (consumed) {
return true;
}
if (!mIsTracking || event.getActionMasked() != MotionEvent.ACTION_UP) {
return false;
}
float percentFromBottom =
Math.abs(mContent.getTranslationY() / mContent.getHeight()) * 100;
if (mIsShowing) {
if (percentFromBottom < sSettleOpenPercentage) {
// panel started to close but did not cross minimum threshold thus we open
// it back up
openCarNotifications(DEFAULT_FLING_VELOCITY);
return true;
}
// panel was lifted more than the threshold thus we close it the rest of the way
closeCarNotifications(DEFAULT_FLING_VELOCITY);
return true;
}
if (percentFromBottom > sSettleClosePercentage) {
// panel was only peeked at thus close it back up
closeCarNotifications(DEFAULT_FLING_VELOCITY);
return true;
}
// panel has been open more than threshold thus open it the rest of the way
openCarNotifications(DEFAULT_FLING_VELOCITY);
return true;
}
}
/**
* Listener called by mGestureDetector. This will be initiated from the
* NotificationPanelTouchListener
*/
private class NotificationGestureListener extends GestureDetector.SimpleOnGestureListener {
private static final int SWIPE_UP_MIN_DISTANCE = 75;
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;
@Override
public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanceX,
float distanceY) {
mIsTracking = true;
mCarNotificationWindow.setVisibility(View.VISIBLE);
mContent.setTranslationY(Math.min(mContent.getTranslationY() - distanceY, 0));
if (mContent.getTranslationY() == 0) {
mIsTracking = false;
}
return true;
}
@Override
public boolean onFling(MotionEvent event1, MotionEvent event2,
float velocityX, float velocityY) {
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;
float distanceDelta = Math.abs(event1.getY() - event2.getY());
if (isUp && distanceDelta > SWIPE_UP_MIN_DISTANCE) {
// fling up
mIsTracking = false;
closeCarNotifications(Math.abs(velocityY));
return true;
} else if (!isUp && distanceDelta > SWIPE_DOWN_MIN_DISTANCE) {
// fling down
mIsTracking = false;
openCarNotifications(velocityY);
return true;
}
return false;
}
}
/**
@@ -139,38 +390,56 @@ public class NotificationsUI extends SystemUI {
};
/**
* Toggles the visiblity of the notifications
* Toggles the visibility of the notifications
*/
public void toggleShowingCarNotifications() {
if (mCarNotificationWindow.getVisibility() == View.VISIBLE) {
closeCarNotifications();
closeCarNotifications(DEFAULT_FLING_VELOCITY);
return;
}
openCarNotifications();
openCarNotifications(DEFAULT_FLING_VELOCITY);
}
/**
* Hides the notifications
*/
public void closeCarNotifications() {
mCarNotificationWindow.setVisibility(View.GONE);
public void closeCarNotifications(float velocityY) {
float closedTranslation = mContent.getHeight() * -1;
ValueAnimator animator =
ValueAnimator.ofFloat(mContent.getTranslationY(), closedTranslation);
animator.addUpdateListener(
animation -> mContent.setTranslationY((Float) animation.getAnimatedValue()));
mFlingAnimationUtils.apply(
animator, mContent.getTranslationY(), closedTranslation, velocityY);
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mCarNotificationWindow.setVisibility(View.GONE);
}
});
animator.start();
mNotificationViewController.disable();
mIsShowing = false;
mIsTracking = false;
RecyclerView notificationListView = mCarNotificationWindow.findViewById(
com.android.car.notification.R.id.recycler_view);
notificationListView.scrollToPosition(0);
}
/**
* Sets the notifications to visible
*/
public void openCarNotifications() {
public void openCarNotifications(float velocityY) {
mCarNotificationWindow.setVisibility(View.VISIBLE);
ValueAnimator animator = ValueAnimator.ofFloat(mContent.getTranslationY(), 0);
animator.addUpdateListener(
animation -> mContent.setTranslationY((Float) animation.getAnimatedValue()));
mFlingAnimationUtils.apply(animator, mContent.getTranslationY(), 0, velocityY);
animator.start();
mNotificationViewController.enable();
mIsShowing = true;
}
/**
* Returns {@code true} if notifications are currently on the screen
*/
public boolean isShowing() {
return mIsShowing;
mIsTracking = false;
}
}

View File

@@ -18,6 +18,7 @@ package com.android.systemui.statusbar.car;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.LinearLayout;
@@ -37,6 +38,8 @@ class CarNavigationBarView extends LinearLayout {
private CarStatusBar mCarStatusBar;
private Context mContext;
private View mLockScreenButtons;
private OnTouchListener mStatusBarWindowTouchListener;
public CarNavigationBarView(Context context, AttributeSet attrs) {
super(context, attrs);
@@ -62,11 +65,22 @@ class CarNavigationBarView extends LinearLayout {
mDarkIconManager.setShouldLog(true);
Dependency.get(StatusBarIconController.class).addIconGroup(mDarkIconManager);
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (mStatusBarWindowTouchListener == null) {
return false;
}
// forward touch events to the status bar window so it can add a drag down
// windows if required (Notification shade)
mStatusBarWindowTouchListener.onTouch(this, ev);
return false;
}
void setStatusBar(CarStatusBar carStatusBar) {
mCarStatusBar = carStatusBar;
mStatusBarWindowTouchListener = carStatusBar.getStatusBarWindowTouchListener();
}
protected void onNotificationsClick(View v) {

View File

@@ -211,6 +211,7 @@ public class CarStatusBar extends StatusBar implements
@Override
public void showKeyguard() {
super.showKeyguard();
getComponent(NotificationsUI.class).closeCarNotifications(0);
if (mNavigationBarView != null) {
mNavigationBarView.showKeyguardButtons();
}
@@ -264,6 +265,12 @@ public class CarStatusBar extends StatusBar implements
mBatteryMeterView.setVisibility(View.GONE);
});
addTemperatureViewToController(mStatusBarWindow);
// The following are the ui elements that the user would call the status bar.
// This will set the status bar so it they can make call backs.
CarNavigationBarView topBar = mStatusBarWindow.findViewById(R.id.car_top_bar);
topBar.setStatusBar(this);
CarNavigationBarView qsTopBar = mStatusBarWindow.findViewById(R.id.qs_car_top_bar);
qsTopBar.setStatusBar(this);
}
@Override
@@ -339,6 +346,7 @@ public class CarStatusBar extends StatusBar implements
lp.setTitle("CarNavigationBar");
lp.windowAnimations = 0;
mWindowManager.addView(mNavigationBarWindow, lp);
mNavigationBarWindow.setOnTouchListener(getStatusBarWindowTouchListener());
}
if (mShowLeft) {
int width = mContext.getResources().getDimensionPixelSize(
@@ -458,10 +466,8 @@ public class CarStatusBar extends StatusBar implements
@Override
protected View.OnTouchListener getStatusBarWindowTouchListener() {
// Usually, a touch on the background window will dismiss the notification shade. However,
// for the car use-case, the shade should remain unless the user switches to a different
// facet (e.g. phone).
return null;
// Gets the car specific notification touch listener
return getComponent(NotificationsUI.class).getDragDownListener();
}
@Override