Files
EhViewer/app/src/main/java/com/hippo/easyrecyclerview/FastScroller.java
T
2021-03-21 12:33:46 +08:00

405 lines
13 KiB
Java

/*
* Copyright 2015 Hippo Seven
*
* 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 com.hippo.easyrecyclerview;
import android.animation.Animator;
import android.animation.ObjectAnimator;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.hippo.ehviewer.R;
import com.hippo.yorozuya.AnimationUtils;
import com.hippo.yorozuya.LayoutUtils;
import com.hippo.yorozuya.MathUtils;
import com.hippo.yorozuya.SimpleAnimatorListener;
import com.hippo.yorozuya.SimpleHandler;
public class FastScroller extends View {
private static final int INVALID = -1;
private static final int SCROLL_BAR_FADE_DURATION = 500;
private static final int SCROLL_BAR_DELAY = 1000;
private static final int MIN_HANDLER_HEIGHT_DP = 32;
private Handler mSimpleHandler;
private boolean mDraggable;
private int mMinHandlerHeight;
private RecyclerView mRecyclerView;
private RecyclerView.OnScrollListener mOnScrollChangeListener;
private RecyclerView.Adapter<?> mAdapter;
private RecyclerView.AdapterDataObserver mAdapterDataObserver;
private Drawable mHandler;
private int mHandlerOffset = INVALID;
private int mHandlerHeight = INVALID;
private float mDownX = INVALID;
private float mDownY = INVALID;
private float mLastMotionY = INVALID;
private boolean mDragged = false;
private boolean mCantDrag = false;
private int mTouchSlop;
private OnDragHandlerListener mListener;
private ObjectAnimator mShowAnimator;
private ObjectAnimator mHideAnimator;
private final Runnable mHideRunnable = new Runnable() {
@Override
public void run() {
mHideAnimator.start();
}
};
public FastScroller(Context context) {
super(context);
init(context, null, 0);
}
public FastScroller(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs, 0);
}
public FastScroller(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs, defStyleAttr);
}
private void init(Context context, AttributeSet attrs, int defStyleAttr) {
mSimpleHandler = SimpleHandler.getInstance();
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FastScroller, defStyleAttr, 0);
mHandler = a.getDrawable(R.styleable.FastScroller_handler);
mDraggable = a.getBoolean(R.styleable.FastScroller_draggable, true);
a.recycle();
setAlpha(0.0f);
setVisibility(INVISIBLE);
mMinHandlerHeight = LayoutUtils.dp2pix(context, MIN_HANDLER_HEIGHT_DP);
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
mShowAnimator = ObjectAnimator.ofFloat(this, "alpha", 1.0f);
mShowAnimator.setInterpolator(AnimationUtils.FAST_SLOW_INTERPOLATOR);
mShowAnimator.setDuration(SCROLL_BAR_FADE_DURATION);
mHideAnimator = ObjectAnimator.ofFloat(this, "alpha", 0.0f);
mHideAnimator.setInterpolator(AnimationUtils.SLOW_FAST_INTERPOLATOR);
mHideAnimator.setDuration(SCROLL_BAR_FADE_DURATION);
mHideAnimator.addListener(new SimpleAnimatorListener() {
private boolean mCancel;
@Override
public void onAnimationCancel(Animator animation) {
mCancel = true;
}
@Override
public void onAnimationEnd(Animator animation) {
if (mCancel) {
mCancel = false;
} else {
setVisibility(INVISIBLE);
}
}
});
}
public void setOnDragHandlerListener(OnDragHandlerListener listener) {
mListener = listener;
}
private void updatePosition(boolean show) {
if (mRecyclerView == null) {
return;
}
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();
int height = getHeight() - paddingTop - paddingBottom;
int offset = mRecyclerView.computeVerticalScrollOffset();
int extent = mRecyclerView.computeVerticalScrollExtent();
int range = mRecyclerView.computeVerticalScrollRange();
if (height <= 0 || extent >= range || extent <= 0) {
return;
}
int endOffset = height * offset / range;
int endHeight = height * extent / range;
endHeight = Math.max(endHeight, mMinHandlerHeight);
endOffset = Math.min(endOffset, height - endHeight);
mHandlerOffset = endOffset + paddingTop;
mHandlerHeight = endHeight;
if (show) {
if (mHideAnimator.isRunning()) {
mHideAnimator.cancel();
mShowAnimator.start();
} else if (getVisibility() != VISIBLE && !mShowAnimator.isRunning()) {
setVisibility(VISIBLE);
mShowAnimator.start();
}
Handler handler = mSimpleHandler;
handler.removeCallbacks(mHideRunnable);
if (!mDragged) {
handler.postDelayed(mHideRunnable, SCROLL_BAR_DELAY);
}
}
}
public void setHandlerDrawable(Drawable drawable) {
mHandler = drawable;
invalidate();
}
public boolean isDraggable() {
return mDraggable;
}
public void setDraggable(boolean draggable) {
mDraggable = draggable;
if (mDragged) {
mDragged = false;
}
mSimpleHandler.removeCallbacks(mHideRunnable);
mHideRunnable.run();
}
public boolean isAttached() {
return mRecyclerView != null;
}
public void attachToRecyclerView(RecyclerView recyclerView) {
if (recyclerView == null) {
return;
}
if (mRecyclerView != null) {
throw new IllegalStateException("The FastScroller is already attached to a RecyclerView, " +
"call detachedFromRecyclerView first");
}
mRecyclerView = recyclerView;
mOnScrollChangeListener = new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
}
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
updatePosition(true);
invalidate();
}
};
recyclerView.addOnScrollListener(mOnScrollChangeListener);
mAdapter = recyclerView.getAdapter();
if (mAdapter != null) {
mAdapterDataObserver = new RecyclerView.AdapterDataObserver() {
@Override
public void onItemRangeChanged(int positionStart, int itemCount) {
super.onItemRangeChanged(positionStart, itemCount);
updatePosition(false);
invalidate();
}
@Override
public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
super.onItemRangeChanged(positionStart, itemCount, payload);
updatePosition(false);
invalidate();
}
@Override
public void onItemRangeInserted(int positionStart, int itemCount) {
super.onItemRangeInserted(positionStart, itemCount);
updatePosition(false);
invalidate();
}
@Override
public void onItemRangeRemoved(int positionStart, int itemCount) {
super.onItemRangeRemoved(positionStart, itemCount);
updatePosition(false);
invalidate();
}
@Override
public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
super.onItemRangeMoved(fromPosition, toPosition, itemCount);
updatePosition(false);
invalidate();
}
};
mAdapter.registerAdapterDataObserver(mAdapterDataObserver);
}
}
public void detachedFromRecyclerView() {
if (mRecyclerView != null && mOnScrollChangeListener != null) {
mRecyclerView.removeOnScrollListener(mOnScrollChangeListener);
}
mRecyclerView = null;
mOnScrollChangeListener = null;
if (mAdapter != null && mAdapterDataObserver != null) {
mAdapter.unregisterAdapterDataObserver(mAdapterDataObserver);
}
mAdapter = null;
mAdapterDataObserver = null;
setAlpha(0.0f);
setVisibility(INVISIBLE);
}
@Override
protected void onDraw(Canvas canvas) {
if (mRecyclerView == null || mHandler == null) {
return;
}
if (mHandlerHeight == INVALID) {
updatePosition(false);
}
if (mHandlerHeight == INVALID) {
return;
}
int paddingLeft = getPaddingLeft();
int saved = canvas.save();
canvas.translate(paddingLeft, mHandlerOffset);
mHandler.setBounds(0, 0, getWidth() - paddingLeft - getPaddingRight(), mHandlerHeight);
mHandler.draw(canvas);
canvas.restoreToCount(saved);
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!mDraggable || getVisibility() != VISIBLE || mRecyclerView == null || mHandlerHeight == INVALID) {
return false;
}
int action = event.getAction();
if (action == MotionEvent.ACTION_DOWN) {
mCantDrag = false;
}
if (mCantDrag) {
return false;
}
switch (action) {
case MotionEvent.ACTION_DOWN: {
mDragged = false;
mDownX = event.getX();
mDownY = event.getY();
if (mDownY < mHandlerOffset || mDownY > mHandlerOffset + mHandlerHeight) {
mCantDrag = true;
return false;
}
break;
}
case MotionEvent.ACTION_MOVE: {
if (!mDragged) {
float x = event.getX();
float y = event.getY();
// Check touch slop
if (MathUtils.dist(x, y, mDownX, mDownY) < mTouchSlop) {
return true;
}
if (Math.abs(x - mDownX) > Math.abs(y - mDownY) ||
y < mHandlerOffset || y > mHandlerOffset + mHandlerHeight) {
mCantDrag = true;
return false;
} else {
mDragged = true;
mSimpleHandler.removeCallbacks(mHideRunnable);
// Update mLastMotionY
if (mDownY < mHandlerOffset || mDownY >= mHandlerOffset + mHandlerHeight) {
// the point out of handler, make the point in handler center
mLastMotionY = mHandlerOffset + mHandlerHeight / 2;
} else {
mLastMotionY = mDownY;
}
// Notify
if (mListener != null) {
mListener.onStartDragHandler();
}
}
}
int range = mRecyclerView.computeVerticalScrollRange();
if (range <= 0) {
break;
}
float y = event.getY();
int scroll = (int) (range * (y - mLastMotionY) / (getHeight() - getPaddingTop() - getPaddingBottom()));
mRecyclerView.scrollBy(0, scroll);
mLastMotionY = y;
break;
}
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
// Notify
if (mDragged && mListener != null) {
mListener.onEndDragHandler();
}
mDragged = false;
mSimpleHandler.postDelayed(mHideRunnable, SCROLL_BAR_DELAY);
break;
}
return true;
}
public interface OnDragHandlerListener {
void onStartDragHandler();
void onEndDragHandler();
}
}