Android Open Source - HorizontalPicker Horizontal Picker






From Project

Back to project page HorizontalPicker.

License

The source code is released under:

Apache License

If you think the Android project HorizontalPicker listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.

Java Source Code

/*
 * Copyright 2014 Bla olar/* ww  w . j av a2s.co  m*/
 *
 * 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.wefika.horizontalpicker;

import android.animation.ArgbEvaluator;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.v4.text.TextDirectionHeuristicCompat;
import android.support.v4.text.TextDirectionHeuristicsCompat;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
import android.support.v4.widget.ExploreByTouchHelper;
import android.text.BoringLayout;
import android.text.Layout;
import android.text.TextPaint;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.accessibility.AccessibilityEvent;
import android.view.animation.DecelerateInterpolator;
import android.widget.EdgeEffect;
import android.widget.OverScroller;

import java.lang.ref.WeakReference;
import java.util.List;

/**
 * Created by Bla olar on 24/01/14.
 */
public class HorizontalPicker extends View {

    public static final String TAG = "HorizontalTimePicker";

    /**
     * The coefficient by which to adjust (divide) the max fling velocity.
     */
    private static final int SELECTOR_MAX_FLING_VELOCITY_ADJUSTMENT = 4;

    /**
     * The the duration for adjusting the selector wheel.
     */
    private static final int SELECTOR_ADJUSTMENT_DURATION_MILLIS = 800;

    /**
     * Determines speed during touch scrolling.
     */
    private VelocityTracker mVelocityTracker;

    /**
     * @see android.view.ViewConfiguration#getScaledMinimumFlingVelocity()
     */
    private int mMinimumFlingVelocity;

    /**
     * @see android.view.ViewConfiguration#getScaledMaximumFlingVelocity()
     */
    private int mMaximumFlingVelocity;

    private final int mOverscrollDistance;

    private int mTouchSlop;

    private CharSequence[] mValues;
    private BoringLayout[] mLayouts;

    private TextPaint mTextPaint;
    private BoringLayout.Metrics mBoringMetrics;
    private TextUtils.TruncateAt mEllipsize;

    private int mItemWidth;
    private RectF mItemClipBounds;
    private RectF mItemClipBoundsOffser;

    private float mLastDownEventX;

    private OverScroller mFlingScrollerX;
    private OverScroller mAdjustScrollerX;

    private int mPreviousScrollerX;

    private boolean mScrollingX;
    private int mPressedItem = -1;

    private ColorStateList mTextColor;

    private OnItemSelected mOnItemSelected;
    private OnItemClicked mOnItemClicked;

    private int mSelectedItem;

    private EdgeEffect mLeftEdgeEffect;
    private EdgeEffect mRightEdgeEffect;

    private Marquee mMarquee;
    private int mMarqueeRepeatLimit = 3;

    private float mDividerSize = 0;

    private int mSideItems = 1;

    private TextDirectionHeuristicCompat mTextDir;

    private final PickerTouchHelper mTouchHelper;

    public HorizontalPicker(Context context) {
        this(context, null);
    }

    public HorizontalPicker(Context context, AttributeSet attrs) {
        this(context, attrs, R.attr.horizontalPickerStyle);
    }

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

        // create the selector wheel paint
        TextPaint paint = new TextPaint();
        paint.setAntiAlias(true);
        mTextPaint = paint;

        TypedArray a = context.getTheme().obtainStyledAttributes(
                attrs,
                R.styleable.HorizontalPicker,
                defStyle, 0
        );

        CharSequence[] values;
        int ellipsize = 3; // END default value
        int sideItems = mSideItems;

        try {
            mTextColor = a.getColorStateList(R.styleable.HorizontalPicker_android_textColor);
            values = a.getTextArray(R.styleable.HorizontalPicker_values);
            ellipsize = a.getInt(R.styleable.HorizontalPicker_android_ellipsize, ellipsize);
            mMarqueeRepeatLimit = a.getInt(R.styleable.HorizontalPicker_android_marqueeRepeatLimit, mMarqueeRepeatLimit);
            mDividerSize = a.getDimension(R.styleable.HorizontalPicker_dividerSize, mDividerSize);
            sideItems = a.getInt(R.styleable.HorizontalPicker_sideItems, sideItems);

            float textSize = a.getDimension(R.styleable.HorizontalPicker_android_textSize, -1);
            if(textSize > -1) {
                setTextSize(textSize);
            }
        } finally {
            a.recycle();
        }

