1296 lines
46 KiB
Java
1296 lines
46 KiB
Java
/*
|
|
* 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 static android.content.res.Resources.ID_NULL;
|
|
|
|
import android.annotation.IdRes;
|
|
import android.content.Context;
|
|
import android.content.res.Configuration;
|
|
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 and its res id
|
|
*/
|
|
private int mMaxWidthResId;
|
|
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;
|
|
|
|
/**
|
|
* Whether {@code mMaxCollapsedHeightSmall} was set explicitly as a layout attribute or
|
|
* inferred by {@code mMaxCollapsedHeight}.
|
|
*/
|
|
private final boolean mIsMaxCollapsedHeightSmallExplicit;
|
|
|
|
private boolean mSmallCollapsed;
|
|
|
|
/**
|
|
* Move views down from the top by this much in px
|
|
*/
|
|
private float mCollapseOffset;
|
|
|
|
/**
|
|
* Track fractions of pixels from drag calculations. Without this, the view offsets get
|
|
* out of sync due to frequently dropping fractions of a pixel from '(int) dy' casts.
|
|
*/
|
|
private float mDragRemainder = 0.0f;
|
|
private int mCollapsibleHeight;
|
|
private int mUncollapsibleHeight;
|
|
private int mAlwaysShowHeight;
|
|
|
|
/**
|
|
* 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;
|
|
@IdRes
|
|
private int mIgnoreOffsetTopLimitViewId = ID_NULL;
|
|
|
|
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 OnCollapsedChangedListener mOnCollapsedChangedListener;
|
|
|
|
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 AbsListView mNestedListChild;
|
|
private RecyclerView mNestedRecyclerChild;
|
|
|
|
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);
|
|
mMaxWidthResId = a.getResourceId(R.styleable.ResolverDrawerLayout_maxWidth, -1);
|
|
mMaxWidth = a.getDimensionPixelSize(R.styleable.ResolverDrawerLayout_maxWidth, -1);
|
|
mMaxCollapsedHeight = a.getDimensionPixelSize(
|
|
R.styleable.ResolverDrawerLayout_maxCollapsedHeight, 0);
|
|
mMaxCollapsedHeightSmall = a.getDimensionPixelSize(
|
|
R.styleable.ResolverDrawerLayout_maxCollapsedHeightSmall,
|
|
mMaxCollapsedHeight);
|
|
mIsMaxCollapsedHeightSmallExplicit =
|
|
a.hasValue(R.styleable.ResolverDrawerLayout_maxCollapsedHeightSmall);
|
|
mShowAtTop = a.getBoolean(R.styleable.ResolverDrawerLayout_showAtTop, false);
|
|
if (a.hasValue(R.styleable.ResolverDrawerLayout_ignoreOffsetTopLimit)) {
|
|
mIgnoreOffsetTopLimitViewId = a.getResourceId(
|
|
R.styleable.ResolverDrawerLayout_ignoreOffsetTopLimit, ID_NULL);
|
|
}
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* Dynamically set the max collapsed height. Note this also updates the small collapsed
|
|
* height if it wasn't specified explicitly.
|
|
*/
|
|
public void setMaxCollapsedHeight(int heightInPixels) {
|
|
if (heightInPixels == mMaxCollapsedHeight) {
|
|
return;
|
|
}
|
|
mMaxCollapsedHeight = heightInPixels;
|
|
if (!mIsMaxCollapsedHeightSmallExplicit) {
|
|
mMaxCollapsedHeightSmall = mMaxCollapsedHeight;
|
|
}
|
|
requestLayout();
|
|
}
|
|
|
|
public void setSmallCollapsed(boolean smallCollapsed) {
|
|
if (mSmallCollapsed != smallCollapsed) {
|
|
mSmallCollapsed = smallCollapsed;
|
|
requestLayout();
|
|
}
|
|
}
|
|
|
|
public boolean isSmallCollapsed() {
|
|
return mSmallCollapsed;
|
|
}
|
|
|
|
public boolean isCollapsed() {
|
|
return mCollapseOffset > 0;
|
|
}
|
|
|
|
public void setShowAtTop(boolean showOnTop) {
|
|
if (mShowAtTop != showOnTop) {
|
|
mShowAtTop = showOnTop;
|
|
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;
|
|
if (oldReserved != mCollapsibleHeightReserved) {
|
|
requestLayout();
|
|
}
|
|
|
|
final int dReserved = mCollapsibleHeightReserved - oldReserved;
|
|
if (dReserved != 0 && mIsDragging) {
|
|
mLastTouchY -= dReserved;
|
|
}
|
|
|
|
final int oldCollapsibleHeight = mCollapsibleHeight;
|
|
mCollapsibleHeight = Math.min(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.
|
|
setCollapseOffset(0);
|
|
return false;
|
|
}
|
|
|
|
if (isLaidOut()) {
|
|
final boolean isCollapsedOld = mCollapseOffset != 0;
|
|
if (remainClosed && (oldCollapsibleHeight < mCollapsibleHeight
|
|
&& mCollapseOffset == oldCollapsibleHeight)) {
|
|
// Stay closed even at the new height.
|
|
setCollapseOffset(mCollapsibleHeight);
|
|
} else {
|
|
setCollapseOffset(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
|
|
setCollapseOffset(mOpenOnLayout ? 0 : mCollapsibleHeight);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private void setCollapseOffset(float collapseOffset) {
|
|
if (mCollapseOffset != collapseOffset) {
|
|
mCollapseOffset = collapseOffset;
|
|
requestLayout();
|
|
}
|
|
}
|
|
|
|
private int getMaxCollapsedHeight() {
|
|
return (isSmallCollapsed() ? mMaxCollapsedHeightSmall : mMaxCollapsedHeight)
|
|
+ mCollapsibleHeightReserved;
|
|
}
|
|
|
|
public void setOnDismissedListener(OnDismissedListener listener) {
|
|
mOnDismissedListener = listener;
|
|
}
|
|
|
|
private boolean isDismissable() {
|
|
return mOnDismissedListener != null && !mDismissLocked;
|
|
}
|
|
|
|
public void setOnCollapsedChangedListener(OnCollapsedChangedListener listener) {
|
|
mOnCollapsedChangedListener = listener;
|
|
}
|
|
|
|
@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;
|
|
}
|
|
|
|
private boolean isNestedListChildScrolled() {
|
|
return mNestedListChild != null
|
|
&& mNestedListChild.getChildCount() > 0
|
|
&& (mNestedListChild.getFirstVisiblePosition() > 0
|
|
|| mNestedListChild.getChildAt(0).getTop() < 0);
|
|
}
|
|
|
|
private boolean isNestedRecyclerChildScrolled() {
|
|
if (mNestedRecyclerChild != null && mNestedRecyclerChild.getChildCount() > 0) {
|
|
final RecyclerView.ViewHolder vh =
|
|
mNestedRecyclerChild.findViewHolderForAdapterPosition(0);
|
|
return vh == null || vh.itemView.getTop() < 0;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
@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;
|
|
if (dy > 0 && isNestedListChildScrolled()) {
|
|
mNestedListChild.smoothScrollBy((int) -dy, 0);
|
|
} else if (dy > 0 && isNestedRecyclerChildScrolled()) {
|
|
mNestedRecyclerChild.scrollBy(0, (int) -dy);
|
|
} else {
|
|
performDrag(dy);
|
|
}
|
|
}
|
|
mLastTouchY = y;
|
|
}
|
|
break;
|
|
|
|
case MotionEvent.ACTION_POINTER_DOWN: {
|
|
final int pointerIndex = ev.getActionIndex();
|
|
mActivePointerId = ev.getPointerId(pointerIndex);
|
|
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 {
|
|
scrollNestedScrollableChildBackToTop();
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* Scroll nested scrollable child back to top if it has been scrolled.
|
|
*/
|
|
public void scrollNestedScrollableChildBackToTop() {
|
|
if (isNestedListChildScrolled()) {
|
|
mNestedListChild.smoothScrollToPosition(0);
|
|
} else if (isNestedRecyclerChildScrolled()) {
|
|
mNestedRecyclerChild.smoothScrollToPosition(0);
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
mDragRemainder += dy - (int) dy;
|
|
if (mDragRemainder >= 1.0f) {
|
|
mDragRemainder -= 1.0f;
|
|
dy += 1.0f;
|
|
} else if (mDragRemainder <= -1.0f) {
|
|
mDragRemainder += 1.0f;
|
|
dy -= 1.0f;
|
|
}
|
|
|
|
boolean isIgnoreOffsetLimitSet = false;
|
|
int ignoreOffsetLimit = 0;
|
|
View ignoreOffsetLimitView = findIgnoreOffsetLimitView();
|
|
if (ignoreOffsetLimitView != null) {
|
|
LayoutParams lp = (LayoutParams) ignoreOffsetLimitView.getLayoutParams();
|
|
ignoreOffsetLimit = ignoreOffsetLimitView.getBottom() + lp.bottomMargin;
|
|
isIgnoreOffsetLimitSet = true;
|
|
}
|
|
final int childCount = getChildCount();
|
|
for (int i = 0; i < childCount; i++) {
|
|
final View child = getChildAt(i);
|
|
if (child.getVisibility() == View.GONE) {
|
|
continue;
|
|
}
|
|
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
|
|
if (!lp.ignoreOffset) {
|
|
child.offsetTopAndBottom((int) dy);
|
|
} else if (isIgnoreOffsetLimitSet) {
|
|
int top = child.getTop();
|
|
int targetTop = Math.max(
|
|
(int) (ignoreOffsetLimit + lp.topMargin + dy),
|
|
lp.mFixedTop);
|
|
if (top != targetTop) {
|
|
child.offsetTopAndBottom(targetTop - top);
|
|
}
|
|
ignoreOffsetLimit = child.getBottom() + lp.bottomMargin;
|
|
}
|
|
}
|
|
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);
|
|
}
|
|
|
|
if (mOnCollapsedChangedListener != null) {
|
|
mOnCollapsedChangedListener.onCollapsedChanged(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) {
|
|
if ((nestedScrollAxes & View.SCROLL_AXIS_VERTICAL) != 0) {
|
|
if (target instanceof AbsListView) {
|
|
mNestedListChild = (AbsListView) target;
|
|
}
|
|
if (target instanceof RecyclerView) {
|
|
mNestedRecyclerChild = (RecyclerView) target;
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
@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;
|
|
}
|
|
|
|
private boolean performAccessibilityActionCommon(int action) {
|
|
switch (action) {
|
|
case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
|
|
case AccessibilityNodeInfo.ACTION_EXPAND:
|
|
case R.id.accessibilityActionScrollDown:
|
|
if (mCollapseOffset != 0) {
|
|
smoothScrollTo(0, 0);
|
|
return true;
|
|
}
|
|
break;
|
|
case AccessibilityNodeInfo.ACTION_COLLAPSE:
|
|
if (mCollapseOffset < mCollapsibleHeight) {
|
|
smoothScrollTo(mCollapsibleHeight, 0);
|
|
return true;
|
|
}
|
|
break;
|
|
case AccessibilityNodeInfo.ACTION_DISMISS:
|
|
if ((mCollapseOffset < mCollapsibleHeight + mUncollapsibleHeight)
|
|
&& isDismissable()) {
|
|
smoothScrollTo(mCollapsibleHeight + mUncollapsibleHeight, 0);
|
|
mDismissOnScrollerFinished = true;
|
|
return true;
|
|
}
|
|
break;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public boolean onNestedPrePerformAccessibilityAction(View target, int action, Bundle args) {
|
|
if (super.onNestedPrePerformAccessibilityAction(target, action, args)) {
|
|
return true;
|
|
}
|
|
|
|
return performAccessibilityActionCommon(action);
|
|
}
|
|
|
|
@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(AccessibilityAction.ACTION_SCROLL_FORWARD);
|
|
info.addAction(AccessibilityAction.ACTION_EXPAND);
|
|
info.addAction(AccessibilityAction.ACTION_SCROLL_DOWN);
|
|
info.setScrollable(true);
|
|
}
|
|
if ((mCollapseOffset < mCollapsibleHeight + mUncollapsibleHeight)
|
|
&& ((mCollapseOffset < mCollapsibleHeight) || isDismissable())) {
|
|
info.addAction(AccessibilityAction.ACTION_SCROLL_UP);
|
|
info.setScrollable(true);
|
|
}
|
|
if (mCollapseOffset < mCollapsibleHeight) {
|
|
info.addAction(AccessibilityAction.ACTION_COLLAPSE);
|
|
}
|
|
if (mCollapseOffset < mCollapsibleHeight + mUncollapsibleHeight && isDismissable()) {
|
|
info.addAction(AccessibilityAction.ACTION_DISMISS);
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
return performAccessibilityActionCommon(action);
|
|
}
|
|
|
|
@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;
|
|
final 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 + getPaddingLeft() + getPaddingRight());
|
|
}
|
|
|
|
final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);
|
|
final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY);
|
|
|
|
// 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 = 0;
|
|
|
|
// 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, 0,
|
|
MeasureSpec.makeMeasureSpec(lp.maxHeight, MeasureSpec.AT_MOST),
|
|
lp.maxHeight > remainingHeight ? lp.maxHeight - remainingHeight : 0);
|
|
} else {
|
|
measureChildWithMargins(child, widthSpec, 0, heightSpec, heightUsed);
|
|
}
|
|
heightUsed += child.getMeasuredHeight();
|
|
}
|
|
}
|
|
|
|
mAlwaysShowHeight = 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, 0,
|
|
MeasureSpec.makeMeasureSpec(lp.maxHeight, MeasureSpec.AT_MOST),
|
|
lp.maxHeight > remainingHeight ? lp.maxHeight - remainingHeight : 0);
|
|
} else {
|
|
measureChildWithMargins(child, widthSpec, 0, heightSpec, heightUsed);
|
|
}
|
|
heightUsed += child.getMeasuredHeight();
|
|
}
|
|
}
|
|
|
|
final int oldCollapsibleHeight = mCollapsibleHeight;
|
|
mCollapsibleHeight = Math.max(0,
|
|
heightUsed - mAlwaysShowHeight - 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 getAlwaysShowHeight() {
|
|
return mAlwaysShowHeight;
|
|
}
|
|
|
|
/**
|
|
* Max width of the drawer needs to be updated after the configuration is changed.
|
|
* For example, foldables have different layout width when the device is folded and unfolded.
|
|
*/
|
|
@Override
|
|
protected void onConfigurationChanged(Configuration newConfig) {
|
|
super.onConfigurationChanged(newConfig);
|
|
if (mMaxWidthResId > 0) {
|
|
mMaxWidth = getResources().getDimensionPixelSize(mMaxWidthResId);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void onLayout(boolean changed, int l, int t, int r, int b) {
|
|
final int width = getWidth();
|
|
|
|
View indicatorHost = null;
|
|
|
|
int ypos = mTopOffset;
|
|
final int leftEdge = getPaddingLeft();
|
|
final int rightEdge = width - getPaddingRight();
|
|
final int widthAvailable = rightEdge - leftEdge;
|
|
|
|
boolean isIgnoreOffsetLimitSet = false;
|
|
int ignoreOffsetLimit = 0;
|
|
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;
|
|
}
|
|
|
|
if (mIgnoreOffsetTopLimitViewId != ID_NULL && !isIgnoreOffsetLimitSet) {
|
|
if (mIgnoreOffsetTopLimitViewId == child.getId()) {
|
|
ignoreOffsetLimit = child.getBottom() + lp.bottomMargin;
|
|
isIgnoreOffsetLimitSet = true;
|
|
}
|
|
}
|
|
|
|
int top = ypos + lp.topMargin;
|
|
if (lp.ignoreOffset) {
|
|
if (!isDragging()) {
|
|
lp.mFixedTop = (int) (top - mCollapseOffset);
|
|
}
|
|
if (isIgnoreOffsetLimitSet) {
|
|
top = Math.max(ignoreOffsetLimit + lp.topMargin, (int) (top - mCollapseOffset));
|
|
ignoreOffsetLimit = top + child.getMeasuredHeight() + lp.bottomMargin;
|
|
} else {
|
|
top -= mCollapseOffset;
|
|
}
|
|
}
|
|
final int bottom = top + child.getMeasuredHeight();
|
|
|
|
final int childWidth = child.getMeasuredWidth();
|
|
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;
|
|
ss.mCollapsibleHeightReserved = mCollapsibleHeightReserved;
|
|
return ss;
|
|
}
|
|
|
|
@Override
|
|
protected void onRestoreInstanceState(Parcelable state) {
|
|
final SavedState ss = (SavedState) state;
|
|
super.onRestoreInstanceState(ss.getSuperState());
|
|
mOpenOnLayout = ss.open;
|
|
mCollapsibleHeightReserved = ss.mCollapsibleHeightReserved;
|
|
}
|
|
|
|
private View findIgnoreOffsetLimitView() {
|
|
if (mIgnoreOffsetTopLimitViewId == ID_NULL) {
|
|
return null;
|
|
}
|
|
View v = findViewById(mIgnoreOffsetTopLimitViewId);
|
|
if (v != null && v != this && v.getParent() == this && v.getVisibility() != View.GONE) {
|
|
return v;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public static class LayoutParams extends MarginLayoutParams {
|
|
public boolean alwaysShow;
|
|
public boolean ignoreOffset;
|
|
public boolean hasNestedScrollIndicator;
|
|
public int maxHeight;
|
|
int mFixedTop;
|
|
|
|
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;
|
|
private int mCollapsibleHeightReserved;
|
|
|
|
SavedState(Parcelable superState) {
|
|
super(superState);
|
|
}
|
|
|
|
private SavedState(Parcel in) {
|
|
super(in);
|
|
open = in.readInt() != 0;
|
|
mCollapsibleHeightReserved = in.readInt();
|
|
}
|
|
|
|
@Override
|
|
public void writeToParcel(Parcel out, int flags) {
|
|
super.writeToParcel(out, flags);
|
|
out.writeInt(open ? 1 : 0);
|
|
out.writeInt(mCollapsibleHeightReserved);
|
|
}
|
|
|
|
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];
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Listener for sheet dismissed events.
|
|
*/
|
|
public interface OnDismissedListener {
|
|
/**
|
|
* Callback when the sheet is dismissed by the user.
|
|
*/
|
|
void onDismissed();
|
|
}
|
|
|
|
/**
|
|
* Listener for sheet collapsed / expanded events.
|
|
*/
|
|
public interface OnCollapsedChangedListener {
|
|
/**
|
|
* Callback when the sheet is either fully expanded or collapsed.
|
|
* @param isCollapsed true when collapsed, false when expanded.
|
|
*/
|
|
void onCollapsedChanged(boolean isCollapsed);
|
|
}
|
|
|
|
private class RunOnDismissedListener implements Runnable {
|
|
@Override
|
|
public void run() {
|
|
dispatchOnDismissed();
|
|
}
|
|
}
|
|
|
|
private MetricsLogger getMetricsLogger() {
|
|
if (mMetricsLogger == null) {
|
|
mMetricsLogger = new MetricsLogger();
|
|
}
|
|
return mMetricsLogger;
|
|
}
|
|
}
|