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 @@ + + + + + + + + + + + + + diff --git a/Clover/app/src/main/res/drawable/recyclerview_fastscroll_track_selector.xml b/Clover/app/src/main/res/drawable/recyclerview_fastscroll_track_selector.xml new file mode 100644 index 00000000..18336b0f --- /dev/null +++ b/Clover/app/src/main/res/drawable/recyclerview_fastscroll_track_selector.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + +