1697 lines
60 KiB
Java
1697 lines
60 KiB
Java
/*
|
|
* Copyright (C) 2008 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 android.widget;
|
|
|
|
import android.animation.Animator;
|
|
import android.animation.Animator.AnimatorListener;
|
|
import android.animation.AnimatorListenerAdapter;
|
|
import android.animation.AnimatorSet;
|
|
import android.animation.ObjectAnimator;
|
|
import android.animation.PropertyValuesHolder;
|
|
import android.annotation.StyleRes;
|
|
import android.compat.annotation.UnsupportedAppUsage;
|
|
import android.content.Context;
|
|
import android.content.res.ColorStateList;
|
|
import android.content.res.TypedArray;
|
|
import android.graphics.Rect;
|
|
import android.graphics.drawable.Drawable;
|
|
import android.os.Build;
|
|
import android.os.SystemClock;
|
|
import android.text.TextUtils;
|
|
import android.text.TextUtils.TruncateAt;
|
|
import android.util.IntProperty;
|
|
import android.util.MathUtils;
|
|
import android.util.Property;
|
|
import android.util.TypedValue;
|
|
import android.view.Gravity;
|
|
import android.view.MotionEvent;
|
|
import android.view.PointerIcon;
|
|
import android.view.View;
|
|
import android.view.View.MeasureSpec;
|
|
import android.view.ViewConfiguration;
|
|
import android.view.ViewGroup.LayoutParams;
|
|
import android.view.ViewGroupOverlay;
|
|
import android.widget.AbsListView.OnScrollListener;
|
|
import android.widget.ImageView.ScaleType;
|
|
|
|
import com.android.internal.R;
|
|
|
|
/**
|
|
* Helper class for AbsListView to draw and control the Fast Scroll thumb
|
|
*/
|
|
class FastScroller {
|
|
/** Duration of fade-out animation. */
|
|
private static final int DURATION_FADE_OUT = 300;
|
|
|
|
/** Duration of fade-in animation. */
|
|
private static final int DURATION_FADE_IN = 150;
|
|
|
|
/** Duration of transition cross-fade animation. */
|
|
private static final int DURATION_CROSS_FADE = 50;
|
|
|
|
/** Duration of transition resize animation. */
|
|
private static final int DURATION_RESIZE = 100;
|
|
|
|
/** Inactivity timeout before fading controls. */
|
|
private static final long FADE_TIMEOUT = 1500;
|
|
|
|
/** Minimum number of pages to justify showing a fast scroll thumb. */
|
|
private static final int MIN_PAGES = 4;
|
|
|
|
/** Scroll thumb and preview not showing. */
|
|
private static final int STATE_NONE = 0;
|
|
|
|
/** Scroll thumb visible and moving along with the scrollbar. */
|
|
private static final int STATE_VISIBLE = 1;
|
|
|
|
/** Scroll thumb and preview being dragged by user. */
|
|
private static final int STATE_DRAGGING = 2;
|
|
|
|
// Positions for preview image and text.
|
|
private static final int OVERLAY_FLOATING = 0;
|
|
private static final int OVERLAY_AT_THUMB = 1;
|
|
private static final int OVERLAY_ABOVE_THUMB = 2;
|
|
|
|
// Positions for thumb in relation to track.
|
|
private static final int THUMB_POSITION_MIDPOINT = 0;
|
|
private static final int THUMB_POSITION_INSIDE = 1;
|
|
|
|
// Indices for mPreviewResId.
|
|
private static final int PREVIEW_LEFT = 0;
|
|
private static final int PREVIEW_RIGHT = 1;
|
|
|
|
/** Delay before considering a tap in the thumb area to be a drag. */
|
|
private static final long TAP_TIMEOUT = ViewConfiguration.getTapTimeout();
|
|
|
|
private final Rect mTempBounds = new Rect();
|
|
private final Rect mTempMargins = new Rect();
|
|
@UnsupportedAppUsage
|
|
private final Rect mContainerRect = new Rect();
|
|
|
|
private final AbsListView mList;
|
|
private final ViewGroupOverlay mOverlay;
|
|
private final TextView mPrimaryText;
|
|
private final TextView mSecondaryText;
|
|
@UnsupportedAppUsage
|
|
private final ImageView mThumbImage;
|
|
@UnsupportedAppUsage
|
|
private final ImageView mTrackImage;
|
|
private final View mPreviewImage;
|
|
/**
|
|
* Preview image resource IDs for left- and right-aligned layouts. See
|
|
* {@link #PREVIEW_LEFT} and {@link #PREVIEW_RIGHT}.
|
|
*/
|
|
private final int[] mPreviewResId = new int[2];
|
|
|
|
/** The minimum touch target size in pixels. */
|
|
@UnsupportedAppUsage
|
|
private final int mMinimumTouchTarget;
|
|
|
|
/**
|
|
* Padding in pixels around the preview text. Applied as layout margins to
|
|
* the preview text and padding to the preview image.
|
|
*/
|
|
private int mPreviewPadding;
|
|
|
|
private int mPreviewMinWidth;
|
|
private int mPreviewMinHeight;
|
|
private int mThumbMinWidth;
|
|
private int mThumbMinHeight;
|
|
|
|
/** Theme-specified text size. Used only if text appearance is not set. */
|
|
private float mTextSize;
|
|
|
|
/** Theme-specified text color. Used only if text appearance is not set. */
|
|
private ColorStateList mTextColor;
|
|
|
|
@UnsupportedAppUsage
|
|
private Drawable mThumbDrawable;
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
private Drawable mTrackDrawable;
|
|
private int mTextAppearance;
|
|
private int mThumbPosition;
|
|
|
|
// Used to convert between y-coordinate and thumb position within track.
|
|
private float mThumbOffset;
|
|
private float mThumbRange;
|
|
|
|
/** Total width of decorations. */
|
|
private int mWidth;
|
|
|
|
/** Set containing decoration transition animations. */
|
|
private AnimatorSet mDecorAnimation;
|
|
|
|
/** Set containing preview text transition animations. */
|
|
private AnimatorSet mPreviewAnimation;
|
|
|
|
/** Whether the primary text is showing. */
|
|
private boolean mShowingPrimary;
|
|
|
|
/** Whether we're waiting for completion of scrollTo(). */
|
|
private boolean mScrollCompleted;
|
|
|
|
/** The position of the first visible item in the list. */
|
|
private int mFirstVisibleItem;
|
|
|
|
/** The number of headers at the top of the view. */
|
|
@UnsupportedAppUsage
|
|
private int mHeaderCount;
|
|
|
|
/** The index of the current section. */
|
|
private int mCurrentSection = -1;
|
|
|
|
/** The current scrollbar position. */
|
|
private int mScrollbarPosition = -1;
|
|
|
|
/** Whether the list is long enough to need a fast scroller. */
|
|
@UnsupportedAppUsage
|
|
private boolean mLongList;
|
|
|
|
private Object[] mSections;
|
|
|
|
/** Whether this view is currently performing layout. */
|
|
private boolean mUpdatingLayout;
|
|
|
|
/**
|
|
* Current decoration state, one of:
|
|
* <ul>
|
|
* <li>{@link #STATE_NONE}, nothing visible
|
|
* <li>{@link #STATE_VISIBLE}, showing track and thumb
|
|
* <li>{@link #STATE_DRAGGING}, visible and showing preview
|
|
* </ul>
|
|
*/
|
|
private int mState;
|
|
|
|
/** Whether the preview image is visible. */
|
|
private boolean mShowingPreview;
|
|
|
|
private Adapter mListAdapter;
|
|
private SectionIndexer mSectionIndexer;
|
|
|
|
/** Whether decorations should be laid out from right to left. */
|
|
private boolean mLayoutFromRight;
|
|
|
|
/** Whether the fast scroller is enabled. */
|
|
private boolean mEnabled;
|
|
|
|
/** Whether the scrollbar and decorations should always be shown. */
|
|
private boolean mAlwaysShow;
|
|
|
|
/**
|
|
* Position for the preview image and text. One of:
|
|
* <ul>
|
|
* <li>{@link #OVERLAY_FLOATING}
|
|
* <li>{@link #OVERLAY_AT_THUMB}
|
|
* <li>{@link #OVERLAY_ABOVE_THUMB}
|
|
* </ul>
|
|
*/
|
|
private int mOverlayPosition;
|
|
|
|
/** Current scrollbar style, including inset and overlay properties. */
|
|
private int mScrollBarStyle;
|
|
|
|
/** Whether to precisely match the thumb position to the list. */
|
|
private boolean mMatchDragPosition;
|
|
|
|
private float mInitialTouchY;
|
|
private long mPendingDrag = -1;
|
|
private int mScaledTouchSlop;
|
|
|
|
private int mOldItemCount;
|
|
private int mOldChildCount;
|
|
|
|
/**
|
|
* Used to delay hiding fast scroll decorations.
|
|
*/
|
|
private final Runnable mDeferHide = new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
setState(STATE_NONE);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Used to effect a transition from primary to secondary text.
|
|
*/
|
|
private final AnimatorListener mSwitchPrimaryListener = new AnimatorListenerAdapter() {
|
|
@Override
|
|
public void onAnimationEnd(Animator animation) {
|
|
mShowingPrimary = !mShowingPrimary;
|
|
}
|
|
};
|
|
|
|
@UnsupportedAppUsage
|
|
public FastScroller(AbsListView listView, int styleResId) {
|
|
mList = listView;
|
|
mOldItemCount = listView.getCount();
|
|
mOldChildCount = listView.getChildCount();
|
|
|
|
final Context context = listView.getContext();
|
|
mScaledTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
|
|
mScrollBarStyle = listView.getScrollBarStyle();
|
|
|
|
mScrollCompleted = true;
|
|
mState = STATE_VISIBLE;
|
|
mMatchDragPosition =
|
|
context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB;
|
|
|
|
mTrackImage = new ImageView(context);
|
|
mTrackImage.setScaleType(ScaleType.FIT_XY);
|
|
mThumbImage = new ImageView(context);
|
|
mThumbImage.setScaleType(ScaleType.FIT_XY);
|
|
mPreviewImage = new View(context);
|
|
mPreviewImage.setAlpha(0f);
|
|
|
|
mPrimaryText = createPreviewTextView(context);
|
|
mSecondaryText = createPreviewTextView(context);
|
|
|
|
mMinimumTouchTarget = listView.getResources().getDimensionPixelSize(
|
|
com.android.internal.R.dimen.fast_scroller_minimum_touch_target);
|
|
|
|
setStyle(styleResId);
|
|
|
|
final ViewGroupOverlay overlay = listView.getOverlay();
|
|
mOverlay = overlay;
|
|
overlay.add(mTrackImage);
|
|
overlay.add(mThumbImage);
|
|
overlay.add(mPreviewImage);
|
|
overlay.add(mPrimaryText);
|
|
overlay.add(mSecondaryText);
|
|
|
|
getSectionsFromIndexer();
|
|
updateLongList(mOldChildCount, mOldItemCount);
|
|
setScrollbarPosition(listView.getVerticalScrollbarPosition());
|
|
postAutoHide();
|
|
}
|
|
|
|
private void updateAppearance() {
|
|
int width = 0;
|
|
|
|
// Add track to overlay if it has an image.
|
|
mTrackImage.setImageDrawable(mTrackDrawable);
|
|
if (mTrackDrawable != null) {
|
|
width = Math.max(width, mTrackDrawable.getIntrinsicWidth());
|
|
}
|
|
|
|
// Add thumb to overlay if it has an image.
|
|
mThumbImage.setImageDrawable(mThumbDrawable);
|
|
mThumbImage.setMinimumWidth(mThumbMinWidth);
|
|
mThumbImage.setMinimumHeight(mThumbMinHeight);
|
|
if (mThumbDrawable != null) {
|
|
width = Math.max(width, mThumbDrawable.getIntrinsicWidth());
|
|
}
|
|
|
|
// Account for minimum thumb width.
|
|
mWidth = Math.max(width, mThumbMinWidth);
|
|
|
|
if (mTextAppearance != 0) {
|
|
mPrimaryText.setTextAppearance(mTextAppearance);
|
|
mSecondaryText.setTextAppearance(mTextAppearance);
|
|
}
|
|
|
|
if (mTextColor != null) {
|
|
mPrimaryText.setTextColor(mTextColor);
|
|
mSecondaryText.setTextColor(mTextColor);
|
|
}
|
|
|
|
if (mTextSize > 0) {
|
|
mPrimaryText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize);
|
|
mSecondaryText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize);
|
|
}
|
|
|
|
final int padding = mPreviewPadding;
|
|
mPrimaryText.setIncludeFontPadding(false);
|
|
mPrimaryText.setPadding(padding, padding, padding, padding);
|
|
mSecondaryText.setIncludeFontPadding(false);
|
|
mSecondaryText.setPadding(padding, padding, padding, padding);
|
|
|
|
refreshDrawablePressedState();
|
|
}
|
|
|
|
public void setStyle(@StyleRes int resId) {
|
|
final Context context = mList.getContext();
|
|
final TypedArray ta = context.obtainStyledAttributes(null,
|
|
R.styleable.FastScroll, R.attr.fastScrollStyle, resId);
|
|
final int N = ta.getIndexCount();
|
|
for (int i = 0; i < N; i++) {
|
|
final int index = ta.getIndex(i);
|
|
switch (index) {
|
|
case R.styleable.FastScroll_position:
|
|
mOverlayPosition = ta.getInt(index, OVERLAY_FLOATING);
|
|
break;
|
|
case R.styleable.FastScroll_backgroundLeft:
|
|
mPreviewResId[PREVIEW_LEFT] = ta.getResourceId(index, 0);
|
|
break;
|
|
case R.styleable.FastScroll_backgroundRight:
|
|
mPreviewResId[PREVIEW_RIGHT] = ta.getResourceId(index, 0);
|
|
break;
|
|
case R.styleable.FastScroll_thumbDrawable:
|
|
mThumbDrawable = ta.getDrawable(index);
|
|
break;
|
|
case R.styleable.FastScroll_trackDrawable:
|
|
mTrackDrawable = ta.getDrawable(index);
|
|
break;
|
|
case R.styleable.FastScroll_textAppearance:
|
|
mTextAppearance = ta.getResourceId(index, 0);
|
|
break;
|
|
case R.styleable.FastScroll_textColor:
|
|
mTextColor = ta.getColorStateList(index);
|
|
break;
|
|
case R.styleable.FastScroll_textSize:
|
|
mTextSize = ta.getDimensionPixelSize(index, 0);
|
|
break;
|
|
case R.styleable.FastScroll_minWidth:
|
|
mPreviewMinWidth = ta.getDimensionPixelSize(index, 0);
|
|
break;
|
|
case R.styleable.FastScroll_minHeight:
|
|
mPreviewMinHeight = ta.getDimensionPixelSize(index, 0);
|
|
break;
|
|
case R.styleable.FastScroll_thumbMinWidth:
|
|
mThumbMinWidth = ta.getDimensionPixelSize(index, 0);
|
|
break;
|
|
case R.styleable.FastScroll_thumbMinHeight:
|
|
mThumbMinHeight = ta.getDimensionPixelSize(index, 0);
|
|
break;
|
|
case R.styleable.FastScroll_padding:
|
|
mPreviewPadding = ta.getDimensionPixelSize(index, 0);
|
|
break;
|
|
case R.styleable.FastScroll_thumbPosition:
|
|
mThumbPosition = ta.getInt(index, THUMB_POSITION_MIDPOINT);
|
|
break;
|
|
}
|
|
}
|
|
ta.recycle();
|
|
|
|
updateAppearance();
|
|
}
|
|
|
|
/**
|
|
* Removes this FastScroller overlay from the host view.
|
|
*/
|
|
@UnsupportedAppUsage
|
|
public void remove() {
|
|
mOverlay.remove(mTrackImage);
|
|
mOverlay.remove(mThumbImage);
|
|
mOverlay.remove(mPreviewImage);
|
|
mOverlay.remove(mPrimaryText);
|
|
mOverlay.remove(mSecondaryText);
|
|
}
|
|
|
|
/**
|
|
* @param enabled Whether the fast scroll thumb is enabled.
|
|
*/
|
|
public void setEnabled(boolean enabled) {
|
|
if (mEnabled != enabled) {
|
|
mEnabled = enabled;
|
|
|
|
onStateDependencyChanged(true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return Whether the fast scroll thumb is enabled.
|
|
*/
|
|
public boolean isEnabled() {
|
|
return mEnabled && (mLongList || mAlwaysShow);
|
|
}
|
|
|
|
/**
|
|
* @param alwaysShow Whether the fast scroll thumb should always be shown
|
|
*/
|
|
public void setAlwaysShow(boolean alwaysShow) {
|
|
if (mAlwaysShow != alwaysShow) {
|
|
mAlwaysShow = alwaysShow;
|
|
|
|
onStateDependencyChanged(false);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return Whether the fast scroll thumb will always be shown
|
|
* @see #setAlwaysShow(boolean)
|
|
*/
|
|
public boolean isAlwaysShowEnabled() {
|
|
return mAlwaysShow;
|
|
}
|
|
|
|
/**
|
|
* Called when one of the variables affecting enabled state changes.
|
|
*
|
|
* @param peekIfEnabled whether the thumb should peek, if enabled
|
|
*/
|
|
private void onStateDependencyChanged(boolean peekIfEnabled) {
|
|
if (isEnabled()) {
|
|
if (isAlwaysShowEnabled()) {
|
|
setState(STATE_VISIBLE);
|
|
} else if (mState == STATE_VISIBLE) {
|
|
postAutoHide();
|
|
} else if (peekIfEnabled) {
|
|
setState(STATE_VISIBLE);
|
|
postAutoHide();
|
|
}
|
|
} else {
|
|
stop();
|
|
}
|
|
|
|
mList.resolvePadding();
|
|
}
|
|
|
|
public void setScrollBarStyle(int style) {
|
|
if (mScrollBarStyle != style) {
|
|
mScrollBarStyle = style;
|
|
|
|
updateLayout();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Immediately transitions the fast scroller decorations to a hidden state.
|
|
*/
|
|
public void stop() {
|
|
setState(STATE_NONE);
|
|
}
|
|
|
|
public void setScrollbarPosition(int position) {
|
|
if (position == View.SCROLLBAR_POSITION_DEFAULT) {
|
|
position = mList.isLayoutRtl() ?
|
|
View.SCROLLBAR_POSITION_LEFT : View.SCROLLBAR_POSITION_RIGHT;
|
|
}
|
|
|
|
if (mScrollbarPosition != position) {
|
|
mScrollbarPosition = position;
|
|
mLayoutFromRight = position != View.SCROLLBAR_POSITION_LEFT;
|
|
|
|
final int previewResId = mPreviewResId[mLayoutFromRight ? PREVIEW_RIGHT : PREVIEW_LEFT];
|
|
mPreviewImage.setBackgroundResource(previewResId);
|
|
|
|
// Propagate padding to text min width/height.
|
|
final int textMinWidth = Math.max(0, mPreviewMinWidth - mPreviewImage.getPaddingLeft()
|
|
- mPreviewImage.getPaddingRight());
|
|
mPrimaryText.setMinimumWidth(textMinWidth);
|
|
mSecondaryText.setMinimumWidth(textMinWidth);
|
|
|
|
final int textMinHeight = Math.max(0, mPreviewMinHeight - mPreviewImage.getPaddingTop()
|
|
- mPreviewImage.getPaddingBottom());
|
|
mPrimaryText.setMinimumHeight(textMinHeight);
|
|
mSecondaryText.setMinimumHeight(textMinHeight);
|
|
|
|
// Requires re-layout.
|
|
updateLayout();
|
|
}
|
|
}
|
|
|
|
public int getWidth() {
|
|
return mWidth;
|
|
}
|
|
|
|
@UnsupportedAppUsage
|
|
public void onSizeChanged(int w, int h, int oldw, int oldh) {
|
|
updateLayout();
|
|
}
|
|
|
|
public void onItemCountChanged(int childCount, int itemCount) {
|
|
if (mOldItemCount != itemCount || mOldChildCount != childCount) {
|
|
mOldItemCount = itemCount;
|
|
mOldChildCount = childCount;
|
|
|
|
final boolean hasMoreItems = itemCount - childCount > 0;
|
|
if (hasMoreItems && mState != STATE_DRAGGING) {
|
|
final int firstVisibleItem = mList.getFirstVisiblePosition();
|
|
setThumbPos(getPosFromItemCount(firstVisibleItem, childCount, itemCount));
|
|
}
|
|
|
|
updateLongList(childCount, itemCount);
|
|
}
|
|
}
|
|
|
|
private void updateLongList(int childCount, int itemCount) {
|
|
final boolean longList = childCount > 0 && itemCount / childCount >= MIN_PAGES;
|
|
if (mLongList != longList) {
|
|
mLongList = longList;
|
|
|
|
onStateDependencyChanged(false);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a view into which preview text can be placed.
|
|
*/
|
|
private TextView createPreviewTextView(Context context) {
|
|
final LayoutParams params = new LayoutParams(
|
|
LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
|
|
final TextView textView = new TextView(context);
|
|
textView.setLayoutParams(params);
|
|
textView.setSingleLine(true);
|
|
textView.setEllipsize(TruncateAt.MIDDLE);
|
|
textView.setGravity(Gravity.CENTER);
|
|
textView.setAlpha(0f);
|
|
|
|
// Manually propagate inherited layout direction.
|
|
textView.setLayoutDirection(mList.getLayoutDirection());
|
|
|
|
return textView;
|
|
}
|
|
|
|
/**
|
|
* Measures and layouts the scrollbar and decorations.
|
|
*/
|
|
public void updateLayout() {
|
|
// Prevent re-entry when RTL properties change as a side-effect of
|
|
// resolving padding.
|
|
if (mUpdatingLayout) {
|
|
return;
|
|
}
|
|
|
|
mUpdatingLayout = true;
|
|
|
|
updateContainerRect();
|
|
|
|
layoutThumb();
|
|
layoutTrack();
|
|
|
|
updateOffsetAndRange();
|
|
|
|
final Rect bounds = mTempBounds;
|
|
measurePreview(mPrimaryText, bounds);
|
|
applyLayout(mPrimaryText, bounds);
|
|
measurePreview(mSecondaryText, bounds);
|
|
applyLayout(mSecondaryText, bounds);
|
|
|
|
if (mPreviewImage != null) {
|
|
// Apply preview image padding.
|
|
bounds.left -= mPreviewImage.getPaddingLeft();
|
|
bounds.top -= mPreviewImage.getPaddingTop();
|
|
bounds.right += mPreviewImage.getPaddingRight();
|
|
bounds.bottom += mPreviewImage.getPaddingBottom();
|
|
applyLayout(mPreviewImage, bounds);
|
|
}
|
|
|
|
mUpdatingLayout = false;
|
|
}
|
|
|
|
/**
|
|
* Layouts a view within the specified bounds and pins the pivot point to
|
|
* the appropriate edge.
|
|
*
|
|
* @param view The view to layout.
|
|
* @param bounds Bounds at which to layout the view.
|
|
*/
|
|
private void applyLayout(View view, Rect bounds) {
|
|
view.layout(bounds.left, bounds.top, bounds.right, bounds.bottom);
|
|
view.setPivotX(mLayoutFromRight ? bounds.right - bounds.left : 0);
|
|
}
|
|
|
|
/**
|
|
* Measures the preview text bounds, taking preview image padding into
|
|
* account. This method should only be called after {@link #layoutThumb()}
|
|
* and {@link #layoutTrack()} have both been called at least once.
|
|
*
|
|
* @param v The preview text view to measure.
|
|
* @param out Rectangle into which measured bounds are placed.
|
|
*/
|
|
private void measurePreview(View v, Rect out) {
|
|
// Apply the preview image's padding as layout margins.
|
|
final Rect margins = mTempMargins;
|
|
margins.left = mPreviewImage.getPaddingLeft();
|
|
margins.top = mPreviewImage.getPaddingTop();
|
|
margins.right = mPreviewImage.getPaddingRight();
|
|
margins.bottom = mPreviewImage.getPaddingBottom();
|
|
|
|
if (mOverlayPosition == OVERLAY_FLOATING) {
|
|
measureFloating(v, margins, out);
|
|
} else {
|
|
measureViewToSide(v, mThumbImage, margins, out);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Measures the bounds for a view that should be laid out against the edge
|
|
* of an adjacent view. If no adjacent view is provided, lays out against
|
|
* the list edge.
|
|
*
|
|
* @param view The view to measure for layout.
|
|
* @param adjacent (Optional) The adjacent view, may be null to align to the
|
|
* list edge.
|
|
* @param margins Layout margins to apply to the view.
|
|
* @param out Rectangle into which measured bounds are placed.
|
|
*/
|
|
private void measureViewToSide(View view, View adjacent, Rect margins, Rect out) {
|
|
final int marginLeft;
|
|
final int marginTop;
|
|
final int marginRight;
|
|
if (margins == null) {
|
|
marginLeft = 0;
|
|
marginTop = 0;
|
|
marginRight = 0;
|
|
} else {
|
|
marginLeft = margins.left;
|
|
marginTop = margins.top;
|
|
marginRight = margins.right;
|
|
}
|
|
|
|
final Rect container = mContainerRect;
|
|
final int containerWidth = container.width();
|
|
final int maxWidth;
|
|
if (adjacent == null) {
|
|
maxWidth = containerWidth;
|
|
} else if (mLayoutFromRight) {
|
|
maxWidth = adjacent.getLeft();
|
|
} else {
|
|
maxWidth = containerWidth - adjacent.getRight();
|
|
}
|
|
|
|
final int adjMaxHeight = Math.max(0, container.height());
|
|
final int adjMaxWidth = Math.max(0, maxWidth - marginLeft - marginRight);
|
|
final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(adjMaxWidth, MeasureSpec.AT_MOST);
|
|
final int heightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
|
|
adjMaxHeight, MeasureSpec.UNSPECIFIED);
|
|
view.measure(widthMeasureSpec, heightMeasureSpec);
|
|
|
|
// Align to the left or right.
|
|
final int width = Math.min(adjMaxWidth, view.getMeasuredWidth());
|
|
final int left;
|
|
final int right;
|
|
if (mLayoutFromRight) {
|
|
right = (adjacent == null ? container.right : adjacent.getLeft()) - marginRight;
|
|
left = right - width;
|
|
} else {
|
|
left = (adjacent == null ? container.left : adjacent.getRight()) + marginLeft;
|
|
right = left + width;
|
|
}
|
|
|
|
// Don't adjust the vertical position.
|
|
final int top = marginTop;
|
|
final int bottom = top + view.getMeasuredHeight();
|
|
out.set(left, top, right, bottom);
|
|
}
|
|
|
|
private void measureFloating(View preview, Rect margins, Rect out) {
|
|
final int marginLeft;
|
|
final int marginTop;
|
|
final int marginRight;
|
|
if (margins == null) {
|
|
marginLeft = 0;
|
|
marginTop = 0;
|
|
marginRight = 0;
|
|
} else {
|
|
marginLeft = margins.left;
|
|
marginTop = margins.top;
|
|
marginRight = margins.right;
|
|
}
|
|
|
|
final Rect container = mContainerRect;
|
|
final int containerWidth = container.width();
|
|
final int adjMaxHeight = Math.max(0, container.height());
|
|
final int adjMaxWidth = Math.max(0, containerWidth - marginLeft - marginRight);
|
|
final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(adjMaxWidth, MeasureSpec.AT_MOST);
|
|
final int heightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
|
|
adjMaxHeight, MeasureSpec.UNSPECIFIED);
|
|
preview.measure(widthMeasureSpec, heightMeasureSpec);
|
|
|
|
// Align at the vertical center, 10% from the top.
|
|
final int containerHeight = container.height();
|
|
final int width = preview.getMeasuredWidth();
|
|
final int top = containerHeight / 10 + marginTop + container.top;
|
|
final int bottom = top + preview.getMeasuredHeight();
|
|
final int left = (containerWidth - width) / 2 + container.left;
|
|
final int right = left + width;
|
|
out.set(left, top, right, bottom);
|
|
}
|
|
|
|
/**
|
|
* Updates the container rectangle used for layout.
|
|
*/
|
|
private void updateContainerRect() {
|
|
final AbsListView list = mList;
|
|
list.resolvePadding();
|
|
|
|
final Rect container = mContainerRect;
|
|
container.left = 0;
|
|
container.top = 0;
|
|
container.right = list.getWidth();
|
|
container.bottom = list.getHeight();
|
|
|
|
final int scrollbarStyle = mScrollBarStyle;
|
|
if (scrollbarStyle == View.SCROLLBARS_INSIDE_INSET
|
|
|| scrollbarStyle == View.SCROLLBARS_INSIDE_OVERLAY) {
|
|
container.left += list.getPaddingLeft();
|
|
container.top += list.getPaddingTop();
|
|
container.right -= list.getPaddingRight();
|
|
container.bottom -= list.getPaddingBottom();
|
|
|
|
// In inset mode, we need to adjust for padded scrollbar width.
|
|
if (scrollbarStyle == View.SCROLLBARS_INSIDE_INSET) {
|
|
final int width = getWidth();
|
|
if (mScrollbarPosition == View.SCROLLBAR_POSITION_RIGHT) {
|
|
container.right += width;
|
|
} else {
|
|
container.left -= width;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Lays out the thumb according to the current scrollbar position.
|
|
*/
|
|
private void layoutThumb() {
|
|
final Rect bounds = mTempBounds;
|
|
measureViewToSide(mThumbImage, null, null, bounds);
|
|
applyLayout(mThumbImage, bounds);
|
|
}
|
|
|
|
/**
|
|
* Lays out the track centered on the thumb. Must be called after
|
|
* {@link #layoutThumb}.
|
|
*/
|
|
private void layoutTrack() {
|
|
final View track = mTrackImage;
|
|
final View thumb = mThumbImage;
|
|
final Rect container = mContainerRect;
|
|
final int maxWidth = Math.max(0, container.width());
|
|
final int maxHeight = Math.max(0, container.height());
|
|
final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.AT_MOST);
|
|
final int heightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
|
|
maxHeight, MeasureSpec.UNSPECIFIED);
|
|
track.measure(widthMeasureSpec, heightMeasureSpec);
|
|
|
|
final int top;
|
|
final int bottom;
|
|
if (mThumbPosition == THUMB_POSITION_INSIDE) {
|
|
top = container.top;
|
|
bottom = container.bottom;
|
|
} else {
|
|
final int thumbHalfHeight = thumb.getHeight() / 2;
|
|
top = container.top + thumbHalfHeight;
|
|
bottom = container.bottom - thumbHalfHeight;
|
|
}
|
|
|
|
final int trackWidth = track.getMeasuredWidth();
|
|
final int left = thumb.getLeft() + (thumb.getWidth() - trackWidth) / 2;
|
|
final int right = left + trackWidth;
|
|
track.layout(left, top, right, bottom);
|
|
}
|
|
|
|
/**
|
|
* Updates the offset and range used to convert from absolute y-position to
|
|
* thumb position within the track.
|
|
*/
|
|
private void updateOffsetAndRange() {
|
|
final View trackImage = mTrackImage;
|
|
final View thumbImage = mThumbImage;
|
|
final float min;
|
|
final float max;
|
|
if (mThumbPosition == THUMB_POSITION_INSIDE) {
|
|
final float halfThumbHeight = thumbImage.getHeight() / 2f;
|
|
min = trackImage.getTop() + halfThumbHeight;
|
|
max = trackImage.getBottom() - halfThumbHeight;
|
|
} else{
|
|
min = trackImage.getTop();
|
|
max = trackImage.getBottom();
|
|
}
|
|
|
|
mThumbOffset = min;
|
|
mThumbRange = max - min;
|
|
}
|
|
|
|
@UnsupportedAppUsage
|
|
private void setState(int state) {
|
|
mList.removeCallbacks(mDeferHide);
|
|
|
|
if (mAlwaysShow && state == STATE_NONE) {
|
|
state = STATE_VISIBLE;
|
|
}
|
|
|
|
if (state == mState) {
|
|
return;
|
|
}
|
|
|
|
switch (state) {
|
|
case STATE_NONE:
|
|
transitionToHidden();
|
|
break;
|
|
case STATE_VISIBLE:
|
|
transitionToVisible();
|
|
break;
|
|
case STATE_DRAGGING:
|
|
if (transitionPreviewLayout(mCurrentSection)) {
|
|
transitionToDragging();
|
|
} else {
|
|
transitionToVisible();
|
|
}
|
|
break;
|
|
}
|
|
|
|
mState = state;
|
|
|
|
refreshDrawablePressedState();
|
|
}
|
|
|
|
private void refreshDrawablePressedState() {
|
|
final boolean isPressed = mState == STATE_DRAGGING;
|
|
mThumbImage.setPressed(isPressed);
|
|
mTrackImage.setPressed(isPressed);
|
|
}
|
|
|
|
/**
|
|
* Shows nothing.
|
|
*/
|
|
private void transitionToHidden() {
|
|
if (mDecorAnimation != null) {
|
|
mDecorAnimation.cancel();
|
|
}
|
|
|
|
final Animator fadeOut = groupAnimatorOfFloat(View.ALPHA, 0f, mThumbImage, mTrackImage,
|
|
mPreviewImage, mPrimaryText, mSecondaryText).setDuration(DURATION_FADE_OUT);
|
|
|
|
// Push the thumb and track outside the list bounds.
|
|
final float offset = mLayoutFromRight ? mThumbImage.getWidth() : -mThumbImage.getWidth();
|
|
final Animator slideOut = groupAnimatorOfFloat(
|
|
View.TRANSLATION_X, offset, mThumbImage, mTrackImage)
|
|
.setDuration(DURATION_FADE_OUT);
|
|
|
|
mDecorAnimation = new AnimatorSet();
|
|
mDecorAnimation.playTogether(fadeOut, slideOut);
|
|
mDecorAnimation.start();
|
|
|
|
mShowingPreview = false;
|
|
}
|
|
|
|
/**
|
|
* Shows the thumb and track.
|
|
*/
|
|
private void transitionToVisible() {
|
|
if (mDecorAnimation != null) {
|
|
mDecorAnimation.cancel();
|
|
}
|
|
|
|
final Animator fadeIn = groupAnimatorOfFloat(View.ALPHA, 1f, mThumbImage, mTrackImage)
|
|
.setDuration(DURATION_FADE_IN);
|
|
final Animator fadeOut = groupAnimatorOfFloat(
|
|
View.ALPHA, 0f, mPreviewImage, mPrimaryText, mSecondaryText)
|
|
.setDuration(DURATION_FADE_OUT);
|
|
final Animator slideIn = groupAnimatorOfFloat(
|
|
View.TRANSLATION_X, 0f, mThumbImage, mTrackImage).setDuration(DURATION_FADE_IN);
|
|
|
|
mDecorAnimation = new AnimatorSet();
|
|
mDecorAnimation.playTogether(fadeIn, fadeOut, slideIn);
|
|
mDecorAnimation.start();
|
|
|
|
mShowingPreview = false;
|
|
}
|
|
|
|
/**
|
|
* Shows the thumb, preview, and track.
|
|
*/
|
|
private void transitionToDragging() {
|
|
if (mDecorAnimation != null) {
|
|
mDecorAnimation.cancel();
|
|
}
|
|
|
|
final Animator fadeIn = groupAnimatorOfFloat(
|
|
View.ALPHA, 1f, mThumbImage, mTrackImage, mPreviewImage)
|
|
.setDuration(DURATION_FADE_IN);
|
|
final Animator slideIn = groupAnimatorOfFloat(
|
|
View.TRANSLATION_X, 0f, mThumbImage, mTrackImage).setDuration(DURATION_FADE_IN);
|
|
|
|
mDecorAnimation = new AnimatorSet();
|
|
mDecorAnimation.playTogether(fadeIn, slideIn);
|
|
mDecorAnimation.start();
|
|
|
|
mShowingPreview = true;
|
|
}
|
|
|
|
private void postAutoHide() {
|
|
mList.removeCallbacks(mDeferHide);
|
|
mList.postDelayed(mDeferHide, FADE_TIMEOUT);
|
|
}
|
|
|
|
public void onScroll(int firstVisibleItem, int visibleItemCount, int totalItemCount) {
|
|
if (!isEnabled()) {
|
|
setState(STATE_NONE);
|
|
return;
|
|
}
|
|
|
|
final boolean hasMoreItems = totalItemCount - visibleItemCount > 0;
|
|
if (hasMoreItems && mState != STATE_DRAGGING) {
|
|
setThumbPos(getPosFromItemCount(firstVisibleItem, visibleItemCount, totalItemCount));
|
|
}
|
|
|
|
mScrollCompleted = true;
|
|
|
|
if (mFirstVisibleItem != firstVisibleItem) {
|
|
mFirstVisibleItem = firstVisibleItem;
|
|
|
|
// Show the thumb, if necessary, and set up auto-fade.
|
|
if (mState != STATE_DRAGGING) {
|
|
setState(STATE_VISIBLE);
|
|
postAutoHide();
|
|
}
|
|
}
|
|
}
|
|
|
|
private void getSectionsFromIndexer() {
|
|
mSectionIndexer = null;
|
|
|
|
Adapter adapter = mList.getAdapter();
|
|
if (adapter instanceof HeaderViewListAdapter) {
|
|
mHeaderCount = ((HeaderViewListAdapter) adapter).getHeadersCount();
|
|
adapter = ((HeaderViewListAdapter) adapter).getWrappedAdapter();
|
|
}
|
|
|
|
if (adapter instanceof ExpandableListConnector) {
|
|
final ExpandableListAdapter expAdapter = ((ExpandableListConnector) adapter)
|
|
.getAdapter();
|
|
if (expAdapter instanceof SectionIndexer) {
|
|
mSectionIndexer = (SectionIndexer) expAdapter;
|
|
mListAdapter = adapter;
|
|
mSections = mSectionIndexer.getSections();
|
|
}
|
|
} else if (adapter instanceof SectionIndexer) {
|
|
mListAdapter = adapter;
|
|
mSectionIndexer = (SectionIndexer) adapter;
|
|
mSections = mSectionIndexer.getSections();
|
|
} else {
|
|
mListAdapter = adapter;
|
|
mSections = null;
|
|
}
|
|
}
|
|
|
|
public void onSectionsChanged() {
|
|
mListAdapter = null;
|
|
}
|
|
|
|
/**
|
|
* Scrolls to a specific position within the section
|
|
* @param position
|
|
*/
|
|
private void scrollTo(float position) {
|
|
mScrollCompleted = false;
|
|
|
|
final int count = mList.getCount();
|
|
final Object[] sections = mSections;
|
|
final int sectionCount = sections == null ? 0 : sections.length;
|
|
int sectionIndex;
|
|
if (sections != null && sectionCount > 1) {
|
|
final int exactSection = MathUtils.constrain(
|
|
(int) (position * sectionCount), 0, sectionCount - 1);
|
|
int targetSection = exactSection;
|
|
int targetIndex = mSectionIndexer.getPositionForSection(targetSection);
|
|
sectionIndex = targetSection;
|
|
|
|
// Given the expected section and index, the following code will
|
|
// try to account for missing sections (no names starting with..)
|
|
// It will compute the scroll space of surrounding empty sections
|
|
// and interpolate the currently visible letter's range across the
|
|
// available space, so that there is always some list movement while
|
|
// the user moves the thumb.
|
|
int nextIndex = count;
|
|
int prevIndex = targetIndex;
|
|
int prevSection = targetSection;
|
|
int nextSection = targetSection + 1;
|
|
|
|
// Assume the next section is unique
|
|
if (targetSection < sectionCount - 1) {
|
|
nextIndex = mSectionIndexer.getPositionForSection(targetSection + 1);
|
|
}
|
|
|
|
// Find the previous index if we're slicing the previous section
|
|
if (nextIndex == targetIndex) {
|
|
// Non-existent letter
|
|
while (targetSection > 0) {
|
|
targetSection--;
|
|
prevIndex = mSectionIndexer.getPositionForSection(targetSection);
|
|
if (prevIndex != targetIndex) {
|
|
prevSection = targetSection;
|
|
sectionIndex = targetSection;
|
|
break;
|
|
} else if (targetSection == 0) {
|
|
// When section reaches 0 here, sectionIndex must follow it.
|
|
// Assuming mSectionIndexer.getPositionForSection(0) == 0.
|
|
sectionIndex = 0;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Find the next index, in case the assumed next index is not
|
|
// unique. For instance, if there is no P, then request for P's
|
|
// position actually returns Q's. So we need to look ahead to make
|
|
// sure that there is really a Q at Q's position. If not, move
|
|
// further down...
|
|
int nextNextSection = nextSection + 1;
|
|
while (nextNextSection < sectionCount &&
|
|
mSectionIndexer.getPositionForSection(nextNextSection) == nextIndex) {
|
|
nextNextSection++;
|
|
nextSection++;
|
|
}
|
|
|
|
// Compute the beginning and ending scroll range percentage of the
|
|
// currently visible section. This could be equal to or greater than
|
|
// (1 / nSections). If the target position is near the previous
|
|
// position, snap to the previous position.
|
|
final float prevPosition = (float) prevSection / sectionCount;
|
|
final float nextPosition = (float) nextSection / sectionCount;
|
|
final float snapThreshold = (count == 0) ? Float.MAX_VALUE : .125f / count;
|
|
if (prevSection == exactSection && position - prevPosition < snapThreshold) {
|
|
targetIndex = prevIndex;
|
|
} else {
|
|
targetIndex = prevIndex + (int) ((nextIndex - prevIndex) * (position - prevPosition)
|
|
/ (nextPosition - prevPosition));
|
|
}
|
|
|
|
// Clamp to valid positions.
|
|
targetIndex = MathUtils.constrain(targetIndex, 0, count - 1);
|
|
|
|
if (mList instanceof ExpandableListView) {
|
|
final ExpandableListView expList = (ExpandableListView) mList;
|
|
expList.setSelectionFromTop(expList.getFlatListPosition(
|
|
ExpandableListView.getPackedPositionForGroup(targetIndex + mHeaderCount)),
|
|
0);
|
|
} else if (mList instanceof ListView) {
|
|
((ListView) mList).setSelectionFromTop(targetIndex + mHeaderCount, 0);
|
|
} else {
|
|
mList.setSelection(targetIndex + mHeaderCount);
|
|
}
|
|
} else {
|
|
final int index = MathUtils.constrain((int) (position * count), 0, count - 1);
|
|
|
|
if (mList instanceof ExpandableListView) {
|
|
ExpandableListView expList = (ExpandableListView) mList;
|
|
expList.setSelectionFromTop(expList.getFlatListPosition(
|
|
ExpandableListView.getPackedPositionForGroup(index + mHeaderCount)), 0);
|
|
} else if (mList instanceof ListView) {
|
|
((ListView)mList).setSelectionFromTop(index + mHeaderCount, 0);
|
|
} else {
|
|
mList.setSelection(index + mHeaderCount);
|
|
}
|
|
|
|
sectionIndex = -1;
|
|
}
|
|
|
|
if (mCurrentSection != sectionIndex) {
|
|
mCurrentSection = sectionIndex;
|
|
|
|
final boolean hasPreview = transitionPreviewLayout(sectionIndex);
|
|
if (!mShowingPreview && hasPreview) {
|
|
transitionToDragging();
|
|
} else if (mShowingPreview && !hasPreview) {
|
|
transitionToVisible();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Transitions the preview text to a new section. Handles animation,
|
|
* measurement, and layout. If the new preview text is empty, returns false.
|
|
*
|
|
* @param sectionIndex The section index to which the preview should
|
|
* transition.
|
|
* @return False if the new preview text is empty.
|
|
*/
|
|
private boolean transitionPreviewLayout(int sectionIndex) {
|
|
final Object[] sections = mSections;
|
|
String text = null;
|
|
if (sections != null && sectionIndex >= 0 && sectionIndex < sections.length) {
|
|
final Object section = sections[sectionIndex];
|
|
if (section != null) {
|
|
text = section.toString();
|
|
}
|
|
}
|
|
|
|
final Rect bounds = mTempBounds;
|
|
final View preview = mPreviewImage;
|
|
final TextView showing;
|
|
final TextView target;
|
|
if (mShowingPrimary) {
|
|
showing = mPrimaryText;
|
|
target = mSecondaryText;
|
|
} else {
|
|
showing = mSecondaryText;
|
|
target = mPrimaryText;
|
|
}
|
|
|
|
// Set and layout target immediately.
|
|
target.setText(text);
|
|
measurePreview(target, bounds);
|
|
applyLayout(target, bounds);
|
|
|
|
if (mPreviewAnimation != null) {
|
|
mPreviewAnimation.cancel();
|
|
}
|
|
|
|
// Cross-fade preview text.
|
|
final Animator showTarget = animateAlpha(target, 1f).setDuration(DURATION_CROSS_FADE);
|
|
final Animator hideShowing = animateAlpha(showing, 0f).setDuration(DURATION_CROSS_FADE);
|
|
hideShowing.addListener(mSwitchPrimaryListener);
|
|
|
|
// Apply preview image padding and animate bounds, if necessary.
|
|
bounds.left -= preview.getPaddingLeft();
|
|
bounds.top -= preview.getPaddingTop();
|
|
bounds.right += preview.getPaddingRight();
|
|
bounds.bottom += preview.getPaddingBottom();
|
|
final Animator resizePreview = animateBounds(preview, bounds);
|
|
resizePreview.setDuration(DURATION_RESIZE);
|
|
|
|
mPreviewAnimation = new AnimatorSet();
|
|
final AnimatorSet.Builder builder = mPreviewAnimation.play(hideShowing).with(showTarget);
|
|
builder.with(resizePreview);
|
|
|
|
// The current preview size is unaffected by hidden or showing. It's
|
|
// used to set starting scales for things that need to be scaled down.
|
|
final int previewWidth = preview.getWidth() - preview.getPaddingLeft()
|
|
- preview.getPaddingRight();
|
|
|
|
// If target is too large, shrink it immediately to fit and expand to
|
|
// target size. Otherwise, start at target size.
|
|
final int targetWidth = target.getWidth();
|
|
if (targetWidth > previewWidth) {
|
|
target.setScaleX((float) previewWidth / targetWidth);
|
|
final Animator scaleAnim = animateScaleX(target, 1f).setDuration(DURATION_RESIZE);
|
|
builder.with(scaleAnim);
|
|
} else {
|
|
target.setScaleX(1f);
|
|
}
|
|
|
|
// If showing is larger than target, shrink to target size.
|
|
final int showingWidth = showing.getWidth();
|
|
if (showingWidth > targetWidth) {
|
|
final float scale = (float) targetWidth / showingWidth;
|
|
final Animator scaleAnim = animateScaleX(showing, scale).setDuration(DURATION_RESIZE);
|
|
builder.with(scaleAnim);
|
|
}
|
|
|
|
mPreviewAnimation.start();
|
|
|
|
return !TextUtils.isEmpty(text);
|
|
}
|
|
|
|
/**
|
|
* Positions the thumb and preview widgets.
|
|
*
|
|
* @param position The position, between 0 and 1, along the track at which
|
|
* to place the thumb.
|
|
*/
|
|
private void setThumbPos(float position) {
|
|
final float thumbMiddle = position * mThumbRange + mThumbOffset;
|
|
mThumbImage.setTranslationY(thumbMiddle - mThumbImage.getHeight() / 2f);
|
|
|
|
final View previewImage = mPreviewImage;
|
|
final float previewHalfHeight = previewImage.getHeight() / 2f;
|
|
final float previewPos;
|
|
switch (mOverlayPosition) {
|
|
case OVERLAY_AT_THUMB:
|
|
previewPos = thumbMiddle;
|
|
break;
|
|
case OVERLAY_ABOVE_THUMB:
|
|
previewPos = thumbMiddle - previewHalfHeight;
|
|
break;
|
|
case OVERLAY_FLOATING:
|
|
default:
|
|
previewPos = 0;
|
|
break;
|
|
}
|
|
|
|
// Center the preview on the thumb, constrained to the list bounds.
|
|
final Rect container = mContainerRect;
|
|
final int top = container.top;
|
|
final int bottom = container.bottom;
|
|
final float minP = top + previewHalfHeight;
|
|
final float maxP = bottom - previewHalfHeight;
|
|
final float previewMiddle = MathUtils.constrain(previewPos, minP, maxP);
|
|
final float previewTop = previewMiddle - previewHalfHeight;
|
|
previewImage.setTranslationY(previewTop);
|
|
|
|
mPrimaryText.setTranslationY(previewTop);
|
|
mSecondaryText.setTranslationY(previewTop);
|
|
}
|
|
|
|
private float getPosFromMotionEvent(float y) {
|
|
// If the list is the same height as the thumbnail or shorter,
|
|
// effectively disable scrolling.
|
|
if (mThumbRange <= 0) {
|
|
return 0f;
|
|
}
|
|
|
|
return MathUtils.constrain((y - mThumbOffset) / mThumbRange, 0f, 1f);
|
|
}
|
|
|
|
/**
|
|
* Calculates the thumb position based on the visible items.
|
|
*
|
|
* @param firstVisibleItem First visible item, >= 0.
|
|
* @param visibleItemCount Number of visible items, >= 0.
|
|
* @param totalItemCount Total number of items, >= 0.
|
|
* @return
|
|
*/
|
|
private float getPosFromItemCount(
|
|
int firstVisibleItem, int visibleItemCount, int totalItemCount) {
|
|
final SectionIndexer sectionIndexer = mSectionIndexer;
|
|
if (sectionIndexer == null || mListAdapter == null) {
|
|
getSectionsFromIndexer();
|
|
}
|
|
|
|
if (visibleItemCount == 0 || totalItemCount == 0) {
|
|
// No items are visible.
|
|
return 0;
|
|
}
|
|
|
|
final boolean hasSections = sectionIndexer != null && mSections != null
|
|
&& mSections.length > 0;
|
|
if (!hasSections || !mMatchDragPosition) {
|
|
if (visibleItemCount == totalItemCount) {
|
|
// All items are visible.
|
|
return 0;
|
|
} else {
|
|
return (float) firstVisibleItem / (totalItemCount - visibleItemCount);
|
|
}
|
|
}
|
|
|
|
// Ignore headers.
|
|
firstVisibleItem -= mHeaderCount;
|
|
if (firstVisibleItem < 0) {
|
|
return 0;
|
|
}
|
|
totalItemCount -= mHeaderCount;
|
|
|
|
// Hidden portion of the first visible row.
|
|
final View child = mList.getChildAt(0);
|
|
final float incrementalPos;
|
|
if (child == null || child.getHeight() == 0) {
|
|
incrementalPos = 0;
|
|
} else {
|
|
incrementalPos = (float) (mList.getPaddingTop() - child.getTop()) / child.getHeight();
|
|
}
|
|
|
|
// Number of rows in this section.
|
|
final int section = sectionIndexer.getSectionForPosition(firstVisibleItem);
|
|
final int sectionPos = sectionIndexer.getPositionForSection(section);
|
|
final int sectionCount = mSections.length;
|
|
final int positionsInSection;
|
|
if (section < sectionCount - 1) {
|
|
final int nextSectionPos;
|
|
if (section + 1 < sectionCount) {
|
|
nextSectionPos = sectionIndexer.getPositionForSection(section + 1);
|
|
} else {
|
|
nextSectionPos = totalItemCount - 1;
|
|
}
|
|
positionsInSection = nextSectionPos - sectionPos;
|
|
} else {
|
|
positionsInSection = totalItemCount - sectionPos;
|
|
}
|
|
|
|
// Position within this section.
|
|
final float posWithinSection;
|
|
if (positionsInSection == 0) {
|
|
posWithinSection = 0;
|
|
} else {
|
|
posWithinSection = (firstVisibleItem + incrementalPos - sectionPos)
|
|
/ positionsInSection;
|
|
}
|
|
|
|
float result = (section + posWithinSection) / sectionCount;
|
|
|
|
// Fake out the scroll bar for the last item. Since the section indexer
|
|
// won't ever actually move the list in this end space, make scrolling
|
|
// across the last item account for whatever space is remaining.
|
|
if (firstVisibleItem > 0 && firstVisibleItem + visibleItemCount == totalItemCount) {
|
|
final View lastChild = mList.getChildAt(visibleItemCount - 1);
|
|
final int bottomPadding = mList.getPaddingBottom();
|
|
final int maxSize;
|
|
final int currentVisibleSize;
|
|
if (mList.getClipToPadding()) {
|
|
maxSize = lastChild.getHeight();
|
|
currentVisibleSize = mList.getHeight() - bottomPadding - lastChild.getTop();
|
|
} else {
|
|
maxSize = lastChild.getHeight() + bottomPadding;
|
|
currentVisibleSize = mList.getHeight() - lastChild.getTop();
|
|
}
|
|
if (currentVisibleSize > 0 && maxSize > 0) {
|
|
result += (1 - result) * ((float) currentVisibleSize / maxSize );
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Cancels an ongoing fling event by injecting a
|
|
* {@link MotionEvent#ACTION_CANCEL} into the host view.
|
|
*/
|
|
private void cancelFling() {
|
|
final MotionEvent cancelFling = MotionEvent.obtain(
|
|
0, 0, MotionEvent.ACTION_CANCEL, 0, 0, 0);
|
|
mList.onTouchEvent(cancelFling);
|
|
cancelFling.recycle();
|
|
}
|
|
|
|
/**
|
|
* Cancels a pending drag.
|
|
*
|
|
* @see #startPendingDrag()
|
|
*/
|
|
private void cancelPendingDrag() {
|
|
mPendingDrag = -1;
|
|
}
|
|
|
|
/**
|
|
* Delays dragging until after the framework has determined that the user is
|
|
* scrolling, rather than tapping.
|
|
*/
|
|
private void startPendingDrag() {
|
|
mPendingDrag = SystemClock.uptimeMillis() + TAP_TIMEOUT;
|
|
}
|
|
|
|
private void beginDrag() {
|
|
mPendingDrag = -1;
|
|
|
|
setState(STATE_DRAGGING);
|
|
|
|
if (mListAdapter == null && mList != null) {
|
|
getSectionsFromIndexer();
|
|
}
|
|
|
|
if (mList != null) {
|
|
mList.requestDisallowInterceptTouchEvent(true);
|
|
mList.reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
|
|
}
|
|
|
|
cancelFling();
|
|
}
|
|
|
|
@UnsupportedAppUsage
|
|
public boolean onInterceptTouchEvent(MotionEvent ev) {
|
|
if (!isEnabled()) {
|
|
return false;
|
|
}
|
|
|
|
switch (ev.getActionMasked()) {
|
|
case MotionEvent.ACTION_DOWN:
|
|
if (isPointInside(ev.getX(), ev.getY())) {
|
|
// If the parent has requested that its children delay
|
|
// pressed state (e.g. is a scrolling container) then we
|
|
// need to allow the parent time to decide whether it wants
|
|
// to intercept events. If it does, we will receive a CANCEL
|
|
// event.
|
|
if (!mList.isInScrollingContainer()) {
|
|
// This will get dispatched to onTouchEvent(). Start
|
|
// dragging there.
|
|
return true;
|
|
}
|
|
|
|
mInitialTouchY = ev.getY();
|
|
startPendingDrag();
|
|
}
|
|
break;
|
|
case MotionEvent.ACTION_MOVE:
|
|
if (!isPointInside(ev.getX(), ev.getY())) {
|
|
cancelPendingDrag();
|
|
} else if (mPendingDrag >= 0 && mPendingDrag <= SystemClock.uptimeMillis()) {
|
|
beginDrag();
|
|
|
|
final float pos = getPosFromMotionEvent(mInitialTouchY);
|
|
scrollTo(pos);
|
|
|
|
// This may get dispatched to onTouchEvent(), but it
|
|
// doesn't really matter since we'll already be in a drag.
|
|
return onTouchEvent(ev);
|
|
}
|
|
break;
|
|
case MotionEvent.ACTION_UP:
|
|
case MotionEvent.ACTION_CANCEL:
|
|
cancelPendingDrag();
|
|
break;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public boolean onInterceptHoverEvent(MotionEvent ev) {
|
|
if (!isEnabled()) {
|
|
return false;
|
|
}
|
|
|
|
final int actionMasked = ev.getActionMasked();
|
|
if ((actionMasked == MotionEvent.ACTION_HOVER_ENTER
|
|
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) && mState == STATE_NONE
|
|
&& isPointInside(ev.getX(), ev.getY())) {
|
|
setState(STATE_VISIBLE);
|
|
postAutoHide();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
|
|
if (mState == STATE_DRAGGING || isPointInside(event.getX(), event.getY())) {
|
|
return PointerIcon.getSystemIcon(mList.getContext(), PointerIcon.TYPE_ARROW);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
@UnsupportedAppUsage
|
|
public boolean onTouchEvent(MotionEvent me) {
|
|
if (!isEnabled()) {
|
|
return false;
|
|
}
|
|
|
|
switch (me.getActionMasked()) {
|
|
case MotionEvent.ACTION_DOWN: {
|
|
if (isPointInside(me.getX(), me.getY())) {
|
|
if (!mList.isInScrollingContainer()) {
|
|
beginDrag();
|
|
return true;
|
|
}
|
|
}
|
|
} break;
|
|
|
|
case MotionEvent.ACTION_UP: {
|
|
if (mPendingDrag >= 0) {
|
|
// Allow a tap to scroll.
|
|
beginDrag();
|
|
|
|
final float pos = getPosFromMotionEvent(me.getY());
|
|
setThumbPos(pos);
|
|
scrollTo(pos);
|
|
|
|
// Will hit the STATE_DRAGGING check below
|
|
}
|
|
|
|
if (mState == STATE_DRAGGING) {
|
|
if (mList != null) {
|
|
// ViewGroup does the right thing already, but there might
|
|
// be other classes that don't properly reset on touch-up,
|
|
// so do this explicitly just in case.
|
|
mList.requestDisallowInterceptTouchEvent(false);
|
|
mList.reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
|
|
}
|
|
|
|
setState(STATE_VISIBLE);
|
|
postAutoHide();
|
|
|
|
return true;
|
|
}
|
|
} break;
|
|
|
|
case MotionEvent.ACTION_MOVE: {
|
|
if (mPendingDrag >= 0 && Math.abs(me.getY() - mInitialTouchY) > mScaledTouchSlop) {
|
|
beginDrag();
|
|
|
|
// Will hit the STATE_DRAGGING check below
|
|
}
|
|
|
|
if (mState == STATE_DRAGGING) {
|
|
// TODO: Ignore jitter.
|
|
final float pos = getPosFromMotionEvent(me.getY());
|
|
setThumbPos(pos);
|
|
|
|
// If the previous scrollTo is still pending
|
|
if (mScrollCompleted) {
|
|
scrollTo(pos);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
} break;
|
|
|
|
case MotionEvent.ACTION_CANCEL: {
|
|
cancelPendingDrag();
|
|
} break;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Returns whether a coordinate is inside the scroller's activation area. If
|
|
* there is a track image, touching anywhere within the thumb-width of the
|
|
* track activates scrolling. Otherwise, the user has to touch inside thumb
|
|
* itself.
|
|
*
|
|
* @param x The x-coordinate.
|
|
* @param y The y-coordinate.
|
|
* @return Whether the coordinate is inside the scroller's activation area.
|
|
*/
|
|
private boolean isPointInside(float x, float y) {
|
|
return isPointInsideX(x) && (mTrackDrawable != null || isPointInsideY(y));
|
|
}
|
|
|
|
private boolean isPointInsideX(float x) {
|
|
final float offset = mThumbImage.getTranslationX();
|
|
final float left = mThumbImage.getLeft() + offset;
|
|
final float right = mThumbImage.getRight() + offset;
|
|
|
|
// Apply the minimum touch target size.
|
|
final float targetSizeDiff = mMinimumTouchTarget - (right - left);
|
|
final float adjust = targetSizeDiff > 0 ? targetSizeDiff : 0;
|
|
|
|
if (mLayoutFromRight) {
|
|
return x >= mThumbImage.getLeft() - adjust;
|
|
} else {
|
|
return x <= mThumbImage.getRight() + adjust;
|
|
}
|
|
}
|
|
|
|
private boolean isPointInsideY(float y) {
|
|
final float offset = mThumbImage.getTranslationY();
|
|
final float top = mThumbImage.getTop() + offset;
|
|
final float bottom = mThumbImage.getBottom() + offset;
|
|
|
|
// Apply the minimum touch target size.
|
|
final float targetSizeDiff = mMinimumTouchTarget - (bottom - top);
|
|
final float adjust = targetSizeDiff > 0 ? targetSizeDiff / 2 : 0;
|
|
|
|
return y >= (top - adjust) && y <= (bottom + adjust);
|
|
}
|
|
|
|
/**
|
|
* Constructs an animator for the specified property on a group of views.
|
|
* See {@link ObjectAnimator#ofFloat(Object, String, float...)} for
|
|
* implementation details.
|
|
*
|
|
* @param property The property being animated.
|
|
* @param value The value to which that property should animate.
|
|
* @param views The target views to animate.
|
|
* @return An animator for all the specified views.
|
|
*/
|
|
private static Animator groupAnimatorOfFloat(
|
|
Property<View, Float> property, float value, View... views) {
|
|
AnimatorSet animSet = new AnimatorSet();
|
|
AnimatorSet.Builder builder = null;
|
|
|
|
for (int i = views.length - 1; i >= 0; i--) {
|
|
final Animator anim = ObjectAnimator.ofFloat(views[i], property, value);
|
|
if (builder == null) {
|
|
builder = animSet.play(anim);
|
|
} else {
|
|
builder.with(anim);
|
|
}
|
|
}
|
|
|
|
return animSet;
|
|
}
|
|
|
|
/**
|
|
* Returns an animator for the view's scaleX value.
|
|
*/
|
|
private static Animator animateScaleX(View v, float target) {
|
|
return ObjectAnimator.ofFloat(v, View.SCALE_X, target);
|
|
}
|
|
|
|
/**
|
|
* Returns an animator for the view's alpha value.
|
|
*/
|
|
private static Animator animateAlpha(View v, float alpha) {
|
|
return ObjectAnimator.ofFloat(v, View.ALPHA, alpha);
|
|
}
|
|
|
|
/**
|
|
* A Property wrapper around the <code>left</code> functionality handled by the
|
|
* {@link View#setLeft(int)} and {@link View#getLeft()} methods.
|
|
*/
|
|
private static Property<View, Integer> LEFT = new IntProperty<View>("left") {
|
|
@Override
|
|
public void setValue(View object, int value) {
|
|
object.setLeft(value);
|
|
}
|
|
|
|
@Override
|
|
public Integer get(View object) {
|
|
return object.getLeft();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* A Property wrapper around the <code>top</code> functionality handled by the
|
|
* {@link View#setTop(int)} and {@link View#getTop()} methods.
|
|
*/
|
|
private static Property<View, Integer> TOP = new IntProperty<View>("top") {
|
|
@Override
|
|
public void setValue(View object, int value) {
|
|
object.setTop(value);
|
|
}
|
|
|
|
@Override
|
|
public Integer get(View object) {
|
|
return object.getTop();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* A Property wrapper around the <code>right</code> functionality handled by the
|
|
* {@link View#setRight(int)} and {@link View#getRight()} methods.
|
|
*/
|
|
private static Property<View, Integer> RIGHT = new IntProperty<View>("right") {
|
|
@Override
|
|
public void setValue(View object, int value) {
|
|
object.setRight(value);
|
|
}
|
|
|
|
@Override
|
|
public Integer get(View object) {
|
|
return object.getRight();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* A Property wrapper around the <code>bottom</code> functionality handled by the
|
|
* {@link View#setBottom(int)} and {@link View#getBottom()} methods.
|
|
*/
|
|
private static Property<View, Integer> BOTTOM = new IntProperty<View>("bottom") {
|
|
@Override
|
|
public void setValue(View object, int value) {
|
|
object.setBottom(value);
|
|
}
|
|
|
|
@Override
|
|
public Integer get(View object) {
|
|
return object.getBottom();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Returns an animator for the view's bounds.
|
|
*/
|
|
private static Animator animateBounds(View v, Rect bounds) {
|
|
final PropertyValuesHolder left = PropertyValuesHolder.ofInt(LEFT, bounds.left);
|
|
final PropertyValuesHolder top = PropertyValuesHolder.ofInt(TOP, bounds.top);
|
|
final PropertyValuesHolder right = PropertyValuesHolder.ofInt(RIGHT, bounds.right);
|
|
final PropertyValuesHolder bottom = PropertyValuesHolder.ofInt(BOTTOM, bounds.bottom);
|
|
return ObjectAnimator.ofPropertyValuesHolder(v, left, top, right, bottom);
|
|
}
|
|
}
|