Java tutorial
/* * Copyright 2016 L4 Digital LLC. All rights reserved. * * 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.l4digital.fastscroll; import android.animation.*; import android.content.*; import android.content.res.*; import android.graphics.*; import android.graphics.drawable.*; import android.os.*; import android.support.annotation.*; import android.support.design.widget.*; import android.support.v4.content.*; import android.support.v4.graphics.drawable.*; import android.support.v4.view.*; import android.support.v7.widget.*; import android.util.*; import android.view.*; import android.widget.*; import net.gnu.explorer.*; import android.support.v4.widget.*; public class FastScroller extends LinearLayout { public interface SectionIndexer { String getSectionText(int position); } private static final int sBubbleAnimDuration = 100; private static final int sScrollbarAnimDuration = 300; private static final int sScrollbarHideDelay = 5000; private static final int sTrackSnapRange = 5; @ColorInt private int mBubbleColor; @ColorInt private int mHandleColor; private int mHeight; private boolean mHideScrollbar; private SectionIndexer mSectionIndexer; private ViewPropertyAnimator mScrollbarAnimator; private ViewPropertyAnimator mBubbleAnimator; private FastScrollRecyclerView mRecyclerView; private TextView mBubbleView; private ImageView mHandleView; private ImageView mTrackView; private View mScrollbar; private Drawable mBubbleImage; private Drawable mHandleImage; private Drawable mTrackImage; private FastScrollStateChangeListener mFastScrollStateChangeListener; SwipeRefreshLayout swipeRefreshLayout; private Runnable mScrollbarHider = new Runnable() { @Override public void run() { hideScrollbar(); } }; private RecyclerView.OnScrollListener mScrollListener = new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { if (!mHandleView.isSelected() && isEnabled()) { setViewPositions(getScrollProportion(recyclerView)); } } @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { super.onScrollStateChanged(recyclerView, newState); if (isEnabled()) { switch (newState) { case RecyclerView.SCROLL_STATE_DRAGGING: getHandler().removeCallbacks(mScrollbarHider); cancelAnimation(mScrollbarAnimator); if (!isViewVisible(mScrollbar)) { showScrollbar(); } break; case RecyclerView.SCROLL_STATE_IDLE: if (mHideScrollbar && !mHandleView.isSelected()) { getHandler().postDelayed(mScrollbarHider, sScrollbarHideDelay); } break; } } } }; public FastScroller(Context context) { super(context); layout(context, null); setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT)); } public FastScroller(Context context, AttributeSet attrs) { this(context, attrs, 0); } public FastScroller(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); layout(context, attrs); setLayoutParams(generateLayoutParams(attrs)); } @Override public void setLayoutParams(@NonNull ViewGroup.LayoutParams params) { params.width = LayoutParams.WRAP_CONTENT; super.setLayoutParams(params); } public void setLayoutParams(@NonNull ViewGroup viewGroup) { if (viewGroup instanceof CoordinatorLayout) { CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams) getLayoutParams(); @IdRes int layoutId = mRecyclerView.getId(); if (layoutId != NO_ID) { layoutParams.setAnchorId(layoutId); layoutParams.anchorGravity = GravityCompat.END; } else { layoutParams.gravity = GravityCompat.END; } setLayoutParams(layoutParams); } else if (viewGroup instanceof FrameLayout) { FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) getLayoutParams(); layoutParams.gravity = GravityCompat.END; setLayoutParams(layoutParams); } else if (viewGroup instanceof RelativeLayout) { RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { layoutParams.addRule(RelativeLayout.ALIGN_PARENT_END); } else { layoutParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT); } setLayoutParams(layoutParams); } else { throw new IllegalArgumentException( "Parent ViewGroup must be a CoordinatorLayout, FrameLayout, or RelativeLayout"); } } public void setSectionIndexer(SectionIndexer sectionIndexer) { mSectionIndexer = sectionIndexer; } public void attachRecyclerView(FastScrollRecyclerView recyclerView) { mRecyclerView = recyclerView; if (mRecyclerView != null) { mRecyclerView.addOnScrollListener(mScrollListener); } } public void detachRecyclerView() { if (mRecyclerView != null) { mRecyclerView.removeOnScrollListener(mScrollListener); mRecyclerView = null; } } /** * Hide the scrollbar when not scrolling. * * @param hideScrollbar True to hide the scrollbar, false to show */ public void setHideScrollbar(boolean hideScrollbar) { mHideScrollbar = hideScrollbar; mScrollbar.setVisibility(hideScrollbar ? GONE : VISIBLE); } /** * Display a scroll track while scrolling. * * @param visible True to show scroll track, false to hide */ public void setTrackVisible(boolean visible) { mTrackView.setVisibility(visible ? VISIBLE : GONE); } /** * Set the color of the scroll track. * * @param color The color for the scroll track */ public void setTrackColor(@ColorInt int color) { @ColorInt int trackColor = color; if (mTrackImage == null) { mTrackImage = DrawableCompat.wrap(ContextCompat.getDrawable(getContext(), R.drawable.fastscroll_track)); mTrackImage.mutate(); } DrawableCompat.setTint(mTrackImage, trackColor); mTrackView.setImageDrawable(mTrackImage); } /** * Set the color for the scroll handle. * * @param color The color for the scroll handle */ public void setHandleColor(@ColorInt int color) { mHandleColor = color; if (mHandleImage == null) { mHandleImage = DrawableCompat .wrap(ContextCompat.getDrawable(getContext(), R.drawable.fastscroll_handle)); mHandleImage.mutate(); } DrawableCompat.setTint(mHandleImage, mHandleColor); mHandleView.setImageDrawable(mHandleImage); } /** * Set the background color of the index bubble. * * @param color The background color for the index bubble */ public void setBubbleColor(@ColorInt int color) { mBubbleColor = color; if (mBubbleImage == null) { mBubbleImage = DrawableCompat .wrap(ContextCompat.getDrawable(getContext(), R.drawable.fastscroll_bubble)); mBubbleImage.mutate(); } DrawableCompat.setTint(mBubbleImage, mBubbleColor); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { mBubbleView.setBackground(mBubbleImage); } else { //noinspection deprecation mBubbleView.setBackgroundDrawable(mBubbleImage); } } /** * Set the text color of the index bubble. * * @param color The text color for the index bubble */ public void setBubbleTextColor(@ColorInt int color) { mBubbleView.setTextColor(color); } /** * Set the fast scroll state change listener. * * @param fastScrollStateChangeListener The interface that will listen to fastscroll state change events */ public void setFastScrollStateChangeListener(FastScrollStateChangeListener fastScrollStateChangeListener) { mFastScrollStateChangeListener = fastScrollStateChangeListener; } @Override public void setEnabled(boolean enabled) { super.setEnabled(enabled); setVisibility(enabled ? VISIBLE : GONE); } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: if (event.getX() < mHandleView.getX() - ViewCompat.getPaddingStart(mHandleView)) { return false; } if (swipeRefreshLayout != null) { swipeRefreshLayout.setEnabled(false); } setHandleSelected(true); getHandler().removeCallbacks(mScrollbarHider); cancelAnimation(mScrollbarAnimator); cancelAnimation(mBubbleAnimator); if (!isViewVisible(mScrollbar)) { showScrollbar(); } if (mSectionIndexer != null && !isViewVisible(mBubbleView)) { showBubble(); } if (mFastScrollStateChangeListener != null) { mFastScrollStateChangeListener.onFastScrollStart(); } case MotionEvent.ACTION_MOVE: final float y = event.getY(); setViewPositions(y); setRecyclerViewPosition(y); return true; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: setHandleSelected(false); if (mHideScrollbar) { getHandler().postDelayed(mScrollbarHider, sScrollbarHideDelay); } if (isViewVisible(mBubbleView)) { hideBubble(); } if (mFastScrollStateChangeListener != null) { mFastScrollStateChangeListener.onFastScrollStop(); } if (swipeRefreshLayout != null) { swipeRefreshLayout.setEnabled(true); } return true; } return super.onTouchEvent(event); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); mHeight = h; } private void setRecyclerViewPosition(float y) { if (mRecyclerView != null && mRecyclerView.getAdapter() != null) { int itemCount = mRecyclerView.getAdapter().getItemCount(); float proportion; if (mHandleView.getY() == 0) { proportion = 0f; } else if (mHandleView.getY() + mHandleView.getHeight() >= mHeight - sTrackSnapRange) { proportion = 1f; } else { proportion = y / (float) mHeight; } int targetPos = getValueInRange(0, itemCount - 1, (int) (proportion * (float) itemCount)); mRecyclerView.getLayoutManager().scrollToPosition(targetPos); if (mSectionIndexer != null) { mBubbleView.setText(mSectionIndexer.getSectionText(targetPos)); } } } private float getScrollProportion(RecyclerView recyclerView) { final int verticalScrollOffset = recyclerView.computeVerticalScrollOffset(); final int verticalScrollRange = recyclerView.computeVerticalScrollRange(); float proportion = (float) verticalScrollOffset / ((float) verticalScrollRange - mHeight); return mHeight * proportion; } private int getValueInRange(int min, int max, int value) { int minimum = Math.max(min, value); return Math.min(minimum, max); } private void setViewPositions(float y) { int bubbleHeight = mBubbleView.getHeight(); int handleHeight = mHandleView.getHeight(); mBubbleView.setY(getValueInRange(0, mHeight - bubbleHeight - handleHeight / 2, (int) (y - bubbleHeight))); mHandleView.setY(getValueInRange(0, mHeight - handleHeight, (int) (y - handleHeight / 2))); } private boolean isViewVisible(View view) { return view != null && view.getVisibility() == VISIBLE; } private void cancelAnimation(ViewPropertyAnimator animator) { if (animator != null) { animator.cancel(); } } private void showBubble() { mBubbleView.setVisibility(VISIBLE); mBubbleAnimator = mBubbleView.animate().alpha(1f).setDuration(sBubbleAnimDuration) .setListener(new AnimatorListenerAdapter() { // adapter required for new alpha value to stick }); } private void hideBubble() { mBubbleAnimator = mBubbleView.animate().alpha(0f).setDuration(sBubbleAnimDuration) .setListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); mBubbleView.setVisibility(GONE); mBubbleAnimator = null; } @Override public void onAnimationCancel(Animator animation) { super.onAnimationCancel(animation); mBubbleView.setVisibility(GONE); mBubbleAnimator = null; } }); } private void showScrollbar() { if (mRecyclerView.computeVerticalScrollRange() - mHeight > 0) { float transX = getResources().getDimensionPixelSize(R.dimen.fastscroll_scrollbar_padding); mScrollbar.setTranslationX(transX); mScrollbar.setVisibility(VISIBLE); mScrollbarAnimator = mScrollbar.animate().translationX(0f).alpha(1f).setDuration(sScrollbarAnimDuration) .setListener(new AnimatorListenerAdapter() { // adapter required for new alpha value to stick }); } } private void hideScrollbar() { float transX = getResources().getDimensionPixelSize(R.dimen.fastscroll_scrollbar_padding); mScrollbarAnimator = mScrollbar.animate().translationX(transX).alpha(0f).setDuration(sScrollbarAnimDuration) .setListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); mScrollbar.setVisibility(GONE); mScrollbarAnimator = null; } @Override public void onAnimationCancel(Animator animation) { super.onAnimationCancel(animation); mScrollbar.setVisibility(GONE); mScrollbarAnimator = null; } }); } private void setHandleSelected(boolean selected) { mHandleView.setSelected(selected); DrawableCompat.setTint(mHandleImage, selected ? mBubbleColor : mHandleColor); } private void layout(Context context, AttributeSet attrs) { inflate(context, R.layout.fastscroller, this); setClipChildren(false); setOrientation(HORIZONTAL); mBubbleView = (TextView) findViewById(R.id.fastscroll_bubble); mHandleView = (ImageView) findViewById(R.id.fastscroll_handle); mTrackView = (ImageView) findViewById(R.id.fastscroll_track); mScrollbar = findViewById(R.id.fastscroll_scrollbar); @ColorInt int bubbleColor = Color.GRAY; @ColorInt int handleColor = Color.DKGRAY; @ColorInt int trackColor = Color.LTGRAY; @ColorInt int textColor = Color.WHITE; boolean hideScrollbar = true; boolean showTrack = false; if (attrs != null) { TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.FastScrollRecyclerView, 0, 0); if (typedArray != null) { try { bubbleColor = typedArray.getColor(R.styleable.FastScrollRecyclerView_bubbleColor, bubbleColor); handleColor = typedArray.getColor(R.styleable.FastScrollRecyclerView_handleColor, handleColor); trackColor = typedArray.getColor(R.styleable.FastScrollRecyclerView_trackColor, trackColor); textColor = typedArray.getColor(R.styleable.FastScrollRecyclerView_bubbleTextColor, textColor); showTrack = typedArray.getBoolean(R.styleable.FastScrollRecyclerView_showTrack, false); hideScrollbar = typedArray.getBoolean(R.styleable.FastScrollRecyclerView_hideScrollbar, true); } finally { typedArray.recycle(); } } } setTrackColor(trackColor); setHandleColor(handleColor); setBubbleColor(bubbleColor); setBubbleTextColor(textColor); setHideScrollbar(hideScrollbar); setTrackVisible(showTrack); } }