/* * 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.content.Context; import android.content.res.ColorStateList; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.graphics.drawable.NinePatchDrawable; import android.os.Handler; import android.os.SystemClock; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.widget.AbsListView.OnScrollListener; /** * Helper class for AbsListView to draw and control the Fast Scroll thumb */ class FastScroller { private static final String TAG = "FastScroller"; // Minimum number of pages to justify showing a fast scroll thumb private static int MIN_PAGES = 4; // Scroll thumb not showing private static final int STATE_NONE = 0; // Not implemented yet - fade-in transition private static final int STATE_ENTER = 1; // Scroll thumb visible and moving along with the scrollbar private static final int STATE_VISIBLE = 2; // Scroll thumb being dragged by user private static final int STATE_DRAGGING = 3; // Scroll thumb fading out due to inactivity timeout private static final int STATE_EXIT = 4; private static final int[] PRESSED_STATES = new int[] { android.R.attr.state_pressed }; private static final int[] DEFAULT_STATES = new int[0]; private static final int[] ATTRS = new int[] { android.R.attr.fastScrollTextColor, android.R.attr.fastScrollThumbDrawable, android.R.attr.fastScrollTrackDrawable, android.R.attr.fastScrollPreviewBackgroundLeft, android.R.attr.fastScrollPreviewBackgroundRight, android.R.attr.fastScrollOverlayPosition }; private static final int TEXT_COLOR = 0; private static final int THUMB_DRAWABLE = 1; private static final int TRACK_DRAWABLE = 2; private static final int PREVIEW_BACKGROUND_LEFT = 3; private static final int PREVIEW_BACKGROUND_RIGHT = 4; private static final int OVERLAY_POSITION = 5; private static final int OVERLAY_FLOATING = 0; private static final int OVERLAY_AT_THUMB = 1; private Drawable mThumbDrawable; private Drawable mOverlayDrawable; private Drawable mTrackDrawable; private Drawable mOverlayDrawableLeft; private Drawable mOverlayDrawableRight; int mThumbH; int mThumbW; int mThumbY; private RectF mOverlayPos; private int mOverlaySize; AbsListView mList; boolean mScrollCompleted; private int mVisibleItem; private Paint mPaint; private int mListOffset; private int mItemCount = -1; private boolean mLongList; private Object [] mSections; private String mSectionText; private boolean mDrawOverlay; private ScrollFade mScrollFade; private int mState; private Handler mHandler = new Handler(); BaseAdapter mListAdapter; private SectionIndexer mSectionIndexer; private boolean mChangedBounds; private int mPosition; private boolean mAlwaysShow; private int mOverlayPosition; private boolean mMatchDragPosition; float mInitialTouchY; boolean mPendingDrag; private int mScaledTouchSlop; private static final int FADE_TIMEOUT = 1500; private static final int PENDING_DRAG_DELAY = 180; private final Rect mTmpRect = new Rect(); private final Runnable mDeferStartDrag = new Runnable() { public void run() { if (mList.mIsAttached) { beginDrag(); final int viewHeight = mList.getHeight(); // Jitter int newThumbY = (int) mInitialTouchY - mThumbH + 10; if (newThumbY < 0) { newThumbY = 0; } else if (newThumbY + mThumbH > viewHeight) { newThumbY = viewHeight - mThumbH; } mThumbY = newThumbY; scrollTo((float) mThumbY / (viewHeight - mThumbH)); } mPendingDrag = false; } }; public FastScroller(Context context, AbsListView listView) { mList = listView; init(context); } public void setAlwaysShow(boolean alwaysShow) { mAlwaysShow = alwaysShow; if (alwaysShow) { mHandler.removeCallbacks(mScrollFade); setState(STATE_VISIBLE); } else if (mState == STATE_VISIBLE) { mHandler.postDelayed(mScrollFade, FADE_TIMEOUT); } } public boolean isAlwaysShowEnabled() { return mAlwaysShow; } private void refreshDrawableState() { int[] state = mState == STATE_DRAGGING ? PRESSED_STATES : DEFAULT_STATES; if (mThumbDrawable != null && mThumbDrawable.isStateful()) { mThumbDrawable.setState(state); } if (mTrackDrawable != null && mTrackDrawable.isStateful()) { mTrackDrawable.setState(state); } } public void setScrollbarPosition(int position) { if (position == View.SCROLLBAR_POSITION_DEFAULT) { position = mList.isLayoutRtl() ? View.SCROLLBAR_POSITION_LEFT : View.SCROLLBAR_POSITION_RIGHT; } mPosition = position; switch (position) { default: case View.SCROLLBAR_POSITION_RIGHT: mOverlayDrawable = mOverlayDrawableRight; break; case View.SCROLLBAR_POSITION_LEFT: mOverlayDrawable = mOverlayDrawableLeft; break; } } public int getWidth() { return mThumbW; } public void setState(int state) { switch (state) { case STATE_NONE: mHandler.removeCallbacks(mScrollFade); mList.invalidate(); break; case STATE_VISIBLE: if (mState != STATE_VISIBLE) { // Optimization resetThumbPos(); } // Fall through case STATE_DRAGGING: mHandler.removeCallbacks(mScrollFade); break; case STATE_EXIT: int viewWidth = mList.getWidth(); mList.invalidate(viewWidth - mThumbW, mThumbY, viewWidth, mThumbY + mThumbH); break; } mState = state; refreshDrawableState(); } public int getState() { return mState; } private void resetThumbPos() { final int viewWidth = mList.getWidth(); // Bounds are always top right. Y coordinate get's translated during draw switch (mPosition) { case View.SCROLLBAR_POSITION_RIGHT: mThumbDrawable.setBounds(viewWidth - mThumbW, 0, viewWidth, mThumbH); break; case View.SCROLLBAR_POSITION_LEFT: mThumbDrawable.setBounds(0, 0, mThumbW, mThumbH); break; } mThumbDrawable.setAlpha(ScrollFade.ALPHA_MAX); } private void useThumbDrawable(Context context, Drawable drawable) { mThumbDrawable = drawable; if (drawable instanceof NinePatchDrawable) { mThumbW = context.getResources().getDimensionPixelSize( com.android.internal.R.dimen.fastscroll_thumb_width); mThumbH = context.getResources().getDimensionPixelSize( com.android.internal.R.dimen.fastscroll_thumb_height); } else { mThumbW = drawable.getIntrinsicWidth(); mThumbH = drawable.getIntrinsicHeight(); } mChangedBounds = true; } private void init(Context context) { // Get both the scrollbar states drawables TypedArray ta = context.getTheme().obtainStyledAttributes(ATTRS); useThumbDrawable(context, ta.getDrawable(THUMB_DRAWABLE)); mTrackDrawable = ta.getDrawable(TRACK_DRAWABLE); mOverlayDrawableLeft = ta.getDrawable(PREVIEW_BACKGROUND_LEFT); mOverlayDrawableRight = ta.getDrawable(PREVIEW_BACKGROUND_RIGHT); mOverlayPosition = ta.getInt(OVERLAY_POSITION, OVERLAY_FLOATING); mScrollCompleted = true; getSectionsFromIndexer(); mOverlaySize = context.getResources().getDimensionPixelSize( com.android.internal.R.dimen.fastscroll_overlay_size); mOverlayPos = new RectF(); mScrollFade = new ScrollFade(); mPaint = new Paint(); mPaint.setAntiAlias(true); mPaint.setTextAlign(Paint.Align.CENTER); mPaint.setTextSize(mOverlaySize / 2); ColorStateList textColor = ta.getColorStateList(TEXT_COLOR); int textColorNormal = textColor.getDefaultColor(); mPaint.setColor(textColorNormal); mPaint.setStyle(Paint.Style.FILL_AND_STROKE); // to show mOverlayDrawable properly if (mList.getWidth() > 0 && mList.getHeight() > 0) { onSizeChanged(mList.getWidth(), mList.getHeight(), 0, 0); } mState = STATE_NONE; refreshDrawableState(); ta.recycle(); mScaledTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); mMatchDragPosition = context.getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.HONEYCOMB; setScrollbarPosition(mList.getVerticalScrollbarPosition()); } void stop() { setState(STATE_NONE); } boolean isVisible() { return !(mState == STATE_NONE); } public void draw(Canvas canvas) { if (mState == STATE_NONE) { // No need to draw anything return; } final int y = mThumbY; final int viewWidth = mList.getWidth(); final FastScroller.ScrollFade scrollFade = mScrollFade; int alpha = -1; if (mState == STATE_EXIT) { alpha = scrollFade.getAlpha(); if (alpha < ScrollFade.ALPHA_MAX / 2) { mThumbDrawable.setAlpha(alpha * 2); } int left = 0; switch (mPosition) { case View.SCROLLBAR_POSITION_RIGHT: left = viewWidth - (mThumbW * alpha) / ScrollFade.ALPHA_MAX; break; case View.SCROLLBAR_POSITION_LEFT: left = -mThumbW + (mThumbW * alpha) / ScrollFade.ALPHA_MAX; break; } mThumbDrawable.setBounds(left, 0, left + mThumbW, mThumbH); mChangedBounds = true; } if (mTrackDrawable != null) { final Rect thumbBounds = mThumbDrawable.getBounds(); final int left = thumbBounds.left; final int halfThumbHeight = (thumbBounds.bottom - thumbBounds.top) / 2; final int trackWidth = mTrackDrawable.getIntrinsicWidth(); final int trackLeft = (left + mThumbW / 2) - trackWidth / 2; mTrackDrawable.setBounds(trackLeft, halfThumbHeight, trackLeft + trackWidth, mList.getHeight() - halfThumbHeight); mTrackDrawable.draw(canvas); } canvas.translate(0, y); mThumbDrawable.draw(canvas); canvas.translate(0, -y); // If user is dragging the scroll bar, draw the alphabet overlay if (mState == STATE_DRAGGING && mDrawOverlay) { if (mOverlayPosition == OVERLAY_AT_THUMB) { int left = 0; switch (mPosition) { default: case View.SCROLLBAR_POSITION_RIGHT: left = Math.max(0, mThumbDrawable.getBounds().left - mThumbW - mOverlaySize); break; case View.SCROLLBAR_POSITION_LEFT: left = Math.min(mThumbDrawable.getBounds().right + mThumbW, mList.getWidth() - mOverlaySize); break; } int top = Math.max(0, Math.min(y + (mThumbH - mOverlaySize) / 2, mList.getHeight() - mOverlaySize)); final RectF pos = mOverlayPos; pos.left = left; pos.right = pos.left + mOverlaySize; pos.top = top; pos.bottom = pos.top + mOverlaySize; if (mOverlayDrawable != null) { mOverlayDrawable.setBounds((int) pos.left, (int) pos.top, (int) pos.right, (int) pos.bottom); } } mOverlayDrawable.draw(canvas); final Paint paint = mPaint; float descent = paint.descent(); final RectF rectF = mOverlayPos; final Rect tmpRect = mTmpRect; mOverlayDrawable.getPadding(tmpRect); final int hOff = (tmpRect.right - tmpRect.left) / 2; final int vOff = (tmpRect.bottom - tmpRect.top) / 2; canvas.drawText(mSectionText, (int) (rectF.left + rectF.right) / 2 - hOff, (int) (rectF.bottom + rectF.top) / 2 + mOverlaySize / 4 - descent - vOff, paint); } else if (mState == STATE_EXIT) { if (alpha == 0) { // Done with exit setState(STATE_NONE); } else if (mTrackDrawable != null) { mList.invalidate(viewWidth - mThumbW, 0, viewWidth, mList.getHeight()); } else { mList.invalidate(viewWidth - mThumbW, y, viewWidth, y + mThumbH); } } } void onSizeChanged(int w, int h, int oldw, int oldh) { if (mThumbDrawable != null) { switch (mPosition) { default: case View.SCROLLBAR_POSITION_RIGHT: mThumbDrawable.setBounds(w - mThumbW, 0, w, mThumbH); break; case View.SCROLLBAR_POSITION_LEFT: mThumbDrawable.setBounds(0, 0, mThumbW, mThumbH); break; } } if (mOverlayPosition == OVERLAY_FLOATING) { final RectF pos = mOverlayPos; pos.left = (w - mOverlaySize) / 2; pos.right = pos.left + mOverlaySize; pos.top = h / 10; // 10% from top pos.bottom = pos.top + mOverlaySize; if (mOverlayDrawable != null) { mOverlayDrawable.setBounds((int) pos.left, (int) pos.top, (int) pos.right, (int) pos.bottom); } } } void onItemCountChanged(int oldCount, int newCount) { if (mAlwaysShow) { mLongList = true; } } void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { // Are there enough pages to require fast scroll? Recompute only if total count changes if (mItemCount != totalItemCount && visibleItemCount > 0) { mItemCount = totalItemCount; mLongList = mItemCount / visibleItemCount >= MIN_PAGES; } if (mAlwaysShow) { mLongList = true; } if (!mLongList) { if (mState != STATE_NONE) { setState(STATE_NONE); } return; } if (totalItemCount - visibleItemCount > 0 && mState != STATE_DRAGGING) { mThumbY = getThumbPositionForListPosition(firstVisibleItem, visibleItemCount, totalItemCount); if (mChangedBounds) { resetThumbPos(); mChangedBounds = false; } } mScrollCompleted = true; if (firstVisibleItem == mVisibleItem) { return; } mVisibleItem = firstVisibleItem; if (mState != STATE_DRAGGING) { setState(STATE_VISIBLE); if (!mAlwaysShow) { mHandler.postDelayed(mScrollFade, FADE_TIMEOUT); } } } SectionIndexer getSectionIndexer() { return mSectionIndexer; } Object[] getSections() { if (mListAdapter == null && mList != null) { getSectionsFromIndexer(); } return mSections; } void getSectionsFromIndexer() { Adapter adapter = mList.getAdapter(); mSectionIndexer = null; if (adapter instanceof HeaderViewListAdapter) { mListOffset = ((HeaderViewListAdapter)adapter).getHeadersCount(); adapter = ((HeaderViewListAdapter)adapter).getWrappedAdapter(); } if (adapter instanceof ExpandableListConnector) { ExpandableListAdapter expAdapter = ((ExpandableListConnector)adapter).getAdapter(); if (expAdapter instanceof SectionIndexer) { mSectionIndexer = (SectionIndexer) expAdapter; mListAdapter = (BaseAdapter) adapter; mSections = mSectionIndexer.getSections(); } } else { if (adapter instanceof SectionIndexer) { mListAdapter = (BaseAdapter) adapter; mSectionIndexer = (SectionIndexer) adapter; mSections = mSectionIndexer.getSections(); if (mSections == null) { mSections = new String[] { " " }; } } else { mListAdapter = (BaseAdapter) adapter; mSections = new String[] { " " }; } } } public void onSectionsChanged() { mListAdapter = null; } void scrollTo(float position) { int count = mList.getCount(); mScrollCompleted = false; float fThreshold = (1.0f / count) / 8; final Object[] sections = mSections; int sectionIndex; if (sections != null && sections.length > 1) { final int nSections = sections.length; int section = (int) (position * nSections); if (section >= nSections) { section = nSections - 1; } int exactSection = section; sectionIndex = section; int index = mSectionIndexer.getPositionForSection(section); // 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 = index; int prevSection = section; int nextSection = section + 1; // Assume the next section is unique if (section < nSections - 1) { nextIndex = mSectionIndexer.getPositionForSection(section + 1); } // Find the previous index if we're slicing the previous section if (nextIndex == index) { // Non-existent letter while (section > 0) { section--; prevIndex = mSectionIndexer.getPositionForSection(section); if (prevIndex != index) { prevSection = section; sectionIndex = section; break; } else if (section == 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 < nSections && mSectionIndexer.getPositionForSection(nextNextSection) == nextIndex) { nextNextSection++; nextSection++; } // Compute the beginning and ending scroll range percentage of the // currently visible letter. This could be equal to or greater than // (1 / nSections). float fPrev = (float) prevSection / nSections; float fNext = (float) nextSection / nSections; if (prevSection == exactSection && position - fPrev < fThreshold) { index = prevIndex; } else { index = prevIndex + (int) ((nextIndex - prevIndex) * (position - fPrev) / (fNext - fPrev)); } // Don't overflow if (index > count - 1) index = count - 1; if (mList instanceof ExpandableListView) { ExpandableListView expList = (ExpandableListView) mList; expList.setSelectionFromTop(expList.getFlatListPosition( ExpandableListView.getPackedPositionForGroup(index + mListOffset)), 0); } else if (mList instanceof ListView) { ((ListView)mList).setSelectionFromTop(index + mListOffset, 0); } else { mList.setSelection(index + mListOffset); } } else { int index = (int) (position * count); // Don't overflow if (index > count - 1) index = count - 1; if (mList instanceof ExpandableListView) { ExpandableListView expList = (ExpandableListView) mList; expList.setSelectionFromTop(expList.getFlatListPosition( ExpandableListView.getPackedPositionForGroup(index + mListOffset)), 0); } else if (mList instanceof ListView) { ((ListView)mList).setSelectionFromTop(index + mListOffset, 0); } else { mList.setSelection(index + mListOffset); } sectionIndex = -1; } if (sectionIndex >= 0) { String text = mSectionText = sections[sectionIndex].toString(); mDrawOverlay = (text.length() != 1 || text.charAt(0) != ' ') && sectionIndex < sections.length; } else { mDrawOverlay = false; } } private int getThumbPositionForListPosition(int firstVisibleItem, int visibleItemCount, int totalItemCount) { if (mSectionIndexer == null || mListAdapter == null) { getSectionsFromIndexer(); } if (mSectionIndexer == null || !mMatchDragPosition) { return ((mList.getHeight() - mThumbH) * firstVisibleItem) / (totalItemCount - visibleItemCount); } firstVisibleItem -= mListOffset; if (firstVisibleItem < 0) { return 0; } totalItemCount -= mListOffset; final int trackHeight = mList.getHeight() - mThumbH; final int section = mSectionIndexer.getSectionForPosition(firstVisibleItem); final int sectionPos = mSectionIndexer.getPositionForSection(section); final int nextSectionPos = mSectionIndexer.getPositionForSection(section + 1); final int sectionCount = mSections.length; final int positionsInSection = nextSectionPos - sectionPos; final View child = mList.getChildAt(0); final float incrementalPos = child == null ? 0 : firstVisibleItem + (float) (mList.getPaddingTop() - child.getTop()) / child.getHeight(); final float posWithinSection = (incrementalPos - sectionPos) / positionsInSection; int result = (int) ((section + posWithinSection) / sectionCount * trackHeight); // Fake out the scrollbar 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 float lastItemVisible = (float) (mList.getHeight() - mList.getPaddingBottom() - lastChild.getTop()) / lastChild.getHeight(); result += (trackHeight - result) * lastItemVisible; } return result; } private void cancelFling() { // Cancel the list fling MotionEvent cancelFling = MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0, 0, 0); mList.onTouchEvent(cancelFling); cancelFling.recycle(); } void cancelPendingDrag() { mList.removeCallbacks(mDeferStartDrag); mPendingDrag = false; } void startPendingDrag() { mPendingDrag = true; mList.postDelayed(mDeferStartDrag, PENDING_DRAG_DELAY); } void beginDrag() { setState(STATE_DRAGGING); if (mListAdapter == null && mList != null) { getSectionsFromIndexer(); } if (mList != null) { mList.requestDisallowInterceptTouchEvent(true); mList.reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); } cancelFling(); } boolean onInterceptTouchEvent(MotionEvent ev) { switch (ev.getActionMasked()) { case MotionEvent.ACTION_DOWN: if (mState > STATE_NONE && isPointInside(ev.getX(), ev.getY())) { if (!mList.isInScrollingContainer()) { beginDrag(); return true; } mInitialTouchY = ev.getY(); startPendingDrag(); } break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: cancelPendingDrag(); break; } return false; } boolean onTouchEvent(MotionEvent me) { if (mState == STATE_NONE) { return false; } final int action = me.getAction(); if (action == MotionEvent.ACTION_DOWN) { if (isPointInside(me.getX(), me.getY())) { if (!mList.isInScrollingContainer()) { beginDrag(); return true; } mInitialTouchY = me.getY(); startPendingDrag(); } } else if (action == MotionEvent.ACTION_UP) { // don't add ACTION_CANCEL here if (mPendingDrag) { // Allow a tap to scroll. beginDrag(); final int viewHeight = mList.getHeight(); // Jitter int newThumbY = (int) me.getY() - mThumbH + 10; if (newThumbY < 0) { newThumbY = 0; } else if (newThumbY + mThumbH > viewHeight) { newThumbY = viewHeight - mThumbH; } mThumbY = newThumbY; scrollTo((float) mThumbY / (viewHeight - mThumbH)); cancelPendingDrag(); // 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); final Handler handler = mHandler; handler.removeCallbacks(mScrollFade); if (!mAlwaysShow) { handler.postDelayed(mScrollFade, 1000); } mList.invalidate(); return true; } } else if (action == MotionEvent.ACTION_MOVE) { if (mPendingDrag) { final float y = me.getY(); if (Math.abs(y - mInitialTouchY) > mScaledTouchSlop) { setState(STATE_DRAGGING); if (mListAdapter == null && mList != null) { getSectionsFromIndexer(); } if (mList != null) { mList.requestDisallowInterceptTouchEvent(true); mList.reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); } cancelFling(); cancelPendingDrag(); // Will hit the STATE_DRAGGING check below } } if (mState == STATE_DRAGGING) { final int viewHeight = mList.getHeight(); // Jitter int newThumbY = (int) me.getY() - mThumbH + 10; if (newThumbY < 0) { newThumbY = 0; } else if (newThumbY + mThumbH > viewHeight) { newThumbY = viewHeight - mThumbH; } if (Math.abs(mThumbY - newThumbY) < 2) { return true; } mThumbY = newThumbY; // If the previous scrollTo is still pending if (mScrollCompleted) { scrollTo((float) mThumbY / (viewHeight - mThumbH)); } return true; } } else if (action == MotionEvent.ACTION_CANCEL) { cancelPendingDrag(); } return false; } boolean isPointInside(float x, float y) { boolean inTrack = false; switch (mPosition) { default: case View.SCROLLBAR_POSITION_RIGHT: inTrack = x > mList.getWidth() - mThumbW; break; case View.SCROLLBAR_POSITION_LEFT: inTrack = x < mThumbW; break; } // Allow taps in the track to start moving. return inTrack && (mTrackDrawable != null || y >= mThumbY && y <= mThumbY + mThumbH); } public class ScrollFade implements Runnable { long mStartTime; long mFadeDuration; static final int ALPHA_MAX = 208; static final long FADE_DURATION = 200; void startFade() { mFadeDuration = FADE_DURATION; mStartTime = SystemClock.uptimeMillis(); setState(STATE_EXIT); } int getAlpha() { if (getState() != STATE_EXIT) { return ALPHA_MAX; } int alpha; long now = SystemClock.uptimeMillis(); if (now > mStartTime + mFadeDuration) { alpha = 0; } else { alpha = (int) (ALPHA_MAX - ((now - mStartTime) * ALPHA_MAX) / mFadeDuration); } return alpha; } public void run() { if (getState() != STATE_EXIT) { startFade(); return; } if (getAlpha() > 0) { mList.invalidate(); } else { setState(STATE_NONE); } } } }