diff options
author | Selim Cinek <cinek@google.com> | 2014-03-27 14:28:55 +0000 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2014-03-27 14:28:56 +0000 |
commit | ddaba4360b86f96307ec88f0d0b145425bc3e128 (patch) | |
tree | 643c4acdfe28e4f497481a4749bc54bfc8f41556 /packages/SystemUI/src/com | |
parent | 73581effb0b4029961501c6f699e95a9930ea1e6 (diff) | |
parent | 67b2260093774f5866f781aede52830440f4ed0e (diff) | |
download | frameworks_base-ddaba4360b86f96307ec88f0d0b145425bc3e128.zip frameworks_base-ddaba4360b86f96307ec88f0d0b145425bc3e128.tar.gz frameworks_base-ddaba4360b86f96307ec88f0d0b145425bc3e128.tar.bz2 |
Merge "Initial implementation of NotificationStackScroller"
Diffstat (limited to 'packages/SystemUI/src/com')
8 files changed, 1615 insertions, 19 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java index e1a674a..bad5641 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java @@ -41,6 +41,7 @@ import android.os.Message; import android.os.PowerManager; import android.os.RemoteException; import android.os.ServiceManager; +import android.os.SystemProperties; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; @@ -77,7 +78,6 @@ import com.android.systemui.RecentsComponent; import com.android.systemui.SearchPanelView; import com.android.systemui.SystemUI; import com.android.systemui.statusbar.phone.KeyguardTouchDelegate; -import com.android.systemui.statusbar.policy.NotificationRowLayout; import java.util.ArrayList; import java.util.Locale; @@ -98,6 +98,8 @@ public abstract class BaseStatusBar extends SystemUI implements protected static final int MSG_HIDE_HEADS_UP = 1027; protected static final int MSG_ESCALATE_HEADS_UP = 1028; + public static final boolean ENABLE_NOTIFICATION_STACK = SystemProperties + .getBoolean("persist.notifications.use_stack", false); protected static final boolean ENABLE_HEADS_UP = true; // scores above this threshold should be displayed in heads up mode. protected static final int INTERRUPTION_THRESHOLD = 10; @@ -118,7 +120,7 @@ public abstract class BaseStatusBar extends SystemUI implements // all notifications protected NotificationData mNotificationData = new NotificationData(); - protected NotificationRowLayout mPile; + protected ViewGroup mPile; protected NotificationData.Entry mInterruptingNotificationEntry; protected long mInterruptingNotificationTime; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java index 650c557..160f2be 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java @@ -87,6 +87,7 @@ import com.android.internal.statusbar.StatusBarIcon; import com.android.systemui.DemoMode; import com.android.systemui.EventLogTags; import com.android.systemui.R; +import com.android.systemui.SwipeHelper; import com.android.systemui.statusbar.BaseStatusBar; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.GestureRecorder; @@ -94,6 +95,7 @@ import com.android.systemui.statusbar.NotificationData; import com.android.systemui.statusbar.NotificationData.Entry; import com.android.systemui.statusbar.SignalClusterView; import com.android.systemui.statusbar.StatusBarIconView; + import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.statusbar.policy.BluetoothController; import com.android.systemui.statusbar.policy.DateView; @@ -104,6 +106,8 @@ import com.android.systemui.statusbar.policy.NotificationRowLayout; import com.android.systemui.statusbar.policy.OnSizeChangedListener; import com.android.systemui.statusbar.policy.RotationLockController; +import com.android.systemui.statusbar.stack.NotificationStackScrollLayout; + import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; @@ -479,11 +483,25 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { mStatusBarContents = (LinearLayout)mStatusBarView.findViewById(R.id.status_bar_contents); mTickerView = mStatusBarView.findViewById(R.id.ticker); - mPile = (NotificationRowLayout)mStatusBarWindow.findViewById(R.id.latestItems); - mPile.setLayoutTransitionsEnabled(false); - mPile.setLongPressListener(getNotificationLongClicker()); + NotificationRowLayout rowLayout + = (NotificationRowLayout) mStatusBarWindow.findViewById(R.id.latestItems); + NotificationStackScrollLayout notificationStack + = (NotificationStackScrollLayout) mStatusBarWindow + .findViewById(R.id.notification_stack_scroller); + if (ENABLE_NOTIFICATION_STACK) { + notificationStack.setLongPressListener(getNotificationLongClicker()); + mPile = notificationStack; + } else { + rowLayout.setLayoutTransitionsEnabled(false); + rowLayout.setLongPressListener(getNotificationLongClicker()); + mPile = rowLayout; + notificationStack.setVisibility(View.GONE); + } + mExpandedContents = mPile; // was: expanded.findViewById(R.id.notificationLinearLayout); + + mNotificationPanelHeader = mStatusBarWindow.findViewById(R.id.header); mClearButton = mStatusBarWindow.findViewById(R.id.clear_all_button); @@ -597,7 +615,8 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { } // set up the dynamic hide/show of the label - mPile.setOnSizeChangedListener(new OnSizeChangedListener() { + if(!ENABLE_NOTIFICATION_STACK) + ((NotificationRowLayout) mPile).setOnSizeChangedListener(new OnSizeChangedListener() { @Override public void onSizeChanged(View view, int w, int h, int oldw, int oldh) { updateCarrierLabelVisibility(false); @@ -1457,7 +1476,9 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { } mExpandedVisible = true; - mPile.setLayoutTransitionsEnabled(true); + if(!ENABLE_NOTIFICATION_STACK) { + ((NotificationRowLayout) mPile).setLayoutTransitionsEnabled(true); + } if (mNavigationBarView != null) mNavigationBarView.setSlippery(true); @@ -1749,7 +1770,9 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { } mExpandedVisible = false; - mPile.setLayoutTransitionsEnabled(false); + if(!ENABLE_NOTIFICATION_STACK) { + ((NotificationRowLayout) mPile).setLayoutTransitionsEnabled(false); + } if (mNavigationBarView != null) mNavigationBarView.setSlippery(false); visibilityChanged(false); @@ -2406,7 +2429,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { final ArrayList<View> snapshot = new ArrayList<View>(numChildren); for (int i=0; i<numChildren; i++) { final View child = mPile.getChildAt(i); - if (mPile.canChildBeDismissed(child) && child.getBottom() > scrollTop && + if (((SwipeHelper.Callback) mPile).canChildBeDismissed(child) && child.getBottom() > scrollTop && child.getTop() < scrollBottom) { snapshot.add(child); } @@ -2424,10 +2447,13 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { int currentDelay = 140; int totalDelay = 0; - // Set the shade-animating state to avoid doing other work during - // all of these animations. In particular, avoid layout and - // redrawing when collapsing the shade. - mPile.setViewRemoval(false); + + if(!ENABLE_NOTIFICATION_STACK) { + // Set the shade-animating state to avoid doing other work during + // all of these animations. In particular, avoid layout and + // redrawing when collapsing the shade. + ((NotificationRowLayout) mPile).setViewRemoval(false); + } mPostCollapseCleanup = new Runnable() { @Override @@ -2436,7 +2462,9 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { Log.v(TAG, "running post-collapse cleanup"); } try { - mPile.setViewRemoval(true); + if (!ENABLE_NOTIFICATION_STACK) { + ((NotificationRowLayout) mPile).setViewRemoval(true); + } mBarService.onClearAllNotifications(mCurrentUserId); } catch (Exception ex) { } } @@ -2450,7 +2478,13 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { mHandler.postDelayed(new Runnable() { @Override public void run() { - mPile.dismissRowAnimated(_v, velocity); + if (!ENABLE_NOTIFICATION_STACK) { + ((NotificationRowLayout) mPile).dismissRowAnimated( + _v, velocity); + } else { + ((NotificationStackScrollLayout) mPile).dismissRowAnimated( + _v, velocity); + } } }, totalDelay); currentDelay = Math.max(50, currentDelay - ROW_DELAY_DECREMENT); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowView.java index 7b03195..925e0d8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowView.java @@ -24,6 +24,7 @@ import android.util.AttributeSet; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; +import android.view.ViewGroup; import android.view.ViewRootImpl; import android.widget.FrameLayout; import android.widget.ScrollView; @@ -31,7 +32,6 @@ import android.widget.ScrollView; import com.android.systemui.ExpandHelper; import com.android.systemui.R; import com.android.systemui.statusbar.BaseStatusBar; -import com.android.systemui.statusbar.policy.NotificationRowLayout; public class StatusBarWindowView extends FrameLayout @@ -40,7 +40,7 @@ public class StatusBarWindowView extends FrameLayout public static final boolean DEBUG = BaseStatusBar.DEBUG; private ExpandHelper mExpandHelper; - private NotificationRowLayout latestItems; + private ViewGroup latestItems; private NotificationPanelView mNotificationPanel; private ScrollView mScrollView; @@ -55,12 +55,18 @@ public class StatusBarWindowView extends FrameLayout @Override protected void onAttachedToWindow () { super.onAttachedToWindow(); - latestItems = (NotificationRowLayout) findViewById(R.id.latestItems); + + if (BaseStatusBar.ENABLE_NOTIFICATION_STACK) { + latestItems = (ViewGroup) findViewById(R.id.notification_stack_scroller); + } else { + latestItems = (ViewGroup) findViewById(R.id.latestItems); + } mScrollView = (ScrollView) findViewById(R.id.scroll); mNotificationPanel = (NotificationPanelView) findViewById(R.id.notification_panel); int minHeight = getResources().getDimensionPixelSize(R.dimen.notification_row_min_height); int maxHeight = getResources().getDimensionPixelSize(R.dimen.notification_row_max_height); - mExpandHelper = new ExpandHelper(getContext(), latestItems, minHeight, maxHeight); + mExpandHelper = new ExpandHelper(getContext(), (ExpandHelper.Callback) latestItems, + minHeight, maxHeight); mExpandHelper.setEventSource(this); mExpandHelper.setScrollView(mScrollView); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java new file mode 100644 index 0000000..edac3a7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java @@ -0,0 +1,816 @@ +/* + * Copyright (C) 2014 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.statusbar.stack; + +import android.content.Context; +import android.content.res.Configuration; + +import android.graphics.Canvas; +import android.graphics.Outline; +import android.graphics.Paint; +import android.util.AttributeSet; +import android.util.Log; + +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.widget.OverScroller; + +import com.android.systemui.ExpandHelper; +import com.android.systemui.R; +import com.android.systemui.SwipeHelper; +import com.android.systemui.statusbar.ExpandableNotificationRow; + +/** + * A layout which handles a dynamic amount of notifications and presents them in a scrollable stack. + */ +public class NotificationStackScrollLayout extends ViewGroup + implements SwipeHelper.Callback, ExpandHelper.Callback { + + private static final String TAG = "NotificationStackScrollLayout"; + private static final boolean DEBUG = false; + + /** + * Sentinel value for no current active pointer. Used by {@link #mActivePointerId}. + */ + private static final int INVALID_POINTER = -1; + + private SwipeHelper mSwipeHelper; + private boolean mAllowScrolling = true; + private int mCurrentStackHeight = Integer.MAX_VALUE; + private int mOwnScrollY; + private int mMaxLayoutHeight; + + private VelocityTracker mVelocityTracker; + private OverScroller mScroller; + private int mTouchSlop; + private int mMinimumVelocity; + private int mMaximumVelocity; + private int mOverscrollDistance; + private int mOverflingDistance; + private boolean mIsBeingDragged; + private int mLastMotionY; + private int mActivePointerId; + + private int mSidePaddings; + private Paint mDebugPaint; + private int mBackgroundRoundedRectCornerRadius; + private int mContentHeight; + private int mCollapsedSize; + private int mBottomStackPeekSize; + private int mEmptyMarginBottom; + private int mPaddingBetweenElements; + + /** + * The algorithm which calculates the properties for our children + */ + private StackScrollAlgorithm mStackScrollAlgorithm; + + /** + * The current State this Layout is in + */ + private StackScrollState mCurrentStackScrollState; + + public NotificationStackScrollLayout(Context context) { + this(context, null); + } + + public NotificationStackScrollLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public NotificationStackScrollLayout(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public NotificationStackScrollLayout(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + initView(context); + if (DEBUG) { + setWillNotDraw(false); + mDebugPaint = new Paint(); + mDebugPaint.setColor(0xffff0000); + mDebugPaint.setStrokeWidth(2); + mDebugPaint.setStyle(Paint.Style.STROKE); + } + } + + @Override + protected void onDraw(Canvas canvas) { + if (DEBUG) { + int y = mCollapsedSize; + canvas.drawLine(0, y, getWidth(), y, mDebugPaint); + y = (int) (getLayoutHeight() - mBottomStackPeekSize - mCollapsedSize); + canvas.drawLine(0, y, getWidth(), y, mDebugPaint); + y = (int) getLayoutHeight(); + canvas.drawLine(0, y, getWidth(), y, mDebugPaint); + } + } + + private void initView(Context context) { + mScroller = new OverScroller(getContext()); + setFocusable(true); + setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); + final ViewConfiguration configuration = ViewConfiguration.get(context); + mTouchSlop = configuration.getScaledTouchSlop(); + mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); + mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); + mOverscrollDistance = configuration.getScaledOverscrollDistance(); + mOverflingDistance = configuration.getScaledOverflingDistance(); + float densityScale = getResources().getDisplayMetrics().density; + float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop(); + mSwipeHelper = new SwipeHelper(SwipeHelper.X, this, densityScale, pagingTouchSlop); + + mSidePaddings = context.getResources() + .getDimensionPixelSize(R.dimen.notification_side_padding); + mBackgroundRoundedRectCornerRadius = context.getResources() + .getDimensionPixelSize( + com.android.internal.R.dimen.notification_quantum_rounded_rect_radius); + mCollapsedSize = context.getResources() + .getDimensionPixelSize(R.dimen.notification_row_min_height); + mBottomStackPeekSize = context.getResources() + .getDimensionPixelSize(R.dimen.bottom_stack_peek_amount); + mEmptyMarginBottom = context.getResources().getDimensionPixelSize( + R.dimen.notification_stack_margin_bottom); + // currently the padding is in the elements themself + mPaddingBetweenElements = 0; + mStackScrollAlgorithm = new StackScrollAlgorithm(context); + mCurrentStackScrollState = null; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + int mode = MeasureSpec.getMode(widthMeasureSpec); + int size = MeasureSpec.getSize(widthMeasureSpec); + int childMeasureSpec = MeasureSpec.makeMeasureSpec(size - 2 * mSidePaddings, mode); + measureChildren(childMeasureSpec, heightMeasureSpec); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + + // we layout all our children centered on the top + float centerX = getWidth() / 2.0f; + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + float width = child.getMeasuredWidth(); + float height = child.getMeasuredHeight(); + int oldWidth = child.getWidth(); + int oldHeight = child.getHeight(); + child.layout((int) (centerX - width / 2.0f), + 0, + (int) (centerX + width / 2.0f), + (int) height); + updateChildOutline(child, width, height, oldWidth, oldHeight); + } + setMaxLayoutHeight(getHeight() - mEmptyMarginBottom); + updateScrollPositionIfNecessary(); + updateChildren(); + updateContentHeight(); + } + + private void setMaxLayoutHeight(int maxLayoutHeight) { + mMaxLayoutHeight = maxLayoutHeight; + updateAlgorithmHeight(); + } + + private void updateAlgorithmHeight() { + mStackScrollAlgorithm.setLayoutHeight(getLayoutHeight()); + } + + /** + * Updates the children views according to the stack scroll algorithm. Call this whenever + * modifications to {@link #mOwnScrollY} are performed to reflect it in the view layout. + */ + private void updateChildren() { + if (!isCurrentlyAnimating()) { + if (mCurrentStackScrollState == null) { + mCurrentStackScrollState = new StackScrollState(this); + } + mCurrentStackScrollState.setScrollY(mOwnScrollY); + mStackScrollAlgorithm.getStackScrollState(mCurrentStackScrollState); + mCurrentStackScrollState.apply(); + mOwnScrollY = mCurrentStackScrollState.getScrollY(); + } else { + // TODO: handle animation + } + } + + private boolean isCurrentlyAnimating() { + return false; + } + + private void updateChildOutline(View child, + float width, + float height, + int oldWidth, + int oldHeight) { + // The children currently have paddings inside themselfs because of the expansion + // visualization. In order for the shadows to work correctly we have to set the correct + // outline. + View container = child.findViewById(R.id.container); + if (container != null && (oldWidth != width || oldHeight != height)) { + Outline outline = getOutlineForSize(container.getLeft(), + container.getTop(), + container.getWidth(), + container.getHeight()); + child.setOutline(outline); + } + } + + private Outline getOutlineForSize(int leftInset, int topInset, int width, int height) { + Outline result = new Outline(); + result.setRoundRect(leftInset, topInset, leftInset + width, topInset + height, + mBackgroundRoundedRectCornerRadius); + return result; + } + + private void updateScrollPositionIfNecessary() { + int scrollRange = getScrollRange(); + if (scrollRange < mOwnScrollY) { + mOwnScrollY = scrollRange; + } + } + + public void setCurrentStackHeight(int currentStackHeight) { + this.mCurrentStackHeight = currentStackHeight; + updateAlgorithmHeight(); + updateChildren(); + } + + /** + * Get the current height of the view. This is at most the size of the view given by a the + * layout but it can also be made smaller by setting {@link #mCurrentStackHeight} + * + * @return either the layout height or the externally defined height, whichever is smaller + */ + private float getLayoutHeight() { + return Math.min(mMaxLayoutHeight, mCurrentStackHeight); + } + + public void setLongPressListener(View.OnLongClickListener listener) { + mSwipeHelper.setLongPressListener(listener); + } + + public void onChildDismissed(View v) { + if (DEBUG) Log.v(TAG, "onChildDismissed: " + v); + final View veto = v.findViewById(R.id.veto); + if (veto != null && veto.getVisibility() != View.GONE) { + veto.performClick(); + } + allowScrolling(true); + } + + public void onBeginDrag(View v) { + allowScrolling(false); + } + + public void onDragCancelled(View v) { + allowScrolling(true); + } + + public View getChildAtPosition(MotionEvent ev) { + return getChildAtPosition(ev.getX(), ev.getY()); + } + + public View getChildAtRawPosition(float touchX, float touchY) { + int[] location = new int[2]; + getLocationOnScreen(location); + return getChildAtPosition(touchX - location[0],touchY - location[1]); + } + + public View getChildAtPosition(float touchX, float touchY) { + // find the view under the pointer, accounting for GONE views + final int count = getChildCount(); + for (int childIdx = 0; childIdx < count; childIdx++) { + View slidingChild = getChildAt(childIdx); + if (slidingChild.getVisibility() == GONE) { + continue; + } + float top = slidingChild.getTranslationY(); + float bottom = top + slidingChild.getMeasuredHeight(); + int left = slidingChild.getLeft(); + int right = slidingChild.getRight(); + + if (touchY >= top && touchY <= bottom && touchX >= left && touchX <= right) { + return slidingChild; + } + } + return null; + } + + public boolean canChildBeExpanded(View v) { + return v instanceof ExpandableNotificationRow + && ((ExpandableNotificationRow) v).isExpandable(); + } + + public void setUserExpandedChild(View v, boolean userExpanded) { + if (v instanceof ExpandableNotificationRow) { + ((ExpandableNotificationRow) v).setUserExpanded(userExpanded); + } + } + + public void setUserLockedChild(View v, boolean userLocked) { + if (v instanceof ExpandableNotificationRow) { + ((ExpandableNotificationRow) v).setUserLocked(userLocked); + } + } + + public View getChildContentView(View v) { + return v; + } + + public boolean canChildBeDismissed(View v) { + final View veto = v.findViewById(R.id.veto); + return (veto != null && veto.getVisibility() != View.GONE); + } + + private void allowScrolling(boolean allow) { + mAllowScrolling = allow; + } + + @Override + protected void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + float densityScale = getResources().getDisplayMetrics().density; + mSwipeHelper.setDensityScale(densityScale); + float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop(); + mSwipeHelper.setPagingTouchSlop(pagingTouchSlop); + initView(getContext()); + } + + public void dismissRowAnimated(View child, int vel) { + mSwipeHelper.dismissChild(child, vel); + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + boolean scrollerWantsIt = false; + if (mAllowScrolling) { + scrollerWantsIt = onScrollTouch(ev); + } + boolean horizontalSwipeWantsIt = false; + if (!mIsBeingDragged) { + horizontalSwipeWantsIt = mSwipeHelper.onTouchEvent(ev); + } + return horizontalSwipeWantsIt || scrollerWantsIt || super.onTouchEvent(ev); + } + + private boolean onScrollTouch(MotionEvent ev) { + initVelocityTrackerIfNotExists(); + mVelocityTracker.addMovement(ev); + + final int action = ev.getAction(); + + switch (action & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: { + if (getChildCount() == 0) { + return false; + } + boolean isBeingDragged = !mScroller.isFinished(); + setIsBeingDragged(isBeingDragged); + if (isBeingDragged) { + final ViewParent parent = getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + } + + /* + * If being flinged and user touches, stop the fling. isFinished + * will be false if being flinged. + */ + if (!mScroller.isFinished()) { + mScroller.abortAnimation(); + } + + // Remember where the motion event started + mLastMotionY = (int) ev.getY(); + mActivePointerId = ev.getPointerId(0); + break; + } + case MotionEvent.ACTION_MOVE: + final int activePointerIndex = ev.findPointerIndex(mActivePointerId); + if (activePointerIndex == -1) { + Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent"); + break; + } + + final int y = (int) ev.getY(activePointerIndex); + int deltaY = mLastMotionY - y; + if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) { + final ViewParent parent = getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + setIsBeingDragged(true); + if (deltaY > 0) { + deltaY -= mTouchSlop; + } else { + deltaY += mTouchSlop; + } + } + if (mIsBeingDragged) { + // Scroll to follow the motion event + mLastMotionY = y; + + final int oldX = mScrollX; + final int oldY = mOwnScrollY; + final int range = getScrollRange(); + final int overscrollMode = getOverScrollMode(); + final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS || + (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0); + + // Calling overScrollBy will call onOverScrolled, which + // calls onScrollChanged if applicable. + if (overScrollBy(0, deltaY, 0, mOwnScrollY, + 0, range, 0, mOverscrollDistance, true)) { + // Break our velocity if we hit a scroll barrier. + mVelocityTracker.clear(); + } + // TODO: Overscroll +// if (canOverscroll) { +// final int pulledToY = oldY + deltaY; +// if (pulledToY < 0) { +// mEdgeGlowTop.onPull((float) deltaY / getHeight()); +// if (!mEdgeGlowBottom.isFinished()) { +// mEdgeGlowBottom.onRelease(); +// } +// } else if (pulledToY > range) { +// mEdgeGlowBottom.onPull((float) deltaY / getHeight()); +// if (!mEdgeGlowTop.isFinished()) { +// mEdgeGlowTop.onRelease(); +// } +// } +// if (mEdgeGlowTop != null +// && (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())){ +// postInvalidateOnAnimation(); +// } +// } + } + break; + case MotionEvent.ACTION_UP: + if (mIsBeingDragged) { + final VelocityTracker velocityTracker = mVelocityTracker; + velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); + int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId); + + if (getChildCount() > 0) { + if ((Math.abs(initialVelocity) > mMinimumVelocity)) { + fling(-initialVelocity); + } else { + if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, + getScrollRange())) { + postInvalidateOnAnimation(); + } + } + } + + mActivePointerId = INVALID_POINTER; + endDrag(); + } + break; + case MotionEvent.ACTION_CANCEL: + if (mIsBeingDragged && getChildCount() > 0) { + if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange())) { + postInvalidateOnAnimation(); + } + mActivePointerId = INVALID_POINTER; + endDrag(); + } + break; + case MotionEvent.ACTION_POINTER_DOWN: { + final int index = ev.getActionIndex(); + mLastMotionY = (int) ev.getY(index); + mActivePointerId = ev.getPointerId(index); + break; + } + case MotionEvent.ACTION_POINTER_UP: + onSecondaryPointerUp(ev); + mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId)); + break; + } + return true; + } + + private void onSecondaryPointerUp(MotionEvent ev) { + final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> + MotionEvent.ACTION_POINTER_INDEX_SHIFT; + final int pointerId = ev.getPointerId(pointerIndex); + if (pointerId == mActivePointerId) { + // This was our active pointer going up. Choose a new + // active pointer and adjust accordingly. + // TODO: Make this decision more intelligent. + final int newPointerIndex = pointerIndex == 0 ? 1 : 0; + mLastMotionY = (int) ev.getY(newPointerIndex); + mActivePointerId = ev.getPointerId(newPointerIndex); + if (mVelocityTracker != null) { + mVelocityTracker.clear(); + } + } + } + + private void initVelocityTrackerIfNotExists() { + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + } + + private void recycleVelocityTracker() { + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + } + + private void initOrResetVelocityTracker() { + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } else { + mVelocityTracker.clear(); + } + } + + @Override + public void computeScroll() { + if (mScroller.computeScrollOffset()) { + // This is called at drawing time by ViewGroup. + int oldX = mScrollX; + int oldY = mOwnScrollY; + int x = mScroller.getCurrX(); + int y = mScroller.getCurrY(); + + if (oldX != x || oldY != y) { + final int range = getScrollRange(); + final int overscrollMode = getOverScrollMode(); + final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS || + (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0); + + overScrollBy(x - oldX, y - oldY, oldX, oldY, 0, range, + 0, mOverflingDistance, false); + onScrollChanged(mScrollX, mOwnScrollY, oldX, oldY); + + if (canOverscroll) { + // TODO: Overscroll +// if (y < 0 && oldY >= 0) { +// mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity()); +// } else if (y > range && oldY <= range) { +// mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity()); +// } + } + updateChildren(); + } + + // Keep on drawing until the animation has finished. + postInvalidateOnAnimation(); + } + } + + public void customScrollBy(int y) { + mOwnScrollY += y; + updateChildren(); + } + + public void customScrollTo(int y) { + mOwnScrollY = y; + updateChildren(); + } + + @Override + protected void onOverScrolled(int scrollX, int scrollY, + boolean clampedX, boolean clampedY) { + // Treat animating scrolls differently; see #computeScroll() for why. + if (!mScroller.isFinished()) { + final int oldX = mScrollX; + final int oldY = mOwnScrollY; + mScrollX = scrollX; + mOwnScrollY = scrollY; + invalidateParentIfNeeded(); + onScrollChanged(mScrollX, mOwnScrollY, oldX, oldY); + if (clampedY) { + mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange()); + } + updateChildren(); + } else { + customScrollTo(scrollY); + scrollTo(scrollX, mScrollY); + } + } + + private int getScrollRange() { + int scrollRange = 0; + if (getChildCount() > 0) { + int contentHeight = getContentHeight(); + scrollRange = Math.max(0, + contentHeight - mMaxLayoutHeight + mCollapsedSize); + } + return scrollRange; + } + + private int getContentHeight() { + return mContentHeight; + } + + private void updateContentHeight() { + int height = 0; + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + height += child.getHeight(); + if (i < getChildCount()-1) { + height += mPaddingBetweenElements; + } + } + mContentHeight = height; + } + + /** + * Fling the scroll view + * + * @param velocityY The initial velocity in the Y direction. Positive + * numbers mean that the finger/cursor is moving down the screen, + * which means we want to scroll towards the top. + */ + private void fling(int velocityY) { + if (getChildCount() > 0) { + int height = (int) getLayoutHeight(); + int bottom = getContentHeight(); + + mScroller.fling(mScrollX, mOwnScrollY, 0, velocityY, 0, 0, 0, + Math.max(0, bottom - height), 0, height/2); + + postInvalidateOnAnimation(); + } + } + + private void endDrag() { + setIsBeingDragged(false); + + recycleVelocityTracker(); + + // TODO: Overscroll +// if (mEdgeGlowTop != null) { +// mEdgeGlowTop.onRelease(); +// mEdgeGlowBottom.onRelease(); +// } + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + boolean scrollWantsIt = false; + if (mAllowScrolling) { + scrollWantsIt = onInterceptTouchEventScroll(ev); + } + boolean swipeWantsIt = false; + if (!mIsBeingDragged) { + swipeWantsIt = mSwipeHelper.onInterceptTouchEvent(ev); + } + return swipeWantsIt || scrollWantsIt || + super.onInterceptTouchEvent(ev); + } + + private boolean onInterceptTouchEventScroll(MotionEvent ev) { + /* + * This method JUST determines whether we want to intercept the motion. + * If we return true, onMotionEvent will be called and we do the actual + * scrolling there. + */ + + /* + * Shortcut the most recurring case: the user is in the dragging + * state and he is moving his finger. We want to intercept this + * motion. + */ + final int action = ev.getAction(); + if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) { + return true; + } + + /* + * Don't try to intercept touch if we can't scroll anyway. + */ + if (mOwnScrollY == 0 && getScrollRange() == 0) { + return false; + } + + switch (action & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_MOVE: { + /* + * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check + * whether the user has moved far enough from his original down touch. + */ + + /* + * Locally do absolute value. mLastMotionY is set to the y value + * of the down event. + */ + final int activePointerId = mActivePointerId; + if (activePointerId == INVALID_POINTER) { + // If we don't have a valid id, the touch down wasn't on content. + break; + } + + final int pointerIndex = ev.findPointerIndex(activePointerId); + if (pointerIndex == -1) { + Log.e(TAG, "Invalid pointerId=" + activePointerId + + " in onInterceptTouchEvent"); + break; + } + + final int y = (int) ev.getY(pointerIndex); + final int yDiff = Math.abs(y - mLastMotionY); + if (yDiff > mTouchSlop) { + setIsBeingDragged(true); + mLastMotionY = y; + initVelocityTrackerIfNotExists(); + mVelocityTracker.addMovement(ev); + final ViewParent parent = getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + } + break; + } + + case MotionEvent.ACTION_DOWN: { + final int y = (int) ev.getY(); + if (getChildAtPosition(ev.getX(), y) == null) { + setIsBeingDragged(false); + recycleVelocityTracker(); + break; + } + + /* + * Remember location of down touch. + * ACTION_DOWN always refers to pointer index 0. + */ + mLastMotionY = y; + mActivePointerId = ev.getPointerId(0); + + initOrResetVelocityTracker(); + mVelocityTracker.addMovement(ev); + /* + * If being flinged and user touches the screen, initiate drag; + * otherwise don't. mScroller.isFinished should be false when + * being flinged. + */ + boolean isBeingDragged = !mScroller.isFinished(); + setIsBeingDragged(isBeingDragged); + break; + } + + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + /* Release the drag */ + setIsBeingDragged(false); + mActivePointerId = INVALID_POINTER; + recycleVelocityTracker(); + if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange())) { + postInvalidateOnAnimation(); + } + break; + case MotionEvent.ACTION_POINTER_UP: + onSecondaryPointerUp(ev); + break; + } + + /* + * The only time we want to intercept motion events is if we are in the + * drag mode. + */ + return mIsBeingDragged; + } + + private void setIsBeingDragged(boolean isDragged) { + mIsBeingDragged = isDragged; + if (isDragged) { + mSwipeHelper.removeLongPressCallback(); + } + } + + @Override + public void onWindowFocusChanged(boolean hasWindowFocus) { + super.onWindowFocusChanged(hasWindowFocus); + if (!hasWindowFocus) { + mSwipeHelper.removeLongPressCallback(); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/PiecewiseLinearIndentationFunctor.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/PiecewiseLinearIndentationFunctor.java new file mode 100644 index 0000000..38b544f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/PiecewiseLinearIndentationFunctor.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2014 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.statusbar.stack; + +import java.util.ArrayList; + +/** + * A Functor which interpolates the stack distance linearly based on base values. + * The base values are based on an interpolation between a linear function and a + * quadratic function + */ +public class PiecewiseLinearIndentationFunctor extends StackIndentationFunctor { + + private final ArrayList<Float> mBaseValues; + private final float mLinearPart; + + /** + * @param maxItemsInStack The maximum number of items which should be visible at the same time, + * i.e the function returns totalTransitionDistance for the element with + * index maxItemsInStack + * @param peekSize The visual appearance of this is how far the cards in the stack peek + * out below the top card and it is measured in real pixels. + * Note that the visual appearance does not necessarily always correspond to + * the actual visual distance below the top card but is a maximum, + * achieved when the next card just starts transitioning into the stack and + * the stack is full. + * If totalTransitionDistance is equal to this, we directly start at the peek, + * otherwise the first element transitions between 0 and + * totalTransitionDistance - peekSize. + * Visualization: + * --------------------------------------------------- --- + * | | | + * | FIRST ITEM | | <- totalTransitionDistance + * | | | + * |---------------------------------------------------| | --- + * |__________________SECOND ITEM______________________| | | <- peekSize + * |===================================================| _|_ _|_ + * + * @param totalTransitionDistance The total transition distance an element has to go through + * @param linearPart The interpolation factor between the linear and the quadratic amount taken. + * This factor must be somewhere in [0 , 1] + */ + PiecewiseLinearIndentationFunctor(int maxItemsInStack, + int peekSize, + int totalTransitionDistance, + float linearPart) { + super(maxItemsInStack, peekSize, totalTransitionDistance); + mBaseValues = new ArrayList<Float>(maxItemsInStack+1); + initBaseValues(); + mLinearPart = linearPart; + } + + private void initBaseValues() { + int sumOfSquares = getSumOfSquares(mMaxItemsInStack-1); + int totalWeight = 0; + mBaseValues.add(0.0f); + for (int i = 0; i < mMaxItemsInStack - 1; i++) { + totalWeight += (mMaxItemsInStack - i - 1) * (mMaxItemsInStack - i - 1); + mBaseValues.add((float) totalWeight / sumOfSquares); + } + } + + /** + * Get the sum of squares up to and including n, i.e sum(i * i, 1, n) + * + * @param n the maximum square to include + * @return + */ + private int getSumOfSquares(int n) { + return n * (n + 1) * (2 * n + 1) / 6; + } + + @Override + public float getValue(float itemsBefore) { + if (mStackStartsAtPeek) { + // We directly start at the stack, so no initial interpolation. + itemsBefore++; + } + if (itemsBefore < 0) { + return 0; + } else if (itemsBefore >= mMaxItemsInStack) { + return mTotalTransitionDistance; + } + int below = (int) itemsBefore; + float partialIn = itemsBefore - below; + + if (below == 0) { + return mDistanceToPeekStart * partialIn; + } else { + float result = mDistanceToPeekStart; + float progress = mBaseValues.get(below - 1) * (1 - partialIn) + + mBaseValues.get(below) * partialIn; + result += (progress * (1 - mLinearPart) + + (itemsBefore - 1) / (mMaxItemsInStack - 1) * mLinearPart) * mPeekSize; + return result; + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackIndentationFunctor.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackIndentationFunctor.java new file mode 100644 index 0000000..f72947a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackIndentationFunctor.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2014 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.statusbar.stack; + +/** + * A functor which can be queried for offset given the number of items before it. + */ +public abstract class StackIndentationFunctor { + + protected final int mTotalTransitionDistance; + protected final int mDistanceToPeekStart; + protected int mMaxItemsInStack; + protected int mPeekSize; + protected boolean mStackStartsAtPeek; + + /** + * @param maxItemsInStack The maximum number of items which should be visible at the same time, + * i.e the function returns totalTransitionDistance for the element with + * index maxItemsInStack + * @param peekSize The visual appearance of this is how far the cards in the stack peek + * out below the top card and it is measured in real pixels. + * Note that the visual appearance does not necessarily always correspond to + * the actual visual distance below the top card but is a maximum, + * achieved when the next card just starts transitioning into the stack and + * the stack is full. + * If totalTransitionDistance is equal to this, we directly start at the peek, + * otherwise the first element transitions between 0 and + * totalTransitionDistance - peekSize. + * Visualization: + * --------------------------------------------------- --- + * | | | + * | FIRST ITEM | | <- totalTransitionDistance + * | | | + * |---------------------------------------------------| | --- + * |__________________SECOND ITEM______________________| | | <- peekSize + * |===================================================| _|_ _|_ + * + * @param totalTransitionDistance The total transition distance an element has to go through + */ + StackIndentationFunctor(int maxItemsInStack, int peekSize, int totalTransitionDistance) { + mTotalTransitionDistance = totalTransitionDistance; + mDistanceToPeekStart = mTotalTransitionDistance - peekSize; + mStackStartsAtPeek = mDistanceToPeekStart == 0; + mMaxItemsInStack = maxItemsInStack; + mPeekSize = peekSize; + + } + + public void setPeekSize(int mPeekSize) { + this.mPeekSize = mPeekSize; + } + + /** + * Gets the offset of this Functor given a the quantity of items before it + * + * @param itemsBefore how many items are already in the stack before this element + * @return the offset + */ + public abstract float getValue(float itemsBefore); +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java new file mode 100644 index 0000000..9db4e77 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java @@ -0,0 +1,399 @@ +/* + * Copyright (C) 2014 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.statusbar.stack; + +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; +import com.android.systemui.R; + +/** + * The Algorithm of the {@link com.android.systemui.statusbar.stack + * .NotificationStackScrollLayout} which can be queried for {@link com.android.systemui.statusbar + * .stack.StackScrollState} + */ +public class StackScrollAlgorithm { + + private static final int MAX_ITEMS_IN_BOTTOM_STACK = 3; + private static final int MAX_ITEMS_IN_TOP_STACK = 3; + + private int mPaddingBetweenElements; + private int mCollapsedSize; + private int mTopStackPeekSize; + private int mBottomStackPeekSize; + private int mZDistanceBetweenElements; + private int mZBasicHeight; + + private StackIndentationFunctor mTopStackIndentationFunctor; + private StackIndentationFunctor mBottomStackIndentationFunctor; + + private float mLayoutHeight; + private StackScrollAlgorithmState mTempAlgorithmState = new StackScrollAlgorithmState(); + + public StackScrollAlgorithm(Context context) { + initConstants(context); + } + + private void initConstants(Context context) { + + // currently the padding is in the elements themself + mPaddingBetweenElements = 0; + mCollapsedSize = context.getResources() + .getDimensionPixelSize(R.dimen.notification_row_min_height); + mTopStackPeekSize = context.getResources() + .getDimensionPixelSize(R.dimen.top_stack_peek_amount); + mBottomStackPeekSize = context.getResources() + .getDimensionPixelSize(R.dimen.bottom_stack_peek_amount); + mZDistanceBetweenElements = context.getResources() + .getDimensionPixelSize(R.dimen.z_distance_between_notifications); + mZBasicHeight = (MAX_ITEMS_IN_BOTTOM_STACK + 1) * mZDistanceBetweenElements; + + mTopStackIndentationFunctor = new PiecewiseLinearIndentationFunctor( + MAX_ITEMS_IN_TOP_STACK, + mTopStackPeekSize, + mCollapsedSize + mPaddingBetweenElements, + 0.5f); + mBottomStackIndentationFunctor = new PiecewiseLinearIndentationFunctor( + MAX_ITEMS_IN_BOTTOM_STACK, + mBottomStackPeekSize, + mBottomStackPeekSize, + 0.5f); + } + + + public void getStackScrollState(StackScrollState resultState) { + // The state of the local variables are saved in an algorithmState to easily subdivide it + // into multiple phases. + StackScrollAlgorithmState algorithmState = mTempAlgorithmState; + + // First we reset the view states to their default values. + resultState.resetViewStates(); + + // The first element is always in there so it's initialized with 1.0f. + algorithmState.itemsInTopStack = 1.0f; + algorithmState.partialInTop = 0.0f; + algorithmState.lastTopStackIndex = 0; + algorithmState.scrollY = resultState.getScrollY(); + algorithmState.itemsInBottomStack = 0.0f; + + // Phase 1: + findNumberOfItemsInTopStackAndUpdateState(resultState, algorithmState); + + // Phase 2: + updatePositionsForState(resultState, algorithmState); + + // Phase 3: + updateZValuesForState(resultState, algorithmState); + + // Write the algorithm state to the result. + resultState.setScrollY(algorithmState.scrollY); + } + + /** + * Determine the positions for the views. This is the main part of the algorithm. + * + * @param resultState The result state to update if a change to the properties of a child occurs + * @param algorithmState The state in which the current pass of the algorithm is currently in + * and which will be updated + */ + private void updatePositionsForState(StackScrollState resultState, + StackScrollAlgorithmState algorithmState) { + float stackHeight = getLayoutHeight(); + + // The position where the bottom stack starts. + float transitioningPositionStart = stackHeight - mCollapsedSize - mBottomStackPeekSize; + + // The y coordinate of the current child. + float currentYPosition = 0.0f; + + // How far in is the element currently transitioning into the bottom stack. + float yPositionInScrollView = 0.0f; + + ViewGroup hostView = resultState.getHostView(); + int childCount = hostView.getChildCount(); + int numberOfElementsCompletelyIn = (int) algorithmState.itemsInTopStack; + for (int i = 0; i < childCount; i++) { + View child = hostView.getChildAt(i); + StackScrollState.ViewState childViewState = resultState.getViewStateForView(child); + childViewState.yTranslation = currentYPosition; + int childHeight = child.getHeight(); + // The y position after this element + float nextYPosition = currentYPosition + childHeight + mPaddingBetweenElements; + float yPositionInScrollViewAfterElement = yPositionInScrollView + + childHeight + + mPaddingBetweenElements; + float scrollOffset = yPositionInScrollViewAfterElement - algorithmState.scrollY; + if (i < algorithmState.lastTopStackIndex) { + // Case 1: + // We are in the top Stack + nextYPosition = updateStateForTopStackChild(algorithmState, + numberOfElementsCompletelyIn, + i, childViewState); + + } else if (i == algorithmState.lastTopStackIndex) { + // Case 2: + // First element of regular scrollview comes next, so the position is just the + // scrolling position + nextYPosition = scrollOffset; + } else if (nextYPosition >= transitioningPositionStart) { + if (currentYPosition >= transitioningPositionStart) { + // Case 3: + // According to the regular scroll view we are fully translated out of the + // bottom of the screen so we are fully in the bottom stack + nextYPosition = updateStateForChildFullyInBottomStack(algorithmState, + transitioningPositionStart, childViewState, childHeight); + + + } else { + // Case 4: + // According to the regular scroll view we are currently translating out of / + // into the bottom of the screen + nextYPosition = updateStateForChildTransitioningInBottom( + algorithmState, stackHeight, transitioningPositionStart, + currentYPosition, childViewState, + childHeight, nextYPosition); + } + } + currentYPosition = nextYPosition; + yPositionInScrollView = yPositionInScrollViewAfterElement; + } + } + + private float updateStateForChildTransitioningInBottom(StackScrollAlgorithmState algorithmState, + float stackHeight, float transitioningPositionStart, float currentYPosition, + StackScrollState.ViewState childViewState, int childHeight, float nextYPosition) { + float newSize = transitioningPositionStart + mCollapsedSize - currentYPosition; + newSize = Math.min(childHeight, newSize); + // Transitioning element on top of bottom stack: + algorithmState.partialInBottom = 1.0f - ( + (stackHeight - mBottomStackPeekSize - nextYPosition) / mCollapsedSize); + // Our element can be expanded, so we might even have to scroll further than + // mCollapsedSize + algorithmState.partialInBottom = Math.min(1.0f, algorithmState.partialInBottom); + float offset = mBottomStackIndentationFunctor.getValue( + algorithmState.partialInBottom); + nextYPosition = transitioningPositionStart + offset; + algorithmState.itemsInBottomStack += algorithmState.partialInBottom; + // TODO: only temporarily collapse + if (childHeight != (int) newSize) { + childViewState.height = (int) newSize; + } + return nextYPosition; + } + + private float updateStateForChildFullyInBottomStack(StackScrollAlgorithmState algorithmState, + float transitioningPositionStart, StackScrollState.ViewState childViewState, + int childHeight) { + + float nextYPosition; + algorithmState.itemsInBottomStack += 1.0f; + if (algorithmState.itemsInBottomStack < MAX_ITEMS_IN_BOTTOM_STACK) { + // We are visually entering the bottom stack + nextYPosition = transitioningPositionStart + + mBottomStackIndentationFunctor.getValue( + algorithmState.itemsInBottomStack); + } else { + // we are fully inside the stack + if (algorithmState.itemsInBottomStack > MAX_ITEMS_IN_BOTTOM_STACK + 2) { + childViewState.alpha = 0.0f; + } else if (algorithmState.itemsInBottomStack + > MAX_ITEMS_IN_BOTTOM_STACK + 1) { + childViewState.alpha = 1.0f - algorithmState.partialInBottom; + } + nextYPosition = transitioningPositionStart + mBottomStackPeekSize; + } + // TODO: only temporarily collapse + if (childHeight != mCollapsedSize) { + childViewState.height = mCollapsedSize; + } + return nextYPosition; + } + + private float updateStateForTopStackChild(StackScrollAlgorithmState algorithmState, + int numberOfElementsCompletelyIn, int i, StackScrollState.ViewState childViewState) { + + float nextYPosition = 0; + + // First we calculate the index relative to the current stack window of size at most + // {@link #MAX_ITEMS_IN_TOP_STACK} + int paddedIndex = i + - Math.max(numberOfElementsCompletelyIn - MAX_ITEMS_IN_TOP_STACK, 0); + if (paddedIndex >= 0) { + // We are currently visually entering the top stack + nextYPosition = mCollapsedSize + mPaddingBetweenElements - + mTopStackIndentationFunctor.getValue( + algorithmState.itemsInTopStack - i - 1); + if (paddedIndex == 0 && i != 0) { + childViewState.alpha = 1.0f - algorithmState.partialInTop; + } + } else { + // We are hidden behind the top card and faded out, so we can hide ourselfs + if (i != 0) { + childViewState.alpha = 0.0f; + } + } + return nextYPosition; + } + + /** + * Find the number of items in the top stack and update the result state if needed. + * + * @param resultState The result state to update if a height change of an child occurs + * @param algorithmState The state in which the current pass of the algorithm is currently in + * and which will be updated + */ + private void findNumberOfItemsInTopStackAndUpdateState(StackScrollState resultState, + StackScrollAlgorithmState algorithmState) { + + // The y Position if the element would be in a regular scrollView + float yPositionInScrollView = 0.0f; + ViewGroup hostView = resultState.getHostView(); + int childCount = hostView.getChildCount(); + + // find the number of elements in the top stack. + for (int i = 0; i < childCount; i++) { + View child = hostView.getChildAt(i); + StackScrollState.ViewState childViewState = resultState.getViewStateForView(child); + int childHeight = child.getHeight(); + float yPositionInScrollViewAfterElement = yPositionInScrollView + + childHeight + + mPaddingBetweenElements; + if (yPositionInScrollView < algorithmState.scrollY) { + if (yPositionInScrollViewAfterElement <= algorithmState.scrollY) { + // According to the regular scroll view we are fully off screen + algorithmState.itemsInTopStack += 1.0f; + if (childHeight != mCollapsedSize) { + childViewState.height = mCollapsedSize; + } + } else { + // According to the regular scroll view we are partially off screen + // If it is expanded we have to collapse it to a new size + float newSize = yPositionInScrollViewAfterElement + - mPaddingBetweenElements + - algorithmState.scrollY; + + // How much did we scroll into this child + algorithmState.partialInTop = (mCollapsedSize - newSize) / (mCollapsedSize + + mPaddingBetweenElements); + + // Our element can be expanded, so this can get negative + algorithmState.partialInTop = Math.max(0.0f, algorithmState.partialInTop); + algorithmState.itemsInTopStack += algorithmState.partialInTop; + // TODO: handle overlapping sizes with end stack + newSize = Math.max(mCollapsedSize, newSize); + // TODO: only temporarily collapse + if (newSize != childHeight) { + childViewState.height = (int) newSize; + + // We decrease scrollY by the same amount we made this child smaller. + // The new scroll position is therefore the start of the element + algorithmState.scrollY = (int) yPositionInScrollView; + resultState.setScrollY(algorithmState.scrollY); + } + if (childHeight > mCollapsedSize) { + // If we are just resizing this child, this element is not treated to be + // transitioning into the stack and therefore it is the last element in + // the stack. + algorithmState.lastTopStackIndex = i; + break; + } + } + } else { + algorithmState.lastTopStackIndex = i; + + // We are already past the stack so we can end the loop + break; + } + yPositionInScrollView = yPositionInScrollViewAfterElement; + } + } + + /** + * Calculate the Z positions for all children based on the number of items in both stacks and + * save it in the resultState + * + * @param resultState The result state to update the zTranslation values + * @param algorithmState The state in which the current pass of the algorithm is currently in + */ + private void updateZValuesForState(StackScrollState resultState, + StackScrollAlgorithmState algorithmState) { + ViewGroup hostView = resultState.getHostView(); + int childCount = hostView.getChildCount(); + for (int i = 0; i < childCount; i++) { + View child = hostView.getChildAt(i); + StackScrollState.ViewState childViewState = resultState.getViewStateForView(child); + if (i < algorithmState.itemsInTopStack) { + float stackIndex = algorithmState.itemsInTopStack - i; + stackIndex = Math.min(stackIndex, MAX_ITEMS_IN_TOP_STACK + 2); + childViewState.zTranslation = mZBasicHeight + + stackIndex * mZDistanceBetweenElements; + } else if (i > (childCount - 1 - algorithmState.itemsInBottomStack)) { + float numItemsAbove = i - (childCount - 1 - algorithmState.itemsInBottomStack); + float translationZ = mZBasicHeight + - numItemsAbove * mZDistanceBetweenElements; + childViewState.zTranslation = translationZ; + } else { + childViewState.zTranslation = mZBasicHeight; + } + } + } + + public float getLayoutHeight() { + return mLayoutHeight; + } + + public void setLayoutHeight(float layoutHeight) { + this.mLayoutHeight = layoutHeight; + } + + class StackScrollAlgorithmState { + + /** + * The scroll position of the algorithm + */ + public int scrollY; + + /** + * The quantity of items which are in the top stack. + */ + public float itemsInTopStack; + + /** + * how far in is the element currently transitioning into the top stack + */ + public float partialInTop; + + /** + * The last item index which is in the top stack. + * NOTE: In the top stack the item after the transitioning element is also in the stack! + * This is needed to ensure a smooth transition between the y position in the regular + * scrollview and the one in the stack. + */ + public int lastTopStackIndex; + + /** + * The quantity of items which are in the bottom stack. + */ + public float itemsInBottomStack; + + /** + * how far in is the element currently transitioning into the bottom stack + */ + public float partialInBottom; + } + +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollState.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollState.java new file mode 100644 index 0000000..f72a52f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollState.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2014 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.statusbar.stack; + +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; + +import java.util.HashMap; +import java.util.Map; + +/** + * A state of a {@link com.android.systemui.statusbar.stack.NotificationStackScrollLayout} which + * can be applied to a viewGroup. + */ +public class StackScrollState { + + private static final String CHILD_NOT_FOUND_TAG = "StackScrollStateNoSuchChild"; + + private final ViewGroup mHostView; + private Map<View, ViewState> mStateMap; + private int mScrollY; + + public int getScrollY() { + return mScrollY; + } + + public void setScrollY(int scrollY) { + this.mScrollY = scrollY; + } + + public StackScrollState(ViewGroup hostView) { + mHostView = hostView; + mStateMap = new HashMap<View, ViewState>(mHostView.getChildCount()); + } + + public ViewGroup getHostView() { + return mHostView; + } + + public void resetViewStates() { + int numChildren = mHostView.getChildCount(); + for (int i = 0; i < numChildren; i++) { + View child = mHostView.getChildAt(i); + ViewState viewState = mStateMap.get(child); + if (viewState == null) { + viewState = new ViewState(); + mStateMap.put(child, viewState); + } + // initialize with the default values of the view + viewState.height = child.getHeight(); + viewState.alpha = 1.0f; + } + } + + + public ViewState getViewStateForView(View requestedView) { + return mStateMap.get(requestedView); + } + + /** + * Apply the properties saved in {@link #mStateMap} to the children of the {@link #mHostView}. + * The properties are only applied if they effectively changed. + */ + public void apply() { + int numChildren = mHostView.getChildCount(); + for (int i = 0; i < numChildren; i++) { + View child = mHostView.getChildAt(i); + ViewState state = mStateMap.get(child); + if (state != null) { + float alpha = child.getAlpha(); + float yTranslation = child.getTranslationY(); + float zTranslation = child.getTranslationZ(); + int height = child.getHeight(); + float newAlpha = state.alpha; + float newYTranslation = state.yTranslation; + float newZTranslation = state.zTranslation; + int newHeight = state.height; + boolean becomesInvisible = newAlpha == 0.0f; + if (alpha != newAlpha) { + // apply layer type + boolean becomesFullyVisible = newAlpha == 1.0f; + boolean newLayerTypeIsHardware = !becomesInvisible && !becomesFullyVisible; + int layerType = child.getLayerType(); + int newLayerType = newLayerTypeIsHardware + ? View.LAYER_TYPE_HARDWARE + : View.LAYER_TYPE_NONE; + if (layerType != newLayerType) { + child.setLayerType(newLayerType, null); + } + + // apply alpha + if (!becomesInvisible) { + child.setAlpha(newAlpha); + } + } + + // apply visibility + int oldVisibility = child.getVisibility(); + int newVisibility = becomesInvisible ? View.INVISIBLE : View.VISIBLE; + if (newVisibility != oldVisibility) { + child.setVisibility(newVisibility); + } + + // apply yTranslation + if (yTranslation != newYTranslation) { + child.setTranslationY(newYTranslation); + } + + // apply zTranslation + if (zTranslation != newZTranslation) { + child.setTranslationZ(newZTranslation); + } + + // apply height + if (height != newHeight) { + applyNewHeight(child, newHeight); + } + } else { + Log.wtf(CHILD_NOT_FOUND_TAG, "No child state was found when applying this state " + + "to the hostView"); + } + } + } + + private void applyNewHeight(View child, int newHeight) { + ViewGroup.LayoutParams lp = child.getLayoutParams(); + lp.height = newHeight; + child.setLayoutParams(lp); + } + + + public class ViewState { + float alpha; + float yTranslation; + float zTranslation; + int height; + } +} |