        switch (ellipsize) {
            case 1:
                setEllipsize(TextUtils.TruncateAt.START);
                break;
            case 2:
                setEllipsize(TextUtils.TruncateAt.MIDDLE);
                break;
            case 3:
                setEllipsize(TextUtils.TruncateAt.END);
                break;
            case 4:
                setEllipsize(TextUtils.TruncateAt.MARQUEE);
                break;
        }

        Paint.FontMetricsInt fontMetricsInt = mTextPaint.getFontMetricsInt();
        mBoringMetrics = new BoringLayout.Metrics();
        mBoringMetrics.ascent = fontMetricsInt.ascent;
        mBoringMetrics.bottom = fontMetricsInt.bottom;
        mBoringMetrics.descent = fontMetricsInt.descent;
        mBoringMetrics.leading = fontMetricsInt.leading;
        mBoringMetrics.top = fontMetricsInt.top;
        mBoringMetrics.width = mItemWidth;

        setWillNotDraw(false);

        mFlingScrollerX = new OverScroller(context);
        mAdjustScrollerX = new OverScroller(context, new DecelerateInterpolator(2.5f));

        // initialize constants
        ViewConfiguration configuration = ViewConfiguration.get(context);
        mTouchSlop = configuration.getScaledTouchSlop();
        mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity();
        mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity()
                / SELECTOR_MAX_FLING_VELOCITY_ADJUSTMENT;
        mOverscrollDistance = configuration.getScaledOverscrollDistance();

        mPreviousScrollerX = Integer.MIN_VALUE;

        setValues(values);
        setSideItems(sideItems);

        mTouchHelper = new PickerTouchHelper(this);
        ViewCompat.setAccessibilityDelegate(this, mTouchHelper);

    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int height;
        if(heightMode == MeasureSpec.EXACTLY) {
            height = heightSize;
        } else {
            Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics();
            int heightText = (int) (Math.abs(fontMetrics.ascent) + Math.abs(fontMetrics.descent));
            heightText += getPaddingTop() + getPaddingBottom();

            height = Math.min(heightSize, heightText);
        }

        setMeasuredDimension(width, height);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        int saveCount = canvas.getSaveCount();
        canvas.save();

        int selectedItem = mSelectedItem;

        float itemWithPadding = mItemWidth + mDividerSize;

        // translate horizontal to center
        canvas.translate(itemWithPadding * mSideItems, 0);

        for (int i = 0; i < mValues.length; i++) {

            // set text color for item
            mTextPaint.setColor(getTextColor(i));

            // get text layout
            BoringLayout layout = mLayouts[i];

            int saveCountHeight = canvas.getSaveCount();
            canvas.save();

            float x = 0;

            float lineWidth = layout.getLineWidth(0);
            if (lineWidth > mItemWidth) {
                if (isRtl(mValues[i])) {
                    x += (lineWidth - mItemWidth) / 2;
                } else {
                    x -= (lineWidth - mItemWidth) / 2;
                }
            }

            if (mMarquee != null && i == selectedItem) {
                x += mMarquee.getScroll();
            }

            // translate vertically to center
            canvas.translate(-x, (canvas.getHeight() - layout.getHeight()) / 2);

            RectF clipBounds;
            if (x == 0) {
                clipBounds = mItemClipBounds;
            } else {
                clipBounds = mItemClipBoundsOffser;
                clipBounds.set(mItemClipBounds);
                clipBounds.offset(x, 0);
            }

            canvas.clipRect(clipBounds);
            layout.draw(canvas);

            if (mMarquee != null && i == selectedItem && mMarquee.shouldDrawGhost()) {
                canvas.translate(mMarquee.getGhostOffset(), 0);
                layout.draw(canvas);
            }

            // restore vertical translation
            canvas.restoreToCount(saveCountHeight);

            // translate horizontal for 1 item
            canvas.translate(itemWithPadding, 0);
        }

        // restore horizontal translation
        canvas.restoreToCount(saveCount);

