info.awesomedevelopment.tvgrid.library.TVGridView.java Source code

Java tutorial

Introduction

Here is the source code for info.awesomedevelopment.tvgrid.library.TVGridView.java

Source

package info.awesomedevelopment.tvgrid.library;

import android.animation.AnimatorSet;
import android.animation.ValueAnimator;
import android.app.ActivityManager;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.v4.util.LruCache;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.text.MessageFormat;

/*
Copyright 2015 Sam Mathias Weggersen
    
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.
 */

public class TVGridView extends RecyclerView {

    private static final int ANIMATION_DURATION = 140;

    @Retention(RetentionPolicy.SOURCE)
    @IntDef({ INSIDE, CENTER, OUTSIDE })
    public @interface StrokePosition {
    }

    public static final int INSIDE = 0;
    public static final int CENTER = 1;
    public static final int OUTSIDE = 2;

    @Retention(RetentionPolicy.SOURCE)
    @IntDef({ OVER, UNDER })
    public @interface SelectorPosition {
    }

    public static final int OVER = 0;
    public static final int UNDER = 1;

    @Retention(RetentionPolicy.SOURCE)
    @IntDef({ RECTANGLE, CIRCLE })
    public @interface SelectorShape {
    }

    public static final int RECTANGLE = 0;
    public static final int CIRCLE = 1;

    private ValueAnimator mYSize;
    private ValueAnimator mXSize;
    private ValueAnimator mYLocation;
    private ValueAnimator mXLocation;

    private LruCache<String, BitmapDrawable> mCache;

    private static Paint sStrokePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    private static Paint sShadowPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    private static Paint sCutoutPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    private static Paint sFillPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

