/*
* Copyright (c) 2016 Tim Malseed
*
* 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 io.hefuyi.listener.widget.fastscroller;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Typeface;
import android.support.annotation.ColorInt;
import android.support.annotation.NonNull;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import io.hefuyi.listener.util.ListenerUtil;
public class FastScrollRecyclerView extends RecyclerView implements RecyclerView.OnItemTouchListener {
private FastScroller mScrollbar;
/**
* The current scroll state of the recycler view. We use this in onUpdateScrollbar()
* and scrollToPositionAtProgress() to determine the scroll position of the recycler view so
* that we can calculate what the scroll bar looks like, and where to jump to from the fast
* scroller.
*/
public static class ScrollPositionState {
// The index of the first visible row
public int rowIndex;
// The offset of the first visible row
public int rowTopOffset;
// The height of a given row (they are currently all the same height)
public int rowHeight;
}
private ScrollPositionState mScrollPosState = new ScrollPositionState();
private int mDownX;
private int mDownY;
private int mLastY;
private OnFastScrollStateChangeListener mStateChangeListener;
public FastScrollRecyclerView(Context context) {
this(context, null);
}
public FastScrollRecyclerView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public FastScrollRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mScrollbar = new FastScroller(context, this, attrs);
}
public int getScrollBarWidth() {
return mScrollbar.getWidth();
}
public int getScrollBarThumbHeight() {
return mScrollbar.getThumbHeight();
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
addOnItemTouchListener(this);
}
/**
* We intercept the touch handling only to support fast scrolling when initiated from the
* scroll bar. Otherwise, we fall back to the default RecyclerView touch handling.
*/
@Override
public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent ev) {
return handleTouchEvent(ev);
}
@Override
public void onTouchEvent(RecyclerView rv, MotionEvent ev) {
handleTouchEvent(ev);
}
/**
* Handles the touch event and determines whether to show the fast scroller (or updates it if
* it is already showing).
*/
private boolean handleTouchEvent(MotionEvent ev) {
int action = ev.getAction();
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (action) {
case MotionEvent.ACTION_DOWN:
// Keep track of the down positions
mDownX = x;
mDownY = mLastY = y;
mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY, mStateChangeListener);
break;
case MotionEvent.ACTION_MOVE:
mLastY = y;
mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY, mStateChangeListener);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY, mStateChangeListener);
break;
}
return mScrollbar.isDragging();
}
@Override
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
}
/**
* Returns the available scroll height:
* AvailableScrollHeight = Total height of the all items - last page height
* <p>
* This assumes that all rows are the same height.
*
* @param yOffset the offset from the top of the recycler view to start tracking.
*/
protected int getAvailableScrollHeight(int rowCount, int rowHeight, int yOffset) {
int visibleHeight = getHeight();
int scrollHeight = getPaddingTop() + yOffset + rowCount * rowHeight + getPaddingBottom();
int availableScrollHeight = scrollHeight - visibleHeight;
return availableScrollHeight;
}
/**
* Returns the available scroll bar height:
* AvailableScrollBarHeight = Total height of the visible view - thumb height
*/
protected int getAvailableScrollBarHeight() {
int visibleHeight = getHeight();
int availableScrollBarHeight = visibleHeight - mScrollbar.getThumbHeight();
return availableScrollBarHeight;
}
@Override
public void draw(Canvas c) {
super.draw(c);
onUpdateScrollbar();
mScrollbar.draw(c);
}
/**
* Updates the scrollbar thumb offset to match the visible scroll of the recycler view. It does
* this by mapping the available scroll area of the recycler view to the available space for the
* scroll bar.
*
* @param scrollPosState the current scroll position
* @param rowCount the number of rows, used to calculate the total scroll height (assumes that
* all rows are the same height)
* @param yOffset the offset to start tracking in the recycler view (only used for all apps)
*/
protected void synchronizeScrollBarThumbOffsetToViewScroll(ScrollPositionState scrollPosState, int rowCount, int yOffset) {
int availableScrollHeight = getAvailableScrollHeight(rowCount, scrollPosState.rowHeight, yOffset);
int availableScrollBarHeight = getAvailableScrollBarHeight();
// Only show the scrollbar if there is height to be scrolled
if (availableScrollHeight <= 0) {
mScrollbar.setThumbPosition(-1, -1);
return;
}
// Calculate the current scroll position, the scrollY of the recycler view accounts for the
// view padding, while the scrollBarY is drawn right up to the background padding (ignoring
// padding)
int scrollY = getPaddingTop() + yOffset + (scrollPosState.rowIndex * scrollPosState.rowHeight) - scrollPosState.rowTopOffset;
int scrollBarY = (int) (((float) scrollY / availableScrollHeight) * availableScrollBarHeight);
// Calculate the position and size of the scroll bar
int scrollBarX;
if (ListenerUtil.isRtl(getResources())) {
scrollBarX = 0;
} else {
scrollBarX = getWidth() - mScrollbar.getWidth();
}
mScrollbar.setThumbPosition(scrollBarX, scrollBarY);
}
/**
* Maps the touch (from 0..1) to the adapter position that should be visible.
*/
public String scrollToPositionAtProgress(float touchFraction) {
int itemCount = getAdapter().getItemCount();
if (itemCount == 0) {
return "";
}
int spanCount = 1;
int rowCount = itemCount;
if (getLayoutManager() instanceof GridLayoutManager) {
spanCount = ((GridLayoutManager) getLayoutManager()).getSpanCount();
rowCount = (int) Math.ceil((double) rowCount / spanCount);
}
// Stop the scroller if it is scrolling
stopScroll();
getCurScrollState(mScrollPosState);
float itemPos = itemCount * touchFraction;
int availableScrollHeight = getAvailableScrollHeight(rowCount, mScrollPosState.rowHeight, 0);
//The exact position of our desired item
int exactItemPos = (int) (availableScrollHeight * touchFraction);
//Scroll to the desired item. The offset used here is kind of hard to explain.
//If the position we wish to scroll to is, say, position 10.5, we scroll to position 10,
//and then offset by 0.5 * rowHeight. This is how we achieve smooth scrolling.
LinearLayoutManager layoutManager = ((LinearLayoutManager) getLayoutManager());
layoutManager.scrollToPositionWithOffset(spanCount * exactItemPos / mScrollPosState.rowHeight,
-(exactItemPos % mScrollPosState.rowHeight));
if (!(getAdapter() instanceof SectionedAdapter)) {
return "";
}
int posInt = (int) ((touchFraction == 1) ? itemPos - 1 : itemPos);
SectionedAdapter sectionedAdapter = (SectionedAdapter) getAdapter();
return sectionedAdapter.getSectionName(posInt);
}
/**
* Updates the bounds for the scrollbar.
*/
public void onUpdateScrollbar() {
if (getAdapter() == null) {
return;
}
int rowCount = getAdapter().getItemCount();
if (getLayoutManager() instanceof GridLayoutManager) {
int spanCount = ((GridLayoutManager) getLayoutManager()).getSpanCount();
rowCount = (int) Math.ceil((double) rowCount / spanCount);
}
// Skip early if, there are no items.
if (rowCount == 0) {
mScrollbar.setThumbPosition(-1, -1);
return;
}
// Skip early if, there no child laid out in the container.
getCurScrollState(mScrollPosState);
if (mScrollPosState.rowIndex < 0) {
mScrollbar.setThumbPosition(-1, -1);
return;
}
synchronizeScrollBarThumbOffsetToViewScroll(mScrollPosState, rowCount, 0);
}
/**
* Returns the current scroll state of the apps rows.
*/
private void getCurScrollState(ScrollPositionState stateOut) {
stateOut.rowIndex = -1;
stateOut.rowTopOffset = -1;
stateOut.rowHeight = -1;
int itemCount = getAdapter().getItemCount();
// Return early if there are no items, or no children.
if (itemCount == 0 || getChildCount() == 0) {
return;
}
View child = getChildAt(0);
stateOut.rowIndex = getChildAdapterPosition(child);
if (getLayoutManager() instanceof GridLayoutManager) {
stateOut.rowIndex = stateOut.rowIndex / ((GridLayoutManager) getLayoutManager()).getSpanCount();
}
stateOut.rowTopOffset = getLayoutManager().getDecoratedTop(child);
stateOut.rowHeight = child.getHeight();
}
public void setThumbColor(@ColorInt int color) {
mScrollbar.setThumbColor(color);
}
public void setTrackColor(@ColorInt int color) {
mScrollbar.setTrackColor(color);
}
public void setPopupBgColor(@ColorInt int color) {
mScrollbar.setPopupBgColor(color);
}
public void setPopupTextColor(@ColorInt int color) {
mScrollbar.setPopupTextColor(color);
}
public void setPopupTextSize(int textSize) {
mScrollbar.setPopupTextSize(textSize);
}
public void setPopUpTypeface(Typeface typeface) {
mScrollbar.setPopupTypeface(typeface);
}
public void setAutoHideDelay(int hideDelay) {
mScrollbar.setAutoHideDelay(hideDelay);
}
public void setAutoHideEnabled(boolean autoHideEnabled) {
mScrollbar.setAutoHideEnabled(autoHideEnabled);
}
public void setStateChangeListener(OnFastScrollStateChangeListener stateChangeListener) {
mStateChangeListener = stateChangeListener;
}
public interface SectionedAdapter {
@NonNull
String getSectionName(int position);
}
}