/* * 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.annotation.Nullable; import android.content.Context; import android.content.res.Configuration; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.PointF; import android.util.AttributeSet; import android.util.Log; import android.util.Pair; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.animation.AnimationUtils; import android.widget.OverScroller; import com.android.systemui.ExpandHelper; import com.android.systemui.R; import com.android.systemui.SwipeHelper; import com.android.systemui.statusbar.ActivatableNotificationView; import com.android.systemui.statusbar.DismissView; import com.android.systemui.statusbar.EmptyShadeView; import com.android.systemui.statusbar.ExpandableNotificationRow; import com.android.systemui.statusbar.ExpandableView; import com.android.systemui.statusbar.NotificationData; import com.android.systemui.statusbar.NotificationOverflowContainer; import com.android.systemui.statusbar.SpeedBumpView; import com.android.systemui.statusbar.StackScrollerDecorView; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.phone.NotificationGroupManager; import com.android.systemui.statusbar.phone.PhoneStatusBar; import com.android.systemui.statusbar.phone.ScrimController; import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.statusbar.policy.ScrollAdapter; import java.util.ArrayList; import java.util.HashSet; /** * 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, ScrollAdapter, ExpandableView.OnHeightChangedListener, NotificationGroupManager.OnGroupChangeListener { private static final String TAG = "NotificationStackScrollLayout"; private static final boolean DEBUG = false; private static final float RUBBER_BAND_FACTOR_NORMAL = 0.35f; private static final float RUBBER_BAND_FACTOR_AFTER_EXPAND = 0.15f; private static final float RUBBER_BAND_FACTOR_ON_PANEL_EXPAND = 0.21f; /** * Sentinel value for no current active pointer. Used by {@link #mActivePointerId}. */ private static final int INVALID_POINTER = -1; private ExpandHelper mExpandHelper; private SwipeHelper mSwipeHelper; private boolean mSwipingInProgress; private int mCurrentStackHeight = Integer.MAX_VALUE; /** * mCurrentStackHeight is the actual stack height, mLastSetStackHeight is the stack height set * externally from {@link #setStackHeight} */ private float mLastSetStackHeight; private int mOwnScrollY; private int mMaxLayoutHeight; private VelocityTracker mVelocityTracker; private OverScroller mScroller; private int mTouchSlop; private int mMinimumVelocity; private int mMaximumVelocity; private int mOverflingDistance; private float mMaxOverScroll; private boolean mIsBeingDragged; private int mLastMotionY; private int mDownX; private int mActivePointerId; private boolean mTouchIsClick; private float mInitialTouchX; private float mInitialTouchY; private int mSidePaddings; private Paint mDebugPaint; private int mContentHeight; private int mCollapsedSize; private int mBottomStackSlowDownHeight; private int mBottomStackPeekSize; private int mPaddingBetweenElements; private int mPaddingBetweenElementsDimmed; private int mPaddingBetweenElementsNormal; private int mTopPadding; private int mCollapseSecondCardPadding; /** * The algorithm which calculates the properties for our children */ private StackScrollAlgorithm mStackScrollAlgorithm; /** * The current State this Layout is in */ private StackScrollState mCurrentStackScrollState = new StackScrollState(this); private AmbientState mAmbientState = new AmbientState(); private NotificationGroupManager mGroupManager; private ArrayList<View> mChildrenToAddAnimated = new ArrayList<>(); private ArrayList<View> mAddedHeadsUpChildren = new ArrayList<>(); private ArrayList<View> mChildrenToRemoveAnimated = new ArrayList<>(); private ArrayList<View> mSnappedBackChildren = new ArrayList<>(); private ArrayList<View> mDragAnimPendingChildren = new ArrayList<>(); private ArrayList<View> mChildrenChangingPositions = new ArrayList<>(); private HashSet<View> mFromMoreCardAdditions = new HashSet<>(); private ArrayList<AnimationEvent> mAnimationEvents = new ArrayList<>(); private ArrayList<View> mSwipedOutViews = new ArrayList<>(); private final StackStateAnimator mStateAnimator = new StackStateAnimator(this); private boolean mAnimationsEnabled; private boolean mChangePositionInProgress; /** * The raw amount of the overScroll on the top, which is not rubber-banded. */ private float mOverScrolledTopPixels; /** * The raw amount of the overScroll on the bottom, which is not rubber-banded. */ private float mOverScrolledBottomPixels; private OnChildLocationsChangedListener mListener; private OnOverscrollTopChangedListener mOverscrollTopChangedListener; private ExpandableView.OnHeightChangedListener mOnHeightChangedListener; private OnEmptySpaceClickListener mOnEmptySpaceClickListener; private boolean mNeedsAnimation; private boolean mTopPaddingNeedsAnimation; private boolean mDimmedNeedsAnimation; private boolean mHideSensitiveNeedsAnimation; private boolean mDarkNeedsAnimation; private int mDarkAnimationOriginIndex; private boolean mActivateNeedsAnimation; private boolean mGoToFullShadeNeedsAnimation; private boolean mIsExpanded = true; private boolean mChildrenUpdateRequested; private SpeedBumpView mSpeedBumpView; private boolean mIsExpansionChanging; private boolean mPanelTracking; private boolean mExpandingNotification; private boolean mExpandedInThisMotion; private boolean mScrollingEnabled; private DismissView mDismissView; private EmptyShadeView mEmptyShadeView; private boolean mDismissAllInProgress; /** * Was the scroller scrolled to the top when the down motion was observed? */ private boolean mScrolledToTopOnFirstDown; /** * The minimal amount of over scroll which is needed in order to switch to the quick settings * when over scrolling on a expanded card. */ private float mMinTopOverScrollToEscape; private int mIntrinsicPadding; private int mNotificationTopPadding; private float mStackTranslation; private float mTopPaddingOverflow; private boolean mDontReportNextOverScroll; private boolean mRequestViewResizeAnimationOnLayout; private boolean mNeedViewResizeAnimation; private View mExpandedGroupView; private boolean mEverythingNeedsAnimation; /** * The maximum scrollPosition which we are allowed to reach when a notification was expanded. * This is needed to avoid scrolling too far after the notification was collapsed in the same * motion. */ private int mMaxScrollAfterExpand; private SwipeHelper.LongPressListener mLongPressListener; /** * Should in this touch motion only be scrolling allowed? It's true when the scroller was * animating. */ private boolean mOnlyScrollingInThisMotion; private ViewGroup mScrollView; private boolean mInterceptDelegateEnabled; private boolean mDelegateToScrollView; private boolean mDisallowScrollingInThisMotion; private long mGoToFullShadeDelay; private ViewTreeObserver.OnPreDrawListener mChildrenUpdater = new ViewTreeObserver.OnPreDrawListener() { @Override public boolean onPreDraw() { updateChildren(); mChildrenUpdateRequested = false; getViewTreeObserver().removeOnPreDrawListener(this); return true; } }; private PhoneStatusBar mPhoneStatusBar; private int[] mTempInt2 = new int[2]; private boolean mGenerateChildOrderChangedEvent; private HashSet<Runnable> mAnimationFinishedRunnables = new HashSet<>(); private HashSet<View> mClearOverlayViewsWhenFinished = new HashSet<>(); private HashSet<Pair<ExpandableNotificationRow, Boolean>> mHeadsUpChangeAnimations = new HashSet<>(); private HeadsUpManager mHeadsUpManager; private boolean mTrackingHeadsUp; private ScrimController mScrimController; private boolean mForceNoOverlappingRendering; private NotificationOverflowContainer mOverflowContainer; private final ArrayList<Pair<ExpandableNotificationRow, Boolean>> mTmpList = new ArrayList<>(); 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); int minHeight = getResources().getDimensionPixelSize(R.dimen.notification_min_height); int maxHeight = getResources().getDimensionPixelSize(R.dimen.notification_max_height); mExpandHelper = new ExpandHelper(getContext(), this, minHeight, maxHeight); mExpandHelper.setEventSource(this); mExpandHelper.setScrollAdapter(this); mSwipeHelper = new SwipeHelper(SwipeHelper.X, this, getContext()); mSwipeHelper.setLongPressListener(mLongPressListener); 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 - mBottomStackSlowDownHeight); canvas.drawLine(0, y, getWidth(), y, mDebugPaint); y = (int) (getLayoutHeight() - mBottomStackPeekSize); canvas.drawLine(0, y, getWidth(), y, mDebugPaint); y = (int) getLayoutHeight(); canvas.drawLine(0, y, getWidth(), y, mDebugPaint); y = getHeight() - getEmptyBottomMargin(); canvas.drawLine(0, y, getWidth(), y, mDebugPaint); } } private void initView(Context context) { mScroller = new OverScroller(getContext()); setFocusable(true); setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); setClipChildren(false); final ViewConfiguration configuration = ViewConfiguration.get(context); mTouchSlop = configuration.getScaledTouchSlop(); mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); mOverflingDistance = configuration.getScaledOverflingDistance(); mSidePaddings = context.getResources() .getDimensionPixelSize(R.dimen.notification_side_padding); mCollapsedSize = context.getResources() .getDimensionPixelSize(R.dimen.notification_min_height); mBottomStackPeekSize = context.getResources() .getDimensionPixelSize(R.dimen.bottom_stack_peek_amount); mStackScrollAlgorithm = new StackScrollAlgorithm(context); mStackScrollAlgorithm.setDimmed(mAmbientState.isDimmed()); mPaddingBetweenElementsDimmed = context.getResources() .getDimensionPixelSize(R.dimen.notification_padding_dimmed); mPaddingBetweenElementsNormal = context.getResources() .getDimensionPixelSize(R.dimen.notification_padding); updatePadding(mAmbientState.isDimmed()); mMinTopOverScrollToEscape = getResources().getDimensionPixelSize( R.dimen.min_top_overscroll_to_qs); mNotificationTopPadding = getResources().getDimensionPixelSize( R.dimen.notifications_top_padding); mCollapseSecondCardPadding = getResources().getDimensionPixelSize( R.dimen.notification_collapse_second_card_padding); } private void updatePadding(boolean dimmed) { mPaddingBetweenElements = dimmed && mStackScrollAlgorithm.shouldScaleDimmed() ? mPaddingBetweenElementsDimmed : mPaddingBetweenElementsNormal; mBottomStackSlowDownHeight = mStackScrollAlgorithm.getBottomStackSlowDownLength(); updateContentHeight(); notifyHeightChangeListener(null); } private void notifyHeightChangeListener(ExpandableView view) { if (mOnHeightChangedListener != null) { mOnHeightChangedListener.onHeightChanged(view, false /* needsAnimation */); } } @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); if (child.getVisibility() == GONE) { continue; } float width = child.getMeasuredWidth(); float height = child.getMeasuredHeight(); child.layout((int) (centerX - width / 2.0f), 0, (int) (centerX + width / 2.0f), (int) height); } setMaxLayoutHeight(getHeight()); updateContentHeight(); clampScrollPosition(); if (mRequestViewResizeAnimationOnLayout) { requestAnimationOnViewResize(); mRequestViewResizeAnimationOnLayout = false; } requestChildrenUpdate(); } private void requestAnimationOnViewResize() { if (mIsExpanded && mAnimationsEnabled) { mNeedViewResizeAnimation = true; mNeedsAnimation = true; } } public void updateSpeedBumpIndex(int newIndex) { int currentIndex = indexOfChild(mSpeedBumpView); // If we are currently layouted before the new speed bump index, we have to decrease it. boolean validIndex = newIndex > 0; if (newIndex > getChildCount() - 1) { validIndex = false; newIndex = -1; } if (validIndex && currentIndex != newIndex) { changeViewPosition(mSpeedBumpView, newIndex); } updateSpeedBump(validIndex); mAmbientState.setSpeedBumpIndex(newIndex); } public void setChildLocationsChangedListener(OnChildLocationsChangedListener listener) { mListener = listener; } /** * Returns the location the given child is currently rendered at. * * @param child the child to get the location for * @return one of {@link StackViewState}'s <code>LOCATION_*</code> constants */ public int getChildLocation(View child) { StackViewState childViewState = mCurrentStackScrollState.getViewStateForView(child); if (childViewState == null) { return StackViewState.LOCATION_UNKNOWN; } if (childViewState.gone) { return StackViewState.LOCATION_GONE; } return childViewState.location; } private void setMaxLayoutHeight(int maxLayoutHeight) { mMaxLayoutHeight = maxLayoutHeight; updateAlgorithmHeightAndPadding(); } private void updateAlgorithmHeightAndPadding() { mAmbientState.setLayoutHeight(getLayoutHeight()); mAmbientState.setTopPadding(mTopPadding); } /** * @return whether the height of the layout needs to be adapted, in order to ensure that the * last child is not in the bottom stack. */ private boolean needsHeightAdaption() { return getNotGoneChildCount() > 1; } /** * 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() { mAmbientState.setScrollY(mOwnScrollY); mStackScrollAlgorithm.getStackScrollState(mAmbientState, mCurrentStackScrollState); if (!isCurrentlyAnimating() && !mNeedsAnimation) { applyCurrentState(); } else { startAnimationToState(); } } private void requestChildrenUpdate() { if (!mChildrenUpdateRequested) { getViewTreeObserver().addOnPreDrawListener(mChildrenUpdater); mChildrenUpdateRequested = true; invalidate(); } } private boolean isCurrentlyAnimating() { return mStateAnimator.isRunning(); } private void clampScrollPosition() { int scrollRange = getScrollRange(); if (scrollRange < mOwnScrollY) { mOwnScrollY = scrollRange; } } public int getTopPadding() { return mTopPadding; } private void setTopPadding(int topPadding, boolean animate) { if (mTopPadding != topPadding) { mTopPadding = topPadding; updateAlgorithmHeightAndPadding(); updateContentHeight(); if (animate && mAnimationsEnabled && mIsExpanded) { mTopPaddingNeedsAnimation = true; mNeedsAnimation = true; } requestChildrenUpdate(); notifyHeightChangeListener(null); } } /** * Update the height of the stack to a new height. * * @param height the new height of the stack */ public void setStackHeight(float height) { mLastSetStackHeight = height; setIsExpanded(height > 0.0f); int newStackHeight = (int) height; int minStackHeight = getMinStackHeight(); int stackHeight; float paddingOffset; boolean trackingHeadsUp = mTrackingHeadsUp || mHeadsUpManager.hasPinnedHeadsUp(); int normalUnfoldPositionStart = trackingHeadsUp ? mHeadsUpManager.getTopHeadsUpHeight() : minStackHeight; if (newStackHeight - mTopPadding - mTopPaddingOverflow >= normalUnfoldPositionStart || getNotGoneChildCount() == 0) { paddingOffset = mTopPaddingOverflow; stackHeight = newStackHeight; } else { // We did not reach the position yet where we actually start growing, // so we translate the stack upwards. int translationY = (newStackHeight - minStackHeight); // A slight parallax effect is introduced in order for the stack to catch up with // the top card. float partiallyThere = (newStackHeight - mTopPadding - mTopPaddingOverflow) / minStackHeight; partiallyThere = Math.max(0, partiallyThere); if (!trackingHeadsUp) { translationY += (1 - partiallyThere) * (mBottomStackPeekSize + mCollapseSecondCardPadding); } else { translationY = (int) (height - mHeadsUpManager.getTopHeadsUpHeight()); } paddingOffset = translationY - mTopPadding; stackHeight = (int) (height - (translationY - mTopPadding)); } if (stackHeight != mCurrentStackHeight) { mCurrentStackHeight = stackHeight; updateAlgorithmHeightAndPadding(); requestChildrenUpdate(); } setStackTranslation(paddingOffset); } public float getStackTranslation() { return mStackTranslation; } private void setStackTranslation(float stackTranslation) { if (stackTranslation != mStackTranslation) { mStackTranslation = stackTranslation; mAmbientState.setStackTranslation(stackTranslation); requestChildrenUpdate(); } } /** * Get the current height of the view. This is at most the msize 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 int getLayoutHeight() { return Math.min(mMaxLayoutHeight, mCurrentStackHeight); } public int getItemHeight() { return mCollapsedSize; } public int getBottomStackPeekSize() { return mBottomStackPeekSize; } public int getCollapseSecondCardPadding() { return mCollapseSecondCardPadding; } public void setLongPressListener(SwipeHelper.LongPressListener listener) { mSwipeHelper.setLongPressListener(listener); mLongPressListener = listener; } public void setScrollView(ViewGroup scrollView) { mScrollView = scrollView; } public void setInterceptDelegateEnabled(boolean interceptDelegateEnabled) { mInterceptDelegateEnabled = interceptDelegateEnabled; } public void onChildDismissed(View v) { if (mDismissAllInProgress) { return; } setSwipingInProgress(false); if (mDragAnimPendingChildren.contains(v)) { // We start the swipe and finish it in the same frame, we don't want any animation // for the drag mDragAnimPendingChildren.remove(v); } mSwipedOutViews.add(v); mAmbientState.onDragFinished(v); if (v instanceof ExpandableNotificationRow) { ExpandableNotificationRow row = (ExpandableNotificationRow) v; if (row.isHeadsUp()) { mHeadsUpManager.addSwipedOutNotification(row.getStatusBarNotification().getKey()); } } final View veto = v.findViewById(R.id.veto); if (veto != null && veto.getVisibility() != View.GONE) { veto.performClick(); } if (DEBUG) Log.v(TAG, "onChildDismissed: " + v); } @Override public void onChildSnappedBack(View animView) { mAmbientState.onDragFinished(animView); if (!mDragAnimPendingChildren.contains(animView)) { if (mAnimationsEnabled) { mSnappedBackChildren.add(animView); mNeedsAnimation = true; } requestChildrenUpdate(); } else { // We start the swipe and snap back in the same frame, we don't want any animation mDragAnimPendingChildren.remove(animView); } } @Override public boolean updateSwipeProgress(View animView, boolean dismissable, float swipeProgress) { if (!mIsExpanded && isPinnedHeadsUp(animView) && canChildBeDismissed(animView)) { mScrimController.setTopHeadsUpDragAmount(animView, Math.min(Math.abs(swipeProgress - 1.0f), 1.0f)); } return false; } public void onBeginDrag(View v) { setSwipingInProgress(true); mAmbientState.onBeginDrag(v); if (mAnimationsEnabled && (mIsExpanded || !isPinnedHeadsUp(v))) { mDragAnimPendingChildren.add(v); mNeedsAnimation = true; } requestChildrenUpdate(); } public static boolean isPinnedHeadsUp(View v) { if (v instanceof ExpandableNotificationRow) { ExpandableNotificationRow row = (ExpandableNotificationRow) v; return row.isHeadsUp() && row.isPinned(); } return false; } private boolean isHeadsUp(View v) { if (v instanceof ExpandableNotificationRow) { ExpandableNotificationRow row = (ExpandableNotificationRow) v; return row.isHeadsUp(); } return false; } public void onDragCancelled(View v) { setSwipingInProgress(false); } @Override public float getFalsingThresholdFactor() { return mPhoneStatusBar.isScreenOnComingFromTouch() ? 1.5f : 1.0f; } public View getChildAtPosition(MotionEvent ev) { return getChildAtPosition(ev.getX(), ev.getY()); } public ExpandableView getClosestChildAtRawPosition(float touchX, float touchY) { getLocationOnScreen(mTempInt2); float localTouchY = touchY - mTempInt2[1]; ExpandableView closestChild = null; float minDist = Float.MAX_VALUE; // find the view closest to the location, accounting for GONE views final int count = getChildCount(); for (int childIdx = 0; childIdx < count; childIdx++) { ExpandableView slidingChild = (ExpandableView) getChildAt(childIdx); if (slidingChild.getVisibility() == GONE || slidingChild instanceof StackScrollerDecorView || slidingChild == mSpeedBumpView) { continue; } float childTop = slidingChild.getTranslationY(); float top = childTop + slidingChild.getClipTopAmount(); float bottom = childTop + slidingChild.getActualHeight(); float dist = Math.min(Math.abs(top - localTouchY), Math.abs(bottom - localTouchY)); if (dist < minDist) { closestChild = slidingChild; minDist = dist; } } return closestChild; } public ExpandableView getChildAtRawPosition(float touchX, float touchY) { getLocationOnScreen(mTempInt2); return getChildAtPosition(touchX - mTempInt2[0], touchY - mTempInt2[1]); } public ExpandableView 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++) { ExpandableView slidingChild = (ExpandableView) getChildAt(childIdx); if (slidingChild.getVisibility() == GONE || slidingChild instanceof StackScrollerDecorView || slidingChild == mSpeedBumpView) { continue; } float childTop = slidingChild.getTranslationY(); float top = childTop + slidingChild.getClipTopAmount(); float bottom = childTop + slidingChild.getActualHeight(); // Allow the full width of this view to prevent gesture conflict on Keyguard (phone and // camera affordance). int left = 0; int right = getWidth(); if (touchY >= top && touchY <= bottom && touchX >= left && touchX <= right) { if (slidingChild instanceof ExpandableNotificationRow) { ExpandableNotificationRow row = (ExpandableNotificationRow) slidingChild; if (!mIsExpanded && row.isHeadsUp() && row.isPinned() && mHeadsUpManager.getTopEntry().entry.row != row) { continue; } return row.getViewAtPosition(touchY - childTop); } return slidingChild; } } return null; } public boolean canChildBeExpanded(View v) { return v instanceof ExpandableNotificationRow && ((ExpandableNotificationRow) v).isExpandable() && !((ExpandableNotificationRow) v).isHeadsUp(); } 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); } removeLongPressCallback(); requestDisallowInterceptTouchEvent(true); } @Override public void expansionStateChanged(boolean isExpanding) { mExpandingNotification = isExpanding; if (!mExpandedInThisMotion) { mMaxScrollAfterExpand = mOwnScrollY; mExpandedInThisMotion = true; } } public void setScrollingEnabled(boolean enable) { mScrollingEnabled = enable; } public void setExpandingEnabled(boolean enable) { mExpandHelper.setEnabled(enable); } private boolean isScrollingEnabled() { return mScrollingEnabled; } public View getChildContentView(View v) { return v; } public boolean canChildBeDismissed(View v) { return StackScrollAlgorithm.canChildBeDismissed(v); } @Override public boolean isAntiFalsingNeeded() { return mPhoneStatusBar.getBarState() == StatusBarState.KEYGUARD; } private void setSwipingInProgress(boolean isSwiped) { mSwipingInProgress = isSwiped; if(isSwiped) { requestDisallowInterceptTouchEvent(true); } } @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 dismissViewAnimated(View child, Runnable endRunnable, int delay, long duration) { mSwipeHelper.dismissChild(child, 0, endRunnable, delay, true, duration); } @Override public boolean onTouchEvent(MotionEvent ev) { boolean isCancelOrUp = ev.getActionMasked() == MotionEvent.ACTION_CANCEL || ev.getActionMasked()== MotionEvent.ACTION_UP; if (mDelegateToScrollView) { if (isCancelOrUp) { mDelegateToScrollView = false; } transformTouchEvent(ev, this, mScrollView); return mScrollView.onTouchEvent(ev); } handleEmptySpaceClick(ev); boolean expandWantsIt = false; if (mIsExpanded && !mSwipingInProgress && !mOnlyScrollingInThisMotion) { if (isCancelOrUp) { mExpandHelper.onlyObserveMovements(false); } boolean wasExpandingBefore = mExpandingNotification; expandWantsIt = mExpandHelper.onTouchEvent(ev); if (mExpandedInThisMotion && !mExpandingNotification && wasExpandingBefore && !mDisallowScrollingInThisMotion) { dispatchDownEventToScroller(ev); } } boolean scrollerWantsIt = false; if (mIsExpanded && !mSwipingInProgress && !mExpandingNotification && !mDisallowScrollingInThisMotion) { scrollerWantsIt = onScrollTouch(ev); } boolean horizontalSwipeWantsIt = false; if (!mIsBeingDragged && !mExpandingNotification && !mExpandedInThisMotion && !mOnlyScrollingInThisMotion) { horizontalSwipeWantsIt = mSwipeHelper.onTouchEvent(ev); } return horizontalSwipeWantsIt || scrollerWantsIt || expandWantsIt || super.onTouchEvent(ev); } private void dispatchDownEventToScroller(MotionEvent ev) { MotionEvent downEvent = MotionEvent.obtain(ev); downEvent.setAction(MotionEvent.ACTION_DOWN); onScrollTouch(downEvent); downEvent.recycle(); } private boolean onScrollTouch(MotionEvent ev) { if (!isScrollingEnabled()) { return false; } initVelocityTrackerIfNotExists(); mVelocityTracker.addMovement(ev); final int action = ev.getAction(); switch (action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: { if (getChildCount() == 0 || !isInContentBounds(ev)) { return false; } boolean isBeingDragged = !mScroller.isFinished(); setIsBeingDragged(isBeingDragged); /* * If being flinged and user touches, stop the fling. isFinished * will be false if being flinged. */ if (!mScroller.isFinished()) { mScroller.forceFinished(true); } // Remember where the motion event started mLastMotionY = (int) ev.getY(); mDownX = (int) ev.getX(); 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); final int x = (int) ev.getX(activePointerIndex); int deltaY = mLastMotionY - y; final int xDiff = Math.abs(x - mDownX); final int yDiff = Math.abs(deltaY); if (!mIsBeingDragged && yDiff > mTouchSlop && yDiff > xDiff) { setIsBeingDragged(true); if (deltaY > 0) { deltaY -= mTouchSlop; } else { deltaY += mTouchSlop; } } if (mIsBeingDragged) { // Scroll to follow the motion event mLastMotionY = y; int range = getScrollRange(); if (mExpandedInThisMotion) { range = Math.min(range, mMaxScrollAfterExpand); } float scrollAmount; if (deltaY < 0) { scrollAmount = overScrollDown(deltaY); } else { scrollAmount = overScrollUp(deltaY, range); } // Calling overScrollBy will call onOverScrolled, which // calls onScrollChanged if applicable. if (scrollAmount != 0.0f) { // The scrolling motion could not be compensated with the // existing overScroll, we have to scroll the view overScrollBy(0, (int) scrollAmount, 0, mOwnScrollY, 0, range, 0, getHeight() / 2, true); } } break; case MotionEvent.ACTION_UP: if (mIsBeingDragged) { final VelocityTracker velocityTracker = mVelocityTracker; velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId); if (shouldOverScrollFling(initialVelocity)) { onOverScrollFling(true, initialVelocity); } else { if (getChildCount() > 0) { if ((Math.abs(initialVelocity) > mMinimumVelocity)) { float currentOverScrollTop = getCurrentOverScrollAmount(true); if (currentOverScrollTop == 0.0f || initialVelocity > 0) { fling(-initialVelocity); } else { onOverScrollFling(false, 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); mDownX = (int) ev.getX(index); mActivePointerId = ev.getPointerId(index); break; } case MotionEvent.ACTION_POINTER_UP: onSecondaryPointerUp(ev); mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId)); mDownX = (int) ev.getX(ev.findPointerIndex(mActivePointerId)); break; } return true; } private void onOverScrollFling(boolean open, int initialVelocity) { if (mOverscrollTopChangedListener != null) { mOverscrollTopChangedListener.flingTopOverscroll(initialVelocity, open); } mDontReportNextOverScroll = true; setOverScrollAmount(0.0f, true, false); } /** * Perform a scroll upwards and adapt the overscroll amounts accordingly * * @param deltaY The amount to scroll upwards, has to be positive. * @return The amount of scrolling to be performed by the scroller, * not handled by the overScroll amount. */ private float overScrollUp(int deltaY, int range) { deltaY = Math.max(deltaY, 0); float currentTopAmount = getCurrentOverScrollAmount(true); float newTopAmount = currentTopAmount - deltaY; if (currentTopAmount > 0) { setOverScrollAmount(newTopAmount, true /* onTop */, false /* animate */); } // Top overScroll might not grab all scrolling motion, // we have to scroll as well. float scrollAmount = newTopAmount < 0 ? -newTopAmount : 0.0f; float newScrollY = mOwnScrollY + scrollAmount; if (newScrollY > range) { if (!mExpandedInThisMotion) { float currentBottomPixels = getCurrentOverScrolledPixels(false); // We overScroll on the top setOverScrolledPixels(currentBottomPixels + newScrollY - range, false /* onTop */, false /* animate */); } mOwnScrollY = range; scrollAmount = 0.0f; } return scrollAmount; } /** * Perform a scroll downward and adapt the overscroll amounts accordingly * * @param deltaY The amount to scroll downwards, has to be negative. * @return The amount of scrolling to be performed by the scroller, * not handled by the overScroll amount. */ private float overScrollDown(int deltaY) { deltaY = Math.min(deltaY, 0); float currentBottomAmount = getCurrentOverScrollAmount(false); float newBottomAmount = currentBottomAmount + deltaY; if (currentBottomAmount > 0) { setOverScrollAmount(newBottomAmount, false /* onTop */, false /* animate */); } // Bottom overScroll might not grab all scrolling motion, // we have to scroll as well. float scrollAmount = newBottomAmount < 0 ? newBottomAmount : 0.0f; float newScrollY = mOwnScrollY + scrollAmount; if (newScrollY < 0) { float currentTopPixels = getCurrentOverScrolledPixels(true); // We overScroll on the top setOverScrolledPixels(currentTopPixels - newScrollY, true /* onTop */, false /* animate */); mOwnScrollY = 0; scrollAmount = 0.0f; } return scrollAmount; } 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(); if (y < 0 && oldY >= 0 || y > range && oldY <= range) { float currVelocity = mScroller.getCurrVelocity(); if (currVelocity >= mMinimumVelocity) { mMaxOverScroll = Math.abs(currVelocity) / 1000 * mOverflingDistance; } } overScrollBy(x - oldX, y - oldY, oldX, oldY, 0, range, 0, (int) (mMaxOverScroll), false); onScrollChanged(mScrollX, mOwnScrollY, oldX, oldY); } // Keep on drawing until the animation has finished. postInvalidateOnAnimation(); } } @Override protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) { int newScrollY = scrollY + deltaY; final int top = -maxOverScrollY; final int bottom = maxOverScrollY + scrollRangeY; boolean clampedY = false; if (newScrollY > bottom) { newScrollY = bottom; clampedY = true; } else if (newScrollY < top) { newScrollY = top; clampedY = true; } onOverScrolled(0, newScrollY, false, clampedY); return clampedY; } /** * Set the amount of overScrolled pixels which will force the view to apply a rubber-banded * overscroll effect based on numPixels. By default this will also cancel animations on the * same overScroll edge. * * @param numPixels The amount of pixels to overScroll by. These will be scaled according to * the rubber-banding logic. * @param onTop Should the effect be applied on top of the scroller. * @param animate Should an animation be performed. */ public void setOverScrolledPixels(float numPixels, boolean onTop, boolean animate) { setOverScrollAmount(numPixels * getRubberBandFactor(onTop), onTop, animate, true); } /** * Set the effective overScroll amount which will be directly reflected in the layout. * By default this will also cancel animations on the same overScroll edge. * * @param amount The amount to overScroll by. * @param onTop Should the effect be applied on top of the scroller. * @param animate Should an animation be performed. */ public void setOverScrollAmount(float amount, boolean onTop, boolean animate) { setOverScrollAmount(amount, onTop, animate, true); } /** * Set the effective overScroll amount which will be directly reflected in the layout. * * @param amount The amount to overScroll by. * @param onTop Should the effect be applied on top of the scroller. * @param animate Should an animation be performed. * @param cancelAnimators Should running animations be cancelled. */ public void setOverScrollAmount(float amount, boolean onTop, boolean animate, boolean cancelAnimators) { setOverScrollAmount(amount, onTop, animate, cancelAnimators, isRubberbanded(onTop)); } /** * Set the effective overScroll amount which will be directly reflected in the layout. * * @param amount The amount to overScroll by. * @param onTop Should the effect be applied on top of the scroller. * @param animate Should an animation be performed. * @param cancelAnimators Should running animations be cancelled. * @param isRubberbanded The value which will be passed to * {@link OnOverscrollTopChangedListener#onOverscrollTopChanged} */ public void setOverScrollAmount(float amount, boolean onTop, boolean animate, boolean cancelAnimators, boolean isRubberbanded) { if (cancelAnimators) { mStateAnimator.cancelOverScrollAnimators(onTop); } setOverScrollAmountInternal(amount, onTop, animate, isRubberbanded); } private void setOverScrollAmountInternal(float amount, boolean onTop, boolean animate, boolean isRubberbanded) { amount = Math.max(0, amount); if (animate) { mStateAnimator.animateOverScrollToAmount(amount, onTop, isRubberbanded); } else { setOverScrolledPixels(amount / getRubberBandFactor(onTop), onTop); mAmbientState.setOverScrollAmount(amount, onTop); if (onTop) { notifyOverscrollTopListener(amount, isRubberbanded); } requestChildrenUpdate(); } } private void notifyOverscrollTopListener(float amount, boolean isRubberbanded) { mExpandHelper.onlyObserveMovements(amount > 1.0f); if (mDontReportNextOverScroll) { mDontReportNextOverScroll = false; return; } if (mOverscrollTopChangedListener != null) { mOverscrollTopChangedListener.onOverscrollTopChanged(amount, isRubberbanded); } } public void setOverscrollTopChangedListener( OnOverscrollTopChangedListener overscrollTopChangedListener) { mOverscrollTopChangedListener = overscrollTopChangedListener; } public float getCurrentOverScrollAmount(boolean top) { return mAmbientState.getOverScrollAmount(top); } public float getCurrentOverScrolledPixels(boolean top) { return top? mOverScrolledTopPixels : mOverScrolledBottomPixels; } private void setOverScrolledPixels(float amount, boolean onTop) { if (onTop) { mOverScrolledTopPixels = amount; } else { mOverScrolledBottomPixels = amount; } } private 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; if (clampedY) { springBack(); } else { onScrollChanged(mScrollX, mOwnScrollY, oldX, oldY); invalidateParentIfNeeded(); updateChildren(); float overScrollTop = getCurrentOverScrollAmount(true); if (mOwnScrollY < 0) { notifyOverscrollTopListener(-mOwnScrollY, isRubberbanded(true)); } else { notifyOverscrollTopListener(overScrollTop, isRubberbanded(true)); } } } else { customScrollTo(scrollY); scrollTo(scrollX, mScrollY); } } private void springBack() { int scrollRange = getScrollRange(); boolean overScrolledTop = mOwnScrollY <= 0; boolean overScrolledBottom = mOwnScrollY >= scrollRange; if (overScrolledTop || overScrolledBottom) { boolean onTop; float newAmount; if (overScrolledTop) { onTop = true; newAmount = -mOwnScrollY; mOwnScrollY = 0; mDontReportNextOverScroll = true; } else { onTop = false; newAmount = mOwnScrollY - scrollRange; mOwnScrollY = scrollRange; } setOverScrollAmount(newAmount, onTop, false); setOverScrollAmount(0.0f, onTop, true); mScroller.forceFinished(true); } } private int getScrollRange() { int scrollRange = 0; ExpandableView firstChild = (ExpandableView) getFirstChildNotGone(); if (firstChild != null) { int contentHeight = getContentHeight(); int firstChildMaxExpandHeight = getMaxExpandHeight(firstChild); scrollRange = Math.max(0, contentHeight - mMaxLayoutHeight + mBottomStackPeekSize + mBottomStackSlowDownHeight); if (scrollRange > 0) { View lastChild = getLastChildNotGone(); // We want to at least be able collapse the first item and not ending in a weird // end state. scrollRange = Math.max(scrollRange, firstChildMaxExpandHeight - mCollapsedSize); } } return scrollRange; } /** * @return the first child which has visibility unequal to GONE */ private View getFirstChildNotGone() { int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { View child = getChildAt(i); if (child.getVisibility() != View.GONE) { return child; } } return null; } /** * @return The first child which has visibility unequal to GONE which is currently below the * given translationY or equal to it. */ private View getFirstChildBelowTranlsationY(float translationY) { int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { View child = getChildAt(i); if (child.getVisibility() != View.GONE && child.getTranslationY() >= translationY) { return child; } } return null; } /** * @return the last child which has visibility unequal to GONE */ public View getLastChildNotGone() { int childCount = getChildCount(); for (int i = childCount - 1; i >= 0; i--) { View child = getChildAt(i); if (child.getVisibility() != View.GONE) { return child; } } return null; } /** * @return the number of children which have visibility unequal to GONE */ public int getNotGoneChildCount() { int childCount = getChildCount(); int count = 0; for (int i = 0; i < childCount; i++) { ExpandableView child = (ExpandableView) getChildAt(i); if (child.getVisibility() != View.GONE && !child.willBeGone()) { count++; } } return count; } private int getMaxExpandHeight(View view) { if (view instanceof ExpandableNotificationRow) { ExpandableNotificationRow row = (ExpandableNotificationRow) view; return row.getIntrinsicHeight(); } return view.getHeight(); } public int getContentHeight() { return mContentHeight; } private void updateContentHeight() { int height = 0; for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); if (child.getVisibility() != View.GONE) { if (height != 0) { // add the padding before this element height += mPaddingBetweenElements; } if (child instanceof ExpandableView) { ExpandableView expandableView = (ExpandableView) child; height += expandableView.getIntrinsicHeight(); } } } mContentHeight = height + mTopPadding; } /** * 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 scrollRange = getScrollRange(); float topAmount = getCurrentOverScrollAmount(true); float bottomAmount = getCurrentOverScrollAmount(false); if (velocityY < 0 && topAmount > 0) { mOwnScrollY -= (int) topAmount; mDontReportNextOverScroll = true; setOverScrollAmount(0, true, false); mMaxOverScroll = Math.abs(velocityY) / 1000f * getRubberBandFactor(true /* onTop */) * mOverflingDistance + topAmount; } else if (velocityY > 0 && bottomAmount > 0) { mOwnScrollY += bottomAmount; setOverScrollAmount(0, false, false); mMaxOverScroll = Math.abs(velocityY) / 1000f * getRubberBandFactor(false /* onTop */) * mOverflingDistance + bottomAmount; } else { // it will be set once we reach the boundary mMaxOverScroll = 0.0f; } mScroller.fling(mScrollX, mOwnScrollY, 1, velocityY, 0, 0, 0, Math.max(0, scrollRange), 0, Integer.MAX_VALUE / 2); postInvalidateOnAnimation(); } } /** * @return Whether a fling performed on the top overscroll edge lead to the expanded * overScroll view (i.e QS). */ private boolean shouldOverScrollFling(int initialVelocity) { float topOverScroll = getCurrentOverScrollAmount(true); return mScrolledToTopOnFirstDown && !mExpandedInThisMotion && topOverScroll > mMinTopOverScrollToEscape && initialVelocity > 0; } /** * Updates the top padding of the notifications, taking {@link #getIntrinsicPadding()} into * account. * * @param qsHeight the top padding imposed by the quick settings panel * @param scrollY how much the notifications are scrolled inside the QS/notifications scroll * container * @param animate whether to animate the change * @param ignoreIntrinsicPadding if true, {@link #getIntrinsicPadding()} is ignored and * {@code qsHeight} is the final top padding */ public void updateTopPadding(float qsHeight, int scrollY, boolean animate, boolean ignoreIntrinsicPadding) { float start = qsHeight - scrollY + mNotificationTopPadding; float stackHeight = getHeight() - start; int minStackHeight = getMinStackHeight(); if (stackHeight <= minStackHeight) { float overflow = minStackHeight - stackHeight; stackHeight = minStackHeight; start = getHeight() - stackHeight; mTopPaddingOverflow = overflow; } else { mTopPaddingOverflow = 0; } setTopPadding(ignoreIntrinsicPadding ? (int) start : clampPadding((int) start), animate); setStackHeight(mLastSetStackHeight); } public int getNotificationTopPadding() { return mNotificationTopPadding; } public int getMinStackHeight() { return mCollapsedSize + mBottomStackPeekSize + mCollapseSecondCardPadding; } public float getTopPaddingOverflow() { return mTopPaddingOverflow; } public int getPeekHeight() { return mIntrinsicPadding + mCollapsedSize + mBottomStackPeekSize + mCollapseSecondCardPadding; } private int clampPadding(int desiredPadding) { return Math.max(desiredPadding, mIntrinsicPadding); } private float getRubberBandFactor(boolean onTop) { if (!onTop) { return RUBBER_BAND_FACTOR_NORMAL; } if (mExpandedInThisMotion) { return RUBBER_BAND_FACTOR_AFTER_EXPAND; } else if (mIsExpansionChanging || mPanelTracking) { return RUBBER_BAND_FACTOR_ON_PANEL_EXPAND; } else if (mScrolledToTopOnFirstDown) { return 1.0f; } return RUBBER_BAND_FACTOR_NORMAL; } /** * Accompanying function for {@link #getRubberBandFactor}: Returns true if the overscroll is * rubberbanded, false if it is technically an overscroll but rather a motion to expand the * overscroll view (e.g. expand QS). */ private boolean isRubberbanded(boolean onTop) { return !onTop || mExpandedInThisMotion || mIsExpansionChanging || mPanelTracking || !mScrolledToTopOnFirstDown; } private void endDrag() { setIsBeingDragged(false); recycleVelocityTracker(); if (getCurrentOverScrollAmount(true /* onTop */) > 0) { setOverScrollAmount(0, true /* onTop */, true /* animate */); } if (getCurrentOverScrollAmount(false /* onTop */) > 0) { setOverScrollAmount(0, false /* onTop */, true /* animate */); } } private void transformTouchEvent(MotionEvent ev, View sourceView, View targetView) { ev.offsetLocation(sourceView.getX(), sourceView.getY()); ev.offsetLocation(-targetView.getX(), -targetView.getY()); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { if (mInterceptDelegateEnabled) { transformTouchEvent(ev, this, mScrollView); if (mScrollView.onInterceptTouchEvent(ev)) { mDelegateToScrollView = true; removeLongPressCallback(); return true; } transformTouchEvent(ev, mScrollView, this); } initDownStates(ev); handleEmptySpaceClick(ev); boolean expandWantsIt = false; if (!mSwipingInProgress && !mOnlyScrollingInThisMotion) { expandWantsIt = mExpandHelper.onInterceptTouchEvent(ev); } boolean scrollWantsIt = false; if (!mSwipingInProgress && !mExpandingNotification) { scrollWantsIt = onInterceptTouchEventScroll(ev); } boolean swipeWantsIt = false; if (!mIsBeingDragged && !mExpandingNotification && !mExpandedInThisMotion && !mOnlyScrollingInThisMotion) { swipeWantsIt = mSwipeHelper.onInterceptTouchEvent(ev); } return swipeWantsIt || scrollWantsIt || expandWantsIt || super.onInterceptTouchEvent(ev); } private void handleEmptySpaceClick(MotionEvent ev) { switch (ev.getActionMasked()) { case MotionEvent.ACTION_MOVE: if (mTouchIsClick && (Math.abs(ev.getY() - mInitialTouchY) > mTouchSlop || Math.abs(ev.getX() - mInitialTouchX) > mTouchSlop )) { mTouchIsClick = false; } break; case MotionEvent.ACTION_UP: if (mPhoneStatusBar.getBarState() != StatusBarState.KEYGUARD && mTouchIsClick && isBelowLastNotification(mInitialTouchX, mInitialTouchY)) { mOnEmptySpaceClickListener.onEmptySpaceClicked(mInitialTouchX, mInitialTouchY); } break; } } private void initDownStates(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { mExpandedInThisMotion = false; mOnlyScrollingInThisMotion = !mScroller.isFinished(); mDisallowScrollingInThisMotion = false; mTouchIsClick = true; mInitialTouchX = ev.getX(); mInitialTouchY = ev.getY(); } } @Override public void onViewRemoved(View child) { super.onViewRemoved(child); // we only call our internal methods if this is actually a removal and not just a // notification which becomes a child notification if (!isChildInGroup(child)) { onViewRemovedInternal(child); } } private void onViewRemovedInternal(View child) { mStackScrollAlgorithm.notifyChildrenChanged(this); if (mChangePositionInProgress) { // This is only a position change, don't do anything special return; } ((ExpandableView) child).setOnHeightChangedListener(null); mCurrentStackScrollState.removeViewStateForView(child); updateScrollStateForRemovedChild(child); boolean animationGenerated = generateRemoveAnimation(child); if (animationGenerated && !mSwipedOutViews.contains(child)) { // Add this view to an overlay in order to ensure that it will still be temporary // drawn when removed getOverlay().add(child); } updateAnimationState(false, child); // Make sure the clipRect we might have set is removed ((ExpandableView) child).setClipTopOptimization(0); } private boolean isChildInGroup(View child) { return child instanceof ExpandableNotificationRow && mGroupManager.isChildInGroupWithSummary( ((ExpandableNotificationRow) child).getStatusBarNotification()); } /** * Generate a remove animation for a child view. * * @param child The view to generate the remove animation for. * @return Whether an animation was generated. */ private boolean generateRemoveAnimation(View child) { if (removeRemovedChildFromHeadsUpChangeAnimations(child)) { mAddedHeadsUpChildren.remove(child); return false; } if (isClickedHeadsUp(child)) { // An animation is already running, add it to the Overlay mClearOverlayViewsWhenFinished.add(child); return true; } if (mIsExpanded && mAnimationsEnabled && !isChildInInvisibleGroup(child)) { if (!mChildrenToAddAnimated.contains(child)) { // Generate Animations mChildrenToRemoveAnimated.add(child); mNeedsAnimation = true; return true; } else { mChildrenToAddAnimated.remove(child); mFromMoreCardAdditions.remove(child); return false; } } return false; } private boolean isClickedHeadsUp(View child) { return HeadsUpManager.isClickedHeadsUpNotification(child); } /** * Remove a removed child view from the heads up animations if it was just added there * * @return whether any child was removed from the list to animate */ private boolean removeRemovedChildFromHeadsUpChangeAnimations(View child) { boolean hasAddEvent = false; for (Pair<ExpandableNotificationRow, Boolean> eventPair : mHeadsUpChangeAnimations) { ExpandableNotificationRow row = eventPair.first; boolean isHeadsUp = eventPair.second; if (child == row) { mTmpList.add(eventPair); hasAddEvent |= isHeadsUp; } } if (hasAddEvent) { // This child was just added lets remove all events. mHeadsUpChangeAnimations.removeAll(mTmpList); } mTmpList.clear(); return hasAddEvent; } /** * @param child the child to query * @return whether a view is not a top level child but a child notification and that group is * not expanded */ private boolean isChildInInvisibleGroup(View child) { if (child instanceof ExpandableNotificationRow) { ExpandableNotificationRow row = (ExpandableNotificationRow) child; ExpandableNotificationRow groupSummary = mGroupManager.getGroupSummary(row.getStatusBarNotification()); if (groupSummary != null && groupSummary != row) { return !groupSummary.areChildrenExpanded(); } } return false; } /** * Updates the scroll position when a child was removed * * @param removedChild the removed child */ private void updateScrollStateForRemovedChild(View removedChild) { int startingPosition = getPositionInLinearLayout(removedChild); int childHeight = getIntrinsicHeight(removedChild) + mPaddingBetweenElements; int endPosition = startingPosition + childHeight; if (endPosition <= mOwnScrollY) { // This child is fully scrolled of the top, so we have to deduct its height from the // scrollPosition mOwnScrollY -= childHeight; } else if (startingPosition < mOwnScrollY) { // This child is currently being scrolled into, set the scroll position to the start of // this child mOwnScrollY = startingPosition; } } private int getIntrinsicHeight(View view) { if (view instanceof ExpandableView) { ExpandableView expandableView = (ExpandableView) view; return expandableView.getIntrinsicHeight(); } return view.getHeight(); } private int getPositionInLinearLayout(View requestedChild) { int position = 0; for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); if (child == requestedChild) { return position; } if (child.getVisibility() != View.GONE) { position += getIntrinsicHeight(child); if (i < getChildCount()-1) { position += mPaddingBetweenElements; } } } return 0; } @Override public void onViewAdded(View child) { super.onViewAdded(child); onViewAddedInternal(child); } private void onViewAddedInternal(View child) { updateHideSensitiveForChild(child); mStackScrollAlgorithm.notifyChildrenChanged(this); ((ExpandableView) child).setOnHeightChangedListener(this); generateAddAnimation(child, false /* fromMoreCard */); updateAnimationState(child); if (canChildBeDismissed(child)) { // Make sure the dismissButton is visible and not in the animated state. // We need to do this to avoid a race where a clearable notification is added after the // dismiss animation is finished mDismissView.showClearButton(); } } private void updateHideSensitiveForChild(View child) { if (mAmbientState.isHideSensitive() && child instanceof ExpandableView) { ExpandableView expandableView = (ExpandableView) child; expandableView.setHideSensitiveForIntrinsicHeight(true); } } public void notifyGroupChildRemoved(View row) { onViewRemovedInternal(row); } public void notifyGroupChildAdded(View row) { onViewAddedInternal(row); } public void setAnimationsEnabled(boolean animationsEnabled) { mAnimationsEnabled = animationsEnabled; updateNotificationAnimationStates(); } private void updateNotificationAnimationStates() { boolean running = mAnimationsEnabled; int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { View child = getChildAt(i); running &= mIsExpanded || isPinnedHeadsUp(child); updateAnimationState(running, child); } } private void updateAnimationState(View child) { updateAnimationState((mAnimationsEnabled || isPinnedHeadsUp(child)) && mIsExpanded, child); } private void updateAnimationState(boolean running, View child) { if (child instanceof ExpandableNotificationRow) { ExpandableNotificationRow row = (ExpandableNotificationRow) child; row.setIconAnimationRunning(running); } } public boolean isAddOrRemoveAnimationPending() { return mNeedsAnimation && (!mChildrenToAddAnimated.isEmpty() || !mChildrenToRemoveAnimated.isEmpty()); } /** * Generate an animation for an added child view. * * @param child The view to be added. * @param fromMoreCard Whether this add is coming from the "more" card on lockscreen. */ public void generateAddAnimation(View child, boolean fromMoreCard) { if (mIsExpanded && mAnimationsEnabled && !mChangePositionInProgress) { // Generate Animations mChildrenToAddAnimated.add(child); if (fromMoreCard) { mFromMoreCardAdditions.add(child); } mNeedsAnimation = true; } if (isHeadsUp(child)) { mAddedHeadsUpChildren.add(child); mChildrenToAddAnimated.remove(child); } } /** * Change the position of child to a new location * * @param child the view to change the position for * @param newIndex the new index */ public void changeViewPosition(View child, int newIndex) { int currentIndex = indexOfChild(child); if (child != null && child.getParent() == this && currentIndex != newIndex) { mChangePositionInProgress = true; removeView(child); addView(child, newIndex); mChangePositionInProgress = false; if (mIsExpanded && mAnimationsEnabled && child.getVisibility() != View.GONE) { mChildrenChangingPositions.add(child); mNeedsAnimation = true; } } } private void startAnimationToState() { if (mNeedsAnimation) { generateChildHierarchyEvents(); mNeedsAnimation = false; } if (!mAnimationEvents.isEmpty() || isCurrentlyAnimating()) { mStateAnimator.startAnimationForEvents(mAnimationEvents, mCurrentStackScrollState, mGoToFullShadeDelay); mAnimationEvents.clear(); } else { applyCurrentState(); } mGoToFullShadeDelay = 0; } private void generateChildHierarchyEvents() { generateHeadsUpAnimationEvents(); generateChildRemovalEvents(); generateChildAdditionEvents(); generatePositionChangeEvents(); generateSnapBackEvents(); generateDragEvents(); generateTopPaddingEvent(); generateActivateEvent(); generateDimmedEvent(); generateHideSensitiveEvent(); generateDarkEvent(); generateGoToFullShadeEvent(); generateViewResizeEvent(); generateGroupExpansionEvent(); generateAnimateEverythingEvent(); mNeedsAnimation = false; } private void generateHeadsUpAnimationEvents() { for (Pair<ExpandableNotificationRow, Boolean> eventPair : mHeadsUpChangeAnimations) { ExpandableNotificationRow row = eventPair.first; boolean isHeadsUp = eventPair.second; int type = AnimationEvent.ANIMATION_TYPE_HEADS_UP_OTHER; boolean onBottom = false; boolean pinnedAndClosed = row.isPinned() && !mIsExpanded; if (!mIsExpanded && !isHeadsUp) { type = AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR; } else { StackViewState viewState = mCurrentStackScrollState.getViewStateForView(row); if (viewState == null) { // A view state was never generated for this view, so we don't need to animate // this. This may happen with notification children. continue; } if (isHeadsUp && (mAddedHeadsUpChildren.contains(row) || pinnedAndClosed)) { if (pinnedAndClosed || shouldHunAppearFromBottom(viewState)) { // Our custom add animation type = AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR; } else { // Normal add animation type = AnimationEvent.ANIMATION_TYPE_ADD; } onBottom = !pinnedAndClosed; } } AnimationEvent event = new AnimationEvent(row, type); event.headsUpFromBottom = onBottom; mAnimationEvents.add(event); } mHeadsUpChangeAnimations.clear(); mAddedHeadsUpChildren.clear(); } private boolean shouldHunAppearFromBottom(StackViewState viewState) { if (viewState.yTranslation + viewState.height < mAmbientState.getMaxHeadsUpTranslation()) { return false; } return true; } private void generateGroupExpansionEvent() { // Generate a group expansion/collapsing event if there is such a group at all if (mExpandedGroupView != null) { mAnimationEvents.add(new AnimationEvent(mExpandedGroupView, AnimationEvent.ANIMATION_TYPE_GROUP_EXPANSION_CHANGED)); mExpandedGroupView = null; } } private void generateViewResizeEvent() { if (mNeedViewResizeAnimation) { mAnimationEvents.add( new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_VIEW_RESIZE)); } mNeedViewResizeAnimation = false; } private void generateSnapBackEvents() { for (View child : mSnappedBackChildren) { mAnimationEvents.add(new AnimationEvent(child, AnimationEvent.ANIMATION_TYPE_SNAP_BACK)); } mSnappedBackChildren.clear(); } private void generateDragEvents() { for (View child : mDragAnimPendingChildren) { mAnimationEvents.add(new AnimationEvent(child, AnimationEvent.ANIMATION_TYPE_START_DRAG)); } mDragAnimPendingChildren.clear(); } private void generateChildRemovalEvents() { for (View child : mChildrenToRemoveAnimated) { boolean childWasSwipedOut = mSwipedOutViews.contains(child); int animationType = childWasSwipedOut ? AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT : AnimationEvent.ANIMATION_TYPE_REMOVE; AnimationEvent event = new AnimationEvent(child, animationType); // we need to know the view after this one event.viewAfterChangingView = getFirstChildBelowTranlsationY(child.getTranslationY()); mAnimationEvents.add(event); } mSwipedOutViews.clear(); mChildrenToRemoveAnimated.clear(); } private void generatePositionChangeEvents() { for (View child : mChildrenChangingPositions) { mAnimationEvents.add(new AnimationEvent(child, AnimationEvent.ANIMATION_TYPE_CHANGE_POSITION)); } mChildrenChangingPositions.clear(); if (mGenerateChildOrderChangedEvent) { mAnimationEvents.add(new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_CHANGE_POSITION)); mGenerateChildOrderChangedEvent = false; } } private void generateChildAdditionEvents() { for (View child : mChildrenToAddAnimated) { if (mFromMoreCardAdditions.contains(child)) { mAnimationEvents.add(new AnimationEvent(child, AnimationEvent.ANIMATION_TYPE_ADD, StackStateAnimator.ANIMATION_DURATION_STANDARD)); } else { mAnimationEvents.add(new AnimationEvent(child, AnimationEvent.ANIMATION_TYPE_ADD)); } } mChildrenToAddAnimated.clear(); mFromMoreCardAdditions.clear(); } private void generateTopPaddingEvent() { if (mTopPaddingNeedsAnimation) { mAnimationEvents.add( new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_TOP_PADDING_CHANGED)); } mTopPaddingNeedsAnimation = false; } private void generateActivateEvent() { if (mActivateNeedsAnimation) { mAnimationEvents.add( new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_ACTIVATED_CHILD)); } mActivateNeedsAnimation = false; } private void generateAnimateEverythingEvent() { if (mEverythingNeedsAnimation) { mAnimationEvents.add( new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_EVERYTHING)); } mEverythingNeedsAnimation = false; } private void generateDimmedEvent() { if (mDimmedNeedsAnimation) { mAnimationEvents.add( new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_DIMMED)); } mDimmedNeedsAnimation = false; } private void generateHideSensitiveEvent() { if (mHideSensitiveNeedsAnimation) { mAnimationEvents.add( new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_HIDE_SENSITIVE)); } mHideSensitiveNeedsAnimation = false; } private void generateDarkEvent() { if (mDarkNeedsAnimation) { AnimationEvent ev = new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_DARK); ev.darkAnimationOriginIndex = mDarkAnimationOriginIndex; mAnimationEvents.add(ev); } mDarkNeedsAnimation = false; } private void generateGoToFullShadeEvent() { if (mGoToFullShadeNeedsAnimation) { mAnimationEvents.add( new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_GO_TO_FULL_SHADE)); } mGoToFullShadeNeedsAnimation = false; } private boolean onInterceptTouchEventScroll(MotionEvent ev) { if (!isScrollingEnabled()) { return false; } /* * 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; } 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 x = (int) ev.getX(pointerIndex); final int yDiff = Math.abs(y - mLastMotionY); final int xDiff = Math.abs(x - mDownX); if (yDiff > mTouchSlop && yDiff > xDiff) { setIsBeingDragged(true); mLastMotionY = y; mDownX = x; initVelocityTrackerIfNotExists(); mVelocityTracker.addMovement(ev); } 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; mDownX = (int) ev.getX(); mActivePointerId = ev.getPointerId(0); mScrolledToTopOnFirstDown = isScrolledToTop(); 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; } /** * @return Whether the specified motion event is actually happening over the content. */ private boolean isInContentBounds(MotionEvent event) { return isInContentBounds(event.getY()); } /** * @return Whether a y coordinate is inside the content. */ public boolean isInContentBounds(float y) { return y < getHeight() - getEmptyBottomMargin(); } private void setIsBeingDragged(boolean isDragged) { mIsBeingDragged = isDragged; if (isDragged) { requestDisallowInterceptTouchEvent(true); removeLongPressCallback(); } } @Override public void onWindowFocusChanged(boolean hasWindowFocus) { super.onWindowFocusChanged(hasWindowFocus); if (!hasWindowFocus) { removeLongPressCallback(); } } public void removeLongPressCallback() { mSwipeHelper.removeLongPressCallback(); } @Override public boolean isScrolledToTop() { return mOwnScrollY == 0; } @Override public boolean isScrolledToBottom() { return mOwnScrollY >= getScrollRange(); } @Override public View getHostView() { return this; } public int getEmptyBottomMargin() { int emptyMargin = mMaxLayoutHeight - mContentHeight - mBottomStackPeekSize; if (needsHeightAdaption()) { emptyMargin -= mBottomStackSlowDownHeight; } else { emptyMargin -= mCollapseSecondCardPadding; } return Math.max(emptyMargin, 0); } public void onExpansionStarted() { mIsExpansionChanging = true; mStackScrollAlgorithm.onExpansionStarted(mCurrentStackScrollState); } public void onExpansionStopped() { mIsExpansionChanging = false; mStackScrollAlgorithm.onExpansionStopped(); if (!mIsExpanded) { mOwnScrollY = 0; // lets make sure nothing is in the overlay anymore getOverlay().clear(); } } public void onPanelTrackingStarted() { mPanelTracking = true; } public void onPanelTrackingStopped() { mPanelTracking = false; } public void resetScrollPosition() { mScroller.abortAnimation(); mOwnScrollY = 0; } private void setIsExpanded(boolean isExpanded) { boolean changed = isExpanded != mIsExpanded; mIsExpanded = isExpanded; mStackScrollAlgorithm.setIsExpanded(isExpanded); if (changed) { updateNotificationAnimationStates(); } } @Override public void onHeightChanged(ExpandableView view, boolean needsAnimation) { updateContentHeight(); updateScrollPositionOnExpandInBottom(view); clampScrollPosition(); notifyHeightChangeListener(view); if (needsAnimation) { requestAnimationOnViewResize(); } requestChildrenUpdate(); } @Override public void onReset(ExpandableView view) { if (mIsExpanded && mAnimationsEnabled) { mRequestViewResizeAnimationOnLayout = true; } mStackScrollAlgorithm.onReset(view); updateAnimationState(view); } private void updateScrollPositionOnExpandInBottom(ExpandableView view) { if (view instanceof ExpandableNotificationRow) { ExpandableNotificationRow row = (ExpandableNotificationRow) view; if (row.isUserLocked() && row != getFirstChildNotGone()) { // We are actually expanding this view float endPosition = row.getTranslationY() + row.getActualHeight(); int stackEnd = mMaxLayoutHeight - mBottomStackPeekSize - mBottomStackSlowDownHeight + (int) mStackTranslation; if (endPosition > stackEnd) { mOwnScrollY += endPosition - stackEnd; mDisallowScrollingInThisMotion = true; } } } } public void setOnHeightChangedListener( ExpandableView.OnHeightChangedListener mOnHeightChangedListener) { this.mOnHeightChangedListener = mOnHeightChangedListener; } public void setOnEmptySpaceClickListener(OnEmptySpaceClickListener listener) { mOnEmptySpaceClickListener = listener; } public void onChildAnimationFinished() { requestChildrenUpdate(); runAnimationFinishedRunnables(); clearViewOverlays(); } private void clearViewOverlays() { for (View view : mClearOverlayViewsWhenFinished) { getOverlay().remove(view); } } private void runAnimationFinishedRunnables() { for (Runnable runnable : mAnimationFinishedRunnables) { runnable.run(); } mAnimationFinishedRunnables.clear(); } /** * See {@link AmbientState#setDimmed}. */ public void setDimmed(boolean dimmed, boolean animate) { mStackScrollAlgorithm.setDimmed(dimmed); mAmbientState.setDimmed(dimmed); updatePadding(dimmed); if (animate && mAnimationsEnabled) { mDimmedNeedsAnimation = true; mNeedsAnimation = true; } requestChildrenUpdate(); } public void setHideSensitive(boolean hideSensitive, boolean animate) { if (hideSensitive != mAmbientState.isHideSensitive()) { int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { ExpandableView v = (ExpandableView) getChildAt(i); v.setHideSensitiveForIntrinsicHeight(hideSensitive); } mAmbientState.setHideSensitive(hideSensitive); if (animate && mAnimationsEnabled) { mHideSensitiveNeedsAnimation = true; mNeedsAnimation = true; } requestChildrenUpdate(); } } /** * See {@link AmbientState#setActivatedChild}. */ public void setActivatedChild(ActivatableNotificationView activatedChild) { mAmbientState.setActivatedChild(activatedChild); if (mAnimationsEnabled) { mActivateNeedsAnimation = true; mNeedsAnimation = true; } requestChildrenUpdate(); } public ActivatableNotificationView getActivatedChild() { return mAmbientState.getActivatedChild(); } private void applyCurrentState() { mCurrentStackScrollState.apply(); if (mListener != null) { mListener.onChildLocationsChanged(this); } runAnimationFinishedRunnables(); } public void setSpeedBumpView(SpeedBumpView speedBumpView) { mSpeedBumpView = speedBumpView; addView(speedBumpView); } private void updateSpeedBump(boolean visible) { boolean notGoneBefore = mSpeedBumpView.getVisibility() != GONE; if (visible != notGoneBefore) { int newVisibility = visible ? VISIBLE : GONE; mSpeedBumpView.setVisibility(newVisibility); if (visible) { // Make invisible to ensure that the appear animation is played. mSpeedBumpView.setInvisible(); } else { // TODO: This doesn't really work, because the view is already set to GONE above. generateRemoveAnimation(mSpeedBumpView); } } } public void goToFullShade(long delay) { updateSpeedBump(true /* visibility */); mDismissView.setInvisible(); mEmptyShadeView.setInvisible(); mGoToFullShadeNeedsAnimation = true; mGoToFullShadeDelay = delay; mNeedsAnimation = true; requestChildrenUpdate(); } public void cancelExpandHelper() { mExpandHelper.cancel(); } public void setIntrinsicPadding(int intrinsicPadding) { mIntrinsicPadding = intrinsicPadding; } public int getIntrinsicPadding() { return mIntrinsicPadding; } /** * @return the y position of the first notification */ public float getNotificationsTopY() { return mTopPadding + getStackTranslation(); } @Override public boolean shouldDelayChildPressedState() { return true; } /** * See {@link AmbientState#setDark}. */ public void setDark(boolean dark, boolean animate, @Nullable PointF touchWakeUpScreenLocation) { mAmbientState.setDark(dark); if (animate && mAnimationsEnabled) { mDarkNeedsAnimation = true; mDarkAnimationOriginIndex = findDarkAnimationOriginIndex(touchWakeUpScreenLocation); mNeedsAnimation = true; } requestChildrenUpdate(); } private int findDarkAnimationOriginIndex(@Nullable PointF screenLocation) { if (screenLocation == null || screenLocation.y < mTopPadding + mTopPaddingOverflow) { return AnimationEvent.DARK_ANIMATION_ORIGIN_INDEX_ABOVE; } if (screenLocation.y > getBottomMostNotificationBottom()) { return AnimationEvent.DARK_ANIMATION_ORIGIN_INDEX_BELOW; } View child = getClosestChildAtRawPosition(screenLocation.x, screenLocation.y); if (child != null) { return getNotGoneIndex(child); } else { return AnimationEvent.DARK_ANIMATION_ORIGIN_INDEX_ABOVE; } } private int getNotGoneIndex(View child) { int count = getChildCount(); int notGoneIndex = 0; for (int i = 0; i < count; i++) { View v = getChildAt(i); if (child == v) { return notGoneIndex; } if (v.getVisibility() != View.GONE) { notGoneIndex++; } } return -1; } public void setDismissView(DismissView dismissView) { mDismissView = dismissView; addView(mDismissView); } public void setEmptyShadeView(EmptyShadeView emptyShadeView) { mEmptyShadeView = emptyShadeView; addView(mEmptyShadeView); } public void updateEmptyShadeView(boolean visible) { int oldVisibility = mEmptyShadeView.willBeGone() ? GONE : mEmptyShadeView.getVisibility(); int newVisibility = visible ? VISIBLE : GONE; if (oldVisibility != newVisibility) { if (newVisibility != GONE) { if (mEmptyShadeView.willBeGone()) { mEmptyShadeView.cancelAnimation(); } else { mEmptyShadeView.setInvisible(); } mEmptyShadeView.setVisibility(newVisibility); mEmptyShadeView.setWillBeGone(false); updateContentHeight(); notifyHeightChangeListener(mEmptyShadeView); } else { Runnable onFinishedRunnable = new Runnable() { @Override public void run() { mEmptyShadeView.setVisibility(GONE); mEmptyShadeView.setWillBeGone(false); updateContentHeight(); notifyHeightChangeListener(mEmptyShadeView); } }; if (mAnimationsEnabled) { mEmptyShadeView.setWillBeGone(true); mEmptyShadeView.performVisibilityAnimation(false, onFinishedRunnable); } else { mEmptyShadeView.setInvisible(); onFinishedRunnable.run(); } } } } public void setOverflowContainer(NotificationOverflowContainer overFlowContainer) { mOverflowContainer = overFlowContainer; addView(mOverflowContainer); } public void updateOverflowContainerVisibility(boolean visible) { int oldVisibility = mOverflowContainer.willBeGone() ? GONE : mOverflowContainer.getVisibility(); final int newVisibility = visible ? VISIBLE : GONE; if (oldVisibility != newVisibility) { Runnable onFinishedRunnable = new Runnable() { @Override public void run() { mOverflowContainer.setVisibility(newVisibility); mOverflowContainer.setWillBeGone(false); updateContentHeight(); notifyHeightChangeListener(mOverflowContainer); } }; if (!mAnimationsEnabled || !mIsExpanded) { mOverflowContainer.cancelAppearDrawing(); onFinishedRunnable.run(); } else if (newVisibility != GONE) { mOverflowContainer.performAddAnimation(0, StackStateAnimator.ANIMATION_DURATION_STANDARD); mOverflowContainer.setVisibility(newVisibility); mOverflowContainer.setWillBeGone(false); updateContentHeight(); notifyHeightChangeListener(mOverflowContainer); } else { mOverflowContainer.performRemoveAnimation( StackStateAnimator.ANIMATION_DURATION_STANDARD, 0.0f, onFinishedRunnable); mOverflowContainer.setWillBeGone(true); } } } public void updateDismissView(boolean visible) { int oldVisibility = mDismissView.willBeGone() ? GONE : mDismissView.getVisibility(); int newVisibility = visible ? VISIBLE : GONE; if (oldVisibility != newVisibility) { if (newVisibility != GONE) { if (mDismissView.willBeGone()) { mDismissView.cancelAnimation(); } else { mDismissView.setInvisible(); } mDismissView.setVisibility(newVisibility); mDismissView.setWillBeGone(false); updateContentHeight(); notifyHeightChangeListener(mDismissView); } else { Runnable dimissHideFinishRunnable = new Runnable() { @Override public void run() { mDismissView.setVisibility(GONE); mDismissView.setWillBeGone(false); updateContentHeight(); notifyHeightChangeListener(mDismissView); } }; if (mDismissView.isButtonVisible() && mIsExpanded && mAnimationsEnabled) { mDismissView.setWillBeGone(true); mDismissView.performVisibilityAnimation(false, dimissHideFinishRunnable); } else { dimissHideFinishRunnable.run(); mDismissView.showClearButton(); } } } } public void setDismissAllInProgress(boolean dismissAllInProgress) { mDismissAllInProgress = dismissAllInProgress; mDismissView.setDismissAllInProgress(dismissAllInProgress); mAmbientState.setDismissAllInProgress(dismissAllInProgress); if (dismissAllInProgress) { disableClipOptimization(); } handleDismissAllClipping(); } private void handleDismissAllClipping() { final int count = getChildCount(); boolean previousChildWillBeDismissed = false; for (int i = 0; i < count; i++) { ExpandableView child = (ExpandableView) getChildAt(i); if (child.getVisibility() == GONE) { continue; } if (mDismissAllInProgress && previousChildWillBeDismissed) { child.setMinClipTopAmount(child.getClipTopAmount()); } else { child.setMinClipTopAmount(0); } previousChildWillBeDismissed = canChildBeDismissed(child); } } private void disableClipOptimization() { final int count = getChildCount(); for (int i = 0; i < count; i++) { ExpandableView child = (ExpandableView) getChildAt(i); if (child.getVisibility() == GONE) { continue; } child.setClipTopOptimization(0); } } public boolean isDismissViewNotGone() { return mDismissView.getVisibility() != View.GONE && !mDismissView.willBeGone(); } public boolean isDismissViewVisible() { return mDismissView.isVisible(); } public int getDismissViewHeight() { int height = mDismissView.getHeight() + mPaddingBetweenElementsNormal; // Hack: Accommodate for additional distance when we only have one notification and the // dismiss all button. if (getNotGoneChildCount() == 2 && getLastChildNotGone() == mDismissView && getFirstChildNotGone() instanceof ActivatableNotificationView) { height += mCollapseSecondCardPadding; } return height; } public int getEmptyShadeViewHeight() { return mEmptyShadeView.getHeight(); } public float getBottomMostNotificationBottom() { final int count = getChildCount(); float max = 0; for (int childIdx = 0; childIdx < count; childIdx++) { ExpandableView child = (ExpandableView) getChildAt(childIdx); if (child.getVisibility() == GONE) { continue; } float bottom = child.getTranslationY() + child.getActualHeight(); if (bottom > max) { max = bottom; } } return max + getStackTranslation(); } /** * @param qsMinHeight The minimum height of the quick settings including padding * See {@link StackScrollAlgorithm#updateIsSmallScreen}. */ public void updateIsSmallScreen(int qsMinHeight) { mStackScrollAlgorithm.updateIsSmallScreen(mMaxLayoutHeight - qsMinHeight); } public void setPhoneStatusBar(PhoneStatusBar phoneStatusBar) { this.mPhoneStatusBar = phoneStatusBar; } public void setGroupManager(NotificationGroupManager groupManager) { this.mGroupManager = groupManager; } public void onGoToKeyguard() { requestAnimateEverything(); } private void requestAnimateEverything() { if (mIsExpanded && mAnimationsEnabled) { mEverythingNeedsAnimation = true; mNeedsAnimation = true; requestChildrenUpdate(); } } public boolean isBelowLastNotification(float touchX, float touchY) { int childCount = getChildCount(); for (int i = childCount - 1; i >= 0; i--) { ExpandableView child = (ExpandableView) getChildAt(i); if (child.getVisibility() != View.GONE) { float childTop = child.getY(); if (childTop > touchY) { // we are above a notification entirely let's abort return false; } boolean belowChild = touchY > childTop + child.getActualHeight(); if (child == mDismissView) { if(!belowChild && !mDismissView.isOnEmptySpace(touchX - mDismissView.getX(), touchY - childTop)) { // We clicked on the dismiss button return false; } } else if (child == mEmptyShadeView) { // We arrived at the empty shade view, for which we accept all clicks return true; } else if (!belowChild){ // We are on a child return false; } } } return touchY > mTopPadding + mStackTranslation; } private void updateExpandButtons() { for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); if (child instanceof ExpandableNotificationRow) { ExpandableNotificationRow row = (ExpandableNotificationRow) child; row.updateExpandButton(); } } } @Override public void onGroupExpansionChanged(ExpandableNotificationRow changedRow, boolean expanded) { boolean animated = mAnimationsEnabled && mIsExpanded; if (animated) { mExpandedGroupView = changedRow; mNeedsAnimation = true; } changedRow.setChildrenExpanded(expanded, animated); onHeightChanged(changedRow, false /* needsAnimation */); } @Override public void onGroupsProhibitedChanged() { updateExpandButtons(); } @Override public void onGroupCreatedFromChildren(NotificationGroupManager.NotificationGroup group) { for (NotificationData.Entry entry : group.children) { ExpandableNotificationRow row = entry.row; if (indexOfChild(row) != -1) { removeView(row); group.summary.row.addChildNotification(row); } } } public void generateChildOrderChangedEvent() { if (mIsExpanded && mAnimationsEnabled) { mGenerateChildOrderChangedEvent = true; mNeedsAnimation = true; requestChildrenUpdate(); } } public void runAfterAnimationFinished(Runnable runnable) { mAnimationFinishedRunnables.add(runnable); } public void setHeadsUpManager(HeadsUpManager headsUpManager) { mHeadsUpManager = headsUpManager; mAmbientState.setHeadsUpManager(headsUpManager); mStackScrollAlgorithm.setHeadsUpManager(headsUpManager); } public void generateHeadsUpAnimation(ExpandableNotificationRow row, boolean isHeadsUp) { if (mAnimationsEnabled) { mHeadsUpChangeAnimations.add(new Pair<>(row, isHeadsUp)); mNeedsAnimation = true; requestChildrenUpdate(); } } public void setShadeExpanded(boolean shadeExpanded) { mAmbientState.setShadeExpanded(shadeExpanded); mStateAnimator.setShadeExpanded(shadeExpanded); } /** * Set the boundary for the bottom heads up position. The heads up will always be above this * position. * * @param height the height of the screen * @param bottomBarHeight the height of the bar on the bottom */ public void setHeadsUpBoundaries(int height, int bottomBarHeight) { mAmbientState.setMaxHeadsUpTranslation(height - bottomBarHeight); mStateAnimator.setHeadsUpAppearHeightBottom(height); requestChildrenUpdate(); } public void setTrackingHeadsUp(boolean trackingHeadsUp) { mTrackingHeadsUp = trackingHeadsUp; } public void setScrimController(ScrimController scrimController) { mScrimController = scrimController; } public void forceNoOverlappingRendering(boolean force) { mForceNoOverlappingRendering = force; } @Override public boolean hasOverlappingRendering() { return !mForceNoOverlappingRendering && super.hasOverlappingRendering(); } /** * A listener that is notified when some child locations might have changed. */ public interface OnChildLocationsChangedListener { public void onChildLocationsChanged(NotificationStackScrollLayout stackScrollLayout); } /** * A listener that is notified when the empty space below the notifications is clicked on */ public interface OnEmptySpaceClickListener { public void onEmptySpaceClicked(float x, float y); } /** * A listener that gets notified when the overscroll at the top has changed. */ public interface OnOverscrollTopChangedListener { /** * Notifies a listener that the overscroll has changed. * * @param amount the amount of overscroll, in pixels * @param isRubberbanded if true, this is a rubberbanded overscroll; if false, this is an * unrubberbanded motion to directly expand overscroll view (e.g expand * QS) */ public void onOverscrollTopChanged(float amount, boolean isRubberbanded); /** * Notify a listener that the scroller wants to escape from the scrolling motion and * start a fling animation to the expanded or collapsed overscroll view (e.g expand the QS) * * @param velocity The velocity that the Scroller had when over flinging * @param open Should the fling open or close the overscroll view. */ public void flingTopOverscroll(float velocity, boolean open); } static class AnimationEvent { static AnimationFilter[] FILTERS = new AnimationFilter[] { // ANIMATION_TYPE_ADD new AnimationFilter() .animateAlpha() .animateHeight() .animateTopInset() .animateY() .animateZ() .hasDelays(), // ANIMATION_TYPE_REMOVE new AnimationFilter() .animateAlpha() .animateHeight() .animateTopInset() .animateY() .animateZ() .hasDelays(), // ANIMATION_TYPE_REMOVE_SWIPED_OUT new AnimationFilter() .animateAlpha() .animateHeight() .animateTopInset() .animateY() .animateZ() .hasDelays(), // ANIMATION_TYPE_TOP_PADDING_CHANGED new AnimationFilter() .animateAlpha() .animateHeight() .animateTopInset() .animateY() .animateDimmed() .animateScale() .animateZ(), // ANIMATION_TYPE_START_DRAG new AnimationFilter() .animateAlpha(), // ANIMATION_TYPE_SNAP_BACK new AnimationFilter() .animateAlpha() .animateHeight(), // ANIMATION_TYPE_ACTIVATED_CHILD new AnimationFilter() .animateScale() .animateAlpha(), // ANIMATION_TYPE_DIMMED new AnimationFilter() .animateY() .animateScale() .animateDimmed(), // ANIMATION_TYPE_CHANGE_POSITION new AnimationFilter() .animateAlpha() .animateHeight() .animateTopInset() .animateY() .animateZ(), // ANIMATION_TYPE_DARK new AnimationFilter() .animateDark() .hasDelays(), // ANIMATION_TYPE_GO_TO_FULL_SHADE new AnimationFilter() .animateAlpha() .animateHeight() .animateTopInset() .animateY() .animateDimmed() .animateScale() .animateZ() .hasDelays(), // ANIMATION_TYPE_HIDE_SENSITIVE new AnimationFilter() .animateHideSensitive(), // ANIMATION_TYPE_VIEW_RESIZE new AnimationFilter() .animateAlpha() .animateHeight() .animateTopInset() .animateY() .animateZ(), // ANIMATION_TYPE_GROUP_EXPANSION_CHANGED new AnimationFilter() .animateAlpha() .animateHeight() .animateTopInset() .animateY() .animateZ(), // ANIMATION_TYPE_HEADS_UP_APPEAR new AnimationFilter() .animateAlpha() .animateHeight() .animateTopInset() .animateY() .animateZ(), // ANIMATION_TYPE_HEADS_UP_DISAPPEAR new AnimationFilter() .animateAlpha() .animateHeight() .animateTopInset() .animateY() .animateZ(), // ANIMATION_TYPE_HEADS_UP_OTHER new AnimationFilter() .animateAlpha() .animateHeight() .animateTopInset() .animateY() .animateZ(), // ANIMATION_TYPE_EVERYTHING new AnimationFilter() .animateAlpha() .animateDark() .animateScale() .animateDimmed() .animateHideSensitive() .animateHeight() .animateTopInset() .animateY() .animateZ(), }; static int[] LENGTHS = new int[] { // ANIMATION_TYPE_ADD StackStateAnimator.ANIMATION_DURATION_APPEAR_DISAPPEAR, // ANIMATION_TYPE_REMOVE StackStateAnimator.ANIMATION_DURATION_APPEAR_DISAPPEAR, // ANIMATION_TYPE_REMOVE_SWIPED_OUT StackStateAnimator.ANIMATION_DURATION_STANDARD, // ANIMATION_TYPE_TOP_PADDING_CHANGED StackStateAnimator.ANIMATION_DURATION_STANDARD, // ANIMATION_TYPE_START_DRAG StackStateAnimator.ANIMATION_DURATION_STANDARD, // ANIMATION_TYPE_SNAP_BACK StackStateAnimator.ANIMATION_DURATION_STANDARD, // ANIMATION_TYPE_ACTIVATED_CHILD StackStateAnimator.ANIMATION_DURATION_DIMMED_ACTIVATED, // ANIMATION_TYPE_DIMMED StackStateAnimator.ANIMATION_DURATION_DIMMED_ACTIVATED, // ANIMATION_TYPE_CHANGE_POSITION StackStateAnimator.ANIMATION_DURATION_STANDARD, // ANIMATION_TYPE_DARK StackStateAnimator.ANIMATION_DURATION_STANDARD, // ANIMATION_TYPE_GO_TO_FULL_SHADE StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE, // ANIMATION_TYPE_HIDE_SENSITIVE StackStateAnimator.ANIMATION_DURATION_STANDARD, // ANIMATION_TYPE_VIEW_RESIZE StackStateAnimator.ANIMATION_DURATION_STANDARD, // ANIMATION_TYPE_GROUP_EXPANSION_CHANGED StackStateAnimator.ANIMATION_DURATION_EXPAND_CLICKED, // ANIMATION_TYPE_HEADS_UP_APPEAR StackStateAnimator.ANIMATION_DURATION_HEADS_UP_APPEAR, // ANIMATION_TYPE_HEADS_UP_DISAPPEAR StackStateAnimator.ANIMATION_DURATION_HEADS_UP_DISAPPEAR, // ANIMATION_TYPE_HEADS_UP_OTHER StackStateAnimator.ANIMATION_DURATION_STANDARD, // ANIMATION_TYPE_EVERYTHING StackStateAnimator.ANIMATION_DURATION_STANDARD, }; static final int ANIMATION_TYPE_ADD = 0; static final int ANIMATION_TYPE_REMOVE = 1; static final int ANIMATION_TYPE_REMOVE_SWIPED_OUT = 2; static final int ANIMATION_TYPE_TOP_PADDING_CHANGED = 3; static final int ANIMATION_TYPE_START_DRAG = 4; static final int ANIMATION_TYPE_SNAP_BACK = 5; static final int ANIMATION_TYPE_ACTIVATED_CHILD = 6; static final int ANIMATION_TYPE_DIMMED = 7; static final int ANIMATION_TYPE_CHANGE_POSITION = 8; static final int ANIMATION_TYPE_DARK = 9; static final int ANIMATION_TYPE_GO_TO_FULL_SHADE = 10; static final int ANIMATION_TYPE_HIDE_SENSITIVE = 11; static final int ANIMATION_TYPE_VIEW_RESIZE = 12; static final int ANIMATION_TYPE_GROUP_EXPANSION_CHANGED = 13; static final int ANIMATION_TYPE_HEADS_UP_APPEAR = 14; static final int ANIMATION_TYPE_HEADS_UP_DISAPPEAR = 15; static final int ANIMATION_TYPE_HEADS_UP_OTHER = 16; static final int ANIMATION_TYPE_EVERYTHING = 17; static final int DARK_ANIMATION_ORIGIN_INDEX_ABOVE = -1; static final int DARK_ANIMATION_ORIGIN_INDEX_BELOW = -2; final long eventStartTime; final View changingView; final int animationType; final AnimationFilter filter; final long length; View viewAfterChangingView; int darkAnimationOriginIndex; boolean headsUpFromBottom; AnimationEvent(View view, int type) { this(view, type, LENGTHS[type]); } AnimationEvent(View view, int type, long length) { eventStartTime = AnimationUtils.currentAnimationTimeMillis(); changingView = view; animationType = type; filter = FILTERS[type]; this.length = length; } /** * Combines the length of several animation events into a single value. * * @param events The events of the lengths to combine. * @return The combined length. Depending on the event types, this might be the maximum of * all events or the length of a specific event. */ static long combineLength(ArrayList<AnimationEvent> events) { long length = 0; int size = events.size(); for (int i = 0; i < size; i++) { AnimationEvent event = events.get(i); length = Math.max(length, event.length); if (event.animationType == ANIMATION_TYPE_GO_TO_FULL_SHADE) { return event.length; } } return length; } } }