    static {
        sStrokePaint.setStyle(Paint.Style.FILL);

        sShadowPaint.setStyle(Paint.Style.FILL);
        sShadowPaint.setColor(Color.BLACK);
        sShadowPaint.setAlpha((int) Math.ceil(0.5 * 255));

        sCutoutPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OUT));

        sFillPaint.setStyle(Paint.Style.FILL);
    }

    private class DeselectRunnable implements Runnable {

        private View view;

        public DeselectRunnable(View view) {
            this.view = view;
        }

        @Override
        public void run() {
            hardUpdateSelector(view, hasFocus(), false);
        }

    }

    private final AnimatorSet mSelectorAnimationSet = new AnimatorSet();
    private final Handler mSelectorDeselectHandler = new Handler();
    private DeselectRunnable mSelectorDeselectRunnable;

    private Drawable mStrokeCell;
    private Rect mStrokeCellPrevBounds;
    private Rect mStrokeCellCurrentBounds;

    private int mScrollY = 0;
    private boolean mHardScrollChange = false;
    private boolean mEdgeChange = false;

    private int mOffsetX = -1;
    private int mOffsetY = -1;
    private boolean mOffsetOnPrev = false;

    @StrokePosition
    private int mStrokePosition;
    @SelectorPosition
    private int mSelectorPosition;
    @SelectorShape
    private int mSelectorShape = RECTANGLE;

    private boolean mAnimateSelectorChanges;

    private float mCornerRadiusX;
    private float mCornerRadiusY;

    private boolean mIsFilled;

    private int mFillColor;
    private int mFillColorSelected;
    private int mFillColorClicked;

    private float mFillAlpha;
    private float mFillAlphaSelected;
    private float mFillAlphaClicked;

    private float mStrokeWidth;

    private int mStrokeColor;
    private int mStrokeColorSelected;
    private int mStrokeColorClicked;

    private float mStrokeMarginLeft;
    private float mStrokeMarginTop;
    private float mStrokeMarginRight;
    private float mStrokeMarginBottom;

    private float mStrokeSpacingLeft;
    private float mStrokeSpacingTop;
    private float mStrokeSpacingRight;
    private float mStrokeSpacingBottom;

    public TVGridView(Context context) {
        super(context);

        init(null);
    }

    public TVGridView(Context context, AttributeSet attrs) {
        super(context, attrs);

        init(attrs);
    }

    public TVGridView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        init(attrs);
    }

    @SuppressWarnings("deprecation")
    private void init(AttributeSet attrs) {
        ActivityManager am = (ActivityManager) getContext().getSystemService(Context.ACTIVITY_SERVICE);
        mCache = new LruCache<>(am.getMemoryClass() * 1024);

        mYSize = new ValueAnimator();
        mXSize = new ValueAnimator();
        mYLocation = new ValueAnimator();
        mXLocation = new ValueAnimator();

        mXSize.addUpdateListener(xSizeListener);
        mYSize.addUpdateListener(ySizeListener);
        mYLocation.addUpdateListener(yLocationListener);
        mXLocation.addUpdateListener(xLocationListener);

        mSelectorAnimationSet.playTogether(mXLocation, mYLocation, mXSize, mYSize);
        mSelectorAnimationSet.setInterpolator(new AccelerateDecelerateInterpolator());
        mSelectorAnimationSet.setDuration(ANIMATION_DURATION);

        TypedValue fillAlpha = new TypedValue();
        getResources().getValue(R.dimen.tvg_defFillAlpha, fillAlpha, true);

        TypedValue fillAlphaSelected = new TypedValue();
        getResources().getValue(R.dimen.tvg_defFillAlphaSelected, fillAlphaSelected, true);

        if (attrs != null) {
            TypedArray a = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.TVGridView, 0, 0);

            try {
                //noinspection ResourceType
                mStrokePosition = a.getInteger(R.styleable.TVGridView_tvg_strokePosition, OUTSIDE);
                //noinspection ResourceType
                mSelectorPosition = a.getInteger(R.styleable.TVGridView_tvg_selectorPosition, OVER);
                //noinspection ResourceType
                mSelectorShape = a.getInteger(R.styleable.TVGridView_tvg_selectorShape, RECTANGLE);

                mAnimateSelectorChanges = a.getBoolean(R.styleable.TVGridView_tvg_animateSelectorChanges,
                        getResources().getInteger(R.integer.tvg_defAnimateSelectorChanges) == 1);
                mIsFilled = a.getBoolean(R.styleable.TVGridView_tvg_filled,
                        getResources().getInteger(R.integer.tvg_defIsFilled) == 1);
                mFillAlpha = a.getFloat(R.styleable.TVGridView_tvg_fillAlpha, fillAlpha.getFloat());
                mFillAlphaSelected = a.getFloat(R.styleable.TVGridView_tvg_fillAlphaSelected,
                        fillAlphaSelected.getFloat());
                mFillColor = a.getColor(R.styleable.TVGridView_tvg_fillColor,
                        getResources().getColor(R.color.tvg_defFillColor));
                mFillColorSelected = a.getColor(R.styleable.TVGridView_tvg_fillColorSelected,
                        getResources().getColor(R.color.tvg_defFillColorSelected));
                mCornerRadiusX = a.getDimension(R.styleable.TVGridView_tvg_cornerRadius,
                        getResources().getDimension(R.dimen.tvg_defCornerRadius));
                mCornerRadiusY = a.getDimension(R.styleable.TVGridView_tvg_cornerRadius,
                        getResources().getDimension(R.dimen.tvg_defCornerRadius));
                mStrokeWidth = a.getDimension(R.styleable.TVGridView_tvg_strokeWidth,
                        getResources().getDimension(R.dimen.tvg_defStrokeWidth));
                mStrokeColor = a.getColor(R.styleable.TVGridView_tvg_strokeColor,
                        getResources().getColor(R.color.tvg_defStrokeColor));
                mStrokeColorSelected = a.getColor(R.styleable.TVGridView_tvg_strokeColorSelected,
                        getResources().getColor(R.color.tvg_defStrokeColorSelected));
                mStrokeMarginLeft = a.getDimension(R.styleable.TVGridView_tvg_marginLeft,
                        getResources().getDimension(R.dimen.tvg_defStrokeMarginLeft));
                mStrokeMarginTop = a.getDimension(R.styleable.TVGridView_tvg_marginTop,
                        getResources().getDimension(R.dimen.tvg_defStrokeMarginTop));
                mStrokeMarginRight = a.getDimension(R.styleable.TVGridView_tvg_marginRight,
                        getResources().getDimension(R.dimen.tvg_defStrokeMarginRight));
                mStrokeMarginBottom = a.getDimension(R.styleable.TVGridView_tvg_marginBottom,
                        getResources().getDimension(R.dimen.tvg_defStrokeMarginBottom));
                mStrokeSpacingLeft = a.getDimension(R.styleable.TVGridView_tvg_spacingLeft,
                        getResources().getDimension(R.dimen.tvg_defStrokeSpacingLeft));
                mStrokeSpacingTop = a.getDimension(R.styleable.TVGridView_tvg_spacingTop,
                        getResources().getDimension(R.dimen.tvg_defStrokeSpacingTop));
                mStrokeSpacingRight = a.getDimension(R.styleable.TVGridView_tvg_spacingRight,
                        getResources().getDimension(R.dimen.tvg_defStrokeSpacingRight));
                mStrokeSpacingBottom = a.getDimension(R.styleable.TVGridView_tvg_spacingBottom,
                        getResources().getDimension(R.dimen.tvg_defStrokeSpacingBottom));
            } finally {
                a.recycle();
            }
        } else {
            mStrokePosition = OUTSIDE;
            mSelectorPosition = OVER;
            mSelectorShape = RECTANGLE;

            mAnimateSelectorChanges = getResources().getInteger(R.integer.tvg_defAnimateSelectorChanges) == 1;
            mIsFilled = getResources().getInteger(R.integer.tvg_defIsFilled) == 1;
            mFillAlpha = fillAlpha.getFloat();
            mFillAlphaSelected = fillAlphaSelected.getFloat();
            mFillColor = getResources().getColor(R.color.tvg_defFillColor);
            mFillColorSelected = getResources().getColor(R.color.tvg_defFillColorSelected);
            mCornerRadiusX = getResources().getDimension(R.dimen.tvg_defCornerRadius);
            mCornerRadiusY = getResources().getDimension(R.dimen.tvg_defCornerRadius);
            mStrokeWidth = getResources().getDimension(R.dimen.tvg_defStrokeWidth);
            mStrokeColor = getResources().getColor(R.color.tvg_defStrokeColor);
            mStrokeColorSelected = getResources().getColor(R.color.tvg_defStrokeColorSelected);
            mStrokeMarginLeft = getResources().getDimension(R.dimen.tvg_defStrokeMarginLeft);
            mStrokeMarginTop = getResources().getDimension(R.dimen.tvg_defStrokeMarginTop);
            mStrokeMarginRight = getResources().getDimension(R.dimen.tvg_defStrokeMarginRight);
            mStrokeMarginBottom = getResources().getDimension(R.dimen.tvg_defStrokeMarginBottom);
            mStrokeSpacingLeft = getResources().getDimension(R.dimen.tvg_defStrokeSpacingLeft);
            mStrokeSpacingTop = getResources().getDimension(R.dimen.tvg_defStrokeSpacingTop);
            mStrokeSpacingRight = getResources().getDimension(R.dimen.tvg_defStrokeSpacingRight);
            mStrokeSpacingBottom = getResources().getDimension(R.dimen.tvg_defStrokeSpacingBottom);
        }

        addOnScrollListener(new OnScrollListener() {
            @Override
            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                super.onScrollStateChanged(recyclerView, newState);
                if (newState == SCROLL_STATE_IDLE) {
                    mEdgeChange = false;
                    mHardScrollChange = false;
                }
            }

            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);
                mScrollY = mScrollY + dy;

                if (mStrokeCellCurrentBounds == null || mStrokeCell == null)
                    return;

                if (useAnimations()) {
                    mSelectorAnimationSet.cancel();

                    mStrokeCellCurrentBounds.offsetTo(mStrokeCellCurrentBounds.left - dx,
                            mStrokeCellCurrentBounds.top - dy);

                    performSelectorAnimation();
                } else if (mHardScrollChange || mEdgeChange) {
                    mStrokeCellCurrentBounds.offsetTo(mStrokeCellCurrentBounds.left - dx,
                            mStrokeCellCurrentBounds.top - dy);
                    setPrevBounds();

                    mStrokeCell.setBounds(mStrokeCellPrevBounds);
                    invalidate();
                }
            }
        });

        setOnTouchListener(new OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                clearHighlightedView();
                return false;
            }
        });
    }

    @SuppressWarnings("unused")
    public int getScroll() {
        return mScrollY;
    }

    @SuppressWarnings("unused")
    public void scrollByY(int y, boolean edgeChange) {
        mEdgeChange = edgeChange;
        mHardScrollChange = false;

        if (useAnimations()) {
            super.smoothScrollBy(0, y);
        } else {
            super.scrollBy(0, y);

            if (!edgeChange) {
                if (mStrokeCellCurrentBounds == null || mStrokeCell == null)
                    return;

                mSelectorAnimationSet.cancel();
                mStrokeCellCurrentBounds.offsetTo(mStrokeCellCurrentBounds.left, mStrokeCellCurrentBounds.top);

                setPrevBounds();

                mStrokeCell.setBounds(mStrokeCellPrevBounds);
                invalidate();
            }
        }
    }

    @Override
    public void smoothScrollToPosition(int position) {
        mHardScrollChange = true;
        super.smoothScrollToPosition(position);
    }

    @Override
    public void scrollToPosition(int position) {
        mHardScrollChange = true;
        super.scrollToPosition(position);
    }

    @Override
    public void scrollTo(int x, int y) {
        mHardScrollChange = true;
        super.scrollTo(x, y);
    }

    @Override
    public void smoothScrollBy(int dx, int dy) {
        mHardScrollChange = true;
        super.smoothScrollBy(dx, dy);
    }

    @Override
    public void scrollBy(int x, int y) {
        mHardScrollChange = true;
        super.scrollBy(x, y);
    }

    @SuppressWarnings("unused")
    public void setStrokePosition(@StrokePosition int strokePosition) {
        mStrokePosition = strokePosition;
    }

    @SuppressWarnings("unused")
    @StrokePosition
    public int getStrokePosition() {
        return mStrokePosition;
    }

    @SuppressWarnings("unused")
    public void setSelectorPosition(@SelectorPosition int position) {
        mSelectorPosition = position;
    }

    @SuppressWarnings("unused")
    @SelectorPosition
    public int setSelectorPosition() {
        return mSelectorPosition;
    }

    @SuppressWarnings("unused")
    public void setSelectorShape(@SelectorShape int shape) {
        mSelectorShape = shape;
    }

    @SuppressWarnings("unused")
    @SelectorShape
    public int getSelectorShape() {
        return mSelectorShape;
    }

    @SuppressWarnings("unused")
    public void setCornerRadius(float radius) {
        mCornerRadiusX = radius;
        mCornerRadiusY = radius;
    }

    @SuppressWarnings("unused")
    public void setCornerRadius(float x, float y) {
        mCornerRadiusX = x;
        mCornerRadiusY = y;
    }

    /**
     * @param animate true if selector should animate between focus views
     */
    @SuppressWarnings("unused")
    public void setAnimateSelectorChanges(boolean animate) {
        mAnimateSelectorChanges = animate;
    }

    @SuppressWarnings("unused")
    public int getAnimationDuration() {
        return ANIMATION_DURATION;
    }

    @SuppressWarnings("unused")
    public float getCornerRadiusX() {
        return mCornerRadiusX;
    }

    @SuppressWarnings("unused")
    public float getCornerRadiusY() {
        return mCornerRadiusY;
    }

    @SuppressWarnings("unused")
    public void setFilled(boolean filled) {
        mIsFilled = filled;
    }

    @SuppressWarnings("unused")
    public boolean isFilled() {
        return mIsFilled;
    }

    @SuppressWarnings("unused")
    public void setFillColor(int color) {
        mFillColor = color;
    }

    @SuppressWarnings("unused")
    public int getFillColor() {
        return mFillColor;
    }

    @SuppressWarnings("unused")
    public void setFillColorSelected(int color) {
        mFillColorSelected = color;
    }

    @SuppressWarnings("unused")
    public int getFillColorSelected() {
        return mFillColorSelected;
    }

    @SuppressWarnings("unused")
    public void setFillColorClicked(int color) {
        mFillColorClicked = color;
    }

    @SuppressWarnings("unused")
    public int getFillColorClicked() {
        return mFillColorClicked;
    }

    @SuppressWarnings("unused")
    public void setFillAlpha(float alpha) {
        mFillAlpha = alpha;
    }

    @SuppressWarnings("unused")
    public float getFillAlpha() {
        return mFillAlpha;
    }

    @SuppressWarnings("unused")
    public void setFillAlphaSelected(float alpha) {
        mFillAlphaSelected = alpha;
    }

    @SuppressWarnings("unused")
    public float getFillAlphaSelected() {
        return mFillAlphaSelected;
    }

    @SuppressWarnings("unused")
    public void setFillAlphaClicked(float alpha) {
        mFillAlphaClicked = alpha;
    }

    @SuppressWarnings("unused")
    public float getFillAlphaClicked() {
        return mFillAlphaClicked;
    }

    @SuppressWarnings("unused")
    public void setStrokeWidth(float width) {
        mStrokeWidth = width;
    }

    @SuppressWarnings("unused")
    public float getStrokeWidth() {
        return mStrokeWidth;
    }

    @SuppressWarnings("unused")
    public void setStrokeColor(int color) {
        mStrokeColorSelected = color;
    }

    @SuppressWarnings("unused")
    public int getStrokeColor() {
        return mStrokeColor;
    }

    @SuppressWarnings("unused")
    public void setStrokeColorSelected(int color) {
        mStrokeColorSelected = color;
    }

    @SuppressWarnings("unused")
    public int getStrokeColorSelected() {
        return mStrokeColorSelected;
    }

    @SuppressWarnings("unused")
    public void setStrokeColorClicked(int color) {
        mStrokeColorClicked = color;
    }

    @SuppressWarnings("unused")
    public int getStrokeColorClicked() {
        return mStrokeColorClicked;
    }

    /**
     * Stroke margin for selector
     *
     *              MARGIN
     * |------------------------------|
     * |                              |
     * |                              |
     * |                              |
     * |                              |
     * |                              |
     * |                              |
     * |------------------------------|
     *
     * @param left margin left
     * @param top margin top
     * @param right margin right
     * @param bottom margin bottom
     */
    @SuppressWarnings("unused")
    public void setStrokeMargin(float left, float top, float right, float bottom) {
        mStrokeMarginLeft = left;
        mStrokeMarginTop = top;
        mStrokeMarginRight = right;
        mStrokeMarginBottom = bottom;
    }

    /**
     * Stroke margin for selector
     *
     *              MARGIN
     * |------------------------------|
     * |                              |
     * |                              |
     * |                              |
     * |                              |
     * |                              |
     * |                              |
     * |------------------------------|
     *
     * @param all same margin in all directions
     */
    @SuppressWarnings("unused")
    public void setStrokeMargin(float all) {
        mStrokeMarginLeft = all;
        mStrokeMarginTop = all;
        mStrokeMarginRight = all;
        mStrokeMarginBottom = all;
    }

    @SuppressWarnings("unused")
    public float getStrokeMarginLeft() {
        return mStrokeMarginLeft;
    }

    @SuppressWarnings("unused")
    public float getStrokeMarginTop() {
        return mStrokeMarginTop;
    }

    @SuppressWarnings("unused")
    public float getStrokeMarginRight() {
        return mStrokeMarginRight;
    }

    @SuppressWarnings("unused")
    public float getStrokeMarginBottom() {
        return mStrokeMarginBottom;
    }

    /**
     * Stroke spacing for selector
     *
     * |------------------------------|
     * |            SPACING           |
     * |                              |
     * |                              |
     * |                              |
     * |                              |
     * |                              |
     * |------------------------------|
     *
     * @param left spacing left
     * @param top spacing top
     * @param right spacing right
     * @param bottom spacing bottom
     */
    @SuppressWarnings("unused")
    public void setStrokeSpacing(float left, float top, float right, float bottom) {
        mStrokeSpacingLeft = left;
        mStrokeSpacingTop = top;
        mStrokeSpacingRight = right;
        mStrokeSpacingBottom = bottom;
    }

    /**
     * Stroke spacing for selector
     *
     * |------------------------------|
     * |            SPACING           |
     * |                              |
     * |                              |
     * |                              |
     * |                              |
     * |                              |
     * |------------------------------|
     *
     * @param all same spacing in all directions
     */
    @SuppressWarnings("unused")
    public void setStrokeSpacing(float all) {
        mStrokeSpacingLeft = all;
        mStrokeSpacingTop = all;
        mStrokeSpacingRight = all;
        mStrokeSpacingBottom = all;
    }

    @SuppressWarnings("unused")
    public float getStrokeSpacingLeft() {
        return mStrokeSpacingLeft;
    }

    @SuppressWarnings("unused")
    public float getStrokeSpacingTop() {
        return mStrokeSpacingTop;
    }

    @SuppressWarnings("unused")
    public float getStrokeSpacingRight() {
        return mStrokeSpacingRight;
    }

    @SuppressWarnings("unused")
    public float getStrokeSpacingBottom() {
        return mStrokeSpacingBottom;
    }

    /**
     * Creates and shows a click selector for a given view
     *
     * @param view view to click
     * @param offsetX offset in x direction, < 0 == 0
     * @param offsetY offset in y direction, < 0 == 0
     */
    @SuppressWarnings("unused")
    public void clickView(final View view, int offsetX, int offsetY) {
        if (view == null)
            return;

        mOffsetX = offsetX;
        mOffsetY = offsetY;

        hardUpdateSelector(view, true, true);
    }

    /**
     * Creates and shows a click selector for a given view
     *
     * @param view view to click
     */
    @SuppressWarnings("unused")
    public void clickView(final View view) {
        if (view == null)
            return;

        final Drawable clone = mStrokeCell.mutate();
        hardUpdateSelector(view, true, true);

        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                mStrokeCell = clone;
                invalidate();
            }
        }, 100);

    }

    /**
     * Creates and shows a selector for a given view
     * with offset
     *
     * @param view view to select
     * @param offsetX offset in x direction, < 0 == 0
     * @param offsetY offset in y direction, < 0 == 0
     * @param focused true if view should get focus
     */
    @SuppressWarnings("unused")
    public void selectView(final View view, int offsetX, int offsetY, boolean focused) {
        if (view == null)
            return;

        mOffsetX = offsetX;
        mOffsetY = offsetY;

        highlightViewBase(view, focused);
    }

    /**
     * Creates and shows a selector for a given view
     *
     * @param view view to select
     * @param focused true if view should get focus
     */
    @SuppressWarnings("unused")
    public void selectView(View view, boolean focused) {
        selectView(view, -1, -1, focused);

    }

    /**
     * General logic for selecting a view
     *
     * @param view view to select or click
     * @param focused true if view should get focus
     */
    private void highlightViewBase(final View view, final boolean focused) {
        if (!focused) {
            if (mSelectorDeselectRunnable == null)
                mSelectorDeselectRunnable = new DeselectRunnable(view);
            mSelectorDeselectHandler.postDelayed(mSelectorDeselectRunnable, 50);
            return;
        }
        mSelectorDeselectHandler.removeCallbacksAndMessages(null);
        if (useAnimations() && mStrokeCell != null) {
            prepareAndPerformSelectorAnimation(view, mSelectorAnimationSet.isRunning());
        } else {
            hardUpdateSelector(view, true, false);
            clearOffset();
        }
    }

    private void hardUpdateSelector(View view, boolean focused, boolean clicked) {
        addStrokedView(view, focused, clicked, true, true);
    }

    private void setPrevBounds() {
        if (mStrokeCellPrevBounds == null) {
            mStrokeCellPrevBounds = new Rect(mStrokeCellCurrentBounds);
        } else {
            mStrokeCellPrevBounds.right = mStrokeCellCurrentBounds.right;
            mStrokeCellPrevBounds.top = mStrokeCellCurrentBounds.top;
            mStrokeCellPrevBounds.left = mStrokeCellCurrentBounds.left;
            mStrokeCellPrevBounds.bottom = mStrokeCellCurrentBounds.bottom;
        }
    }

    private void prepareAndPerformSelectorAnimation(View view, boolean running) {
        if (running)
            mSelectorAnimationSet.cancel();
        else
            setPrevBounds();

        if (mOffsetOnPrev && (mOffsetX == -1 || mOffsetY == -1)) {
            mStrokeCellPrevBounds.offset(-(mOffsetX == -1 ? 0 : mOffsetX), -(mOffsetY == -1 ? 0 : mOffsetY));
        }

        addStrokedView(view, true, false, !running, false);

        performSelectorAnimation();

    }

    private ValueAnimator.AnimatorUpdateListener ySizeListener = new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            mStrokeCellPrevBounds.set(mStrokeCellPrevBounds.left, mStrokeCellPrevBounds.top,
                    mStrokeCellPrevBounds.right, (int) animation.getAnimatedValue());
            mStrokeCell.setBounds(mStrokeCellPrevBounds);
            invalidate();
        }
    };

    private ValueAnimator.AnimatorUpdateListener xSizeListener = new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            mStrokeCellPrevBounds.set(mStrokeCellPrevBounds.left, mStrokeCellPrevBounds.top,
                    (int) animation.getAnimatedValue(), mStrokeCellPrevBounds.bottom);
            mStrokeCell.setBounds(mStrokeCellPrevBounds);
            invalidate();
        }
    };

    private ValueAnimator.AnimatorUpdateListener yLocationListener = new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            mStrokeCellPrevBounds.offsetTo(mStrokeCellPrevBounds.left, (int) animation.getAnimatedValue());
            mStrokeCell.setBounds(mStrokeCellPrevBounds);
            invalidate();
        }
    };

    private ValueAnimator.AnimatorUpdateListener xLocationListener = new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            mStrokeCellPrevBounds.offsetTo((int) animation.getAnimatedValue(), mStrokeCellPrevBounds.top);
            mStrokeCell.setBounds(mStrokeCellPrevBounds);
            invalidate();
        }
    };

    private void performSelectorAnimation() {
        if (mStrokeCellPrevBounds == null || mStrokeCell == null)
            return;

        mYSize.setIntValues(mStrokeCellPrevBounds.bottom, mStrokeCellCurrentBounds.bottom);
        mXSize.setIntValues(mStrokeCellPrevBounds.right, mStrokeCellCurrentBounds.right);

        mStrokeCellPrevBounds.right = mStrokeCellPrevBounds.left
                + (mStrokeCellCurrentBounds.right - mStrokeCellCurrentBounds.left);
        mStrokeCellPrevBounds.bottom = mStrokeCellPrevBounds.top
                + (mStrokeCellCurrentBounds.bottom - mStrokeCellCurrentBounds.top);

        mYLocation.setIntValues(mStrokeCellPrevBounds.top, mStrokeCellCurrentBounds.top);
        mXLocation.setIntValues(mStrokeCellPrevBounds.left, mStrokeCellCurrentBounds.left);

        mSelectorAnimationSet.start();
    }

    /**
     * Creates the stroke cell with the appropriate bitmap and of appropriate
     * size. The stroke cell's BitmapDrawable is drawn on top or under of the bitmap every
     * single time an invalidate call is made.
     */
    private void addStrokedView(final View view, final boolean focused, final boolean clicked,
            final boolean setBounds, final boolean invalidate) {
        setCorrectBounds(view);

        String id = MessageFormat.format("{0}:{1}:{2}:{3}:{4}:{5}", mIsFilled, mStrokeWidth, mStrokeColor,
                mFillColor, view.getHeight(), view.getWidth());

        BitmapDrawable bd = mCache.get(id);
        if (bd == null) {
            bd = new BitmapDrawable(getResources(),
                    generateBitmap(view.getWidth(), view.getHeight(), focused, clicked));
            mCache.put(id, bd);
        }
        mStrokeCell = bd;
        if (setBounds)
            mStrokeCell.setBounds(mStrokeCellCurrentBounds);
        if (invalidate)
            invalidate();
    }

    private void setCorrectBounds(View v) {
        int spacing = 0;
        switch (mStrokePosition) {
        case INSIDE:
            spacing = 0;
            break;
        case CENTER:
            spacing = (int) mStrokeWidth;
            break;
        case OUTSIDE:
            spacing = (int) mStrokeWidth * 2;
            break;
        }

        mOffsetOnPrev = mOffsetX != -1 || mOffsetY != -1;

        int w = v.getWidth();
        int h = v.getHeight();
        int w_scaled = v.getWidth() + spacing;
        int h_scaled = v.getHeight() + spacing;
        int top = v.getTop() - ((h_scaled - h) / 2) + (mOffsetY == -1 ? 0 : mOffsetY);
        int left = v.getLeft() - ((w_scaled - w) / 2) + (mOffsetX == -1 ? 0 : mOffsetX);

        mStrokeCellCurrentBounds = new Rect((int) (left - mStrokeSpacingLeft), (int) (top - mStrokeSpacingTop),
                (int) (left + w_scaled + mStrokeSpacingRight), (int) (top + h_scaled + mStrokeSpacingBottom));
    }

    /**
     * Generates a bitmap according to the size and state of a view in the recycler view
     *
     * @param w width of the bitmap
     * @param h height of the bitmap
     * @param focused true if the view is focused
     * @param clicked true if the view is clicked
     * @return Bitmap
     */
    private Bitmap generateBitmap(int w, int h, boolean focused, boolean clicked) {
        Bitmap bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);

        RectF fillRect = new RectF(mStrokeMarginLeft, mStrokeMarginTop, w - mStrokeMarginRight,
                h - mStrokeMarginBottom);
        RectF shadowRect = new RectF(mStrokeWidth + mStrokeMarginLeft, mStrokeWidth + mStrokeMarginTop,
                w - mStrokeWidth - mStrokeMarginRight, h - mStrokeWidth - mStrokeMarginBottom);
        RectF cutoutRect = new RectF(mStrokeWidth + mStrokeMarginLeft + 2, mStrokeWidth + mStrokeMarginTop + 2,
                w - mStrokeWidth - mStrokeMarginRight - 1, h - mStrokeWidth - mStrokeMarginBottom - 1);

        if (mStrokeWidth > 0.0f) {
            sStrokePaint.setColor(clicked ? mStrokeColorClicked : focused ? mStrokeColor : mStrokeColorSelected);
            paintCanvas(canvas, fillRect, sStrokePaint);

            if (!mIsFilled) {
                paintCanvas(canvas, shadowRect, sShadowPaint);
            } else {
                cutoutRect = shadowRect;
            }

            paintCanvas(canvas, cutoutRect, sCutoutPaint);
        }

        if (mIsFilled) {
            sFillPaint.setColor(clicked ? mFillColorClicked : focused ? mFillColor : mFillColorSelected);
            sFillPaint.setAlpha((int) Math
                    .ceil((clicked ? mFillAlphaClicked : focused ? mFillAlpha : mFillAlphaSelected) * 255));
            paintCanvas(canvas, cutoutRect, sFillPaint);
        }

        return bitmap;
    }

    /**
     * Helper method to paint the canvas used in generate bitmap
     *
     * @param canvas the canvas used to draw onto
     * @param rectF size
     * @param paint paint
     */
    private void paintCanvas(Canvas canvas, RectF rectF, Paint paint) {
        if (mSelectorShape == RECTANGLE) {
            canvas.drawRoundRect(rectF, mCornerRadiusX, mCornerRadiusY, paint);
        } else if (mSelectorShape == CIRCLE) {
            canvas.drawCircle(rectF.centerX(), rectF.centerY(), rectF.width() / 2, paint);
        } else {
            throw new IllegalArgumentException("Selector shape must be one of RECTANGLE or CIRCLE");
        }
    }

    /**
     * onDraw gets invoked before all the child views are about to be drawn.
     * By overriding this method, the stroke cell (BitmapDrawable) can be drawn
     * under the RecyclerViews' items whenever the RecyclerViews is redrawn.
     *
     * @param c canvas
     */
    @Override
    public void onDraw(@NonNull final Canvas c) {
        if (mSelectorPosition == UNDER) {
            if (mStrokeCell != null) {
                mStrokeCell.draw(c);
            }
        }
        super.onDraw(c);
    }

    /**
     * dispatchDraw gets invoked when all the child views are about to be drawn.
     * By overriding this method, the stroke cell (BitmapDrawable) can be drawn
     * over the RecyclerViews' items whenever the RecyclerViews is redrawn.
     *
     * @param c canvas
     */
    @Override
    protected void dispatchDraw(@NonNull final Canvas c) {
        super.dispatchDraw(c);

        if (mSelectorPosition == OVER) {
            if (mStrokeCell != null) {
                mStrokeCell.draw(c);
            }
        }
    }

    /**
     * @return true if selector should animate movement
     */
    public boolean useAnimations() {
        return mAnimateSelectorChanges;
    }

    /**
     * Clear highlighted view
     */
    public void clearHighlightedView() {
        mStrokeCell = null;
        mStrokeCellPrevBounds = null;
        invalidate();
        requestLayout();
    }

    /**
     * Clear offset
     */
    private void clearOffset() {
        mOffsetY = -1;
        mOffsetX = -1;
    }

    @Override
    protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
        return true;
    }
}