com.devbrackets.android.recyclerext.widget.FastScroll.java Source code

Java tutorial

Introduction

Here is the source code for com.devbrackets.android.recyclerext.widget.FastScroll.java

Source

/*
 * Copyright (C) 2016 Brian Wernick
 *
 * 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.devbrackets.android.recyclerext.widget;

import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Handler;
import android.support.annotation.ColorInt;
import android.support.annotation.ColorRes;
import android.support.annotation.DimenRes;
import android.support.annotation.DrawableRes;
import android.support.annotation.IntRange;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.view.ViewCompat;
import android.support.v7.widget.AppCompatDrawableManager;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewTreeObserver;
import android.view.animation.Animation;
import android.widget.FrameLayout;

import com.devbrackets.android.recyclerext.R;
import com.devbrackets.android.recyclerext.animation.FastScrollBubbleVisibilityAnimation;
import com.devbrackets.android.recyclerext.animation.FastScrollHandleVisibilityAnimation;

/**
 * A class that provides the functionality of a fast scroll
 * for the attached {@link android.support.v7.widget.RecyclerView}
 */
@SuppressWarnings("unused")
public class FastScroll extends FrameLayout {
    private static final String TAG = "FastScroll";
    public static final long INVALID_POPUP_ID = -1;

    @Nullable
    protected PopupCallbacks popupCallbacks;

    @NonNull
    @SuppressWarnings("NullableProblems")
    protected PositionSupportImageView handle;
    @NonNull
    @SuppressWarnings("NullableProblems")
    protected PositionSupportTextView bubble;

    protected RecyclerView recyclerView;

    @NonNull
    protected RecyclerScrollListener scrollListener = new RecyclerScrollListener();
    @NonNull
    protected Handler delayHandler = new Handler();

    @NonNull
    protected HandleHideRunnable handleHideRunnable = new HandleHideRunnable();
    @NonNull
    protected BubbleHideRunnable bubbleHideRunnable = new BubbleHideRunnable();

    @Nullable
    protected AnimationProvider animationProvider;
    @NonNull
    protected BubbleAlignment bubbleAlignment = BubbleAlignment.TOP;

    protected int height;
    protected boolean showBubble;

    protected int minDisplayPageCount = 4;
    protected int calculatedMinDisplayHeight = 0;
    protected boolean hideOnShortLists = true;

    protected boolean hideHandleAllowed = true;
    protected boolean draggingHandle = false;
    protected boolean trackClicksAllowed = false;

    // The offset for the finger from the center of the drag handle
    protected float fingerCenterOffset;

    protected long handleHideDelay = 1_000; //Milliseconds
    protected long bubbleHideDelay = 0; //Milliseconds

    /**
     * Only {@code null} before an initial request for a visibility change
     */
    protected Boolean requestedHandleVisibility;
    protected long currentSectionId = INVALID_POPUP_ID;

    public FastScroll(Context context) {
        super(context);
        init(context, null);
    }

