add fastscroll support.

the scroller is attached to the thread recyclerview. it is a modified
version of the FastScroller recently added to the support library,
that adds support for padding and some other tweaks.

the scrollbar looks just as thin as the normal scrollbar, but the grab
area is bigger than the visible bar.
refactor-toolbar
Floens 8 years ago
parent 3c7f68e21d
commit 10155cf644
  1. 44
      Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadListLayout.java
  2. 662
      Clover/app/src/main/java/org/floens/chan/ui/view/FastScroller.java
  3. 29
      Clover/app/src/main/res/drawable/recyclerview_fastscroll_thumb_selector.xml
  4. 29
      Clover/app/src/main/res/drawable/recyclerview_fastscroll_track_selector.xml

@ -20,9 +20,12 @@ package org.floens.chan.ui.layout;
import android.animation.Animator; import android.animation.Animator;
import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorListenerAdapter;
import android.content.Context; import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.StateListDrawable;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.support.v7.widget.GridLayoutManager; 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.PostCellInterface;
import org.floens.chan.ui.cell.ThreadStatusCell; import org.floens.chan.ui.cell.ThreadStatusCell;
import org.floens.chan.ui.toolbar.Toolbar; 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.ui.view.ThumbnailView;
import org.floens.chan.utils.AndroidUtils; 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.dp;
import static org.floens.chan.utils.AndroidUtils.getAttrColor; import static org.floens.chan.utils.AndroidUtils.getAttrColor;
import static org.floens.chan.utils.AndroidUtils.getDimen; 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. * 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 TextView searchStatus;
private RecyclerView recyclerView; private RecyclerView recyclerView;
private RecyclerView.LayoutManager layoutManager; private RecyclerView.LayoutManager layoutManager;
private FastScroller fastScroller;
private PostAdapter postAdapter; private PostAdapter postAdapter;
private ChanThread showingThread; private ChanThread showingThread;
private ThreadListLayoutPresenterCallback callback; private ThreadListLayoutPresenterCallback callback;
@ -120,6 +126,8 @@ public class ThreadListLayout extends FrameLayout implements ReplyLayout.ReplyLa
recyclerView.setAdapter(postAdapter); recyclerView.setAdapter(postAdapter);
recyclerView.addOnScrollListener(scrollListener); recyclerView.addOnScrollListener(scrollListener);
setFastScroll(false);
attachToolbarScroll(true); attachToolbarScroll(true);
reply.setPadding(0, toolbarHeight(), 0, 0); reply.setPadding(0, toolbarHeight(), 0, 0);
@ -226,6 +234,8 @@ public class ThreadListLayout extends FrameLayout implements ReplyLayout.ReplyLa
party(); party();
} }
setFastScroll(true);
postAdapter.setThread(thread, filter); 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() { private void setRecyclerViewPadding() {
int defaultPadding = 0; int defaultPadding = 0;
if (postViewMode == ChanSettings.PostViewMode.CARD) { if (postViewMode == ChanSettings.PostViewMode.CARD) {

@ -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.
* <p>
* 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();
}
}
}

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?><!--
Clover - 4chan browser https://github.com/Floens/Clover/
Copyright (C) 2014 Floens
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape android:shape="rectangle">
<solid android:color="#ff009688" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<solid android:color="#5a000000" />
</shape>
</item>
</selector>

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?><!--
Clover - 4chan browser https://github.com/Floens/Clover/
Copyright (C) 2014 Floens
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape android:shape="rectangle">
<solid android:color="#12000000" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<solid android:color="#00000000" />
</shape>
</item>
</selector>
Loading…
Cancel
Save