        drawEdgeEffect(canvas, mLeftEdgeEffect, 270);
        drawEdgeEffect(canvas, mRightEdgeEffect, 90);
    }

    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
    @Override
    public void onRtlPropertiesChanged(int layoutDirection) {
        super.onRtlPropertiesChanged(layoutDirection);

        mTextDir = getTextDirectionHeuristic();
    }

    /**
     * TODO cache values
     * @param text
     * @return
     */
    private boolean isRtl(CharSequence text) {
        if (mTextDir == null) {
            mTextDir = getTextDirectionHeuristic();
        }

        return mTextDir.isRtl(text, 0, text.length());
    }

    private TextDirectionHeuristicCompat getTextDirectionHeuristic() {

        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {

            return TextDirectionHeuristicsCompat.FIRSTSTRONG_LTR;

        } else {

            // Always need to resolve layout direction first
            final boolean defaultIsRtl = (getLayoutDirection() == LAYOUT_DIRECTION_RTL);

            switch (getTextDirection()) {
                default:
                case TEXT_DIRECTION_FIRST_STRONG:
                    return (defaultIsRtl ? TextDirectionHeuristicsCompat.FIRSTSTRONG_RTL :
                            TextDirectionHeuristicsCompat.FIRSTSTRONG_LTR);
                case TEXT_DIRECTION_ANY_RTL:
                    return TextDirectionHeuristicsCompat.ANYRTL_LTR;
                case TEXT_DIRECTION_LTR:
                    return TextDirectionHeuristicsCompat.LTR;
                case TEXT_DIRECTION_RTL:
                    return TextDirectionHeuristicsCompat.RTL;
                case TEXT_DIRECTION_LOCALE:
                    return TextDirectionHeuristicsCompat.LOCALE;
            }
        }
    }

    private void remakeLayout() {

        if (mLayouts != null && mLayouts.length > 0 && getWidth() > 0)  {
            for (int i = 0; i < mLayouts.length; i++) {
                mLayouts[i].replaceOrMake(mValues[i], mTextPaint, mItemWidth,
                        Layout.Alignment.ALIGN_CENTER, 1f, 1f, mBoringMetrics, false, mEllipsize,
                        mItemWidth);
            }
        }

    }

    private void drawEdgeEffect(Canvas canvas, EdgeEffect edgeEffect, int degrees) {

        if (canvas == null || edgeEffect == null || (degrees != 90 && degrees != 270)) {
            return;
        }

        if(!edgeEffect.isFinished()) {
            final int restoreCount = canvas.getSaveCount();
            final int width = getWidth();
            final int height = getHeight() - getPaddingTop() - getPaddingBottom();

            canvas.rotate(degrees);

            if (degrees == 270) {
                canvas.translate(-height + getPaddingTop(), Math.max(0, getScrollX()));
            } else { // 90
                canvas.translate(-getPaddingTop(), -(Math.max(getScrollRange(), getScaleX()) + width));
            }

            edgeEffect.setSize(height, width);
            if(edgeEffect.draw(canvas)) {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                    postInvalidateOnAnimation();
                } else {
                    postInvalidate();
                }
            }

            canvas.restoreToCount(restoreCount);
        }

    }

    /**
     * Calculates text color for specified item based on its position and state.
     *
     * @param item Index of item to get text color for
     * @return Item text color
     */
    private int getTextColor(int item) {

        int scrollX = getScrollX();

        // set color of text
        int color = mTextColor.getDefaultColor();
        int itemWithPadding = (int) (mItemWidth + mDividerSize);
        if (scrollX > itemWithPadding * item - itemWithPadding / 2 &&
                scrollX < itemWithPadding * (item + 1) - itemWithPadding / 2) {
            int position = scrollX - itemWithPadding / 2;
            color = getColor(position, item);
        } else if(item == mPressedItem) {
            color = mTextColor.getColorForState(new int[] { android.R.attr.state_pressed }, color);
        }

        return color;

    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

        calculateItemSize(w, h);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if(!isEnabled()) {
            return false;
        }

        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(event);

        int action = event.getActionMasked();
        switch (action) {
            case MotionEvent.ACTION_MOVE:

                float currentMoveX = event.getX();

                int deltaMoveX = (int) (mLastDownEventX - currentMoveX);

                if(mScrollingX || (Math.abs(deltaMoveX) > mTouchSlop)) {

                    if(!mScrollingX) {
                        deltaMoveX = 0;
                        mPressedItem = -1;
                        mScrollingX = true;
                        stopMarqueeIfNeeded();
                    }

                    final int range = getScrollRange();

                    if(overScrollBy(deltaMoveX, 0, getScrollX(), 0, range, 0,
                            mOverscrollDistance, 0, true)) {
                        mVelocityTracker.clear();
                    }

                    final float pulledToX = getScrollX() + deltaMoveX;
                    if(pulledToX < 0) {
                        mLeftEdgeEffect.onPull((float) deltaMoveX / getWidth());
                        if(!mRightEdgeEffect.isFinished()) {
                            mRightEdgeEffect.onRelease();
                        }
                    } else if(pulledToX > range) {
                        mRightEdgeEffect.onPull((float) deltaMoveX / getWidth());
                        if(!mLeftEdgeEffect.isFinished()) {
                            mLeftEdgeEffect.onRelease();
                        }
                    }

                    mLastDownEventX = currentMoveX;
                    invalidate();

                }

                break;
            case MotionEvent.ACTION_DOWN:

                if(!mAdjustScrollerX.isFinished()) {
                    mAdjustScrollerX.forceFinished(true);
                } else if(!mFlingScrollerX.isFinished()) {
                    mFlingScrollerX.forceFinished(true);
                } else {
                    mScrollingX = false;
                }

                mLastDownEventX = event.getX();

                if(!mScrollingX) {
                    mPressedItem = getPositionFromTouch(event.getX());
                }
                invalidate();

                break;
            case MotionEvent.ACTION_UP:

                VelocityTracker velocityTracker = mVelocityTracker;
                velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
                int initialVelocityX = (int) velocityTracker.getXVelocity();

                if(mScrollingX && Math.abs(initialVelocityX) > mMinimumFlingVelocity) {
                    flingX(initialVelocityX);
                } else {
                    float positionX = event.getX();
                    if(!mScrollingX) {

                        int itemPos = getPositionOnScreen(positionX);
                        int relativePos = itemPos - mSideItems;

                        if (relativePos == 0) {
                            selectItem();
                        } else {
                            smoothScrollBy(relativePos);
                        }

                    } else if(mScrollingX) {
                        finishScrolling();
                    }
                }

                mVelocityTracker.recycle();
                mVelocityTracker = null;

                if(mLeftEdgeEffect != null) {
                    mLeftEdgeEffect.onRelease();
                    mRightEdgeEffect.onRelease();
                }

            case MotionEvent.ACTION_CANCEL:
                mPressedItem = -1;
                invalidate();

                if(mLeftEdgeEffect != null) {
                    mLeftEdgeEffect.onRelease();
                    mRightEdgeEffect.onRelease();
                }

                break;
        }

        return true;
    }

    private void selectItem() {

        if (mOnItemClicked != null) {
            mOnItemClicked.onItemClicked(getSelectedItem());
        }

        adjustToNearestItemX();
    }

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {

        if (!isEnabled()) {
            return super.onKeyDown(keyCode, event);
        }

        switch (keyCode) {
            case KeyEvent.KEYCODE_DPAD_CENTER:
            case KeyEvent.KEYCODE_ENTER:
                selectItem();
                return true;
            case KeyEvent.KEYCODE_DPAD_LEFT:
                smoothScrollBy(-1);
                return true;
            case KeyEvent.KEYCODE_DPAD_RIGHT:
                smoothScrollBy(1);
                return true;
            default:
                return super.onKeyDown(keyCode, event);
        }

    }

    @Override
    protected boolean dispatchHoverEvent(MotionEvent event) {

        if (mTouchHelper.dispatchHoverEvent(event)) {
            return true;
        }

        return super.dispatchHoverEvent(event);
    }

    @Override
    public void computeScroll() {
        computeScrollX();
    }

    @Override
    public void getFocusedRect(Rect r) {
        super.getFocusedRect(r); // TODO this should only be current item
    }

    public void setOnItemSelectedListener(OnItemSelected onItemSelected) {
        mOnItemSelected = onItemSelected;
    }

    public void setOnItemClickedListener(OnItemClicked onItemClicked) {
        mOnItemClicked = onItemClicked;
    }

    public int getSelectedItem() {
        int x = getScrollX();
        return getPositionFromCoordinates(x);
    }

    public void setSelectedItem(int index) {
        mSelectedItem = index;
        scrollToItem(index);
    }

    public int getMarqueeRepeatLimit() {
        return mMarqueeRepeatLimit;
    }

    public void setMarqueeRepeatLimit(int marqueeRepeatLimit) {
        mMarqueeRepeatLimit = marqueeRepeatLimit;
    }

    /**
     * @return Number of items on each side of current item.
     */
    public int getSideItems() {
        return mSideItems;
    }

    public void setSideItems(int sideItems) {
        if (mSideItems < 0) {
            throw new IllegalArgumentException("Number of items on each side must be grater or equal to 0.");
        } else if (mSideItems != sideItems) {
            mSideItems = sideItems;
            calculateItemSize(getWidth(), getHeight());
        }
    }

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

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

    /**
     * @return
     */
    public CharSequence[] getValues() {
        return mValues;
    }

    /**
     * Sets values to choose from
     * @param values New values to choose from
     */
    public void setValues(CharSequence[] values) {

        if (mValues != values) {
            mValues = values;

            if (mValues == null) {
                mValues = new CharSequence[0];
            }

            mLayouts = new BoringLayout[mValues.length];
            for (int i = 0; i < mLayouts.length; i++) {
                mLayouts[i] = new BoringLayout(mValues[i], mTextPaint, mItemWidth, Layout.Alignment.ALIGN_CENTER,
                        1f, 1f, mBoringMetrics, false, mEllipsize, mItemWidth);
            }

            // start marque only if has already been measured
            if (getWidth() > 0) {
                startMarqueeIfNeeded();
            }

            requestLayout();
            invalidate();
        }

    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {

        if (!(state instanceof SavedState)) {
            super.onRestoreInstanceState(state);
            return;
        }

        SavedState ss = (SavedState) state;
        super.onRestoreInstanceState(ss.getSuperState());

        setSelectedItem(ss.mSelItem);


    }

    @Override
    protected Parcelable onSaveInstanceState() {
        Parcelable superState = super.onSaveInstanceState();

        SavedState savedState = new SavedState(superState);
        savedState.mSelItem = mSelectedItem;

        return savedState;

    }

    @Override
    public void setOverScrollMode(int overScrollMode) {
        if(overScrollMode != OVER_SCROLL_NEVER) {
            Context context = getContext();
            mLeftEdgeEffect = new EdgeEffect(context);
            mRightEdgeEffect = new EdgeEffect(context);
        } else {
            mLeftEdgeEffect = null;
            mRightEdgeEffect = null;
        }

        super.setOverScrollMode(overScrollMode);
    }

    public TextUtils.TruncateAt getEllipsize() {
        return mEllipsize;
    }

    public void setEllipsize(TextUtils.TruncateAt ellipsize) {
        if (mEllipsize != ellipsize) {
            mEllipsize = ellipsize;

            remakeLayout();
            invalidate();
        }
    }

    @Override
    protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
        super.scrollTo(scrollX, scrollY);

        if(!mFlingScrollerX.isFinished() && clampedX) {
            mFlingScrollerX.springBack(scrollX, scrollY, 0, getScrollRange(), 0, 0);
        }
    }

    @Override
    protected void drawableStateChanged() {
        super.drawableStateChanged(); //TODO
    }

    private int getPositionFromTouch(float x) {
        return getPositionFromCoordinates((int) (getScrollX() - (mItemWidth + mDividerSize) * (mSideItems + .5f) + x));
    }

    private void computeScrollX() {
        OverScroller scroller = mFlingScrollerX;
        if(scroller.isFinished()) {
            scroller = mAdjustScrollerX;
            if(scroller.isFinished()) {
                return;
            }
        }

        if(scroller.computeScrollOffset()) {

            int currentScrollerX = scroller.getCurrX();
            if(mPreviousScrollerX == Integer.MIN_VALUE) {
                mPreviousScrollerX = scroller.getStartX();
            }

            int range = getScrollRange();
            if(mPreviousScrollerX >= 0 && currentScrollerX < 0) {
                mLeftEdgeEffect.onAbsorb((int) scroller.getCurrVelocity());
            } else if(mPreviousScrollerX <= range && currentScrollerX > range) {
                mRightEdgeEffect.onAbsorb((int) scroller.getCurrVelocity());
            }

            overScrollBy(currentScrollerX - mPreviousScrollerX, 0, mPreviousScrollerX, getScrollY(),
                    getScrollRange(), 0, mOverscrollDistance, 0, false);
            mPreviousScrollerX = currentScrollerX;

            if(scroller.isFinished()) {
                onScrollerFinishedX(scroller);
            }

            postInvalidate();
//            postInvalidateOnAnimation(); // TODO
        }
    }

    private void flingX(int velocityX) {

        mPreviousScrollerX = Integer.MIN_VALUE;
        mFlingScrollerX.fling(getScrollX(), getScrollY(), -velocityX, 0, 0,
                (int) (mItemWidth + mDividerSize) * (mValues.length - 1), 0, 0, getWidth() / 2, 0);

        invalidate();
    }

    private void adjustToNearestItemX() {

        int x = getScrollX();
        int item = Math.round(x / (mItemWidth + mDividerSize * 1f));

        if(item < 0) {
            item = 0;
        } else if(item > mValues.length) {
            item = mValues.length;
        }

        mSelectedItem = item;

        int itemX = (mItemWidth + (int) mDividerSize) * item;

        int deltaX = itemX - x;

        mAdjustScrollerX.startScroll(x, 0, deltaX, 0, SELECTOR_ADJUSTMENT_DURATION_MILLIS);
        invalidate();
    }

    private void calculateItemSize(int w, int h) {

        int items = mSideItems * 2 + 1;
        int totalPadding = ((int) mDividerSize * (items - 1));
        mItemWidth = (w - totalPadding) / items;

        mItemClipBounds = new RectF(0, 0, mItemWidth, h);
        mItemClipBoundsOffser = new RectF(mItemClipBounds);

        scrollToItem(mSelectedItem);

        remakeLayout();
        startMarqueeIfNeeded();

    }

    private void onScrollerFinishedX(OverScroller scroller) {
        if(scroller == mFlingScrollerX) {
            finishScrolling();
        }
    }

    private void finishScrolling() {

        adjustToNearestItemX();
        mScrollingX = false;

        if (mOnItemSelected != null) {
            mOnItemSelected.onItemSelected(getPositionFromCoordinates(getScrollX()));
        }

        startMarqueeIfNeeded();
    }

    private void startMarqueeIfNeeded() {

        stopMarqueeIfNeeded();

        int item = getSelectedItem();
        Layout layout = mLayouts[item];
        if (mEllipsize == TextUtils.TruncateAt.MARQUEE
                && mItemWidth < layout.getLineWidth(0)) {
            mMarquee = new Marquee(this, layout, isRtl(mValues[item]));
            mMarquee.start(mMarqueeRepeatLimit);
        }

    }

    private void stopMarqueeIfNeeded() {

        if (mMarquee != null) {
            mMarquee.stop();
            mMarquee = null;
        }

    }

    private int getPositionOnScreen(float x) {
        return (int) (x / (mItemWidth + mDividerSize));
    }

    private void smoothScrollBy(int i) {
        int deltaMoveX = (mItemWidth + (int) mDividerSize) * i;
        deltaMoveX = getRelativeInBound(deltaMoveX);

        mFlingScrollerX.startScroll(getScrollX(), 0, deltaMoveX, 0);
        stopMarqueeIfNeeded();
        invalidate();
    }

    /**
     * Calculates color for specific position on time picker
     * @param scrollX
     * @return
     */
    private int getColor(int scrollX, int position) {
        int itemWithPadding = (int) (mItemWidth + mDividerSize);
        float proportion = Math.abs(((1f * scrollX % itemWithPadding) / 2) / (itemWithPadding / 2f));
        if(proportion > .5) {
            proportion = (proportion - .5f);
        } else {
            proportion = .5f - proportion;
        }
        proportion *= 2;

        int defaultColor;
        int selectedColor;

        if(mPressedItem == position) {
            defaultColor = mTextColor.getColorForState(new int[] { android.R.attr.state_pressed }, mTextColor.getDefaultColor());
            selectedColor = mTextColor.getColorForState(new int[] { android.R.attr.state_pressed, android.R.attr.state_selected }, defaultColor);
        } else {
            defaultColor = mTextColor.getDefaultColor();
            selectedColor = mTextColor.getColorForState(new int[] { android.R.attr.state_selected }, defaultColor);
        }
        return (Integer) new ArgbEvaluator().evaluate(proportion, selectedColor, defaultColor);
    }

    /**
     * Sets text size for items
     * @param size New item text size in px.
     */
    private void setTextSize(float size) {
        if(size != mTextPaint.getTextSize()) {
            mTextPaint.setTextSize(size);

            requestLayout();
            invalidate();
        }
    }

    /**
     * Calculates item from x coordinate position.
     * @param x Scroll position to calculate.
     * @return Selected item from scrolling position in {param x}
     */
    private int getPositionFromCoordinates(int x) {
        return Math.round(x / (mItemWidth + mDividerSize));
    }

    /**
     * Scrolls to specified item.
     * @param index Index of an item to scroll to
     */
    private void scrollToItem(int index) {
        scrollTo((mItemWidth + (int) mDividerSize) * index, 0);
        invalidate();
    }

    /**
     * Calculates relative horizontal scroll position to be within our scroll bounds.
     * {@link com.wefika.horizontalpicker.HorizontalPicker#getInBoundsX(int)}
     * @param x Relative scroll position to calculate
     * @return Current scroll position + {param x} if is within our scroll bounds, otherwise it
     * will return min/max scroll position.
     */
    private int getRelativeInBound(int x) {
        int scrollX = getScrollX();
        return getInBoundsX(scrollX + x) - scrollX;
    }

    /**
     * Calculates x scroll position that is still in range of view scroller
     * @param x Scroll position to calculate.
     * @return {param x} if is within bounds of over scroller, otherwise it will return min/max
     * value of scoll position.
     */
    private int getInBoundsX(int x) {

        if(x < 0) {
            x = 0;
        } else if(x > ((mItemWidth + (int) mDividerSize) * (mValues.length - 1))) {
            x = ((mItemWidth + (int) mDividerSize) * (mValues.length - 1));
        }
        return x;
    }

    private int getScrollRange() {
        int scrollRange = 0;
        if(mValues != null && mValues.length != 0) {
            scrollRange = Math.max(0, ((mItemWidth + (int) mDividerSize) * (mValues.length - 1)));
        }
        return scrollRange;
    }

    public interface OnItemSelected {

        public void onItemSelected(int index);

    }

    public interface OnItemClicked {

        public void onItemClicked(int index);

    }

    private static final class Marquee extends Handler {
        // TODO: Add an option to configure this
        private static final float MARQUEE_DELTA_MAX = 0.07f;
        private static final int MARQUEE_DELAY = 1200;
        private static final int MARQUEE_RESTART_DELAY = 1200;
        private static final int MARQUEE_RESOLUTION = 1000 / 30;
        private static final int MARQUEE_PIXELS_PER_SECOND = 30;

        private static final byte MARQUEE_STOPPED = 0x0;
        private static final byte MARQUEE_STARTING = 0x1;
        private static final byte MARQUEE_RUNNING = 0x2;

        private static final int MESSAGE_START = 0x1;
        private static final int MESSAGE_TICK = 0x2;
        private static final int MESSAGE_RESTART = 0x3;

        private final WeakReference<HorizontalPicker> mView;
        private final WeakReference<Layout> mLayout;

        private byte mStatus = MARQUEE_STOPPED;
        private final float mScrollUnit;
        private float mMaxScroll;
        private float mMaxFadeScroll;
        private float mGhostStart;
        private float mGhostOffset;
        private float mFadeStop;
        private int mRepeatLimit;

        private float mScroll;

        private boolean mRtl;

        Marquee(HorizontalPicker v, Layout l, boolean rtl) {
            final float density = v.getContext().getResources().getDisplayMetrics().density;
            float scrollUnit = (MARQUEE_PIXELS_PER_SECOND * density) / MARQUEE_RESOLUTION;
            if (rtl) {
                mScrollUnit = -scrollUnit;
            } else {
                mScrollUnit = scrollUnit;
            }

            mView = new WeakReference<HorizontalPicker>(v);
            mLayout = new WeakReference<Layout>(l);
            mRtl = rtl;
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MESSAGE_START:
                    mStatus = MARQUEE_RUNNING;
                    tick();
                    break;
                case MESSAGE_TICK:
                    tick();
                    break;
                case MESSAGE_RESTART:
                    if (mStatus == MARQUEE_RUNNING) {
                        if (mRepeatLimit >= 0) {
                            mRepeatLimit--;
                        }
                        start(mRepeatLimit);
                    }
                    break;
            }
        }

        void tick() {
            if (mStatus != MARQUEE_RUNNING) {
                return;
            }

            removeMessages(MESSAGE_TICK);

            final HorizontalPicker view = mView.get();
            final Layout layout = mLayout.get();
            if (view != null && layout != null && (view.isFocused() || view.isSelected())) {
                mScroll += mScrollUnit;
                if (Math.abs(mScroll) > mMaxScroll) {
                    mScroll = mMaxScroll;
                    if (mRtl) {
                        mScroll *= -1;
                    }
                    sendEmptyMessageDelayed(MESSAGE_RESTART, MARQUEE_RESTART_DELAY);
                } else {
                    sendEmptyMessageDelayed(MESSAGE_TICK, MARQUEE_RESOLUTION);
                }
                view.invalidate();
            }
        }

        void stop() {
            mStatus = MARQUEE_STOPPED;
            removeMessages(MESSAGE_START);
            removeMessages(MESSAGE_RESTART);
            removeMessages(MESSAGE_TICK);
            resetScroll();
        }

        private void resetScroll() {
            mScroll = 0.0f;
            final HorizontalPicker view = mView.get();
            if (view != null) view.invalidate();
        }

        void start(int repeatLimit) {
            if (repeatLimit == 0) {
                stop();
                return;
            }
            mRepeatLimit = repeatLimit;
            final HorizontalPicker view = mView.get();
            final Layout layout = mLayout.get();
            if (view != null && layout != null) {
                mStatus = MARQUEE_STARTING;
                mScroll = 0.0f;
                final int textWidth = view.mItemWidth;
                final float lineWidth = layout.getLineWidth(0);
                final float gap = textWidth / 3.0f;
                mGhostStart = lineWidth - textWidth + gap;
                mMaxScroll = mGhostStart + textWidth;
                mGhostOffset = lineWidth + gap;
                mFadeStop = lineWidth + textWidth / 6.0f;
                mMaxFadeScroll = mGhostStart + lineWidth + lineWidth;

                if (mRtl) {
                    mGhostOffset *= -1;
                }

                view.invalidate();
                sendEmptyMessageDelayed(MESSAGE_START, MARQUEE_DELAY);
            }
        }

        float getGhostOffset() {
            return mGhostOffset;
        }

        float getScroll() {
            return mScroll;
        }

        float getMaxFadeScroll() {
            return mMaxFadeScroll;
        }

        boolean shouldDrawLeftFade() {
            return mScroll <= mFadeStop;
        }

        boolean shouldDrawGhost() {
            return mStatus == MARQUEE_RUNNING && Math.abs(mScroll) > mGhostStart;
        }

        boolean isRunning() {
            return mStatus == MARQUEE_RUNNING;
        }

        boolean isStopped() {
            return mStatus == MARQUEE_STOPPED;
        }
    }

    public static class SavedState extends BaseSavedState {

        private int mSelItem;

        public SavedState(Parcelable superState) {
            super(superState);
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            super.writeToParcel(dest, flags);

            dest.writeInt(mSelItem);
        }

        @Override
        public String toString() {
            return  "HorizontalPicker.SavedState{"
                    + Integer.toHexString(System.identityHashCode(this))
                    + " selItem=" + mSelItem
                    + "}";
        }
    }

    private static class PickerTouchHelper extends ExploreByTouchHelper {

        private HorizontalPicker mPicker;

        public PickerTouchHelper(HorizontalPicker picker) {
            super(picker);
            mPicker = picker;
        }

        @Override
        protected int getVirtualViewAt(float x, float y) {

            float itemWidth = mPicker.mItemWidth + mPicker.mDividerSize;
            float position = mPicker.getScrollX() + x - itemWidth * mPicker.mSideItems;

            float item = position / itemWidth;

            if (item < 0 || item > mPicker.mValues.length) {
                return INVALID_ID;
            }

            return (int) item;

        }

        @Override
        protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {

            float itemWidth = mPicker.mItemWidth + mPicker.mDividerSize;
            float position = mPicker.getScrollX() - itemWidth * mPicker.mSideItems;

            int first = (int) (position / itemWidth);

            int items = mPicker.mSideItems * 2 + 1;

            if (position % itemWidth != 0) { // if start next item is starting to appear on screen
                items++;
            }

            if (first < 0) {
                items += first;
                first = 0;
            } else if (first + items > mPicker.mValues.length) {
                items = mPicker.mValues.length - first;
            }

            for (int i = 0; i < items; i++) {
                virtualViewIds.add(first + i);
            }

        }

        @Override
        protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
            event.setContentDescription(mPicker.mValues[virtualViewId]);
        }

        @Override
        protected void onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfoCompat node) {

            float itemWidth = mPicker.mItemWidth + mPicker.mDividerSize;
            float scrollOffset = mPicker.getScrollX() - itemWidth * mPicker.mSideItems;

            int left = (int) (virtualViewId * itemWidth - scrollOffset);
            int right = left + mPicker.mItemWidth;

            node.setContentDescription(mPicker.mValues[virtualViewId]);
            node.setBoundsInParent(new Rect(left, 0, right, mPicker.getHeight()));
            node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);

        }

        @Override
        protected boolean onPerformActionForVirtualView(int virtualViewId, int action, Bundle arguments) {
            return false;
        }

    }

}




Java Source Code List

com.wefika.horizontalpicker.HorizontalPicker.java
com.wefika.horizontalpicker.example.MainActivity.java