New layout structure for the expanded status bar.
This also includes a new (very WIP) interaction to get to quick
settings, with clumping the cards and a scrollable container with
the cards and the bottom QS part in it.
Change-Id: Ib073bb0174cddcf60347a5e3bb474fb3b6385bcf
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
index 627b80f..f63ba9c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
@@ -16,11 +16,17 @@
package com.android.systemui.statusbar.phone;
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
+import android.view.VelocityTracker;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
import com.android.systemui.R;
import com.android.systemui.statusbar.ExpandableView;
@@ -29,18 +35,40 @@
import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
public class NotificationPanelView extends PanelView implements
- ExpandableView.OnHeightChangedListener {
+ ExpandableView.OnHeightChangedListener, ObservableScrollView.Listener,
+ View.OnClickListener {
public static final boolean DEBUG_GESTURES = true;
+ private static final int EXPANSION_ANIMATION_LENGTH = 375;
PhoneStatusBar mStatusBar;
- private View mHeader;
+ private StatusBarHeaderView mHeader;
+ private QuickSettingsContainerView mQsContainer;
private View mKeyguardStatusView;
+ private ObservableScrollView mScrollView;
+ private View mStackScrollerContainer;
private NotificationStackScrollLayout mNotificationStackScroller;
- private boolean mTrackingSettings;
private int mNotificationTopPadding;
private boolean mAnimateNextTopPaddingChange;
+ private Interpolator mExpansionInterpolator;
+
+ private int mTrackingPointer;
+ private VelocityTracker mVelocityTracker;
+ private boolean mTracking;
+ private boolean mQsExpanded;
+ private float mInitialHeightOnTouch;
+ private float mInitialTouchX;
+ private float mInitialTouchY;
+ private float mQsExpansionHeight;
+ private int mQsMinExpansionHeight;
+ private int mQsMaxExpansionHeight;
+ private int mMinStackHeight;
+ private float mNotificationTranslation;
+ private int mStackScrollerIntrinsicPadding;
+ private boolean mQsExpansionEnabled = true;
+ private ValueAnimator mQsExpansionAnimator;
+
public NotificationPanelView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@@ -63,14 +91,21 @@
@Override
protected void onFinishInflate() {
super.onFinishInflate();
-
- mHeader = findViewById(R.id.header);
+ mHeader = (StatusBarHeaderView) findViewById(R.id.header);
+ mHeader.getBackgroundView().setOnClickListener(this);
mKeyguardStatusView = findViewById(R.id.keyguard_status_view);
+ mStackScrollerContainer = findViewById(R.id.notification_container_parent);
+ mQsContainer = (QuickSettingsContainerView) findViewById(R.id.quick_settings_container);
+ mScrollView = (ObservableScrollView) findViewById(R.id.scroll_view);
+ mScrollView.setListener(this);
mNotificationStackScroller = (NotificationStackScrollLayout)
findViewById(R.id.notification_stack_scroller);
mNotificationStackScroller.setOnHeightChangedListener(this);
mNotificationTopPadding = getResources().getDimensionPixelSize(
R.dimen.notifications_top_padding);
+ mMinStackHeight = getResources().getDimensionPixelSize(R.dimen.collapsed_stack_height);
+ mExpansionInterpolator = AnimationUtils.loadInterpolator(
+ getContext(), android.R.interpolator.fast_out_slow_in);
}
@Override
@@ -78,11 +113,21 @@
super.onLayout(changed, left, top, right, bottom);
int keyguardBottomMargin =
((MarginLayoutParams) mKeyguardStatusView.getLayoutParams()).bottomMargin;
- mNotificationStackScroller.setTopPadding(mStatusBar.getBarState() == StatusBarState.KEYGUARD
- ? mKeyguardStatusView.getBottom() + keyguardBottomMargin
- : mHeader.getBottom() + mNotificationTopPadding,
- mAnimateNextTopPaddingChange);
- mAnimateNextTopPaddingChange = false;
+ if (!mQsExpanded) {
+ mStackScrollerIntrinsicPadding = mStatusBar.getBarState() == StatusBarState.KEYGUARD
+ ? mKeyguardStatusView.getBottom() + keyguardBottomMargin
+ : mHeader.getBottom() + mNotificationTopPadding;
+ mNotificationStackScroller.setTopPadding(mStackScrollerIntrinsicPadding,
+ mAnimateNextTopPaddingChange);
+ mAnimateNextTopPaddingChange = false;
+ }
+
+ // Calculate quick setting heights.
+ mQsMinExpansionHeight = mHeader.getCollapsedHeight();
+ mQsMaxExpansionHeight = mHeader.getExpandedHeight() + mQsContainer.getHeight();
+ if (mQsExpansionHeight == 0) {
+ mQsExpansionHeight = mQsMinExpansionHeight;
+ }
}
public void animateNextTopPaddingChange() {
@@ -90,6 +135,30 @@
requestLayout();
}
+ /**
+ * @return Whether Quick Settings are currently expanded.
+ */
+ public boolean isQsExpanded() {
+ return mQsExpanded;
+ }
+
+ public void setQsExpansionEnabled(boolean qsExpansionEnabled) {
+ mQsExpansionEnabled = qsExpansionEnabled;
+ mHeader.setExpansionEnabled(qsExpansionEnabled);
+ }
+
+ public void closeQs() {
+ cancelAnimation();
+ setQsExpansion(mQsMinExpansionHeight);
+ }
+
+ public void openQs() {
+ cancelAnimation();
+ if (mQsExpansionEnabled) {
+ setQsExpansion(mQsMaxExpansionHeight);
+ }
+ }
+
@Override
public void fling(float vel, boolean always) {
GestureRecorder gr = ((PhoneStatusBarView) mBar).mBar.getGestureRecorder();
@@ -114,42 +183,245 @@
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
- // intercept for quick settings
- if (event.getAction() == MotionEvent.ACTION_DOWN) {
- final View target = mStatusBar.getBarState() == StatusBarState.KEYGUARD
- ? mKeyguardStatusView
- : mHeader;
- final boolean inTarget = PhoneStatusBar.inBounds(target, event, true);
- if (inTarget && !isInSettings()) {
- mTrackingSettings = true;
- requestDisallowInterceptTouchEvent(true);
- return true;
- }
- if (!inTarget && isInSettings()) {
- mTrackingSettings = true;
- requestDisallowInterceptTouchEvent(true);
- return true;
- }
+ int pointerIndex = event.findPointerIndex(mTrackingPointer);
+ if (pointerIndex < 0) {
+ pointerIndex = 0;
+ mTrackingPointer = event.getPointerId(pointerIndex);
}
- return super.onInterceptTouchEvent(event);
+ final float x = event.getX(pointerIndex);
+ final float y = event.getY(pointerIndex);
+
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN:
+ mInitialTouchY = y;
+ mInitialTouchX = x;
+ initVelocityTracker();
+ trackMovement(event);
+ if (shouldIntercept(mInitialTouchX, mInitialTouchY, 0)) {
+ getParent().requestDisallowInterceptTouchEvent(true);
+ }
+ break;
+ case MotionEvent.ACTION_POINTER_UP:
+ final int upPointer = event.getPointerId(event.getActionIndex());
+ if (mTrackingPointer == upPointer) {
+ // gesture is ongoing, find a new pointer to track
+ final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
+ mTrackingPointer = event.getPointerId(newIndex);
+ mInitialTouchX = event.getX(newIndex);
+ mInitialTouchY = event.getY(newIndex);
+ }
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+ final float h = y - mInitialTouchY;
+ trackMovement(event);
+ if (Math.abs(h) > mTouchSlop && Math.abs(h) > Math.abs(x - mInitialTouchX)
+ && shouldIntercept(mInitialTouchX, mInitialTouchY, h)) {
+ onQsExpansionStarted();
+ mInitialHeightOnTouch = mQsExpansionHeight;
+ mInitialTouchY = y;
+ mInitialTouchX = x;
+ mTracking = true;
+ return true;
+ }
+ break;
+ }
+ return !mQsExpanded && super.onInterceptTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// TODO: Handle doublefinger swipe to notifications again. Look at history for a reference
// implementation.
- if (mTrackingSettings) {
- mStatusBar.onSettingsEvent(event);
- if (event.getAction() == MotionEvent.ACTION_UP
- || event.getAction() == MotionEvent.ACTION_CANCEL) {
- mTrackingSettings = false;
+ if (mTracking) {
+ int pointerIndex = event.findPointerIndex(mTrackingPointer);
+ if (pointerIndex < 0) {
+ pointerIndex = 0;
+ mTrackingPointer = event.getPointerId(pointerIndex);
+ }
+ final float y = event.getY(pointerIndex);
+ final float x = event.getX(pointerIndex);
+
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN:
+ mTracking = true;
+ mInitialTouchY = y;
+ mInitialTouchX = x;
+ onQsExpansionStarted();
+ mInitialHeightOnTouch = mQsExpansionHeight;
+ initVelocityTracker();
+ trackMovement(event);
+ break;
+
+ case MotionEvent.ACTION_POINTER_UP:
+ final int upPointer = event.getPointerId(event.getActionIndex());
+ if (mTrackingPointer == upPointer) {
+ // gesture is ongoing, find a new pointer to track
+ final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
+ final float newY = event.getY(newIndex);
+ final float newX = event.getX(newIndex);
+ mTrackingPointer = event.getPointerId(newIndex);
+ mInitialHeightOnTouch = mQsExpansionHeight;
+ mInitialTouchY = newY;
+ mInitialTouchX = newX;
+ }
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+ final float h = y - mInitialTouchY;
+ setQsExpansion(h + mInitialHeightOnTouch);
+ trackMovement(event);
+ break;
+
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ mTracking = false;
+ mTrackingPointer = -1;
+ trackMovement(event);
+
+ float vel = getCurrentVelocity();
+
+ // TODO: Better logic whether we should expand or not.
+ flingSettings(vel, vel > 0);
+
+ if (mVelocityTracker != null) {
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ }
+ break;
}
return true;
}
- if (isInSettings()) {
- return true;
+
+ // Consume touch events when QS are expanded.
+ return mQsExpanded || super.onTouchEvent(event);
+ }
+
+ private void onQsExpansionStarted() {
+ cancelAnimation();
+
+ // Reset scroll position and apply that position to the expanded height.
+ float height = mQsExpansionHeight - mScrollView.getScrollY();
+ mScrollView.scrollTo(0, 0);
+ setQsExpansion(height);
+ }
+
+ private void expandQs() {
+ mHeader.setExpanded(true);
+ mNotificationStackScroller.setEnabled(false);
+ mScrollView.setVisibility(View.VISIBLE);
+ mQsExpanded = true;
+ }
+
+ private void collapseQs() {
+ mHeader.setExpanded(false);
+ mNotificationStackScroller.setEnabled(true);
+ mScrollView.setVisibility(View.INVISIBLE);
+ mQsExpanded = false;
+ }
+
+ private void setQsExpansion(float height) {
+ height = Math.min(Math.max(height, mQsMinExpansionHeight), mQsMaxExpansionHeight);
+ if (height > mQsMinExpansionHeight && !mQsExpanded) {
+ expandQs();
+ } else if (height <= mQsMinExpansionHeight && mQsExpanded) {
+ collapseQs();
}
- return super.onTouchEvent(event);
+ mQsExpansionHeight = height;
+ mHeader.setExpansion(height);
+ setQsTranslation(height);
+ setQsStackScrollerPadding(height);
+ }
+
+ private void setQsTranslation(float height) {
+ mQsContainer.setY(height - mQsContainer.getHeight());
+ }
+
+ private void setQsStackScrollerPadding(float height) {
+ float start = height - mScrollView.getScrollY() + mNotificationTopPadding;
+ float stackHeight = mNotificationStackScroller.getHeight() - start;
+ if (stackHeight <= mMinStackHeight) {
+ float overflow = mMinStackHeight - stackHeight;
+ stackHeight = mMinStackHeight;
+ start = mNotificationStackScroller.getHeight() - stackHeight;
+ mNotificationStackScroller.setTranslationY(overflow);
+ mNotificationTranslation = overflow + mScrollView.getScrollY();
+ } else {
+ mNotificationStackScroller.setTranslationY(0);
+ mNotificationTranslation = mScrollView.getScrollY();
+ }
+ mNotificationStackScroller.setTopPadding(clampQsStackScrollerPadding((int) start), false);
+ }
+
+ private int clampQsStackScrollerPadding(int desiredPadding) {
+ return Math.max(desiredPadding, mStackScrollerIntrinsicPadding);
+ }
+
+ private void trackMovement(MotionEvent event) {
+ if (mVelocityTracker != null) mVelocityTracker.addMovement(event);
+ }
+
+ private void initVelocityTracker() {
+ if (mVelocityTracker != null) {
+ mVelocityTracker.recycle();
+ }
+ mVelocityTracker = VelocityTracker.obtain();
+ }
+
+ private float getCurrentVelocity() {
+ if (mVelocityTracker == null) {
+ return 0;
+ }
+ mVelocityTracker.computeCurrentVelocity(1000);
+ return mVelocityTracker.getYVelocity();
+ }
+
+ private void cancelAnimation() {
+ if (mQsExpansionAnimator != null) {
+ mQsExpansionAnimator.cancel();
+ }
+ }
+ private void flingSettings(float vel, boolean expand) {
+
+ // TODO: Actually use velocity.
+
+ float target = expand ? mQsMaxExpansionHeight : mQsMinExpansionHeight;
+ ValueAnimator animator = ValueAnimator.ofFloat(mQsExpansionHeight, target);
+ animator.setDuration(EXPANSION_ANIMATION_LENGTH);
+ animator.setInterpolator(mExpansionInterpolator);
+ animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ setQsExpansion((Float) animation.getAnimatedValue());
+ }
+ });
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mQsExpansionAnimator = null;
+ }
+ });
+ animator.start();
+ mQsExpansionAnimator = animator;
+ }
+
+ /**
+ * @return Whether we should intercept a gesture to open Quick Settings.
+ */
+ private boolean shouldIntercept(float x, float y, float yDiff) {
+ if (!mQsExpansionEnabled) {
+ return false;
+ }
+ View headerView = mStatusBar.getBarState() == StatusBarState.KEYGUARD && !mQsExpanded
+ ? mKeyguardStatusView
+ : mHeader;
+ boolean onHeader = x >= headerView.getLeft() && x <= headerView.getRight()
+ && y >= headerView.getTop() && y <= headerView.getBottom();
+ if (mQsExpanded) {
+ return onHeader || (mScrollView.isScrolledToBottom() && yDiff < 0);
+ } else {
+ return onHeader;
+ }
}
@Override
@@ -164,14 +436,16 @@
protected int getMaxPanelHeight() {
if (!isInSettings()) {
int maxPanelHeight = super.getMaxPanelHeight();
- int emptyBottomMargin = mNotificationStackScroller.getEmptyBottomMargin();
+ int notificationMarginBottom = mStackScrollerContainer.getPaddingBottom();
+ int emptyBottomMargin = notificationMarginBottom
+ + mNotificationStackScroller.getEmptyBottomMargin();
return maxPanelHeight - emptyBottomMargin;
}
return super.getMaxPanelHeight();
}
private boolean isInSettings() {
- return mStatusBar != null && mStatusBar.isFlippedToSettings();
+ return mQsExpanded;
}
@Override
@@ -200,4 +474,24 @@
public void onHeightChanged(ExpandableView view) {
requestPanelHeightUpdate();
}
+
+ @Override
+ public void onScrollChanged() {
+ if (mQsExpanded) {
+ mNotificationStackScroller.setTranslationY(
+ mNotificationTranslation - mScrollView.getScrollY());
+ }
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (v == mHeader.getBackgroundView()) {
+ onQsExpansionStarted();
+ if (mQsExpanded) {
+ flingSettings(0 /* vel */, false /* expand */);
+ } else if (mQsExpansionEnabled) {
+ flingSettings(0 /* vel */, true /* expand */);
+ }
+ }
+ }
}