| /* |
| * 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.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.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; |
| |
| /** |
| * 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; |
| |
| // 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(); |
| private final Rect mContainerRect = new Rect(); |
| |
| private final AbsListView mList; |
| private final ViewGroupOverlay mOverlay; |
| private final TextView mPrimaryText; |
| private final TextView mSecondaryText; |
| private final ImageView mThumbImage; |
| 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. */ |
| 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; |
| |
| private Drawable mThumbDrawable; |
| private Drawable mTrackDrawable; |
| private int mTextAppearance; |
| |
| /** 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. */ |
| 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. */ |
| 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; |
| } |
| }; |
| |
| 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() { |
| final Context context = mList.getContext(); |
| 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); |
| |
| mPreviewImage.setMinimumWidth(mPreviewMinWidth); |
| mPreviewImage.setMinimumHeight(mPreviewMinHeight); |
| |
| if (mTextAppearance != 0) { |
| mPrimaryText.setTextAppearance(context, mTextAppearance); |
| mSecondaryText.setTextAppearance(context, 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 textMinSize = Math.max(0, mPreviewMinHeight); |
| mPrimaryText.setMinimumWidth(textMinSize); |
| mPrimaryText.setMinimumHeight(textMinSize); |
| mPrimaryText.setIncludeFontPadding(false); |
| mSecondaryText.setMinimumWidth(textMinSize); |
| mSecondaryText.setMinimumHeight(textMinSize); |
| mSecondaryText.setIncludeFontPadding(false); |
| |
| refreshDrawablePressedState(); |
| } |
| |
| public void setStyle(int resId) { |
| final Context context = mList.getContext(); |
| final TypedArray ta = context.obtainStyledAttributes(null, |
| com.android.internal.R.styleable.FastScroll, android.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 com.android.internal.R.styleable.FastScroll_position: |
| mOverlayPosition = ta.getInt(index, OVERLAY_FLOATING); |
| break; |
| case com.android.internal.R.styleable.FastScroll_backgroundLeft: |
| mPreviewResId[PREVIEW_LEFT] = ta.getResourceId(index, 0); |
| break; |
| case com.android.internal.R.styleable.FastScroll_backgroundRight: |
| mPreviewResId[PREVIEW_RIGHT] = ta.getResourceId(index, 0); |
| break; |
| case com.android.internal.R.styleable.FastScroll_thumbDrawable: |
| mThumbDrawable = ta.getDrawable(index); |
| break; |
| case com.android.internal.R.styleable.FastScroll_trackDrawable: |
| mTrackDrawable = ta.getDrawable(index); |
| break; |
| case com.android.internal.R.styleable.FastScroll_textAppearance: |
| mTextAppearance = ta.getResourceId(index, 0); |
| break; |
| case com.android.internal.R.styleable.FastScroll_textColor: |
| mTextColor = ta.getColorStateList(index); |
| break; |
| case com.android.internal.R.styleable.FastScroll_textSize: |
| mTextSize = ta.getDimensionPixelSize(index, 0); |
| break; |
| case com.android.internal.R.styleable.FastScroll_minWidth: |
| mPreviewMinWidth = ta.getDimensionPixelSize(index, 0); |
| break; |
| case com.android.internal.R.styleable.FastScroll_minHeight: |
| mPreviewMinHeight = ta.getDimensionPixelSize(index, 0); |
| break; |
| case com.android.internal.R.styleable.FastScroll_thumbMinWidth: |
| mThumbMinWidth = ta.getDimensionPixelSize(index, 0); |
| break; |
| case com.android.internal.R.styleable.FastScroll_thumbMinHeight: |
| mThumbMinHeight = ta.getDimensionPixelSize(index, 0); |
| break; |
| case com.android.internal.R.styleable.FastScroll_padding: |
| mPreviewPadding = ta.getDimensionPixelSize(index, 0); |
| break; |
| } |
| } |
| |
| updateAppearance(); |
| } |
| |
| /** |
| * Removes this FastScroller overlay from the host view. |
| */ |
| 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); |
| |
| // Add extra padding for text. |
| final Drawable background = mPreviewImage.getBackground(); |
| if (background != null) { |
| final Rect padding = mTempBounds; |
| background.getPadding(padding); |
| padding.offset(mPreviewPadding, mPreviewPadding); |
| mPreviewImage.setPadding(padding.left, padding.top, padding.right, padding.bottom); |
| } |
| |
| // Requires re-layout. |
| updateLayout(); |
| } |
| } |
| |
| public int getWidth() { |
| return mWidth; |
| } |
| |
| 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(); |
| |
| 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 adjMaxWidth = maxWidth - marginLeft - marginRight; |
| final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(adjMaxWidth, MeasureSpec.AT_MOST); |
| final int heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, 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 adjMaxWidth = containerWidth - marginLeft - marginRight; |
| final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(adjMaxWidth, MeasureSpec.AT_MOST); |
| final int heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, 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 containerWidth = container.width(); |
| final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(containerWidth, MeasureSpec.AT_MOST); |
| final int heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); |
| track.measure(widthMeasureSpec, heightMeasureSpec); |
| |
| final int trackWidth = track.getMeasuredWidth(); |
| final int thumbHalfHeight = thumb == null ? 0 : thumb.getHeight() / 2; |
| final int left = thumb.getLeft() + (thumb.getWidth() - trackWidth) / 2; |
| final int right = left + trackWidth; |
| final int top = container.top + thumbHalfHeight; |
| final int bottom = container.bottom - thumbHalfHeight; |
| track.layout(left, top, right, bottom); |
| } |
| |
| 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 Rect container = mContainerRect; |
| final int top = container.top; |
| final int bottom = container.bottom; |
| |
| final View trackImage = mTrackImage; |
| final View thumbImage = mThumbImage; |
| final float min = trackImage.getTop(); |
| final float max = trackImage.getBottom(); |
| final float offset = min; |
| final float range = max - min; |
| final float thumbMiddle = position * range + offset; |
| thumbImage.setTranslationY(thumbMiddle - thumbImage.getHeight() / 2); |
| |
| 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 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) { |
| final View trackImage = mTrackImage; |
| final float min = trackImage.getTop(); |
| final float max = trackImage.getBottom(); |
| final float offset = min; |
| final float range = max - min; |
| |
| // If the list is the same height as the thumbnail or shorter, |
| // effectively disable scrolling. |
| if (range <= 0) { |
| return 0f; |
| } |
| |
| return MathUtils.constrain((y - offset) / range, 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(); |
| } |
| |
| 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()) { |
| beginDrag(); |
| 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); |
| |
| 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 boolean onTouchEvent(MotionEvent me) { |
| if (!isEnabled()) { |
| return false; |
| } |
| |
| switch (me.getActionMasked()) { |
| 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); |
| } |
| } |