diff --git a/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadListLayout.java b/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadListLayout.java index 0de92a64..c7f0101d 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadListLayout.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadListLayout.java @@ -20,9 +20,12 @@ package org.floens.chan.ui.layout; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.content.Context; +import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.StateListDrawable; import android.os.Handler; import android.os.Looper; import android.support.v7.widget.GridLayoutManager; @@ -49,6 +52,7 @@ import org.floens.chan.ui.cell.PostCell; import org.floens.chan.ui.cell.PostCellInterface; import org.floens.chan.ui.cell.ThreadStatusCell; import org.floens.chan.ui.toolbar.Toolbar; +import org.floens.chan.ui.view.FastScroller; import org.floens.chan.ui.view.ThumbnailView; import org.floens.chan.utils.AndroidUtils; @@ -59,6 +63,7 @@ import static org.floens.chan.utils.AndroidUtils.ROBOTO_MEDIUM; import static org.floens.chan.utils.AndroidUtils.dp; import static org.floens.chan.utils.AndroidUtils.getAttrColor; import static org.floens.chan.utils.AndroidUtils.getDimen; +import static org.floens.chan.utils.AndroidUtils.getRes; /** * A layout that wraps around a {@link RecyclerView} and a {@link ReplyLayout} to manage showing and replying to posts. @@ -70,6 +75,7 @@ public class ThreadListLayout extends FrameLayout implements ReplyLayout.ReplyLa private TextView searchStatus; private RecyclerView recyclerView; private RecyclerView.LayoutManager layoutManager; + private FastScroller fastScroller; private PostAdapter postAdapter; private ChanThread showingThread; private ThreadListLayoutPresenterCallback callback; @@ -120,6 +126,8 @@ public class ThreadListLayout extends FrameLayout implements ReplyLayout.ReplyLa recyclerView.setAdapter(postAdapter); recyclerView.addOnScrollListener(scrollListener); + setFastScroll(false); + attachToolbarScroll(true); reply.setPadding(0, toolbarHeight(), 0, 0); @@ -226,6 +234,8 @@ public class ThreadListLayout extends FrameLayout implements ReplyLayout.ReplyLa party(); } + setFastScroll(true); + postAdapter.setThread(thread, filter); } @@ -530,6 +540,40 @@ public class ThreadListLayout extends FrameLayout implements ReplyLayout.ReplyLa } } + private void setFastScroll(boolean enabled) { + if (!enabled) { + if (fastScroller != null) { + recyclerView.removeItemDecoration(fastScroller); + fastScroller = null; + } + } else { + if (fastScroller == null) { + Resources resources = getResources(); + StateListDrawable verticalThumbDrawable = (StateListDrawable) resources + .getDrawable(R.drawable.recyclerview_fastscroll_thumb_selector); + Drawable verticalTrackDrawable = getRes() + .getDrawable(R.drawable.recyclerview_fastscroll_track_selector); + StateListDrawable horizontalThumbDrawable = (StateListDrawable) resources + .getDrawable(R.drawable.recyclerview_fastscroll_thumb_selector); + Drawable horizontalTrackDrawable = resources + .getDrawable(R.drawable.recyclerview_fastscroll_track_selector); + + final int defaultThickness = dp(4); + final int targetWidth = dp(8); + final int minimumRange = dp(50); + final int margin = dp(0); + final int thumbMinLength = dp(23); + + fastScroller = new FastScroller(recyclerView, + verticalThumbDrawable, verticalTrackDrawable, + horizontalThumbDrawable, horizontalTrackDrawable, + defaultThickness, minimumRange, margin, thumbMinLength, targetWidth); + } + } + + recyclerView.setVerticalScrollBarEnabled(!enabled); + } + private void setRecyclerViewPadding() { int defaultPadding = 0; if (postViewMode == ChanSettings.PostViewMode.CARD) { diff --git a/Clover/app/src/main/java/org/floens/chan/ui/view/FastScroller.java b/Clover/app/src/main/java/org/floens/chan/ui/view/FastScroller.java new file mode 100644 index 00000000..0fd5f804 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/ui/view/FastScroller.java @@ -0,0 +1,662 @@ +/* + * Copyright (C) 2017 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 org.floens.chan.ui.view; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.StateListDrawable; +import android.support.annotation.IntDef; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; +import android.support.v4.view.ViewCompat; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.RecyclerView.ItemDecoration; +import android.support.v7.widget.RecyclerView.OnItemTouchListener; +import android.support.v7.widget.RecyclerView.OnScrollListener; +import android.view.MotionEvent; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Class responsible to animate and provide a fast scroller. + *
+ * Clover changed: the original FastScroller didn't account for the recyclerview top padding we
+ * require. A minimum thumb length parameter was also added.
+ */
+public class FastScroller extends ItemDecoration implements OnItemTouchListener {
+ @IntDef({STATE_HIDDEN, STATE_VISIBLE, STATE_DRAGGING})
+ @Retention(RetentionPolicy.SOURCE)
+ private @interface State {
+ }
+
+ // Scroll thumb not showing
+ private static final int STATE_HIDDEN = 0;
+ // Scroll thumb visible and moving along with the scrollbar
+ private static final int STATE_VISIBLE = 1;
+ // Scroll thumb being dragged by user
+ private static final int STATE_DRAGGING = 2;
+
+ @IntDef({DRAG_X, DRAG_Y, DRAG_NONE})
+ @Retention(RetentionPolicy.SOURCE)
+ private @interface DragState {
+ }
+
+ private static final int DRAG_NONE = 0;
+ private static final int DRAG_X = 1;
+ private static final int DRAG_Y = 2;
+
+ @IntDef({ANIMATION_STATE_OUT, ANIMATION_STATE_FADING_IN, ANIMATION_STATE_IN,
+ ANIMATION_STATE_FADING_OUT})
+ @Retention(RetentionPolicy.SOURCE)
+ private @interface AnimationState {
+ }
+
+ private static final int ANIMATION_STATE_OUT = 0;
+ private static final int ANIMATION_STATE_FADING_IN = 1;
+ private static final int ANIMATION_STATE_IN = 2;
+ private static final int ANIMATION_STATE_FADING_OUT = 3;
+
+ private static final int SHOW_DURATION_MS = 500;
+ private static final int HIDE_DELAY_AFTER_VISIBLE_MS = 1500;
+ private static final int HIDE_DELAY_AFTER_DRAGGING_MS = 1200;
+ private static final int HIDE_DURATION_MS = 500;
+ private static final int SCROLLBAR_FULL_OPAQUE = 255;
+
+ private static final int[] PRESSED_STATE_SET = new int[]{android.R.attr.state_pressed};
+ private static final int[] EMPTY_STATE_SET = new int[]{};
+
+ private final int mScrollbarMinimumRange;
+ private final int mMargin;
+ private final int mThumbMinLength;
+ private final int mTargetWidth;
+
+ // Final values for the vertical scroll bar
+ private final StateListDrawable mVerticalThumbDrawable;
+ private final Drawable mVerticalTrackDrawable;
+ private final int mVerticalThumbWidth;
+ private final int mVerticalTrackWidth;
+
+ // Final values for the horizontal scroll bar
+ private final StateListDrawable mHorizontalThumbDrawable;
+ private final Drawable mHorizontalTrackDrawable;
+ private final int mHorizontalThumbHeight;
+ private final int mHorizontalTrackHeight;
+
+ // Dynamic values for the vertical scroll bar
+ int mVerticalThumbHeight;
+ int mVerticalThumbCenterY;
+ float mVerticalDragY;
+ int mVerticalDragThumbHeight;
+
+ // Dynamic values for the horizontal scroll bar
+ int mHorizontalThumbWidth;
+ int mHorizontalThumbCenterX;
+ float mHorizontalDragX;
+ int mHorizontalDragThumbWidth;
+
+ private int mRecyclerViewWidth = 0;
+ private int mRecyclerViewHeight = 0;
+
+ private int mRecyclerViewLeftPadding = 0;
+ private int mRecyclerViewTopPadding = 0;
+ private int mRecyclerViewRightPadding = 0;
+ private int mRecyclerViewBottomPadding = 0;
+
+ private RecyclerView mRecyclerView;
+ /**
+ * Whether the document is long/wide enough to require scrolling. If not, we don't show the
+ * relevant scroller.
+ */
+ private boolean mNeedVerticalScrollbar = false;
+ private boolean mNeedHorizontalScrollbar = false;
+ @State
+ private int mState = STATE_HIDDEN;
+ @DragState
+ private int mDragState = DRAG_NONE;
+
+ private final int[] mVerticalRange = new int[2];
+ private final int[] mHorizontalRange = new int[2];
+ private final ValueAnimator mShowHideAnimator = ValueAnimator.ofFloat(0, 1);
+ @AnimationState
+ private int mAnimationState = ANIMATION_STATE_OUT;
+ private final Runnable mHideRunnable = new Runnable() {
+ @Override
+ public void run() {
+ hide(HIDE_DURATION_MS);
+ }
+ };
+ private final OnScrollListener mOnScrollListener = new OnScrollListener() {
+ @Override
+ public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
+ updateScrollPosition(recyclerView.computeHorizontalScrollOffset(),
+ recyclerView.computeVerticalScrollOffset());
+ }
+ };
+
+ public FastScroller(RecyclerView recyclerView, StateListDrawable verticalThumbDrawable,
+ Drawable verticalTrackDrawable, StateListDrawable horizontalThumbDrawable,
+ Drawable horizontalTrackDrawable, int defaultWidth, int scrollbarMinimumRange,
+ int margin, int thumbMinLength, int targetWidth) {
+ mVerticalThumbDrawable = verticalThumbDrawable;
+ mVerticalTrackDrawable = verticalTrackDrawable;
+ mHorizontalThumbDrawable = horizontalThumbDrawable;
+ mHorizontalTrackDrawable = horizontalTrackDrawable;
+ mVerticalThumbWidth = Math.max(defaultWidth, verticalThumbDrawable.getIntrinsicWidth());
+ mVerticalTrackWidth = Math.max(defaultWidth, verticalTrackDrawable.getIntrinsicWidth());
+ mHorizontalThumbHeight = Math
+ .max(defaultWidth, horizontalThumbDrawable.getIntrinsicWidth());
+ mHorizontalTrackHeight = Math
+ .max(defaultWidth, horizontalTrackDrawable.getIntrinsicWidth());
+ mScrollbarMinimumRange = scrollbarMinimumRange;
+ mMargin = margin;
+ mThumbMinLength = thumbMinLength;
+ mTargetWidth = targetWidth;
+ mVerticalThumbDrawable.setAlpha(SCROLLBAR_FULL_OPAQUE);
+ mVerticalTrackDrawable.setAlpha(SCROLLBAR_FULL_OPAQUE);
+
+ mShowHideAnimator.addListener(new AnimatorListener());
+ mShowHideAnimator.addUpdateListener(new AnimatorUpdater());
+
+ attachToRecyclerView(recyclerView);
+ }
+
+ public void attachToRecyclerView(@Nullable RecyclerView recyclerView) {
+ if (mRecyclerView == recyclerView) {
+ return; // nothing to do
+ }
+ if (mRecyclerView != null) {
+ destroyCallbacks();
+ }
+ mRecyclerView = recyclerView;
+ if (mRecyclerView != null) {
+ setupCallbacks();
+ }
+ }
+
+ private void setupCallbacks() {
+ mRecyclerView.addItemDecoration(this);
+ mRecyclerView.addOnItemTouchListener(this);
+ mRecyclerView.addOnScrollListener(mOnScrollListener);
+ }
+
+ private void destroyCallbacks() {
+ mRecyclerView.removeItemDecoration(this);
+ mRecyclerView.removeOnItemTouchListener(this);
+ mRecyclerView.removeOnScrollListener(mOnScrollListener);
+ cancelHide();
+ }
+
+ private void requestRedraw() {
+ mRecyclerView.invalidate();
+ }
+
+ private void setState(@State int state) {
+ if (state == STATE_DRAGGING && mState != STATE_DRAGGING) {
+ mVerticalThumbDrawable.setState(PRESSED_STATE_SET);
+ mVerticalTrackDrawable.setState(PRESSED_STATE_SET);
+ cancelHide();
+ }
+
+ if (state == STATE_HIDDEN) {
+ requestRedraw();
+ } else {
+ show();
+ }
+
+ if (mState == STATE_DRAGGING && state != STATE_DRAGGING) {
+ mVerticalThumbDrawable.setState(EMPTY_STATE_SET);
+ mVerticalTrackDrawable.setState(EMPTY_STATE_SET);
+ resetHideDelay(HIDE_DELAY_AFTER_DRAGGING_MS);
+ } else if (state == STATE_VISIBLE) {
+ resetHideDelay(HIDE_DELAY_AFTER_VISIBLE_MS);
+ }
+ mState = state;
+ }
+
+ private boolean isLayoutRTL() {
+ return ViewCompat.getLayoutDirection(mRecyclerView) == ViewCompat.LAYOUT_DIRECTION_RTL;
+ }
+
+ public boolean isDragging() {
+ return mState == STATE_DRAGGING;
+ }
+
+ @VisibleForTesting
+ boolean isVisible() {
+ return mState == STATE_VISIBLE;
+ }
+
+ @VisibleForTesting
+ boolean isHidden() {
+ return mState == STATE_HIDDEN;
+ }
+
+
+ public void show() {
+ switch (mAnimationState) {
+ case ANIMATION_STATE_FADING_OUT:
+ mShowHideAnimator.cancel();
+ // fall through
+ case ANIMATION_STATE_OUT:
+ mAnimationState = ANIMATION_STATE_FADING_IN;
+ mShowHideAnimator.setFloatValues((float) mShowHideAnimator.getAnimatedValue(), 1);
+ mShowHideAnimator.setDuration(SHOW_DURATION_MS);
+ mShowHideAnimator.setStartDelay(0);
+ mShowHideAnimator.start();
+ break;
+ }
+ }
+
+ public void hide() {
+ hide(0);
+ }
+
+ @VisibleForTesting
+ void hide(int duration) {
+ switch (mAnimationState) {
+ case ANIMATION_STATE_FADING_IN:
+ mShowHideAnimator.cancel();
+ // fall through
+ case ANIMATION_STATE_IN:
+ mAnimationState = ANIMATION_STATE_FADING_OUT;
+ mShowHideAnimator.setFloatValues((float) mShowHideAnimator.getAnimatedValue(), 0);
+ mShowHideAnimator.setDuration(duration);
+ mShowHideAnimator.start();
+ break;
+ }
+ }
+
+ private void cancelHide() {
+ mRecyclerView.removeCallbacks(mHideRunnable);
+ }
+
+ private void resetHideDelay(int delay) {
+ cancelHide();
+ mRecyclerView.postDelayed(mHideRunnable, delay);
+ }
+
+ @Override
+ public void onDrawOver(Canvas canvas, RecyclerView parent, RecyclerView.State state) {
+ if (mRecyclerViewWidth != getRecyclerViewWidth()
+ || mRecyclerViewHeight != getRecyclerViewHeight()) {
+ mRecyclerViewWidth = getRecyclerViewWidth();
+ mRecyclerViewHeight = getRecyclerViewHeight();
+ mRecyclerViewLeftPadding = getRecyclerViewLeftPadding();
+ mRecyclerViewTopPadding = getRecyclerViewTopPadding();
+ mRecyclerViewRightPadding = getRecyclerViewRightPadding();
+ mRecyclerViewBottomPadding = getRecyclerViewBottomPadding();
+
+ // This is due to the different events ordering when keyboard is opened or
+ // retracted vs rotate. Hence to avoid corner cases we just disable the
+ // scroller when size changed, and wait until the scroll position is recomputed
+ // before showing it back.
+ setState(STATE_HIDDEN);
+ return;
+ }
+
+ if (mAnimationState != ANIMATION_STATE_OUT) {
+ if (mNeedVerticalScrollbar) {
+ drawVerticalScrollbar(canvas);
+ }
+ if (mNeedHorizontalScrollbar) {
+ drawHorizontalScrollbar(canvas);
+ }
+ }
+ }
+
+ private int getRecyclerViewWidth() {
+ return mNeedVerticalScrollbar ? mRecyclerView.getWidth() :
+ mRecyclerView.getWidth() - mRecyclerView.getPaddingLeft() -
+ mRecyclerView.getPaddingRight();
+ }
+
+ private int getRecyclerViewHeight() {
+ return mNeedHorizontalScrollbar ? mRecyclerView.getHeight() :
+ mRecyclerView.getHeight() - mRecyclerView.getPaddingTop() -
+ mRecyclerView.getPaddingBottom();
+ }
+
+ private int getRecyclerViewLeftPadding() {
+ return mNeedVerticalScrollbar ? 0 : mRecyclerView.getPaddingLeft();
+ }
+
+ private int getRecyclerViewTopPadding() {
+ return mNeedHorizontalScrollbar ? 0 : mRecyclerView.getPaddingTop();
+ }
+
+ private int getRecyclerViewRightPadding() {
+ return mNeedVerticalScrollbar ? 0 : mRecyclerView.getPaddingRight();
+ }
+
+ private int getRecyclerViewBottomPadding() {
+ return mNeedHorizontalScrollbar ? 0 : mRecyclerView.getPaddingBottom();
+ }
+
+ private void drawVerticalScrollbar(Canvas canvas) {
+ int viewWidth = mRecyclerViewWidth;
+
+ int left = mRecyclerViewLeftPadding + viewWidth - mVerticalThumbWidth;
+ int top = mVerticalThumbCenterY - mVerticalThumbHeight / 2;
+ mVerticalThumbDrawable.setBounds(0, 0, mVerticalThumbWidth, mVerticalThumbHeight);
+
+ int trackLength = mRecyclerViewHeight + mRecyclerViewTopPadding +
+ mRecyclerViewBottomPadding;
+ mVerticalTrackDrawable
+ .setBounds(0, 0, mVerticalTrackWidth, trackLength);
+
+ if (isLayoutRTL()) {
+ mVerticalTrackDrawable.draw(canvas);
+ canvas.translate(mVerticalThumbWidth, top);
+ canvas.scale(-1, 1);
+ mVerticalThumbDrawable.draw(canvas);
+ canvas.scale(1, 1);
+ canvas.translate(-mVerticalThumbWidth, -top);
+ } else {
+ canvas.translate(left, 0);
+ mVerticalTrackDrawable.draw(canvas);
+ canvas.translate(0, top);
+ mVerticalThumbDrawable.draw(canvas);
+ canvas.translate(-left, -top);
+ }
+ }
+
+ private void drawHorizontalScrollbar(Canvas canvas) {
+ int viewHeight = mRecyclerViewHeight;
+
+ int top = mRecyclerViewTopPadding + viewHeight - mHorizontalThumbHeight;
+ int left = mHorizontalThumbCenterX - mHorizontalThumbWidth / 2;
+ mHorizontalThumbDrawable.setBounds(0, 0, mHorizontalThumbWidth, mHorizontalThumbHeight);
+
+ int trackLength = mRecyclerViewWidth + mRecyclerViewLeftPadding +
+ mRecyclerViewRightPadding;
+ mHorizontalTrackDrawable
+ .setBounds(0, 0, trackLength, mHorizontalTrackHeight);
+
+ canvas.translate(0, top);
+ mHorizontalTrackDrawable.draw(canvas);
+ canvas.translate(left, 0);
+ mHorizontalThumbDrawable.draw(canvas);
+ canvas.translate(-left, -top);
+ }
+
+ /**
+ * Notify the scroller of external change of the scroll, e.g. through dragging or flinging on
+ * the view itself.
+ *
+ * @param offsetX The new scroll X offset.
+ * @param offsetY The new scroll Y offset.
+ */
+ void updateScrollPosition(int offsetX, int offsetY) {
+ int verticalContentLength = mRecyclerView.computeVerticalScrollRange();
+ int verticalVisibleLength = mRecyclerViewHeight;
+ mNeedVerticalScrollbar = verticalContentLength - verticalVisibleLength > 0
+ && mRecyclerViewHeight >= mScrollbarMinimumRange;
+
+ int horizontalContentLength = mRecyclerView.computeHorizontalScrollRange();
+ int horizontalVisibleLength = mRecyclerViewWidth;
+ mNeedHorizontalScrollbar = horizontalContentLength - horizontalVisibleLength > 0
+ && mRecyclerViewWidth >= mScrollbarMinimumRange;
+
+ if (!mNeedVerticalScrollbar && !mNeedHorizontalScrollbar) {
+ if (mState != STATE_HIDDEN) {
+ setState(STATE_HIDDEN);
+ }
+ return;
+ }
+
+ if (mNeedVerticalScrollbar) {
+ float middleScreenPos = offsetY + verticalVisibleLength / 2.0f;
+ mVerticalThumbCenterY = mRecyclerViewTopPadding +
+ (int) ((verticalVisibleLength * middleScreenPos) / verticalContentLength);
+ int length = Math.min(verticalVisibleLength,
+ (verticalVisibleLength * verticalVisibleLength) / verticalContentLength);
+ mVerticalThumbHeight = Math.max(mThumbMinLength, length);
+ }
+
+ if (mNeedHorizontalScrollbar) {
+ float middleScreenPos = offsetX + horizontalVisibleLength / 2.0f;
+ mHorizontalThumbCenterX = mRecyclerViewLeftPadding +
+ (int) ((horizontalVisibleLength * middleScreenPos) / horizontalContentLength);
+ int length = Math.min(horizontalVisibleLength,
+ (horizontalVisibleLength * horizontalVisibleLength) / horizontalContentLength);
+ mHorizontalThumbWidth = Math.max(mThumbMinLength, length);
+ }
+
+ if (mState == STATE_HIDDEN || mState == STATE_VISIBLE) {
+ setState(STATE_VISIBLE);
+ }
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(RecyclerView recyclerView, MotionEvent ev) {
+ final boolean handled;
+ if (mState == STATE_VISIBLE) {
+ boolean insideVerticalThumb = isPointInsideVerticalThumb(ev.getX(), ev.getY());
+ boolean insideHorizontalThumb = isPointInsideHorizontalThumb(ev.getX(), ev.getY());
+ if (ev.getAction() == MotionEvent.ACTION_DOWN
+ && (insideVerticalThumb || insideHorizontalThumb)) {
+ if (insideHorizontalThumb) {
+ mDragState = DRAG_X;
+ mHorizontalDragX = (int) ev.getX();
+ mHorizontalDragThumbWidth = mHorizontalThumbWidth;
+ } else if (insideVerticalThumb) {
+ mDragState = DRAG_Y;
+ mVerticalDragY = (int) ev.getY();
+ mVerticalDragThumbHeight = mVerticalThumbHeight;
+ }
+
+ setState(STATE_DRAGGING);
+ handled = true;
+ } else {
+ handled = false;
+ }
+ } else if (mState == STATE_DRAGGING) {
+ handled = true;
+ } else {
+ handled = false;
+ }
+ return handled;
+ }
+
+ @Override
+ public void onTouchEvent(RecyclerView recyclerView, MotionEvent me) {
+ if (mState == STATE_HIDDEN) {
+ return;
+ }
+
+ if (me.getAction() == MotionEvent.ACTION_DOWN) {
+ boolean insideVerticalThumb = isPointInsideVerticalThumb(me.getX(), me.getY());
+ boolean insideHorizontalThumb = isPointInsideHorizontalThumb(me.getX(), me.getY());
+ if (insideVerticalThumb || insideHorizontalThumb) {
+ if (insideHorizontalThumb) {
+ mDragState = DRAG_X;
+ mHorizontalDragX = (int) me.getX();
+ mHorizontalDragThumbWidth = mHorizontalThumbWidth;
+ } else if (insideVerticalThumb) {
+ mDragState = DRAG_Y;
+ mVerticalDragY = (int) me.getY();
+ mVerticalDragThumbHeight = mVerticalThumbHeight;
+ }
+ setState(STATE_DRAGGING);
+ }
+ } else if (me.getAction() == MotionEvent.ACTION_UP && mState == STATE_DRAGGING) {
+ mVerticalDragY = 0;
+ mHorizontalDragX = 0;
+ setState(STATE_VISIBLE);
+ mDragState = DRAG_NONE;
+ } else if (me.getAction() == MotionEvent.ACTION_MOVE && mState == STATE_DRAGGING) {
+ show();
+ if (mDragState == DRAG_X) {
+ horizontalScrollTo(me.getX());
+ }
+ if (mDragState == DRAG_Y) {
+ verticalScrollTo(me.getY());
+ }
+ }
+ }
+
+ @Override
+ public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
+ }
+
+ private void verticalScrollTo(float y) {
+ final int[] scrollbarRange = getVerticalRange();
+ y = Math.max(scrollbarRange[0], Math.min(scrollbarRange[1], y));
+ if (Math.abs(mVerticalThumbCenterY - y) < 2) {
+ return;
+ }
+ int scrollingBy = scrollTo(mVerticalDragY, y, scrollbarRange,
+ mRecyclerView.computeVerticalScrollRange(),
+ mRecyclerView.computeVerticalScrollOffset(),
+ mRecyclerViewHeight, mVerticalDragThumbHeight);
+ if (scrollingBy != 0) {
+ mRecyclerView.scrollBy(0, scrollingBy);
+ }
+ mVerticalDragY = y;
+ }
+
+ private void horizontalScrollTo(float x) {
+ final int[] scrollbarRange = getHorizontalRange();
+ x = Math.max(scrollbarRange[0], Math.min(scrollbarRange[1], x));
+ if (Math.abs(mHorizontalThumbCenterX - x) < 2) {
+ return;
+ }
+
+ int scrollingBy = scrollTo(mHorizontalDragX, x, scrollbarRange,
+ mRecyclerView.computeHorizontalScrollRange(),
+ mRecyclerView.computeHorizontalScrollOffset(),
+ mRecyclerViewWidth, mHorizontalDragThumbWidth);
+ if (scrollingBy != 0) {
+ mRecyclerView.scrollBy(scrollingBy, 0);
+ }
+
+ mHorizontalDragX = x;
+ }
+
+ private int scrollTo(float oldDragPos, float newDragPos, int[] scrollbarRange, int scrollRange,
+ int scrollOffset, int viewLength, int dragThumbLength) {
+ int scrollbarLength = scrollbarRange[1] - scrollbarRange[0] - dragThumbLength;
+ if (scrollbarLength == 0) {
+ return 0;
+ }
+ float percentage = ((newDragPos - oldDragPos) / (float) scrollbarLength);
+ int totalPossibleOffset = scrollRange - viewLength;
+ int scrollingBy = (int) (percentage * totalPossibleOffset);
+ int absoluteOffset = scrollOffset + scrollingBy;
+ if (absoluteOffset < totalPossibleOffset && absoluteOffset >= 0) {
+ return scrollingBy;
+ } else {
+ return 0;
+ }
+ }
+
+ @VisibleForTesting
+ boolean isPointInsideVerticalThumb(float x, float y) {
+ // width divided by 2 for rtl? keeping it the same as upstream, but seems illogical.
+ return (isLayoutRTL() ? x <= mRecyclerViewLeftPadding + mTargetWidth / 2
+ : x >= mRecyclerViewLeftPadding + mRecyclerViewWidth - mTargetWidth)
+ && y >= mVerticalThumbCenterY - mVerticalThumbHeight / 2 - mTargetWidth
+ && y <= mVerticalThumbCenterY + mVerticalThumbHeight / 2 + mTargetWidth;
+ }
+
+ @VisibleForTesting
+ boolean isPointInsideHorizontalThumb(float x, float y) {
+ return (y >= mRecyclerViewTopPadding + mRecyclerViewHeight - mTargetWidth)
+ && x >= mHorizontalThumbCenterX - mHorizontalThumbWidth / 2 - mTargetWidth
+ && x <= mHorizontalThumbCenterX + mHorizontalThumbWidth / 2 + mTargetWidth;
+ }
+
+ @VisibleForTesting
+ Drawable getHorizontalTrackDrawable() {
+ return mHorizontalTrackDrawable;
+ }
+
+ @VisibleForTesting
+ Drawable getHorizontalThumbDrawable() {
+ return mHorizontalThumbDrawable;
+ }
+
+ @VisibleForTesting
+ Drawable getVerticalTrackDrawable() {
+ return mVerticalTrackDrawable;
+ }
+
+ @VisibleForTesting
+ Drawable getVerticalThumbDrawable() {
+ return mVerticalThumbDrawable;
+ }
+
+ /**
+ * Gets the (min, max) vertical positions of the vertical scroll bar.
+ */
+ private int[] getVerticalRange() {
+ mVerticalRange[0] = mRecyclerViewTopPadding + mMargin;
+ mVerticalRange[1] = mRecyclerViewTopPadding + mRecyclerViewHeight - mMargin;
+ return mVerticalRange;
+ }
+
+ /**
+ * Gets the (min, max) horizontal positions of the horizontal scroll bar.
+ */
+ private int[] getHorizontalRange() {
+ mHorizontalRange[0] = mRecyclerViewLeftPadding + mMargin;
+ mHorizontalRange[1] = mRecyclerViewLeftPadding + mRecyclerViewWidth - mMargin;
+ return mHorizontalRange;
+ }
+
+ private class AnimatorListener extends AnimatorListenerAdapter {
+
+ private boolean mCanceled = false;
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ // Cancel is always followed by a new directive, so don't update state.
+ if (mCanceled) {
+ mCanceled = false;
+ return;
+ }
+ if ((float) mShowHideAnimator.getAnimatedValue() == 0) {
+ mAnimationState = ANIMATION_STATE_OUT;
+ setState(STATE_HIDDEN);
+ } else {
+ mAnimationState = ANIMATION_STATE_IN;
+ requestRedraw();
+ }
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ mCanceled = true;
+ }
+ }
+
+ private class AnimatorUpdater implements AnimatorUpdateListener {
+
+ @Override
+ public void onAnimationUpdate(ValueAnimator valueAnimator) {
+ int alpha = (int) (SCROLLBAR_FULL_OPAQUE * ((float) valueAnimator.getAnimatedValue()));
+ mVerticalThumbDrawable.setAlpha(alpha);
+ mVerticalTrackDrawable.setAlpha(alpha);
+ requestRedraw();
+ }
+ }
+}
diff --git a/Clover/app/src/main/res/drawable/recyclerview_fastscroll_thumb_selector.xml b/Clover/app/src/main/res/drawable/recyclerview_fastscroll_thumb_selector.xml
new file mode 100644
index 00000000..9aeee17d
--- /dev/null
+++ b/Clover/app/src/main/res/drawable/recyclerview_fastscroll_thumb_selector.xml
@@ -0,0 +1,29 @@
+
+