From 5ec990d4368622ed0ade5731efaf46c2357d920d Mon Sep 17 00:00:00 2001 From: Tobias Erthal Date: Mon, 19 Sep 2016 17:55:09 +0200 Subject: [PATCH] Added FastScroll capability to KeyListFragment. Removed unnecessary dependency. --- OpenKeychain/build.gradle | 2 - .../fastscroll/FastScrollRecyclerView.java | 122 ++++++++ .../recyclerview/fastscroll/FastScroller.java | 268 ++++++++++++++++++ .../src/main/res/layout/key_list_fragment.xml | 10 +- OpenKeychain/src/main/res/values/attr.xml | 10 + 5 files changed, 405 insertions(+), 7 deletions(-) create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/recyclerview/fastscroll/FastScrollRecyclerView.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/recyclerview/fastscroll/FastScroller.java diff --git a/OpenKeychain/build.gradle b/OpenKeychain/build.gradle index 1030b0204..cef6f9133 100644 --- a/OpenKeychain/build.gradle +++ b/OpenKeychain/build.gradle @@ -33,7 +33,6 @@ dependencies { compile 'org.thoughtcrime.ssl.pinning:AndroidPinning:1.0.0' compile 'com.cocosw:bottomsheet:1.3.0@aar' compile 'com.tonicartos:superslim:0.4.13' - compile 'com.simplecityapps:recyclerview-fastscroll:1.0.10' // Material Drawer compile 'com.mikepenz:materialdrawer:5.2.2@aar' @@ -110,7 +109,6 @@ dependencyVerification { 'org.thoughtcrime.ssl.pinning:AndroidPinning:afa1d74e699257fa75cb109ff29bac50726ef269c6e306bdeffe8223cee06ef4', 'com.cocosw:bottomsheet:4af6112a7f4cad4e2b70e5fdf1edc39f51275523a0f53011a012837dc103e597', 'com.tonicartos:superslim:ca89b5c674660cc6918a8f8fd385065bffeee27983e0d33c7c2f0ad7b34d2d49', - 'com.simplecityapps:recyclerview-fastscroll:3b4c7ac18c6cff367723f5954643735b777cd9fe32a1a200273259145b5e17b4', 'com.mikepenz:materialdrawer:4169462fdde042e2bb53a7c2b4e2334d569d16b2020781ee05741b50e1a2967d', 'com.mikepenz:fastadapter:1bfc00216d71dfdfe0d8e7a9d92bb97bfaa1794543930e34b1f79d5d7adbddf6', 'com.mikepenz:materialize:575195b2fa5b2414fb14a59470ee21d8a8cd8355b651e0cf52e477e3ff1cd96c', diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/recyclerview/fastscroll/FastScrollRecyclerView.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/recyclerview/fastscroll/FastScrollRecyclerView.java new file mode 100644 index 000000000..7b1c7a388 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/recyclerview/fastscroll/FastScrollRecyclerView.java @@ -0,0 +1,122 @@ +package org.sufficientlysecure.keychain.ui.util.recyclerview.fastscroll; + +import android.content.Context; +import android.graphics.Canvas; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.StaggeredGridLayoutManager; +import android.util.AttributeSet; +import android.view.MotionEvent; + +import com.tonicartos.superslim.LayoutManager; + +public class FastScrollRecyclerView extends RecyclerView implements RecyclerView.OnItemTouchListener { + private FastScroller mFastScroller; + + private int mLastX; + private int mLastY; + + public FastScrollRecyclerView(Context context) { + this(context, null); + } + + public FastScrollRecyclerView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public FastScrollRecyclerView(Context context, AttributeSet attributeSet, int defStyleAttr) { + super(context, attributeSet, defStyleAttr); + mFastScroller = new FastScroller(this, attributeSet); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + addOnItemTouchListener(this); + } + + @Override + public void onDraw(Canvas canvas) { + super.onDraw(canvas); + mFastScroller.draw(canvas); + } + + @Override + public void onScrolled(int x, int y) { + super.onScrolled(x, y); + mFastScroller.updateThumb( + computeVerticalScrollOffset(), + computeVerticalScrollExtent()); + } + + @Override + public void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + mFastScroller.updateContainer(top, right, bottom); + mFastScroller.updateThumb( + computeVerticalScrollOffset(), + computeVerticalScrollExtent()); + } + + @Override + public void onScrollStateChanged(int state) { + switch (state) { + case SCROLL_STATE_IDLE: + mFastScroller.hideBar(); + break; + case SCROLL_STATE_DRAGGING: + mFastScroller.showBar(); + break; + } + } + + @Override + public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) { + if(mFastScroller.onInterceptTouchEvent(e)) { + onTouchEvent(rv, e); + return true; + } else { + return false; + } + } + + @Override + public void onTouchEvent(RecyclerView rv, MotionEvent event) { + int x = (int) event.getX(); + int y = (int) event.getY(); + + mFastScroller.handleTouchEvent(event.getAction(), x, y, mLastX, mLastY); + mLastX = x; + mLastY = y; + } + + @Override + public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { + + } + + private int getItemCount() { + return getAdapter() != null ? + getAdapter().getItemCount() : 0; + } + + public void scrollToFraction(float fraction) { + int count = getItemCount(); + if (count > 0) { + stopScroll(); + scrollToPosition((int) ((count - 1) * fraction)); + } + } + + public void scrollByFraction(float fraction) { + int count = getItemCount(); + if (count > 0) { + stopScroll(); + + + int pixelsToScroll = (int) (computeVerticalScrollRange() * fraction); + System.out.println("ScrollBy Fraction: " + fraction + ", Pixel: " + pixelsToScroll); + + scrollBy(0, pixelsToScroll); + } + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/recyclerview/fastscroll/FastScroller.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/recyclerview/fastscroll/FastScroller.java new file mode 100644 index 000000000..8b55221ac --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/recyclerview/fastscroll/FastScroller.java @@ -0,0 +1,268 @@ +package org.sufficientlysecure.keychain.ui.util.recyclerview.fastscroll; + +import android.animation.ValueAnimator; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.support.v4.view.animation.FastOutLinearInInterpolator; +import android.support.v4.view.animation.FastOutSlowInInterpolator; +import android.support.v7.widget.RecyclerView; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.ViewConfiguration; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.ui.util.FormattingUtils; + +public class FastScroller { + private static final int DEFAULT_AUTO_HIDE_DELAY = 1500; + private static final boolean DEFAULT_AUTO_HIDE_ENABLED = true; + + private Paint mTrack; + private Paint mThumb; + + private int mPosX; + private int mPosY; + private int mWidth; + private int mHeight; + + private Rect mContainer; + private boolean mIsDragging; + + private int mAutoHideDelay; + private boolean mAutoHideEnabled; + + private final int mTrackColorNormal; + private final int mTrackColorDragging; + + private final int mThumbColorNormal; + private final int mThumbColorDragging; + + private ValueAnimator mBarAnimator; + private final FastScrollRecyclerView mRecyclerView; + + private final int mTouchSlop; + private final int mTouchInset; + private final int[] mKeyFrames; + + //private final Runnable mHideRunnable; + + public FastScroller(final FastScrollRecyclerView recyclerView, AttributeSet attributeSet) { + Context context = recyclerView.getContext(); + mRecyclerView = recyclerView; + + mTrack = new Paint(Paint.ANTI_ALIAS_FLAG); + mThumb = new Paint(Paint.ANTI_ALIAS_FLAG); + + mKeyFrames = new int[2]; + mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); + mTouchInset = FormattingUtils.dpToPx(context, 8); + + TypedArray typedArray = context.getTheme().obtainStyledAttributes( + attributeSet, R.styleable.FastScroller, 0, 0); + + try { + mWidth = typedArray.getDimensionPixelSize( + R.styleable.FastScroller_fastScrollScrollBarWidth, FormattingUtils.dpToPx(context, 6)); + + mAutoHideDelay = typedArray.getInteger( + R.styleable.FastScroller_fastScrollAutoHideDelay, DEFAULT_AUTO_HIDE_DELAY); + mAutoHideEnabled = typedArray.getBoolean( + R.styleable.FastScroller_fastScrollAutoHideEnabled, DEFAULT_AUTO_HIDE_ENABLED); + + mTrackColorNormal = typedArray.getColor( + R.styleable.FastScroller_fastScrollTrackColorNormal, Color.LTGRAY); + mTrackColorDragging = typedArray.getColor( + R.styleable.FastScroller_fastScrollTrackColorDragging, Color.GRAY); + + mThumbColorNormal = typedArray.getColor( + R.styleable.FastScroller_fastScrollThumbColorNormal, Color.GRAY); + mThumbColorDragging = typedArray.getColor( + R.styleable.FastScroller_fastScrollThumbColorDragging, Color.BLUE); + + mTrack.setColor(mTrackColorNormal); + mThumb.setColor(mThumbColorNormal); + } finally { + typedArray.recycle(); + } + } + + public void hideBar() { + if(mPosX >= mWidth || !mAutoHideEnabled) { + return; + } + + mKeyFrames[0] = mPosX; + mKeyFrames[1] = mWidth; + + prepareAnimator(); + mBarAnimator.setIntValues(mKeyFrames); + mBarAnimator.setStartDelay(mAutoHideDelay); + mBarAnimator.setInterpolator(new FastOutLinearInInterpolator()); + mBarAnimator.start(); + } + + public void showBar() { + if(mPosX < 1) { + return; + } + + mKeyFrames[0] = mPosX; + mKeyFrames[1] = 0; + + prepareAnimator(); + mBarAnimator.setStartDelay(0); + mBarAnimator.setIntValues(mKeyFrames); + mBarAnimator.setInterpolator(new FastOutSlowInInterpolator()); + mBarAnimator.start(); + } + + public boolean isAnimatingShow() { + return mKeyFrames[1] == 0 && mBarAnimator.isStarted(); + } + + public boolean isAnimatingHide() { + return mKeyFrames[1] == mWidth && mBarAnimator.isStarted(); + } + + public boolean isBarFullyShown() { + return mPosX < 1; + } + + public boolean onInterceptTouchEvent(MotionEvent event) { + return mContainer != null + && (event.getX() < mContainer.right) + && (event.getX() > mContainer.left - mTouchInset) + && (event.getY() < mContainer.bottom) + && (event.getY() > mContainer.top); + } + + private boolean isInThumb(int x, int y) { + return x > (mContainer.left + mPosX - mTouchInset) + && x < (mContainer.right + mTouchInset) + && y > (mContainer.top + mPosY - mTouchInset) + && y < (mContainer.top + mPosY + mHeight + mTouchInset); + } + + public void draw(Canvas canvas) { + if(mPosX < 0 || mPosX >= mWidth) { + return; + } + + if(!mRecyclerView.canScrollVertically(-1) + && ! mRecyclerView.canScrollVertically(1)) { + return; + } + + int topBound = mContainer.top + mPosY; + int leftBound = mContainer.left + mPosX; + + canvas.drawRect(leftBound, mContainer.top, mContainer.right, mContainer.bottom, mTrack); + canvas.drawRect(leftBound, topBound, mContainer.right, topBound + mHeight, mThumb); + } + + public void updateThumb(int offset, int extent) { + mPosY = offset; + mHeight = extent; + } + + public void updateContainer(int top, int right, int bottom) { + mPosX = 0; + mContainer = new Rect(right - mWidth, top, right, bottom); + + invalidate(); + } + + private void invalidate() { + mRecyclerView.invalidate(mContainer); + } + + private void prepareAnimator() { + if (mBarAnimator != null) { + mBarAnimator.cancel(); + } else { + mBarAnimator = new ValueAnimator(); + mBarAnimator.setDuration(150); + mBarAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + mPosX = (Integer) animation.getAnimatedValue(); + + invalidate(); + } + }); + } + } + + public void handleTouchEvent(int action, int x, int y, int lastX, int lastY) { + if(!mRecyclerView.canScrollVertically(-1) + && ! mRecyclerView.canScrollVertically(1)) { + return; + } + + switch (action) { + case MotionEvent.ACTION_DOWN: + if(isBarFullyShown()) { // + prepareAnimator(); // cancel any pending animations + + mTrack.setColor(mTrackColorDragging); + mThumb.setColor(mThumbColorDragging); + + if(!isInThumb(x, y)) { + // jump to point + mPosY = Math.min( + Math.max(mContainer.top, y - (mHeight / 2)), + mContainer.bottom - mHeight + ); + + float range = (mContainer.bottom - mContainer.top) - mHeight; + mRecyclerView.scrollToFraction(mPosY / range); + } else { + invalidate(); + } + } else { + if(!isAnimatingShow()) { + showBar(); + } + } + break; + case MotionEvent.ACTION_MOVE: + if((!mIsDragging + && isInThumb(x, y) + && Math.abs(y - lastY) > mTouchSlop) + || mIsDragging) { + + if(!mIsDragging) { + mIsDragging = true; + } + + float dist = y - lastY; + float range = (mContainer.bottom - mContainer.top) - mHeight; + + if(mRecyclerView.canScrollVertically(dist < 0 ? -1 : 1)) { + mRecyclerView.scrollByFraction(dist / range); + } + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + if(mIsDragging) { + mIsDragging = false; + } + + mTrack.setColor(mTrackColorNormal); + mThumb.setColor(mThumbColorNormal); + + if(!mBarAnimator.isRunning() + && mRecyclerView.getScrollState() + == RecyclerView.SCROLL_STATE_IDLE) { + hideBar(); + invalidate(); + } + break; + } + } +} diff --git a/OpenKeychain/src/main/res/layout/key_list_fragment.xml b/OpenKeychain/src/main/res/layout/key_list_fragment.xml index 3fb47d8f1..05a32efaf 100644 --- a/OpenKeychain/src/main/res/layout/key_list_fragment.xml +++ b/OpenKeychain/src/main/res/layout/key_list_fragment.xml @@ -1,6 +1,5 @@ - - + custom:fastScrollScrollBarWidth="8dp" + custom:fastScrollThumbColorNormal="@color/selected_gray" + custom:fastScrollThumbColorDragging="@color/accent" /> + + + + + + + + + + \ No newline at end of file