| /* |
| * 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.internal.widget; |
| |
| import android.content.Context; |
| import android.content.res.TypedArray; |
| import android.graphics.Canvas; |
| import android.graphics.Rect; |
| import android.graphics.drawable.Drawable; |
| import android.metrics.LogMaker; |
| import android.os.Bundle; |
| import android.os.Parcel; |
| import android.os.Parcelable; |
| 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.view.ViewTreeObserver; |
| import android.view.accessibility.AccessibilityEvent; |
| import android.view.accessibility.AccessibilityNodeInfo; |
| import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; |
| import android.view.animation.AnimationUtils; |
| import android.widget.AbsListView; |
| import android.widget.OverScroller; |
| |
| import com.android.internal.R; |
| import com.android.internal.logging.MetricsLogger; |
| import com.android.internal.logging.nano.MetricsProto.MetricsEvent; |
| |
| public class ResolverDrawerLayout extends ViewGroup { |
| private static final String TAG = "ResolverDrawerLayout"; |
| private MetricsLogger mMetricsLogger; |
| |
| /** |
| * Max width of the whole drawer layout |
| */ |
| private int mMaxWidth; |
| |
| /** |
| * Max total visible height of views not marked always-show when in the closed/initial state |
| */ |
| private int mMaxCollapsedHeight; |
| |
| /** |
| * Max total visible height of views not marked always-show when in the closed/initial state |
| * when a default option is present |
| */ |
| private int mMaxCollapsedHeightSmall; |
| |
| private boolean mSmallCollapsed; |
| |
| /** |
| * Move views down from the top by this much in px |
| */ |
| private float mCollapseOffset; |
| |
| private int mCollapsibleHeight; |
| private int mUncollapsibleHeight; |
| |
| /** |
| * The height in pixels of reserved space added to the top of the collapsed UI; |
| * e.g. chooser targets |
| */ |
| private int mCollapsibleHeightReserved; |
| |
| private int mTopOffset; |
| private boolean mShowAtTop; |
| |
| private boolean mIsDragging; |
| private boolean mOpenOnClick; |
| private boolean mOpenOnLayout; |
| private boolean mDismissOnScrollerFinished; |
| private final int mTouchSlop; |
| private final float mMinFlingVelocity; |
| private final OverScroller mScroller; |
| private final VelocityTracker mVelocityTracker; |
| |
| private Drawable mScrollIndicatorDrawable; |
| |
| private OnDismissedListener mOnDismissedListener; |
| private RunOnDismissedListener mRunOnDismissedListener; |
| |
| private boolean mDismissLocked; |
| |
| private float mInitialTouchX; |
| private float mInitialTouchY; |
| private float mLastTouchY; |
| private int mActivePointerId = MotionEvent.INVALID_POINTER_ID; |
| |
| private final Rect mTempRect = new Rect(); |
| |
| private final ViewTreeObserver.OnTouchModeChangeListener mTouchModeChangeListener = |
| new ViewTreeObserver.OnTouchModeChangeListener() { |
| @Override |
| public void onTouchModeChanged(boolean isInTouchMode) { |
| if (!isInTouchMode && hasFocus() && isDescendantClipped(getFocusedChild())) { |
| smoothScrollTo(0, 0); |
| } |
| } |
| }; |
| |
| public ResolverDrawerLayout(Context context) { |
| this(context, null); |
| } |
| |
| public ResolverDrawerLayout(Context context, AttributeSet attrs) { |
| this(context, attrs, 0); |
| } |
| |
| public ResolverDrawerLayout(Context context, AttributeSet attrs, int defStyleAttr) { |
| super(context, attrs, defStyleAttr); |
| |
| final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ResolverDrawerLayout, |
| defStyleAttr, 0); |
| mMaxWidth = a.getDimensionPixelSize(R.styleable.ResolverDrawerLayout_maxWidth, -1); |
| mMaxCollapsedHeight = a.getDimensionPixelSize( |
| R.styleable.ResolverDrawerLayout_maxCollapsedHeight, 0); |
| mMaxCollapsedHeightSmall = a.getDimensionPixelSize( |
| R.styleable.ResolverDrawerLayout_maxCollapsedHeightSmall, |
| mMaxCollapsedHeight); |
| mShowAtTop = a.getBoolean(R.styleable.ResolverDrawerLayout_showAtTop, false); |
| a.recycle(); |
| |
| mScrollIndicatorDrawable = mContext.getDrawable(R.drawable.scroll_indicator_material); |
| |
| mScroller = new OverScroller(context, AnimationUtils.loadInterpolator(context, |
| android.R.interpolator.decelerate_quint)); |
| mVelocityTracker = VelocityTracker.obtain(); |
| |
| final ViewConfiguration vc = ViewConfiguration.get(context); |
| mTouchSlop = vc.getScaledTouchSlop(); |
| mMinFlingVelocity = vc.getScaledMinimumFlingVelocity(); |
| |
| setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); |
| } |
| |
| public void setSmallCollapsed(boolean smallCollapsed) { |
| mSmallCollapsed = smallCollapsed; |
| requestLayout(); |
| } |
| |
| public boolean isSmallCollapsed() { |
| return mSmallCollapsed; |
| } |
| |
| public boolean isCollapsed() { |
| return mCollapseOffset > 0; |
| } |
| |
| public void setShowAtTop(boolean showOnTop) { |
| mShowAtTop = showOnTop; |
| invalidate(); |
| requestLayout(); |
| } |
| |
| public boolean getShowAtTop() { |
| return mShowAtTop; |
| } |
| |
| public void setCollapsed(boolean collapsed) { |
| if (!isLaidOut()) { |
| mOpenOnLayout = collapsed; |
| } else { |
| smoothScrollTo(collapsed ? mCollapsibleHeight : 0, 0); |
| } |
| } |
| |
| public void setCollapsibleHeightReserved(int heightPixels) { |
| final int oldReserved = mCollapsibleHeightReserved; |
| mCollapsibleHeightReserved = heightPixels; |
| |
| final int dReserved = mCollapsibleHeightReserved - oldReserved; |
| if (dReserved != 0 && mIsDragging) { |
| mLastTouchY -= dReserved; |
| } |
| |
| final int oldCollapsibleHeight = mCollapsibleHeight; |
| mCollapsibleHeight = Math.max(mCollapsibleHeight, getMaxCollapsedHeight()); |
| |
| if (updateCollapseOffset(oldCollapsibleHeight, !isDragging())) { |
| return; |
| } |
| |
| invalidate(); |
| } |
| |
| public void setDismissLocked(boolean locked) { |
| mDismissLocked = locked; |
| } |
| |
| private boolean isMoving() { |
| return mIsDragging || !mScroller.isFinished(); |
| } |
| |
| private boolean isDragging() { |
| return mIsDragging || getNestedScrollAxes() == SCROLL_AXIS_VERTICAL; |
| } |
| |
| private boolean updateCollapseOffset(int oldCollapsibleHeight, boolean remainClosed) { |
| if (oldCollapsibleHeight == mCollapsibleHeight) { |
| return false; |
| } |
| |
| if (getShowAtTop()) { |
| // Keep the drawer fully open. |
| mCollapseOffset = 0; |
| return false; |
| } |
| |
| if (isLaidOut()) { |
| final boolean isCollapsedOld = mCollapseOffset != 0; |
| if (remainClosed && (oldCollapsibleHeight < mCollapsibleHeight |
| && mCollapseOffset == oldCollapsibleHeight)) { |
| // Stay closed even at the new height. |
| mCollapseOffset = mCollapsibleHeight; |
| } else { |
| mCollapseOffset = Math.min(mCollapseOffset, mCollapsibleHeight); |
| } |
| final boolean isCollapsedNew = mCollapseOffset != 0; |
| if (isCollapsedOld != isCollapsedNew) { |
| onCollapsedChanged(isCollapsedNew); |
| } |
| } else { |
| // Start out collapsed at first unless we restored state for otherwise |
| mCollapseOffset = mOpenOnLayout ? 0 : mCollapsibleHeight; |
| } |
| return true; |
| } |
| |
| private int getMaxCollapsedHeight() { |
| return (isSmallCollapsed() ? mMaxCollapsedHeightSmall : mMaxCollapsedHeight) |
| + mCollapsibleHeightReserved; |
| } |
| |
| public void setOnDismissedListener(OnDismissedListener listener) { |
| mOnDismissedListener = listener; |
| } |
| |
| private boolean isDismissable() { |
| return mOnDismissedListener != null && !mDismissLocked; |
| } |
| |
| @Override |
| public boolean onInterceptTouchEvent(MotionEvent ev) { |
| final int action = ev.getActionMasked(); |
| |
| if (action == MotionEvent.ACTION_DOWN) { |
| mVelocityTracker.clear(); |
| } |
| |
| mVelocityTracker.addMovement(ev); |
| |
| switch (action) { |
| case MotionEvent.ACTION_DOWN: { |
| final float x = ev.getX(); |
| final float y = ev.getY(); |
| mInitialTouchX = x; |
| mInitialTouchY = mLastTouchY = y; |
| mOpenOnClick = isListChildUnderClipped(x, y) && mCollapseOffset > 0; |
| } |
| break; |
| |
| case MotionEvent.ACTION_MOVE: { |
| final float x = ev.getX(); |
| final float y = ev.getY(); |
| final float dy = y - mInitialTouchY; |
| if (Math.abs(dy) > mTouchSlop && findChildUnder(x, y) != null && |
| (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) { |
| mActivePointerId = ev.getPointerId(0); |
| mIsDragging = true; |
| mLastTouchY = Math.max(mLastTouchY - mTouchSlop, |
| Math.min(mLastTouchY + dy, mLastTouchY + mTouchSlop)); |
| } |
| } |
| break; |
| |
| case MotionEvent.ACTION_POINTER_UP: { |
| onSecondaryPointerUp(ev); |
| } |
| break; |
| |
| case MotionEvent.ACTION_CANCEL: |
| case MotionEvent.ACTION_UP: { |
| resetTouch(); |
| } |
| break; |
| } |
| |
| if (mIsDragging) { |
| abortAnimation(); |
| } |
| return mIsDragging || mOpenOnClick; |
| } |
| |
| @Override |
| public boolean onTouchEvent(MotionEvent ev) { |
| final int action = ev.getActionMasked(); |
| |
| mVelocityTracker.addMovement(ev); |
| |
| boolean handled = false; |
| switch (action) { |
| case MotionEvent.ACTION_DOWN: { |
| final float x = ev.getX(); |
| final float y = ev.getY(); |
| mInitialTouchX = x; |
| mInitialTouchY = mLastTouchY = y; |
| mActivePointerId = ev.getPointerId(0); |
| final boolean hitView = findChildUnder(mInitialTouchX, mInitialTouchY) != null; |
| handled = isDismissable() || mCollapsibleHeight > 0; |
| mIsDragging = hitView && handled; |
| abortAnimation(); |
| } |
| break; |
| |
| case MotionEvent.ACTION_MOVE: { |
| int index = ev.findPointerIndex(mActivePointerId); |
| if (index < 0) { |
| Log.e(TAG, "Bad pointer id " + mActivePointerId + ", resetting"); |
| index = 0; |
| mActivePointerId = ev.getPointerId(0); |
| mInitialTouchX = ev.getX(); |
| mInitialTouchY = mLastTouchY = ev.getY(); |
| } |
| final float x = ev.getX(index); |
| final float y = ev.getY(index); |
| if (!mIsDragging) { |
| final float dy = y - mInitialTouchY; |
| if (Math.abs(dy) > mTouchSlop && findChildUnder(x, y) != null) { |
| handled = mIsDragging = true; |
| mLastTouchY = Math.max(mLastTouchY - mTouchSlop, |
| Math.min(mLastTouchY + dy, mLastTouchY + mTouchSlop)); |
| } |
| } |
| if (mIsDragging) { |
| final float dy = y - mLastTouchY; |
| performDrag(dy); |
| } |
| mLastTouchY = y; |
| } |
| break; |
| |
| case MotionEvent.ACTION_POINTER_DOWN: { |
| final int pointerIndex = ev.getActionIndex(); |
| final int pointerId = ev.getPointerId(pointerIndex); |
| mActivePointerId = pointerId; |
| mInitialTouchX = ev.getX(pointerIndex); |
| mInitialTouchY = mLastTouchY = ev.getY(pointerIndex); |
| } |
| break; |
| |
| case MotionEvent.ACTION_POINTER_UP: { |
| onSecondaryPointerUp(ev); |
| } |
| break; |
| |
| case MotionEvent.ACTION_UP: { |
| final boolean wasDragging = mIsDragging; |
| mIsDragging = false; |
| if (!wasDragging && findChildUnder(mInitialTouchX, mInitialTouchY) == null && |
| findChildUnder(ev.getX(), ev.getY()) == null) { |
| if (isDismissable()) { |
| dispatchOnDismissed(); |
| resetTouch(); |
| return true; |
| } |
| } |
| if (mOpenOnClick && Math.abs(ev.getX() - mInitialTouchX) < mTouchSlop && |
| Math.abs(ev.getY() - mInitialTouchY) < mTouchSlop) { |
| smoothScrollTo(0, 0); |
| return true; |
| } |
| mVelocityTracker.computeCurrentVelocity(1000); |
| final float yvel = mVelocityTracker.getYVelocity(mActivePointerId); |
| if (Math.abs(yvel) > mMinFlingVelocity) { |
| if (getShowAtTop()) { |
| if (isDismissable() && yvel < 0) { |
| abortAnimation(); |
| dismiss(); |
| } else { |
| smoothScrollTo(yvel < 0 ? 0 : mCollapsibleHeight, yvel); |
| } |
| } else { |
| if (isDismissable() |
| && yvel > 0 && mCollapseOffset > mCollapsibleHeight) { |
| smoothScrollTo(mCollapsibleHeight + mUncollapsibleHeight, yvel); |
| mDismissOnScrollerFinished = true; |
| } else { |
| smoothScrollTo(yvel < 0 ? 0 : mCollapsibleHeight, yvel); |
| } |
| } |
| }else { |
| smoothScrollTo( |
| mCollapseOffset < mCollapsibleHeight / 2 ? 0 : mCollapsibleHeight, 0); |
| } |
| resetTouch(); |
| } |
| break; |
| |
| case MotionEvent.ACTION_CANCEL: { |
| if (mIsDragging) { |
| smoothScrollTo( |
| mCollapseOffset < mCollapsibleHeight / 2 ? 0 : mCollapsibleHeight, 0); |
| } |
| resetTouch(); |
| return true; |
| } |
| } |
| |
| return handled; |
| } |
| |
| private void onSecondaryPointerUp(MotionEvent ev) { |
| final int pointerIndex = ev.getActionIndex(); |
| final int pointerId = ev.getPointerId(pointerIndex); |
| if (pointerId == mActivePointerId) { |
| // This was our active pointer going up. Choose a new |
| // active pointer and adjust accordingly. |
| final int newPointerIndex = pointerIndex == 0 ? 1 : 0; |
| mInitialTouchX = ev.getX(newPointerIndex); |
| mInitialTouchY = mLastTouchY = ev.getY(newPointerIndex); |
| mActivePointerId = ev.getPointerId(newPointerIndex); |
| } |
| } |
| |
| private void resetTouch() { |
| mActivePointerId = MotionEvent.INVALID_POINTER_ID; |
| mIsDragging = false; |
| mOpenOnClick = false; |
| mInitialTouchX = mInitialTouchY = mLastTouchY = 0; |
| mVelocityTracker.clear(); |
| } |
| |
| private void dismiss() { |
| mRunOnDismissedListener = new RunOnDismissedListener(); |
| post(mRunOnDismissedListener); |
| } |
| |
| @Override |
| public void computeScroll() { |
| super.computeScroll(); |
| if (mScroller.computeScrollOffset()) { |
| final boolean keepGoing = !mScroller.isFinished(); |
| performDrag(mScroller.getCurrY() - mCollapseOffset); |
| if (keepGoing) { |
| postInvalidateOnAnimation(); |
| } else if (mDismissOnScrollerFinished && mOnDismissedListener != null) { |
| dismiss(); |
| } |
| } |
| } |
| |
| private void abortAnimation() { |
| mScroller.abortAnimation(); |
| mRunOnDismissedListener = null; |
| mDismissOnScrollerFinished = false; |
| } |
| |
| private float performDrag(float dy) { |
| if (getShowAtTop()) { |
| return 0; |
| } |
| |
| final float newPos = Math.max(0, Math.min(mCollapseOffset + dy, |
| mCollapsibleHeight + mUncollapsibleHeight)); |
| if (newPos != mCollapseOffset) { |
| dy = newPos - mCollapseOffset; |
| final int childCount = getChildCount(); |
| for (int i = 0; i < childCount; i++) { |
| final View child = getChildAt(i); |
| final LayoutParams lp = (LayoutParams) child.getLayoutParams(); |
| if (!lp.ignoreOffset) { |
| child.offsetTopAndBottom((int) dy); |
| } |
| } |
| final boolean isCollapsedOld = mCollapseOffset != 0; |
| mCollapseOffset = newPos; |
| mTopOffset += dy; |
| final boolean isCollapsedNew = newPos != 0; |
| if (isCollapsedOld != isCollapsedNew) { |
| onCollapsedChanged(isCollapsedNew); |
| getMetricsLogger().write( |
| new LogMaker(MetricsEvent.ACTION_SHARESHEET_COLLAPSED_CHANGED) |
| .setSubtype(isCollapsedNew ? 1 : 0)); |
| } |
| onScrollChanged(0, (int) newPos, 0, (int) (newPos - dy)); |
| postInvalidateOnAnimation(); |
| return dy; |
| } |
| return 0; |
| } |
| |
| private void onCollapsedChanged(boolean isCollapsed) { |
| notifyViewAccessibilityStateChangedIfNeeded( |
| AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED); |
| |
| if (mScrollIndicatorDrawable != null) { |
| setWillNotDraw(!isCollapsed); |
| } |
| } |
| |
| void dispatchOnDismissed() { |
| if (mOnDismissedListener != null) { |
| mOnDismissedListener.onDismissed(); |
| } |
| if (mRunOnDismissedListener != null) { |
| removeCallbacks(mRunOnDismissedListener); |
| mRunOnDismissedListener = null; |
| } |
| } |
| |
| private void smoothScrollTo(int yOffset, float velocity) { |
| abortAnimation(); |
| final int sy = (int) mCollapseOffset; |
| int dy = yOffset - sy; |
| if (dy == 0) { |
| return; |
| } |
| |
| final int height = getHeight(); |
| final int halfHeight = height / 2; |
| final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dy) / height); |
| final float distance = halfHeight + halfHeight * |
| distanceInfluenceForSnapDuration(distanceRatio); |
| |
| int duration = 0; |
| velocity = Math.abs(velocity); |
| if (velocity > 0) { |
| duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); |
| } else { |
| final float pageDelta = (float) Math.abs(dy) / height; |
| duration = (int) ((pageDelta + 1) * 100); |
| } |
| duration = Math.min(duration, 300); |
| |
| mScroller.startScroll(0, sy, 0, dy, duration); |
| postInvalidateOnAnimation(); |
| } |
| |
| private float distanceInfluenceForSnapDuration(float f) { |
| f -= 0.5f; // center the values about 0. |
| f *= 0.3f * Math.PI / 2.0f; |
| return (float) Math.sin(f); |
| } |
| |
| /** |
| * Note: this method doesn't take Z into account for overlapping views |
| * since it is only used in contexts where this doesn't affect the outcome. |
| */ |
| private View findChildUnder(float x, float y) { |
| return findChildUnder(this, x, y); |
| } |
| |
| private static View findChildUnder(ViewGroup parent, float x, float y) { |
| final int childCount = parent.getChildCount(); |
| for (int i = childCount - 1; i >= 0; i--) { |
| final View child = parent.getChildAt(i); |
| if (isChildUnder(child, x, y)) { |
| return child; |
| } |
| } |
| return null; |
| } |
| |
| private View findListChildUnder(float x, float y) { |
| View v = findChildUnder(x, y); |
| while (v != null) { |
| x -= v.getX(); |
| y -= v.getY(); |
| if (v instanceof AbsListView) { |
| // One more after this. |
| return findChildUnder((ViewGroup) v, x, y); |
| } |
| v = v instanceof ViewGroup ? findChildUnder((ViewGroup) v, x, y) : null; |
| } |
| return v; |
| } |
| |
| /** |
| * This only checks clipping along the bottom edge. |
| */ |
| private boolean isListChildUnderClipped(float x, float y) { |
| final View listChild = findListChildUnder(x, y); |
| return listChild != null && isDescendantClipped(listChild); |
| } |
| |
| private boolean isDescendantClipped(View child) { |
| mTempRect.set(0, 0, child.getWidth(), child.getHeight()); |
| offsetDescendantRectToMyCoords(child, mTempRect); |
| View directChild; |
| if (child.getParent() == this) { |
| directChild = child; |
| } else { |
| View v = child; |
| ViewParent p = child.getParent(); |
| while (p != this) { |
| v = (View) p; |
| p = v.getParent(); |
| } |
| directChild = v; |
| } |
| |
| // ResolverDrawerLayout lays out vertically in child order; |
| // the next view and forward is what to check against. |
| int clipEdge = getHeight() - getPaddingBottom(); |
| final int childCount = getChildCount(); |
| for (int i = indexOfChild(directChild) + 1; i < childCount; i++) { |
| final View nextChild = getChildAt(i); |
| if (nextChild.getVisibility() == GONE) { |
| continue; |
| } |
| clipEdge = Math.min(clipEdge, nextChild.getTop()); |
| } |
| return mTempRect.bottom > clipEdge; |
| } |
| |
| private static boolean isChildUnder(View child, float x, float y) { |
| final float left = child.getX(); |
| final float top = child.getY(); |
| final float right = left + child.getWidth(); |
| final float bottom = top + child.getHeight(); |
| return x >= left && y >= top && x < right && y < bottom; |
| } |
| |
| @Override |
| public void requestChildFocus(View child, View focused) { |
| super.requestChildFocus(child, focused); |
| if (!isInTouchMode() && isDescendantClipped(focused)) { |
| smoothScrollTo(0, 0); |
| } |
| } |
| |
| @Override |
| protected void onAttachedToWindow() { |
| super.onAttachedToWindow(); |
| getViewTreeObserver().addOnTouchModeChangeListener(mTouchModeChangeListener); |
| } |
| |
| @Override |
| protected void onDetachedFromWindow() { |
| super.onDetachedFromWindow(); |
| getViewTreeObserver().removeOnTouchModeChangeListener(mTouchModeChangeListener); |
| abortAnimation(); |
| } |
| |
| @Override |
| public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { |
| return (nestedScrollAxes & View.SCROLL_AXIS_VERTICAL) != 0; |
| } |
| |
| @Override |
| public void onNestedScrollAccepted(View child, View target, int axes) { |
| super.onNestedScrollAccepted(child, target, axes); |
| } |
| |
| @Override |
| public void onStopNestedScroll(View child) { |
| super.onStopNestedScroll(child); |
| if (mScroller.isFinished()) { |
| smoothScrollTo(mCollapseOffset < mCollapsibleHeight / 2 ? 0 : mCollapsibleHeight, 0); |
| } |
| } |
| |
| @Override |
| public void onNestedScroll(View target, int dxConsumed, int dyConsumed, |
| int dxUnconsumed, int dyUnconsumed) { |
| if (dyUnconsumed < 0) { |
| performDrag(-dyUnconsumed); |
| } |
| } |
| |
| @Override |
| public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { |
| if (dy > 0) { |
| consumed[1] = (int) -performDrag(-dy); |
| } |
| } |
| |
| @Override |
| public boolean onNestedPreFling(View target, float velocityX, float velocityY) { |
| if (!getShowAtTop() && velocityY > mMinFlingVelocity && mCollapseOffset != 0) { |
| smoothScrollTo(0, velocityY); |
| return true; |
| } |
| return false; |
| } |
| |
| @Override |
| public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { |
| if (!consumed && Math.abs(velocityY) > mMinFlingVelocity) { |
| if (getShowAtTop()) { |
| if (isDismissable() && velocityY > 0) { |
| abortAnimation(); |
| dismiss(); |
| } else { |
| smoothScrollTo(velocityY < 0 ? mCollapsibleHeight : 0, velocityY); |
| } |
| } else { |
| if (isDismissable() |
| && velocityY < 0 && mCollapseOffset > mCollapsibleHeight) { |
| smoothScrollTo(mCollapsibleHeight + mUncollapsibleHeight, velocityY); |
| mDismissOnScrollerFinished = true; |
| } else { |
| smoothScrollTo(velocityY > 0 ? 0 : mCollapsibleHeight, velocityY); |
| } |
| } |
| return true; |
| } |
| return false; |
| } |
| |
| @Override |
| public boolean onNestedPrePerformAccessibilityAction(View target, int action, Bundle args) { |
| if (super.onNestedPrePerformAccessibilityAction(target, action, args)) { |
| return true; |
| } |
| |
| if (action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD && mCollapseOffset != 0) { |
| smoothScrollTo(0, 0); |
| return true; |
| } |
| return false; |
| } |
| |
| @Override |
| public CharSequence getAccessibilityClassName() { |
| // Since we support scrolling, make this ViewGroup look like a |
| // ScrollView. This is kind of a hack until we have support for |
| // specifying auto-scroll behavior. |
| return android.widget.ScrollView.class.getName(); |
| } |
| |
| @Override |
| public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { |
| super.onInitializeAccessibilityNodeInfoInternal(info); |
| |
| if (isEnabled()) { |
| if (mCollapseOffset != 0) { |
| info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); |
| info.setScrollable(true); |
| } |
| } |
| |
| // This view should never get accessibility focus, but it's interactive |
| // via nested scrolling, so we can't hide it completely. |
| info.removeAction(AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS); |
| } |
| |
| @Override |
| public boolean performAccessibilityActionInternal(int action, Bundle arguments) { |
| if (action == AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS.getId()) { |
| // This view should never get accessibility focus. |
| return false; |
| } |
| |
| if (super.performAccessibilityActionInternal(action, arguments)) { |
| return true; |
| } |
| |
| if (action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD && mCollapseOffset != 0) { |
| smoothScrollTo(0, 0); |
| return true; |
| } |
| |
| return false; |
| } |
| |
| @Override |
| public void onDrawForeground(Canvas canvas) { |
| if (mScrollIndicatorDrawable != null) { |
| mScrollIndicatorDrawable.draw(canvas); |
| } |
| |
| super.onDrawForeground(canvas); |
| } |
| |
| @Override |
| protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
| final int sourceWidth = MeasureSpec.getSize(widthMeasureSpec); |
| int widthSize = sourceWidth; |
| int heightSize = MeasureSpec.getSize(heightMeasureSpec); |
| |
| // Single-use layout; just ignore the mode and use available space. |
| // Clamp to maxWidth. |
| if (mMaxWidth >= 0) { |
| widthSize = Math.min(widthSize, mMaxWidth); |
| } |
| |
| final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY); |
| final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY); |
| final int widthPadding = getPaddingLeft() + getPaddingRight(); |
| |
| // Currently we allot more height than is really needed so that the entirety of the |
| // sheet may be pulled up. |
| // TODO: Restrict the height here to be the right value. |
| int heightUsed = getPaddingTop() + getPaddingBottom(); |
| |
| // Measure always-show children first. |
| final int childCount = getChildCount(); |
| for (int i = 0; i < childCount; i++) { |
| final View child = getChildAt(i); |
| final LayoutParams lp = (LayoutParams) child.getLayoutParams(); |
| if (lp.alwaysShow && child.getVisibility() != GONE) { |
| if (lp.maxHeight != -1) { |
| final int remainingHeight = heightSize - heightUsed; |
| measureChildWithMargins(child, widthSpec, widthPadding, |
| MeasureSpec.makeMeasureSpec(lp.maxHeight, MeasureSpec.AT_MOST), |
| lp.maxHeight > remainingHeight ? lp.maxHeight - remainingHeight : 0); |
| } else { |
| measureChildWithMargins(child, widthSpec, widthPadding, heightSpec, heightUsed); |
| } |
| heightUsed += child.getMeasuredHeight(); |
| } |
| } |
| |
| final int alwaysShowHeight = heightUsed; |
| |
| // And now the rest. |
| for (int i = 0; i < childCount; i++) { |
| final View child = getChildAt(i); |
| |
| final LayoutParams lp = (LayoutParams) child.getLayoutParams(); |
| if (!lp.alwaysShow && child.getVisibility() != GONE) { |
| if (lp.maxHeight != -1) { |
| final int remainingHeight = heightSize - heightUsed; |
| measureChildWithMargins(child, widthSpec, widthPadding, |
| MeasureSpec.makeMeasureSpec(lp.maxHeight, MeasureSpec.AT_MOST), |
| lp.maxHeight > remainingHeight ? lp.maxHeight - remainingHeight : 0); |
| } else { |
| measureChildWithMargins(child, widthSpec, widthPadding, heightSpec, heightUsed); |
| } |
| heightUsed += child.getMeasuredHeight(); |
| } |
| } |
| |
| final int oldCollapsibleHeight = mCollapsibleHeight; |
| mCollapsibleHeight = Math.max(0, |
| heightUsed - alwaysShowHeight - getMaxCollapsedHeight()); |
| mUncollapsibleHeight = heightUsed - mCollapsibleHeight; |
| |
| updateCollapseOffset(oldCollapsibleHeight, !isDragging()); |
| |
| if (getShowAtTop()) { |
| mTopOffset = 0; |
| } else { |
| mTopOffset = Math.max(0, heightSize - heightUsed) + (int) mCollapseOffset; |
| } |
| |
| setMeasuredDimension(sourceWidth, heightSize); |
| } |
| |
| /** |
| * @return The space reserved by views with 'alwaysShow=true' |
| */ |
| public int getUncollapsibleHeight() { |
| return mUncollapsibleHeight; |
| } |
| |
| @Override |
| protected void onLayout(boolean changed, int l, int t, int r, int b) { |
| final int width = getWidth(); |
| |
| View indicatorHost = null; |
| |
| int ypos = mTopOffset; |
| int leftEdge = getPaddingLeft(); |
| int rightEdge = width - getPaddingRight(); |
| |
| final int childCount = getChildCount(); |
| for (int i = 0; i < childCount; i++) { |
| final View child = getChildAt(i); |
| final LayoutParams lp = (LayoutParams) child.getLayoutParams(); |
| if (lp.hasNestedScrollIndicator) { |
| indicatorHost = child; |
| } |
| |
| if (child.getVisibility() == GONE) { |
| continue; |
| } |
| |
| int top = ypos + lp.topMargin; |
| if (lp.ignoreOffset) { |
| top -= mCollapseOffset; |
| } |
| final int bottom = top + child.getMeasuredHeight(); |
| |
| final int childWidth = child.getMeasuredWidth(); |
| final int widthAvailable = rightEdge - leftEdge; |
| final int left = leftEdge + (widthAvailable - childWidth) / 2; |
| final int right = left + childWidth; |
| |
| child.layout(left, top, right, bottom); |
| |
| ypos = bottom + lp.bottomMargin; |
| } |
| |
| if (mScrollIndicatorDrawable != null) { |
| if (indicatorHost != null) { |
| final int left = indicatorHost.getLeft(); |
| final int right = indicatorHost.getRight(); |
| final int bottom = indicatorHost.getTop(); |
| final int top = bottom - mScrollIndicatorDrawable.getIntrinsicHeight(); |
| mScrollIndicatorDrawable.setBounds(left, top, right, bottom); |
| setWillNotDraw(!isCollapsed()); |
| } else { |
| mScrollIndicatorDrawable = null; |
| setWillNotDraw(true); |
| } |
| } |
| } |
| |
| @Override |
| public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { |
| return new LayoutParams(getContext(), attrs); |
| } |
| |
| @Override |
| protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { |
| if (p instanceof LayoutParams) { |
| return new LayoutParams((LayoutParams) p); |
| } else if (p instanceof MarginLayoutParams) { |
| return new LayoutParams((MarginLayoutParams) p); |
| } |
| return new LayoutParams(p); |
| } |
| |
| @Override |
| protected ViewGroup.LayoutParams generateDefaultLayoutParams() { |
| return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); |
| } |
| |
| @Override |
| protected Parcelable onSaveInstanceState() { |
| final SavedState ss = new SavedState(super.onSaveInstanceState()); |
| ss.open = mCollapsibleHeight > 0 && mCollapseOffset == 0; |
| return ss; |
| } |
| |
| @Override |
| protected void onRestoreInstanceState(Parcelable state) { |
| final SavedState ss = (SavedState) state; |
| super.onRestoreInstanceState(ss.getSuperState()); |
| mOpenOnLayout = ss.open; |
| } |
| |
| public static class LayoutParams extends MarginLayoutParams { |
| public boolean alwaysShow; |
| public boolean ignoreOffset; |
| public boolean hasNestedScrollIndicator; |
| public int maxHeight; |
| |
| public LayoutParams(Context c, AttributeSet attrs) { |
| super(c, attrs); |
| |
| final TypedArray a = c.obtainStyledAttributes(attrs, |
| R.styleable.ResolverDrawerLayout_LayoutParams); |
| alwaysShow = a.getBoolean( |
| R.styleable.ResolverDrawerLayout_LayoutParams_layout_alwaysShow, |
| false); |
| ignoreOffset = a.getBoolean( |
| R.styleable.ResolverDrawerLayout_LayoutParams_layout_ignoreOffset, |
| false); |
| hasNestedScrollIndicator = a.getBoolean( |
| R.styleable.ResolverDrawerLayout_LayoutParams_layout_hasNestedScrollIndicator, |
| false); |
| maxHeight = a.getDimensionPixelSize( |
| R.styleable.ResolverDrawerLayout_LayoutParams_layout_maxHeight, -1); |
| a.recycle(); |
| } |
| |
| public LayoutParams(int width, int height) { |
| super(width, height); |
| } |
| |
| public LayoutParams(LayoutParams source) { |
| super(source); |
| this.alwaysShow = source.alwaysShow; |
| this.ignoreOffset = source.ignoreOffset; |
| this.hasNestedScrollIndicator = source.hasNestedScrollIndicator; |
| this.maxHeight = source.maxHeight; |
| } |
| |
| public LayoutParams(MarginLayoutParams source) { |
| super(source); |
| } |
| |
| public LayoutParams(ViewGroup.LayoutParams source) { |
| super(source); |
| } |
| } |
| |
| static class SavedState extends BaseSavedState { |
| boolean open; |
| |
| SavedState(Parcelable superState) { |
| super(superState); |
| } |
| |
| private SavedState(Parcel in) { |
| super(in); |
| open = in.readInt() != 0; |
| } |
| |
| @Override |
| public void writeToParcel(Parcel out, int flags) { |
| super.writeToParcel(out, flags); |
| out.writeInt(open ? 1 : 0); |
| } |
| |
| public static final Parcelable.Creator<SavedState> CREATOR = |
| new Parcelable.Creator<SavedState>() { |
| @Override |
| public SavedState createFromParcel(Parcel in) { |
| return new SavedState(in); |
| } |
| |
| @Override |
| public SavedState[] newArray(int size) { |
| return new SavedState[size]; |
| } |
| }; |
| } |
| |
| public interface OnDismissedListener { |
| public void onDismissed(); |
| } |
| |
| private class RunOnDismissedListener implements Runnable { |
| @Override |
| public void run() { |
| dispatchOnDismissed(); |
| } |
| } |
| |
| private MetricsLogger getMetricsLogger() { |
| if (mMetricsLogger == null) { |
| mMetricsLogger = new MetricsLogger(); |
| } |
| return mMetricsLogger; |
| } |
| } |