    public FastScroll(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs);
    }

    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    public FastScroll(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public FastScroll(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init(context, attrs);
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        if (recyclerView != null) {
            recyclerView.removeOnScrollListener(scrollListener);
        }
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldW, int oldH) {
        super.onSizeChanged(w, h, oldW, oldH);
        height = h;
        calculatedMinDisplayHeight = h * minDisplayPageCount;
    }

    @Override
    public boolean onTouchEvent(@NonNull MotionEvent event) {
        //Filters out touch events we don't need to handle
        if (!draggingHandle && event.getAction() != MotionEvent.ACTION_DOWN) {
            return super.onTouchEvent(event);
        }

        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            //Verifies the event is within the allowed coordinates
            if (ignoreTouchDown(event.getX(), event.getY())) {
                return false;
            }

            delayHandler.removeCallbacks(handleHideRunnable);
            updateHandleVisibility(true);

            if (bubble.getVisibility() != VISIBLE) {
                updateBubbleVisibility(true);
            }

            draggingHandle = true;
            float halfHandle = handle.getHeight() / 2F;

            fingerCenterOffset = handle.getY() + halfHandle - event.getY();
            fingerCenterOffset = boxValue(fingerCenterOffset, halfHandle);
            handle.setSelected(true);
            //Purposefully falls through

        case MotionEvent.ACTION_MOVE:
            setBubbleAndHandlePosition(event.getY() + fingerCenterOffset);
            setRecyclerViewPosition(event.getY() + fingerCenterOffset);
            return true;

        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
            draggingHandle = false;
            handle.setSelected(false);
            hideBubbleDelayed();
            hideHandleDelayed();
            return true;
        }

        return super.onTouchEvent(event);
    }

    /**
     * Links this widget to the {@code recyclerView}. This is necessary for the
     * FastScroll to function.
     *
     * @param recyclerView The {@link RecyclerView} to attach to
     */
    public void attach(@NonNull final RecyclerView recyclerView) {
        if (showBubble && !(recyclerView.getAdapter() instanceof PopupCallbacks)) {
            Log.e(TAG, "The RecyclerView Adapter specified needs to implement "
                    + PopupCallbacks.class.getSimpleName());
            return;
        }

        this.recyclerView = recyclerView;

        if (showBubble) {
            popupCallbacks = (PopupCallbacks) recyclerView.getAdapter();
        }

        recyclerView.addOnScrollListener(scrollListener);

        recyclerView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
            @Override
            public boolean onPreDraw() {
                recyclerView.getViewTreeObserver().removeOnPreDrawListener(this);
                scrollListener.onScrolled(recyclerView, 0, 0);

                return true;
            }
        });
    }

    /**
     * Specifies if the popup bubble should be shown when the handle is
     * being dragged.
     *
     * @param showBubble {@code true} if the popup bubble should be shown
     */
    public void setShowBubble(boolean showBubble) {
        if (this.showBubble == showBubble) {
            return;
        }

        this.showBubble = showBubble;
        if (recyclerView == null || !showBubble) {
            return;
        }

        if (!(recyclerView.getAdapter() instanceof PopupCallbacks)) {
            Log.e(TAG, "The RecyclerView Adapter specified needs to implement "
                    + PopupCallbacks.class.getSimpleName());
            return;
        }

        popupCallbacks = (PopupCallbacks) recyclerView.getAdapter();
    }

    /**
     * Specifies if clicks on the track should scroll to that position.
     *
     * @param allowed {@code true} to allow clicking on the track [default: {@code false}]
     */
    public void setTrackClicksAllowed(boolean allowed) {
        this.trackClicksAllowed = allowed;
    }

    /**
     * Specifies if the drag handle can hide after a short delay (see {@link #setHandleHideDelay(long)})
     * after scrolling has completely stopped
     *
     * @param allowed {@code true} if the drag handle can hide [default: {@code true}]
     */
    public void setHideHandleAllowed(boolean allowed) {
        this.hideHandleAllowed = allowed;
    }

    /**
     * Sets the delay used when hiding the drag handle, which occurs after scrolling
     * has completely stopped.
     *
     * @param delayMilliseconds the delay to hide the drag handle [default: {@code 1_000}]
     */
    public void setHandleHideDelay(long delayMilliseconds) {
        this.handleHideDelay = delayMilliseconds;
    }

    /**
     * Retrieves the delay used when hiding the drag handle which occurs after scrolling
     * has completely stopped.
     *
     * @return The millisecond delay used for hiding the drag handle [default: {@code 1_000}]
     */
    public long getHandleHideDelay() {
        return handleHideDelay;
    }

    /**
     * Sets the delay used when hiding the bubble which occurs after the drag handle
     * is released
     *
     * @param delayMilliseconds The delay to hide the bubble
     */
    public void setBubbleHideDelay(long delayMilliseconds) {
        this.bubbleHideDelay = delayMilliseconds;
    }

    /**
     * Retrieves the delay used when hiding the bubble (occurs after the drag handle
     * is released)
     *
     * @return The millisecond delay used for hiding the bubble
     */
    public long getBubbleHideDelay() {
        return bubbleHideDelay;
    }

    /**
     * Specifies if the FastScroll should be hidden when the list is shorter.
     * This value is determined by {@link #setMinDisplayPageCount(int)}
     *
     * @param hideOnShortLists {@code true} if the FastScroll should be hidden on shorter lists [default: {@code true}]
     */
    public void setHideOnShortLists(boolean hideOnShortLists) {
        this.hideOnShortLists = hideOnShortLists;
    }

    /**
     * Determines if the FastScroll should be hidden when the list is shorter.
     * This value is determined by {@link #setMinDisplayPageCount(int)}
     *
     * @return {@code true} if the FastScroll will be hidden on shorter lists [default: {@code true}]
     */
    public boolean getHideOnShortLists() {
        return hideOnShortLists;
    }

    /**
     * Specifies the minimum amount of pages to be contained in the list before the
     * FastScroll will be displayed. This will only have an affect if {@link #setHideOnShortLists(boolean)}
     * is enabled. If the {@code minDisplayPageCount} is set to 0 the FastScroll will always be shown
     *
     * @param minDisplayPageCount The minimum amount of pages in the list to show the FastScroll [default: {@code 4}]
     */
    public void setMinDisplayPageCount(@IntRange(from = 0) int minDisplayPageCount) {
        this.minDisplayPageCount = minDisplayPageCount;
        calculatedMinDisplayHeight = height * minDisplayPageCount;
    }

    /**
     * Determines the minimum amount of pages to be contained in the list before the
     * FastScroll will be displayed. This will only have an affect if {@link #setHideOnShortLists(boolean)}
     * is enabled.
     *
     * @return The minimum amount of pages to display the FastScroll [default: {@code 4}]
     */
    @IntRange(from = 0)
    public int getMinDisplayPageCount() {
        return minDisplayPageCount;
    }

    /**
     * Sets the text color for the popup bubble
     * This can also be specified with {@code re_bubble_text_color} in xml
     *
     * @param colorRes The resource id for the color
     */
    public void setTextColorRes(@ColorRes int colorRes) {
        setTextColor(getColor(colorRes));
    }

    /**
     * Sets the text color for the popup bubble
     * This can also be specified with {@code re_bubble_text_color} in xml
     *
     * @param color The integer representation for the color
     */
    public void setTextColor(@ColorInt int color) {
        bubble.setTextColor(color);
    }

    /**
     * Sets the text size of the popup bubble via the {@code dimeRes}
     * This can also be specified with {@code re_bubble_text_size} in xml
     *
     * @param dimenRes The dimension resource for the text size
     */
    public void setTextSize(@DimenRes int dimenRes) {
        bubble.setTextSize(TypedValue.COMPLEX_UNIT_PX, getResources().getDimensionPixelSize(dimenRes));
    }

    /**
     * Sets the text size of the popup bubble, interpreted as "scaled pixel" units.
     * This size is adjusted based on the current density and user font size preference.
     * This can also be specified with {@code re_bubble_text_size} in xml
     *
     * @param size The scaled pixel size
     */
    public void setTextSize(float size) {
        bubble.setTextSize(size);
    }

    /**
     * Tints the popup bubble background (see {@link #setBubbleDrawable(Drawable)}) with the
     * color defined by {@code colorRes}.
     * This can also be specified with {@code re_bubble_color} in xml
     *
     * @param colorRes The resource id for the color to tint the popup bubble with
     */
    public void setBubbleTintRes(@ColorRes int colorRes) {
        setBubbleTint(getColor(colorRes));
    }

    /**
     * Tints the popup bubble background (see {@link #setBubbleDrawable(Drawable)}) with the
     * specified color.
     * This can also be specified with {@code re_bubble_color} in xml
     *
     * @param tint The integer representation for the tint color
     */
    public void setBubbleTint(@ColorInt int tint) {
        bubble.setBackground(tint(getDrawable(R.drawable.recyclerext_fast_scroll_bubble), tint));
    }

    /**
     * Sets the background drawable for the popup bubble.
     * This can also be specified with {@code re_bubble_background} in xml
     *
     * @param drawable The drawable for the popup bubble background
     */
    public void setBubbleDrawable(@Nullable Drawable drawable) {
        bubble.setBackground(drawable);
    }

    /**
     * Tints the drag handle background (see {@link #setHandleDrawable(Drawable)}) with the
     * color defined by {@code colorRes}.
     * This can also be specified with {@code re_handle_color} in xml
     *
     * @param colorRes The resource id for the color to tint the drag handle with
     */
    public void setHandleTintRes(@ColorRes int colorRes) {
        setHandleTint(getColor(colorRes));
    }

    /**
     * Tints the drag handle background (see {@link #setHandleDrawable(Drawable)} with the specified color.
     * This can also be specified with {@code re_handle_color} in xml
     *
     * @param tint The integer representation for the tint color
     */
    public void setHandleTint(@ColorInt int tint) {
        handle.setBackground(tint(getDrawable(R.drawable.recyclerext_fast_scroll_handle), tint));
    }

    /**
     * Sets the drawable for the drag handle.
     * This can also be specified with {@code re_handle_background} in xml
     *
     * @param drawable The drawable for the drag handle background
     */
    public void setHandleDrawable(@Nullable Drawable drawable) {
        handle.setBackground(drawable);
    }

    /**
     * Sets the provider that allows the animations for the popup bubble and drag handle
     * to be customized or overridden
     *
     * @param animationProvider The animation provider for the popup bubble and drag handle
     */
    public void setAnimationProvider(@Nullable AnimationProvider animationProvider) {
        this.animationProvider = animationProvider;
    }

    /**
     * Specifies the alignment the popup bubble has in relation to the drag handle,
     * see {@link BubbleAlignment} for more details
     * This can also be specified with {@code re_bubble_alignment} in xml
     *
     * @param alignment The alignment type
     */
    public void setBubbleAlignment(@NonNull BubbleAlignment alignment) {
        bubbleAlignment = alignment;
    }

    /**
     * The base initialization method (called from constructors) that
     * inflates and configures the widget.
     *
     * @param context The context of the widget
     * @param attrs The attributes associated with the widget
     */
    protected void init(@NonNull Context context, @Nullable AttributeSet attrs) {
        LayoutInflater inflater = LayoutInflater.from(context);
        inflater.inflate(R.layout.recyclerext_fast_scroll, this, true);

        bubble = (PositionSupportTextView) findViewById(R.id.recyclerext_fast_scroll_bubble);
        handle = (PositionSupportImageView) findViewById(R.id.recyclerext_fast_scroll_handle);

        bubble.setVisibility(View.GONE);

        readAttributes(context, attrs);
    }

    /**
     * Reads the attributes associated with this view, setting any values found
     *
     * @param context The context to retrieve the styled attributes with
     * @param attrs The {@link AttributeSet} to retrieve the values from
     */
    protected void readAttributes(@NonNull Context context, @Nullable AttributeSet attrs) {
        if (attrs == null || isInEditMode()) {
            return;
        }

        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.FastScroll);
        if (typedArray == null) {
            return;
        }

        retrieveBubbleAttributes(typedArray);
        retrieveHandleAttributes(typedArray);

        typedArray.recycle();
    }

    /**
     * Retrieves the xml attributes associated with the popup bubble.
     * This includes the drawable, tint color, alignment, font options, etc.
     *
     * @param typedArray The array of attributes to use
     */
    protected void retrieveBubbleAttributes(@NonNull TypedArray typedArray) {
        showBubble = typedArray.getBoolean(R.styleable.FastScroll_re_show_bubble, true);
        bubbleAlignment = BubbleAlignment.get(typedArray.getInt(R.styleable.FastScroll_re_bubble_alignment, 3));

        int textColor = getColor(R.color.recyclerext_fast_scroll_bubble_text_color_default);
        textColor = typedArray.getColor(R.styleable.FastScroll_re_bubble_text_color, textColor);

        int textSize = getResources()
                .getDimensionPixelSize(R.dimen.recyclerext_fast_scroll_bubble_text_size_default);
        textSize = typedArray.getDimensionPixelSize(R.styleable.FastScroll_re_bubble_text_size, textSize);

        Drawable backgroundDrawable = getDrawable(typedArray, R.styleable.FastScroll_re_bubble_background);
        int backgroundColor = getColor(R.color.recyclerext_fast_scroll_bubble_color_default);
        backgroundColor = typedArray.getColor(R.styleable.FastScroll_re_bubble_color, backgroundColor);

        if (backgroundDrawable == null) {
            backgroundDrawable = tint(getDrawable(R.drawable.recyclerext_fast_scroll_bubble), backgroundColor);
        }

        bubble.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
        bubble.setTextColor(textColor);
        bubble.setBackground(backgroundDrawable);
    }

    /**
     * Retrieves the xml attributes associated with the drag handle.
     * This includes the drawable and tint color
     *
     * @param typedArray The array of attributes to use
     */
    protected void retrieveHandleAttributes(@NonNull TypedArray typedArray) {
        Drawable backgroundDrawable = getDrawable(typedArray, R.styleable.FastScroll_re_handle_background);
        int backgroundColor = getColor(R.color.recyclerext_fast_scroll_handle_color_default);
        backgroundColor = typedArray.getColor(R.styleable.FastScroll_re_handle_color, backgroundColor);

        if (backgroundDrawable == null) {
            backgroundDrawable = tint(getDrawable(R.drawable.recyclerext_fast_scroll_handle), backgroundColor);
        }

        handle.setBackground(backgroundDrawable);
    }

    /**
     * Determines if the {@link MotionEvent#ACTION_DOWN} event should be ignored.
     * This occurs when the event position is outside the bounds of the drag handle or
     * the track (of the drag handle) when disabled (see {@link #setTrackClicksAllowed(boolean)}
     *
     * @param xPos The x coordinate of the event
     * @param yPos The y coordinate of the event
     * @return {@code true} if the event should be ignored
     */
    protected boolean ignoreTouchDown(float xPos, float yPos) {
        //Verifies the event is within the allowed X coordinates
        if (ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_LTR) {
            if (xPos < handle.getX() - ViewCompat.getPaddingStart(handle)) {
                return true;
            }
        } else {
            if (xPos > handle.getX() + handle.getWidth() + ViewCompat.getPaddingStart(handle)) {
                return true;
            }
        }

        if (!trackClicksAllowed) {
            //Enforces selection to only occur on the handle
            if (yPos < handle.getY() - handle.getPaddingTop()
                    || yPos > handle.getY() + handle.getHeight() + handle.getPaddingBottom()) {
                return true;
            }
        }

        return false;
    }

    /**
     * Updates the scroll position of the {@link #recyclerView}
     * by determining the adapter position proportionally related to
     * the {@code y} position
     *
     * @param y The y coordinate to find the adapter position for
     */
    protected void setRecyclerViewPosition(float y) {
        //Boxes the ratio between handleCenter and height - handleCenter
        float ratio;
        int halfHandle = handle.getHeight() / 2;
        if (y <= halfHandle) {
            ratio = 0;
        } else if (y >= (height - halfHandle)) {
            ratio = 1;
        } else {
            ratio = (y - halfHandle) / (height - handle.getHeight());
        }

        //Performs the distance and scrolling
        scrollToLocation(ratio);

        //Displays the popup bubble when enabled
        if (showBubble && popupCallbacks != null) {
            int itemCount = recyclerView.getAdapter().getItemCount();
            int position = getValueInRange(0, itemCount - 1, (int) (ratio * itemCount));

            long sectionId = popupCallbacks.getSectionId(position);
            if (currentSectionId != sectionId) {
                currentSectionId = sectionId;
                String bubbleText = popupCallbacks.getPopupText(position, sectionId);
                bubble.setText(bubbleText);
            }
        }
    }

    /**
     * Informs the {@link #recyclerView} that we need to smoothly scroll
     * to the requested position.
     *
     * @param ratio The scroll location as a ratio of the total in the range [0, 1]
     */
    protected void scrollToLocation(float ratio) {
        int scrollRange = recyclerView.computeVerticalScrollRange() - recyclerView.computeVerticalScrollExtent();
        if (scrollRange > 0) {
            int deltaY = (int) (ratio * scrollRange) - recyclerView.computeVerticalScrollOffset();
            recyclerView.scrollBy(0, deltaY);
        }
    }

    /**
     * Updates the position both the drag handle and popup bubble
     * have in relation to y (the users finger)
     *
     * @param y The position to place the drag handle at
     */
    protected void setBubbleAndHandlePosition(float y) {
        int handleHeight = handle.getHeight();
        float handleY = getValueInRange(0, height - handleHeight, (int) (y - handleHeight / 2));
        handle.setY(handleY);

        if (showBubble) {
            setBubblePosition(handleY);
        }
    }

    /**
     * Updates the position of the popup bubble in relation to the
     * drag handle. This depends on the value of {@link #bubbleAlignment}
     *
     * @param handleY The position the drag handle has for relational alignment
     */
    protected void setBubblePosition(float handleY) {
        int maxY = height - bubble.getHeight();

        float handleCenter = handleY + (handle.getHeight() / 2);
        float handleBottom = handleY + handle.getHeight();

        switch (bubbleAlignment) {
        case TOP: //TOP_TO_TOP
            bubble.setY(getValueInRange(0, maxY, (int) handleY));
            break;
        case CENTER: //CENTER_TO_CENTER
            bubble.setY(getValueInRange(0, maxY, (int) (handleCenter - (bubble.getHeight() / 2))));
            break;
        case BOTTOM: //BOTTOM_TO_BOTTOM
            bubble.setY(getValueInRange(0, maxY, (int) (handleBottom - bubble.getHeight())));
            break;
        case BOTTOM_TO_TOP: //Bubble bottom to handle top
            bubble.setY(getValueInRange(0, maxY, (int) (handleY - bubble.getHeight())));
            break;
        case TOP_TO_BOTTOM: //Bubble top to handle bottom
            bubble.setY(getValueInRange(0, maxY, (int) handleBottom));
            break;
        case BOTTOM_TO_CENTER: //Bubble bottom to handle center
            bubble.setY(getValueInRange(0, maxY, (int) (handleCenter - bubble.getHeight())));
            break;
        }
    }

    /**
     * Updates the visibility of the bubble representing the current location.
     * Typically this bubble will contain the first letter of the section that
     * is at the top of the RecyclerView.
     *
     * @param toVisible {@code true} if the bubble should be visible at the end of the animation
     */
    protected void updateBubbleVisibility(boolean toVisible) {
        if (!showBubble) {
            return;
        }

        bubble.clearAnimation();

        Log.d(TAG, "updating bubble visibility " + toVisible);
        bubble.startAnimation(getBubbleAnimation(bubble, toVisible));
    }

    /**
     * Updates the visibility of the drag handle, storing the requested state
     * so that we aren't continuously requesting visibility animations.
     *
     * @param toVisible {@code true} if the drag handle should be visible at the end of the change
     */
    protected void updateHandleVisibility(boolean toVisible) {
        if (requestedHandleVisibility != null && requestedHandleVisibility == toVisible) {
            return;
        }

        requestedHandleVisibility = toVisible;
        handle.clearAnimation();

        Log.d(TAG, "updating handle visibility " + toVisible);
        handle.startAnimation(getHandleAnimation(handle, toVisible));
    }

    /**
     * Handles the functionality to delay the hiding of the
     * bubble if the bubble is shown
     */
    protected void hideBubbleDelayed() {
        delayHandler.removeCallbacks(bubbleHideRunnable);
        if (showBubble && !draggingHandle) {
            delayHandler.postDelayed(bubbleHideRunnable, bubbleHideDelay);
        }
    }

    /**
     * Handles the functionality to delay the hiding of the
     * handle
     */
    protected void hideHandleDelayed() {
        delayHandler.removeCallbacks(handleHideRunnable);
        if (hideHandleAllowed && !draggingHandle) {
            delayHandler.postDelayed(handleHideRunnable, handleHideDelay);
        }
    }

    /**
     * Retrieves the animation for hiding or showing the popup bubble
     *
     * @param bubble The view representing the popup bubble to animate
     * @param toVisible {@code true} if the animation should show the bubble
     * @return The animation for hiding or showing the bubble
     */
    @NonNull
    protected Animation getBubbleAnimation(@NonNull View bubble, final boolean toVisible) {
        Animation animation = animationProvider != null ? animationProvider.getBubbleAnimation(bubble, toVisible)
                : null;
        if (animation == null) {
            animation = new FastScrollBubbleVisibilityAnimation(bubble, toVisible);
        }

        return animation;
    }

    /**
     * Retrieves the animation for hiding or showing the drag handle
     *
     * @param handle The view representing the handle to animate
     * @param toVisible {@code true} if the animation should show the handle
     * @return The animation for hiding or showing the handle
     */
    @NonNull
    protected Animation getHandleAnimation(@NonNull View handle, final boolean toVisible) {
        Animation animation = animationProvider != null ? animationProvider.getHandleAnimation(handle, toVisible)
                : null;
        if (animation == null) {
            animation = new FastScrollHandleVisibilityAnimation(handle, toVisible);
        }

        return animation;
    }

    /**
     * Tints the {@code drawable} with the {@code color}
     *
     * @param drawable The drawable to ting
     * @param color The color to tint the {@code drawable} with
     * @return The tinted {@code drawable}
     */
    @Nullable
    protected Drawable tint(@Nullable Drawable drawable, @ColorInt int color) {
        if (drawable != null) {
            drawable.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY));
        }

        return drawable;
    }

    /**
     * A utility method to retrieve a drawable that correctly abides by the
     * theme in Lollipop (API 23) +
     *
     * @param resourceId The resource id for the drawable
     * @return The drawable associated with {@code resourceId}
     */
    @Nullable
    protected Drawable getDrawable(@DrawableRes int resourceId) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            return getResources().getDrawable(resourceId, getContext().getTheme());
        }

        return AppCompatDrawableManager.get().getDrawable(getContext(), resourceId);
    }

    /**
     * Retrieves the specified image drawable in a manner that will correctly
     * wrap VectorDrawables on platforms that don't natively support them
     *
     * @param typedArray The TypedArray containing the attributes for the view
     * @param index The index in the {@code typedArray} for the drawable
     */
    @Nullable
    protected Drawable getDrawable(@NonNull TypedArray typedArray, int index) {
        int imageResId = typedArray.getResourceId(index, 0);
        if (imageResId == 0) {
            return null;
        }

        return AppCompatDrawableManager.get().getDrawable(getContext(), imageResId);
    }

    /**
     * A utility method to retrieve a color that correctly abides by the
     * theme in Marshmallow (API 23) +
     *
     * @param res The resource id associated with the requested color
     * @return The integer representing the color associated with {@code res}
     */
    @ColorInt
    protected int getColor(@ColorRes int res) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            return getResources().getColor(res, getContext().getTheme());
        }

        //noinspection deprecation
        return getResources().getColor(res);
    }

    /**
     * Enforces the restrictions on range provided by {@code min} and {@code max}
     * on {@code value}. If {@code value} is greater than {@code max} then the result will
     * be {@code max}. Likewise if {@code value} is less than {@code min} then the result
     * will be {@code min}.
     *
     * @param min The minimum amount {@code value} can represent
     * @param max The maximum amount {@code value} can represent
     * @param value The amount to constrain
     * @return {@code value}, {@code min} or {@code max} when constrained
     */
    protected int getValueInRange(int min, int max, int value) {
        int minimum = Math.max(min, value);
        return Math.min(minimum, max);
    }

    /**
     * Enforces a restriction on the <code>value</code> to be at most or at least
     * the <code>amount</code>
     *
     * @param value The value value to make sure is no smaller or larger than the <code>amount</code>
     * @param amount The largest amount the <code>value</code> can have
     * @return The value boxed to the amount
     */
    protected float boxValue(float value, float amount) {
        if (Math.abs(value) < Math.abs(amount)) {
            return value;
        }

        return value < 0 ? -amount : amount;
    }

    /**
     * Listens to the scroll position changes of the parent (RecyclerView)
     * so that the handle will always have the correct position
     */
    protected class RecyclerScrollListener extends RecyclerView.OnScrollListener {
        @Override
        public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
            int verticalRange = recyclerView.computeVerticalScrollRange();

            //Makes sure the FastScroll is correctly hidden on shorter lists
            if (hideOnShortLists && verticalRange < calculatedMinDisplayHeight) {
                updateHandleVisibility(false);
                return;
            }

            //Makes sure the handle is shown when scrolling
            updateHandleVisibility(true);
            delayHandler.removeCallbacks(handleHideRunnable);

            if (handle.isSelected()) {
                return;
            }

            hideHandleDelayed();

            float ratio = (float) recyclerView.computeVerticalScrollOffset()
                    / (float) (verticalRange - recyclerView.computeVerticalScrollExtent());
            float halfHandleHeight = (handle.getHeight() / 2);
            setBubbleAndHandlePosition((height - handle.getHeight()) * ratio + halfHandleHeight);
        }
    }

    /**
     * Runnable used to delay the hiding of the drag handle
     */
    protected class HandleHideRunnable implements Runnable {
        @Override
        public void run() {
            updateHandleVisibility(false);
        }
    }

    /**
     * Runnable used to delay the hiding of the popup bubble
     */
    protected class BubbleHideRunnable implements Runnable {
        @Override
        public void run() {
            updateBubbleVisibility(false);
        }
    }

    /**
     * A contract that allows the user to provide particular Fast Scroll
     * animations for hiding and showing the drag handle and popup bubble
     */
    public interface AnimationProvider {
        /**
         * Retrieves the animation to use for showing or hiding the popup bubble.
         * By default this uses the simple alpha animation {@link FastScrollBubbleVisibilityAnimation}
         *
         * @param bubble The view that represents the popup bubble
         * @param toVisible {@code true} if the returned animation should handle showing the bubble
         * @return The custom animation for hiding or showing the popup bubble, null to use the default
         */
        @Nullable
        Animation getBubbleAnimation(@NonNull View bubble, boolean toVisible);

        /**
         * Retrieves the animation to use for showing or hiding the drag handle.
         * By default this uses the simple alpha animation {@link FastScrollHandleVisibilityAnimation}
         *
         * @param handle The view that represents the drag handle
         * @param toVisible {@code true} if the returned animation should handle showing the drag handle
         * @return The custom animation for hiding or showing the drag handle, null to use the default
         */
        @Nullable
        Animation getHandleAnimation(@NonNull View handle, boolean toVisible);
    }

    /**
     * Callback used to request the title for the fast scroll bubble
     * when enabled.
     */
    public interface PopupCallbacks {
        /**
         * Called when the section id specified with {@link #getSectionId(int)} changes,
         * indicating the popup text needs to be changed. This will only be called if
         * {@link #setShowBubble(boolean)} is true.
         *
         * @param position The position for the item with the {@code sectionId}
         * @param sectionId The id for the section the text is associated with
         * @return The text for the bubble
         */
        @NonNull
        String getPopupText(int position, long sectionId);

        /**
         * Called for each item as the list is scrolled via the FastScroll to determine what the
         * items section is. This will only be called if {@link #setShowBubble(boolean)} is true.
         *
         * @param position The position to determine the section id for
         * @return The id associated with the section the {@code position} is a member of
         */
        @IntRange(from = INVALID_POPUP_ID)
        long getSectionId(@IntRange(from = 0) int position);
    }

    /**
     * Alignment types associated with the popup bubble (see {@link #setShowBubble(boolean)}
     */
    public enum BubbleAlignment {
        /**
         * The top of the popup bubble is even with the top of the drag handle
         */
        TOP,

        /**
         * The center (y) of the popup bubble is even with the center (y) of the drag handle
         */
        CENTER,

        /**
         * The bottom of the popup bubble is even with the bottom of the drag handle
         */
        BOTTOM,

        /**
         * The bottom of the popup bubble is even with the top of the drag handle
         */
        BOTTOM_TO_TOP,

        /**
         * The top of the popup bubble is even with the bottom of the drag handle
         */
        TOP_TO_BOTTOM,

        /**
         * The bottom of the popup bubble is even with the center (y) of the drag handle
         */
        BOTTOM_TO_CENTER;

        @NonNull
        private static BubbleAlignment get(@IntRange(from = 0, to = 5) int index) {
            return BubbleAlignment.values()[index];
        }
    }
}