com.albedinsky.android.ui.widget.SeekBarWidget.java Source code

Java tutorial

Introduction

Here is the source code for com.albedinsky.android.ui.widget.SeekBarWidget.java

Source

/*
 * =================================================================================================
 *                             Copyright (C) 2014 Martin Albedinsky
 * =================================================================================================
 *         Licensed under the Apache License, Version 2.0 or later (further "License" only).
 * -------------------------------------------------------------------------------------------------
 * You may use this file only in compliance with the License. More details and copy of this License 
 * you may obtain at
 * 
 *       http://www.apache.org/licenses/LICENSE-2.0
 * 
 * You can redistribute, modify or publish any part of the code written within this file but as it 
 * is described in the License, the software distributed under the License is distributed on an 
 * "AS IS" BASIS, WITHOUT WARRANTIES or CONDITIONS OF ANY KIND.
 * 
 * See the License for the specific language governing permissions and limitations under the License.
 * =================================================================================================
 */
package com.albedinsky.android.ui.widget;

import android.animation.ObjectAnimator;
import android.annotation.SuppressLint;
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.PorterDuff;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.graphics.drawable.RippleDrawable;
import android.graphics.drawable.ScaleDrawable;
import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.ColorInt;
import android.support.annotation.DrawableRes;
import android.support.annotation.FloatRange;
import android.support.annotation.Keep;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.StyleRes;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.animation.Animation;
import android.view.animation.Transformation;
import android.widget.SeekBar;

import com.albedinsky.android.ui.R;
import com.albedinsky.android.ui.UiConfig;
import com.albedinsky.android.ui.graphics.drawable.TintDrawable;
import com.albedinsky.android.ui.graphics.drawable.TintLayerDrawable;
import com.albedinsky.android.ui.util.ResourceUtils;

/**
 * Extended version of {@link android.widget.SeekBar}. This updated SeekBar supports tinting for the
 * Android versions below {@link android.os.Build.VERSION_CODES#LOLLIPOP LOLLIPOP} and other useful
 * features described below including <b>discrete</b> mode that should be enabled whenever a particular
 * SeekBarWidget represents setting for which a user needs to know the exact value of the setting.
 *
 * <h3>Tinting</h3>
 * <b>Note, that in the current version of SeekBarWidget, tinting for thumb and track below LOLLIPOP
 * is somehow not working properly on all supported Android versions. This issue is scheduled to be
 * resolved in the feature.</b>
 * <p>
 * Tinting is supported via Xml attributes listed below:
 * <ul>
 * <li>{@link R.attr#uiThumbTint uiThumbTint}</li>
 * <li>{@link R.attr#uiThumbTintMode uiThumbTintMode}</li>
 * <li>{@link R.attr#uiProgressTint uiProgressTint}</li>
 * <li>{@link R.attr#uiProgressTintMode uiProgressTintMode}</li>
 * <li>{@link R.attr#uiDiscreteIndicatorTint uiDiscreteIndicatorTint}</li>
 * <li>{@link R.attr#uiDiscreteIndicatorTintMode uiDiscreteIndicatorTintMode}</li>
 * </ul>
 * <p>
 * <b>Note, that on {@link android.os.Build.VERSION_CODES#LOLLIPOP LOLLIPOP} and above SDK versions
 * can be also used Xml attributes listed above where in such case will be used the native tinting.</b>
 * <p>
 * This widget also overrides all SDK methods used to tint its components like {@link #setThumbTintList(android.content.res.ColorStateList)}
 * or {@link #setThumbTintMode(android.graphics.PorterDuff.Mode)}, so these can be used regardless
 * the current version of SDK but invoking of these methods below {@link android.os.Build.VERSION_CODES#LOLLIPOP LOLLIPOP}
 * can be done only directly upon instance of this widget otherwise {@link NoSuchMethodException}
 * will be thrown.
 *
 * <h3>Discrete mode</h3>
 * SeekBarWidget with the <b>discrete mode</b> enabled via {@link #setDiscrete(boolean)} will draw
 * above its progress track and thumb discrete indicator to show to a user current progress value.
 * There will be also drawn tick marks for discrete interval of which range can be specified via
 * {@link #setDiscreteIntervalRatio(float)}. These tick marks should serve as a clue for user so
 * he/she will know more precisely where to drag the thumb to pick its desired progress value.
 * <p>
 * <b>Simple view hierarchy model:</b>
 * <pre>
 *        __
 *       |28|
 *        \/
 * .---.--O---.---.---.---.---.
 *
 * </pre>
 *
 * <h3>Sliding</h3>
 * This updated view allows updating of its current position along <b>x</b> and <b>y</b> axis by
 * changing <b>fraction</b> of these properties depending on its current size using the new animation
 * framework introduced in {@link android.os.Build.VERSION_CODES#HONEYCOMB HONEYCOMB} via
 * {@link android.animation.ObjectAnimator ObjectAnimator}s API.
 * <p>
 * Changing of fraction of X or Y is supported via these two methods:
 * <ul>
 * <li>{@link #setFractionX(float)}</li>
 * <li>{@link #setFractionY(float)}</li>
 * </ul>
 * <p>
 * For example if an instance of this view class needs to be slided to the right by its whole width,
 * an Xml file with ObjectAnimator would look like this:
 * <pre>
 *  &lt;objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
 *                  android:propertyName="fractionX"
 *                  android:valueFrom="0.0"
 *                  android:valueTo="1.0"
 *                  android:duration="300"/&gt;
 * </pre>
 *
 * <h3>XML attributes</h3>
 * See {@link SeekBar},
 * {@link R.styleable#Ui_SeekBar SeekBarWidget Attributes}
 *
 * <h3>Default style attribute</h3>
 * {@link android.R.attr#seekBarStyle android:seekBarStyle}
 *
 * @author Martin Albedinsky
 */
public class SeekBarWidget extends SeekBar implements Widget, FontWidget {

    /**
     * Interface ===================================================================================
     */

    /**
     * Constants ===================================================================================
     */

    /**
     * Log TAG.
     */
    // private static final String TAG = "SeekBarWidget";

    /**
     * Duration for how long will be discrete components previewed (visible) after this SeekBar has
     * been attached to window.
     */
    private static final long PREVIEW_DISCRETE_COMPONENTS_DURATION = 2000;

    /**
     * Maximum level value that can be applied to this seek bar as progress value.
     */
    private static int MAX_LEVEL = 10000;

    /**
     * Boolean flag indicating whether we can draw discrete interval at thumb position or not.
     */
    private static final boolean CAN_DRAW_DISCRETE_INTERVAL_OVER_THUMB = !UiConfig.MATERIALIZED;

    /**
     * Flag indicating whether this seek bar is discrete or not.
     */
    private static final int PFLAG_DISCRETE = 0x00000001 << 16;

    /**
     * Flag indicating whether this seek bar should indicate to a user that it is discrete or not.
     */
    private static final int PFLAG_DISCRETE_PREVIEW_ENABLED = 0x00000001 << 17;

    /**
     * Static members ==============================================================================
     */

    /**
     * Members =====================================================================================
     */

    /**
     * Graphics info for discrete indicator's text.
     */
    private final DiscreteIndicatorTextInfo DISCRETE_INDICATOR_TEXT_INFO = new DiscreteIndicatorTextInfo();

    /**
     * Graphics info for discrete interval tick marks.
     */
    private final DiscreteIntervalTickMarkInfo DISCRETE_INTERVAL_TICK_MARK_INFO = new DiscreteIntervalTickMarkInfo();

    /**
     * Decorator used to extend API of this widget by functionality otherwise not supported or not
     * available due to current API level.
     */
    private Decorator mDecorator;

    /**
     * Drawable used to draw thumb of this seek bar.
     */
    private Drawable mThumb;

    /**
     * Drawable used to draw discrete indicator of this seek bar.
     */
    private Drawable mDiscreteIndicator;

    /**
     * Resource id of the {@link #mDiscreteIndicator} if indicator has been specified via
     * {@link #setDiscreteIndicator(int)}.
     */
    private int mDiscreteIndicatorRes;

    /**
     * Drawable used to draw progress of this seek bar.
     */
    private Drawable mProgressDrawable;

    /**
     * Ratio used to compute count of tick marks of the discrete interval.
     */
    private float mDiscreteIntervalRatio = 0.2f;

    /**
     * Dimension size of the discrete indicator's drawable.
     */
    private int mDiscreteIndicatorWidth, mDiscreteIndicatorHeight;

    /**
     * Current progress set to this seek bar by {@link #setProgress(int)}.
     */
    private int mProgress;

    /**
     * Animations interface used to hide implementation details of animations performed upon this view.
     */
    private Animations mAnimations;

    /**
     * Helper rect used when we need to obtain bounds or padding of some component.
     */
    private Rect mRect;

    /**
     * Ripple background drawable that has been detached in discrete mode if not {@code null}. Should
     * be re-attached when discrete mode is disabled.
     */
    private Drawable mRippleBackgroundDrawable;

    /**
     * Constructors ================================================================================
     */

    /**
     * Same as {@link #SeekBarWidget(android.content.Context, android.util.AttributeSet)} without
     * attributes.
     */
    public SeekBarWidget(Context context) {
        this(context, null);
    }

    /**
     * Same as {@link #SeekBarWidget(android.content.Context, android.util.AttributeSet, int)} with
     * {@link android.R.attr#seekBarStyle} as attribute for default style.
     */
    public SeekBarWidget(Context context, AttributeSet attrs) {
        this(context, attrs, android.R.attr.seekBarStyle);
    }

    /**
     * Same as {@link #SeekBarWidget(android.content.Context, android.util.AttributeSet, int, int)}
     * with {@code 0} as default style.
     */
    public SeekBarWidget(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.init(context, attrs, defStyleAttr, 0);
    }

    /**
     * Creates a new instance of SeekBarWidget within the given <var>context</var>.
     *
     * @param context      Context in which will be the new view presented.
     * @param attrs        Set of Xml attributes used to configure the new instance of this view.
     * @param defStyleAttr An attribute which contains a reference to a default style resource for
     *                     this view within a theme of the given context.
     * @param defStyleRes  Resource id of the default style for the new view.
     */
    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public SeekBarWidget(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        this.init(context, attrs, defStyleAttr, defStyleRes);
    }

    /**
     * Methods =====================================================================================
     */

    /**
     * Called from one of constructors of this view to perform its initialization.
     * <p>
     * Initialization is done via parsing of the specified <var>attrs</var> set and obtaining for
     * this view specific data from it that can be used to configure this new view instance. The
     * specified <var>defStyleAttr</var> and <var>defStyleRes</var> are used to obtain default data
     * from the current theme provided by the specified <var>context</var>.
     */
    @SuppressWarnings("ConstantConditions")
    private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        this.ensureRect();
        this.ensureDecorator();
        mDecorator.processAttributes(context, attrs, defStyleAttr, defStyleRes);
        this.mAnimations = Animations.get(this);

        /**
         * Process attributes.
         */
        final TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.Ui_SeekBar, defStyleAttr,
                defStyleRes);
        if (typedArray != null) {
            final Rect indicatorTextPadding = new Rect();
            final int n = typedArray.getIndexCount();
            for (int i = 0; i < n; i++) {
                final int index = typedArray.getIndex(i);
                if (index == R.styleable.Ui_SeekBar_android_enabled) {
                    setEnabled(typedArray.getBoolean(index, true));
                } else if (index == R.styleable.Ui_SeekBar_uiDiscrete) {
                    setDiscrete(typedArray.getBoolean(index, false));
                } else if (index == R.styleable.Ui_SeekBar_uiDiscretePreviewEnabled) {
                    setDiscretePreviewEnabled(typedArray.getBoolean(index, true));
                } else if (index == R.styleable.Ui_SeekBar_uiDiscreteIntervalRatio) {
                    setDiscreteIntervalRatio(typedArray.getFloat(index, mDiscreteIntervalRatio));
                } else if (index == R.styleable.Ui_SeekBar_uiDiscreteIndicator) {
                    setDiscreteIndicator(typedArray.getResourceId(index, 0));
                } else if (index == R.styleable.Ui_SeekBar_uiDiscreteIndicatorTextAppearance) {
                    setDiscreteIndicatorTextAppearance(typedArray.getResourceId(index, 0));
                } else if (index == R.styleable.Ui_SeekBar_uiDiscreteIndicatorTextGravity) {
                    setDiscreteIndicatorTextGravity(
                            typedArray.getInteger(index, DISCRETE_INDICATOR_TEXT_INFO.gravity));
                } else if (index == R.styleable.Ui_SeekBar_uiDiscreteIndicatorTextPaddingStart) {
                    indicatorTextPadding.left = typedArray.getDimensionPixelSize(index, 0);
                } else if (index == R.styleable.Ui_SeekBar_uiDiscreteIndicatorTextPaddingTop) {
                    indicatorTextPadding.top = typedArray.getDimensionPixelSize(index, 0);
                } else if (index == R.styleable.Ui_SeekBar_uiDiscreteIndicatorTextPaddingEnd) {
                    indicatorTextPadding.right = typedArray.getDimensionPixelSize(index, 0);
                } else if (index == R.styleable.Ui_SeekBar_uiDiscreteIndicatorTextPaddingBottom) {
                    indicatorTextPadding.bottom = typedArray.getDimensionPixelSize(index, 0);
                } else if (index == R.styleable.Ui_SeekBar_uiDiscreteIntervalTickMarkColor) {
                    setDiscreteIntervalTickMarkColor(typedArray.getColorStateList(index));
                } else if (index == R.styleable.Ui_SeekBar_uiDiscreteIntervalTickMarkRadius) {
                    setDiscreteIntervalTickMarkRadius(typedArray.getDimensionPixelSize(index, 0));
                }
            }
            setDiscreteIndicatorTextPadding(indicatorTextPadding.left, indicatorTextPadding.top,
                    indicatorTextPadding.right, indicatorTextPadding.bottom);
            typedArray.recycle();
        }
        this.applyProgressTints();
        this.applyThumbTint();
    }

    /**
     * Ensures that the {@link #mRect} is initialized.
     */
    private void ensureRect() {
        if (mRect == null)
            this.mRect = new Rect();
    }

    /**
     * Ensures that the decorator for this view is initialized.
     */
    private void ensureDecorator() {
        if (mDecorator == null)
            this.mDecorator = new Decorator(this);
    }

    /**
     */
    @Override
    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    public void onInitializeAccessibilityEvent(@NonNull AccessibilityEvent event) {
        super.onInitializeAccessibilityEvent(event);
        event.setClassName(SeekBarWidget.class.getName());
    }

    /**
     */
    @Override
    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    public void onInitializeAccessibilityNodeInfo(@NonNull AccessibilityNodeInfo info) {
        super.onInitializeAccessibilityNodeInfo(info);
        info.setClassName(SeekBarWidget.class.getName());
    }

    /**
     */
    @Override
    @SuppressWarnings("deprecation")
    public void setBackgroundDrawable(Drawable background) {
        super.setBackgroundDrawable(background);
        this.ensureDecorator();
        mDecorator.applyBackgroundTint();
    }

    /**
     * <b>Note, that on pre {@link android.os.Build.VERSION_CODES#LOLLIPOP LOLLIPOP} Android versions
     * this method will return an instance of {@link TintDrawable TintDrawable} if tint has been applied
     * via {@link #setBackgroundTintList(ColorStateList)}.</b>
     * <p>
     * The original wrapped drawable can be obtained via {@link TintDrawable#getDrawable()}.
     */
    @Override
    public Drawable getBackground() {
        return super.getBackground();
    }

    /**
     */
    @Override
    public void setBackgroundTintList(@Nullable ColorStateList tint) {
        this.ensureDecorator();
        mDecorator.setBackgroundTintList(tint);
    }

    /**
     */
    @Nullable
    @Override
    public ColorStateList getBackgroundTintList() {
        this.ensureDecorator();
        return mDecorator.getBackgroundTintList();
    }

    /**
     */
    @Override
    public void setBackgroundTintMode(@Nullable PorterDuff.Mode tintMode) {
        this.ensureDecorator();
        mDecorator.setBackgroundTintMode(tintMode);
    }

    /**
     */
    @Nullable
    @Override
    public PorterDuff.Mode getBackgroundTintMode() {
        this.ensureDecorator();
        return mDecorator.getBackgroundTintMode();
    }

    /**
     */
    @Override
    public void setFractionX(float fraction) {
        this.ensureDecorator();
        mDecorator.setFractionX(fraction);
    }

    /**
     */
    @Override
    public float getFractionX() {
        this.ensureDecorator();
        return mDecorator.getFractionX();
    }

    /**
     */
    @Override
    public void setFractionY(float fraction) {
        this.ensureDecorator();
        mDecorator.setFractionY(fraction);
    }

    /**
     */
    @Override
    public float getFractionY() {
        this.ensureDecorator();
        return mDecorator.getFractionY();
    }

    /**
     */
    @Override
    public void setPressed(boolean pressed) {
        final boolean isPressed = isPressed();
        super.setPressed(pressed);
        if (!isPressed && pressed)
            onPressed();
        else if (isPressed)
            onReleased();
    }

    /**
     * Invoked whenever {@link #setPressed(boolean)} is called with {@code true} and this view
     * isn't in the pressed state yet.
     */
    protected void onPressed() {
        this.revealDiscreteComponents();
    }

    /**
     * Invoked whenever {@link #setPressed(boolean)} is called with {@code false} and this view
     * is currently in the pressed state.
     */
    protected void onReleased() {
        this.concealDiscreteComponents();
    }

    /**
     */
    @Override
    public void setSelected(boolean selected) {
        this.ensureDecorator();
        mDecorator.setSelected(selected);
    }

    /**
     */
    @Override
    public void setSelectionState(boolean selected) {
        this.ensureDecorator();
        mDecorator.setSelectionState(selected);
    }

    /**
     */
    @Override
    public void setAllowDefaultSelection(boolean allow) {
        this.ensureDecorator();
        mDecorator.setAllowDefaultSelection(allow);
    }

    /**
     */
    @Override
    public boolean allowsDefaultSelection() {
        this.ensureDecorator();
        return mDecorator.allowsDefaultSelection();
    }

    /**
     */
    @Override
    @SuppressLint("NewApi")
    public boolean onTouchEvent(@NonNull MotionEvent event) {
        final boolean processed = super.onTouchEvent(event);
        final int progress = getProgress();
        if (processed) {
            if (progress != mProgress) {
                this.handleProgressChange(progress);
            }
            this.ensureDecorator();
            switch (event.getAction() & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_DOWN:
                if (mDecorator.hasPrivateFlag(PFLAG_DISCRETE_PREVIEW_ENABLED)) {
                    this.revealDiscreteComponents();
                }
                break;
            case MotionEvent.ACTION_MOVE:
                final Drawable background = getBackground();
                if (background != null && mAnimations.shouldDraw() && UiConfig.MATERIALIZED) {
                    // Cancel the revealed circle around the thumb.
                    background.setHotspotBounds(0, 0, 0, 0);
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                if (mDecorator.hasPrivateFlag(PFLAG_DISCRETE_PREVIEW_ENABLED)) {
                    this.concealDiscreteComponents();
                }
                break;
            }
        }
        return processed;
    }

    /**
     */
    @Override
    public void setVisibility(int visibility) {
        super.setVisibility(visibility);
        if (mDiscreteIndicator != null)
            mDiscreteIndicator.setVisible(visibility == VISIBLE, false);
    }

    /**
     */
    @Override
    public synchronized void setProgress(int progress) {
        super.setProgress(progress);
        if (mProgress != progress)
            this.handleProgressChange(progress);
    }

    /**
     * Handles change in the current progress. If discrete indicator is enabled for this seek bar,
     * its position will be updated according to the specified progress value.
     *
     * @param progress The current progress of this SeekBarWidget.
     */
    private void handleProgressChange(int progress) {
        this.mProgress = progress;
        this.ensureDecorator();
        if (mDecorator.hasPrivateFlag(PFLAG_DISCRETE) && mDiscreteIndicatorHeight > 0) {
            this.updateDiscreteIndicatorPosition(getWidth(), getHeight());
            if (!UiConfig.MATERIALIZED) {
                this.updateThumbPosition();
            }
            // todo: update current tint to gray color if progress is 0 otherwise to the specified tint
        }
        invalidate();
    }

    /**
     * Sets a flag indicating whether this seek bar is <b>discrete</b> or not.
     * <p>
     * SeekBarWidget in the discrete mode draws, above the progress track and the thumb, a discrete
     * indicator to show to a user current progress value. Discrete indicator's drawable can be set
     * via {@link #setDiscreteIndicator(android.graphics.drawable.Drawable)}.
     *
     * @param discrete {@code True} to enable discrete mode, {@code false} otherwise.
     * @see R.attr#uiDiscrete ui:uiDiscrete
     * @see #isDiscrete()
     */
    public void setDiscrete(boolean discrete) {
        if (discrete && UiConfig.MATERIALIZED) {
            final Drawable background = getBackground();
            if (background instanceof RippleDrawable) {
                // This is a little bit harsh, but the RippleDrawable background is showing a ripple
                // in discrete mode in top left corner of this view's bounds which is kind of weird
                // behaviour.
                this.mRippleBackgroundDrawable = background;
                setBackgroundDrawable(null);
            }
        } else if (!discrete && mRippleBackgroundDrawable != null) {
            setBackgroundDrawable(mRippleBackgroundDrawable);
            this.mRippleBackgroundDrawable = null;
        }
        this.ensureDecorator();
        if (mDecorator.hasPrivateFlag(PFLAG_DISCRETE) != discrete) {
            mDecorator.updatePrivateFlags(PFLAG_DISCRETE, discrete);
            this.updateThumb(mThumb);
            this.updateDiscreteIndicator(mDiscreteIndicator);
            requestLayout();
        }
    }

    /**
     * Returns the flag indicating whether this seek bar is <b>discrete</b> or not.
     *
     * @return {@code True} if the discrete mode is enabled, {@code false} otherwise.
     * @see #setDiscrete(boolean)
     */
    public boolean isDiscrete() {
        this.ensureDecorator();
        return mDecorator.hasPrivateFlag(PFLAG_DISCRETE);
    }

    /**
     * Sets a flag indicating whether a preview of the discrete indicator is enabled or not. This
     * feature is by default disabled.
     * <p>
     * When the preview is enabled, the discrete indicator will be showed whenever a user presses this
     * seek bar widget and will be dismissed after a while if the user does not moves it.
     * <p>
     * If this feature is enabled, the indicator will be also previewed whenever is this seek bar
     * attached to the window.
     *
     * @param enabled {@code True} to enabled discrete preview, {@code false} otherwise.
     * @see R.attr#uiDiscretePreviewEnabled ui:uiDiscretePreviewEnabled
     * @see #isDiscretePreviewEnabled()
     */
    public void setDiscretePreviewEnabled(boolean enabled) {
        this.ensureDecorator();
        mDecorator.updatePrivateFlags(PFLAG_DISCRETE_PREVIEW_ENABLED, enabled);
    }

    /**
     * Returns the flag indicating whether a preview of the discrete indicator is enabled or not.
     *
     * @return {@code True} if discrete preview is enabled, {@code false} otherwise.
     * @see #setDiscretePreviewEnabled(boolean)
     */
    public boolean isDiscretePreviewEnabled() {
        this.ensureDecorator();
        return mDecorator.hasPrivateFlag(PFLAG_DISCRETE_PREVIEW_ENABLED);
    }

    /**
     */
    @Override
    public void setThumb(Drawable thumb) {
        this.updateThumb(thumb);
    }

    /**
     * Updates current thumb to the specified one. If this seek bar has discrete mode enabled
     * ({@link #isDiscrete()}), the given thumb will be updated to scaleable drawable if it is not yet.
     *
     * @param thumb The new thumb to update to.
     */
    private void updateThumb(Drawable thumb) {
        this.ensureDecorator();
        if (mDecorator.hasPrivateFlag(PFLAG_DISCRETE)) {
            thumb = mAnimations.makeThumbScaleable(thumb, Gravity.CENTER);
        }
        if (mThumb != thumb) {
            super.setThumb(mThumb = thumb);
            this.applyThumbTint();
        }
    }

    /**
     * <b>Note, that on pre {@link android.os.Build.VERSION_CODES#LOLLIPOP LOLLIPOP} Android versions
     * this method will return an instance of {@link TintDrawable TintDrawable} if tint has been applied
     * via {@link #setThumbTintList(ColorStateList)}.</b>
     * <p>
     * The original wrapped drawable can be obtained via {@link TintDrawable#getDrawable()}.
     */
    @Override
    public Drawable getThumb() {
        if (mThumb instanceof ScaleDrawable)
            return ((ScaleDrawable) mThumb).getDrawable();
        return mThumb;
    }

    /**
     * Applies current thumb tint from {@link Decorator#mTintInfo} to the current thumb drawable.
     * <p>
     * <b>Note</b>, that for post {@link android.os.Build.VERSION_CODES#LOLLIPOP LOLLIPOP} this
     * method does nothing.
     */
    @SuppressWarnings("ConstantConditions")
    private void applyThumbTint() {
        this.ensureDecorator();
        if (UiConfig.MATERIALIZED || mThumb == null || !mDecorator.hasTintInfo()) {
            return;
        }
        final Drawable thumb = mThumb instanceof ScaleDrawable ? ((ScaleDrawable) mThumb).getDrawable() : mThumb;
        final SeekBarTintInfo tintInfo = mDecorator.getTintInfo();
        if ((!tintInfo.hasTintList && !tintInfo.hasTintMode)) {
            return;
        }
        final boolean isTintDrawable = thumb instanceof TintDrawable;
        final TintDrawable tintDrawable = isTintDrawable ? (TintDrawable) thumb : new TintDrawable(thumb);
        if (tintInfo.hasTintList) {
            tintDrawable.setTintList(tintInfo.tintList);
        }
        if (tintInfo.hasTintMode) {
            tintDrawable.setTintMode(tintInfo.tintMode);
        }
        if (tintDrawable.isStateful()) {
            tintDrawable.setState(getDrawableState());
        }
        if (isTintDrawable) {
            return;
        }
        final int thumbOffset = getThumbOffset();
        this.mThumb = mDecorator.hasPrivateFlag(PFLAG_DISCRETE)
                ? mAnimations.makeThumbScaleable(tintDrawable, Gravity.CENTER)
                : tintDrawable;

        super.setThumb(mThumb);
        tintDrawable.attachCallback();
        setThumbOffset(thumbOffset);
    }

    /**
     */
    @Override
    @SuppressLint("NewApi")
    public void setThumbTintList(@Nullable ColorStateList tint) {
        if (UiConfig.MATERIALIZED) {
            super.setThumbTintList(tint);
            return;
        }
        this.ensureDecorator();
        final SeekBarTintInfo tintInfo = mDecorator.getTintInfo();
        tintInfo.tintList = tint;
        tintInfo.hasTintList = true;
        this.applyThumbTint();
    }

    /**
     */
    @Nullable
    @Override
    @SuppressLint("NewApi")
    public ColorStateList getThumbTintList() {
        if (UiConfig.MATERIALIZED) {
            return super.getThumbTintList();
        }
        this.ensureDecorator();
        return mDecorator.hasTintInfo() ? mDecorator.getTintInfo().tintList : null;
    }

    /**
     */
    @Override
    @SuppressLint("NewApi")
    public void setThumbTintMode(@Nullable PorterDuff.Mode tintMode) {
        if (UiConfig.MATERIALIZED) {
            super.setThumbTintMode(tintMode);
            return;
        }
        this.ensureDecorator();
        final SeekBarTintInfo tintInfo = mDecorator.getTintInfo();
        tintInfo.tintMode = tintMode;
        tintInfo.hasTintMode = true;
        this.applyThumbTint();
    }

    /**
     */
    @Nullable
    @Override
    @SuppressLint("NewApi")
    public PorterDuff.Mode getThumbTintMode() {
        if (UiConfig.MATERIALIZED) {
            return super.getThumbTintMode();
        }
        this.ensureDecorator();
        return mDecorator.hasTintInfo() ? mDecorator.getTintInfo().tintMode : null;
    }

    /**
     */
    @Override
    public void setProgressDrawable(Drawable drawable) {
        super.setProgressDrawable(mProgressDrawable = drawable);
        this.applyProgressTints();
    }

    /**
     * <b>Note, that on pre {@link android.os.Build.VERSION_CODES#LOLLIPOP LOLLIPOP} Android versions
     * this method will return an instance of {@link TintLayerDrawable TintLayerDrawable} if tint has
     * been applied to one of progress layers via {@link #setProgressTintList(ColorStateList)} or
     * {@link #setSecondaryProgressTintList(ColorStateList)} or {@link #setProgressBackgroundTintList(ColorStateList)}.</b>
     * <p>
     * The original wrapped drawable can be obtained via {@link TintDrawable#getDrawable()}.
     */
    @Override
    public Drawable getProgressDrawable() {
        return super.getProgressDrawable();
    }

    /**
     * Applies current progress tints from {@link Decorator#mTintInfo} to the progress drawable (its
     * layers respectively if it is instance of LayerDrawable or to the whole drawable if it is just
     * simple drawable).
     * <p>
     * <b>Note</b>, that for post {@link android.os.Build.VERSION_CODES#LOLLIPOP LOLLIPOP} this method
     * does nothing.
     */
    private void applyProgressTints() {
        if (UiConfig.MATERIALIZED) {
            return;
        }
        this.ensureDecorator();
        if (mProgressDrawable == null || !mDecorator.hasTintInfo()) {
            return;
        }
        final SeekBarTintInfo tintInfo = mDecorator.getTintInfo();
        if (!tintInfo.hasPrimaryProgressTintList && !tintInfo.hasPrimaryProgressTintMode
                && !tintInfo.hasSecondaryProgressTintList && !tintInfo.hasSecondaryProgressTintMode
                && !tintInfo.hasProgressBackgroundTintList && !tintInfo.hasProgressBackgroundTintMode) {
            return;
        }
        if (mProgressDrawable instanceof TintLayerDrawable) {
            final TintLayerDrawable tintDrawable = (TintLayerDrawable) mProgressDrawable;
            if (tintInfo.hasProgressBackgroundTintList) {
                tintDrawable.setTintList(tintInfo.progressBackgroundTintList, android.R.id.background);
            }
            if (tintInfo.hasProgressBackgroundTintMode) {
                tintDrawable.setTintMode(tintInfo.progressBackgroundTintMode, android.R.id.background);
            }
            if (tintInfo.hasSecondaryProgressTintList) {
                tintDrawable.setTintList(tintInfo.secondaryProgressTintList, android.R.id.secondaryProgress);
            }
            if (tintInfo.hasSecondaryProgressTintMode) {
                tintDrawable.setTintMode(tintInfo.secondaryProgressTintMode, android.R.id.secondaryProgress);
            }
            if (tintInfo.hasPrimaryProgressTintList) {
                tintDrawable.setTintList(tintInfo.primaryProgressTintList, android.R.id.progress);
            }
            if (tintInfo.hasPrimaryProgressTintMode) {
                tintDrawable.setTintMode(tintInfo.primaryProgressTintMode, android.R.id.progress);
            }
            if (mProgressDrawable.isStateful()) {
                mProgressDrawable.setState(getDrawableState());
            }
            return;
        } else if (mProgressDrawable instanceof TintDrawable) {
            final TintDrawable tintDrawable = (TintDrawable) mProgressDrawable;
            if (tintInfo.hasPrimaryProgressTintList) {
                tintDrawable.setTintList(tintInfo.primaryProgressTintList);
            }
            if (tintInfo.hasPrimaryProgressTintMode) {
                tintDrawable.setTintMode(tintInfo.primaryProgressTintMode);
            }
            if (mProgressDrawable.isStateful()) {
                mProgressDrawable.setState(getDrawableState());
            }
            return;
        }
        if (mProgressDrawable instanceof LayerDrawable) {
            final TintLayerDrawable tintDrawable = new TintLayerDrawable((LayerDrawable) mProgressDrawable);
            this.mProgressDrawable = tintDrawable;
            this.applyProgressTint();
            this.applySecondaryProgressTint();
            this.applyProgressBackgroundTint();
            if (mProgressDrawable.isStateful()) {
                mProgressDrawable.setState(getDrawableState());
            }
            super.setProgressDrawable(mProgressDrawable);
            tintDrawable.attachCallback();
            tintDrawable.setLevel((int) (getProgress() / (float) getMax() * MAX_LEVEL));
        } else {
            final TintDrawable tintDrawable = new TintDrawable(mProgressDrawable);
            this.mProgressDrawable = tintDrawable;
            this.applySimpleProgressTint();
            super.setProgressDrawable(mProgressDrawable);
            tintDrawable.attachCallback();
            tintDrawable.setLevel((int) (getProgress() / (float) getMax() * MAX_LEVEL));
        }
    }

    /**
     * Applies current first valid tint from {@link Decorator#mTintInfo} to the progress drawable as
     * whole.
     *
     * @see #applyProgressTints()
     */
    private void applySimpleProgressTint() {
        if (mProgressDrawable instanceof TintDrawable) {
            final SeekBarTintInfo tintInfo = mDecorator.getTintInfo();
            final TintDrawable tintDrawable = (TintDrawable) mProgressDrawable;
            boolean hasTintList, hasTintMode;
            hasTintList = hasTintMode = false;
            ColorStateList tintList = null;
            PorterDuff.Mode tintMode = null;
            if (tintInfo.hasPrimaryProgressTintList || tintInfo.hasPrimaryProgressTintMode) {
                hasTintList = tintInfo.hasPrimaryProgressTintList;
                tintList = tintInfo.primaryProgressTintList;
                hasTintMode = tintInfo.hasPrimaryProgressTintMode;
                tintMode = tintInfo.primaryProgressTintMode;
            } else if (tintInfo.hasSecondaryProgressTintList || tintInfo.hasSecondaryProgressTintMode) {
                hasTintList = tintInfo.hasSecondaryProgressTintList;
                tintList = tintInfo.secondaryProgressTintList;
                hasTintMode = tintInfo.hasSecondaryProgressTintMode;
                tintMode = tintInfo.secondaryProgressTintMode;
            } else if (tintInfo.hasProgressBackgroundTintList || tintInfo.hasProgressBackgroundTintMode) {
                hasTintList = tintInfo.hasProgressBackgroundTintList;
                tintList = tintInfo.progressBackgroundTintList;
                hasTintMode = tintInfo.hasProgressBackgroundTintMode;
                tintMode = tintInfo.progressBackgroundTintMode;
            }
            if (hasTintList)
                tintDrawable.setTintList(tintList);
            if (hasTintMode)
                tintDrawable.setTintMode(tintMode);
            if (mProgressDrawable.isStateful()) {
                mProgressDrawable.setState(getDrawableState());
            }
        }
    }

    /**
     * Applies current primary progress tint from {@link Decorator#mTintInfo} to the progress layer
     * of the progress drawable.
     *
     * @see #applyProgressTints()
     */
    private void applyProgressTint() {
        if (!(mProgressDrawable instanceof TintLayerDrawable) || !mDecorator.hasTintInfo())
            return;
        final SeekBarTintInfo tintInfo = mDecorator.getTintInfo();
        if (tintInfo.hasPrimaryProgressTintList || tintInfo.hasPrimaryProgressTintMode) {
            final TintLayerDrawable tintDrawable = (TintLayerDrawable) mProgressDrawable;
            if (tintInfo.hasPrimaryProgressTintList) {
                tintDrawable.setTintList(tintInfo.primaryProgressTintList, android.R.id.progress);
            }
            if (tintInfo.hasPrimaryProgressTintMode) {
                tintDrawable.setTintMode(tintInfo.primaryProgressTintMode, android.R.id.progress);
            }
        }
    }

    /**
     * Applies current secondary progress tint from {@link Decorator#mTintInfo} to the secondary
     * progress layer of the progress drawable.
     *
     * @see #applyProgressTints()
     */
    private void applySecondaryProgressTint() {
        if (!(mProgressDrawable instanceof TintLayerDrawable) || !mDecorator.hasTintInfo())
            return;
        final SeekBarTintInfo tintInfo = mDecorator.getTintInfo();
        if (tintInfo.hasSecondaryProgressTintList || tintInfo.hasSecondaryProgressTintMode) {
            final TintLayerDrawable tintDrawable = (TintLayerDrawable) mProgressDrawable;
            if (tintInfo.hasSecondaryProgressTintList) {
                tintDrawable.setTintList(tintInfo.secondaryProgressTintList, android.R.id.secondaryProgress);
            }
            if (tintInfo.hasSecondaryProgressTintMode) {
                tintDrawable.setTintMode(tintInfo.secondaryProgressTintMode, android.R.id.secondaryProgress);
            }
        }
    }

    /**
     * Applies current progress background tint from {@link Decorator#mTintInfo} to the background
     * layer of the progress drawable.
     *
     * @see #applyProgressTints()
     */
    private void applyProgressBackgroundTint() {
        if (!(mProgressDrawable instanceof TintLayerDrawable) || !mDecorator.hasTintInfo())
            return;
        final SeekBarTintInfo tintInfo = mDecorator.getTintInfo();
        if (tintInfo.hasProgressBackgroundTintList || tintInfo.hasProgressBackgroundTintMode) {
            final TintLayerDrawable tintDrawable = (TintLayerDrawable) mProgressDrawable;
            if (tintInfo.hasProgressBackgroundTintList) {
                tintDrawable.setTintList(tintInfo.progressBackgroundTintList, android.R.id.background);
            }
            if (tintInfo.hasProgressBackgroundTintMode) {
                tintDrawable.setTintMode(tintInfo.progressBackgroundTintMode, android.R.id.background);
            }
        }
    }

    /**
     */
    @Override
    @SuppressLint("NewApi")
    public void setProgressTintList(@Nullable ColorStateList tint) {
        if (UiConfig.MATERIALIZED) {
            super.setProgressTintList(tint);
            return;
        }
        this.ensureDecorator();
        final SeekBarTintInfo tintInfo = mDecorator.getTintInfo();
        tintInfo.primaryProgressTintList = tint;
        tintInfo.hasPrimaryProgressTintList = true;
        this.applyProgressTints();
    }

    /**
     */
    @Nullable
    @Override
    @SuppressLint("NewApi")
    public ColorStateList getProgressTintList() {
        if (UiConfig.MATERIALIZED) {
            return super.getProgressTintList();
        }
        this.ensureDecorator();
        return mDecorator.hasTintInfo() ? mDecorator.getTintInfo().primaryProgressTintList : null;
    }

    /**
     */
    @Override
    @SuppressLint("NewApi")
    public void setProgressTintMode(@Nullable PorterDuff.Mode tintMode) {
        if (UiConfig.MATERIALIZED) {
            super.setProgressTintMode(tintMode);
            return;
        }
        this.ensureDecorator();
        final SeekBarTintInfo tintInfo = mDecorator.getTintInfo();
        tintInfo.primaryProgressTintMode = tintMode;
        tintInfo.hasPrimaryProgressTintMode = true;
        this.applyProgressTints();
    }

    /**
     */
    @Nullable
    @Override
    @SuppressLint("NewApi")
    public PorterDuff.Mode getProgressTintMode() {
        if (UiConfig.MATERIALIZED) {
            return super.getProgressTintMode();
        }
        this.ensureDecorator();
        return mDecorator.hasTintInfo() ? mDecorator.getTintInfo().primaryProgressTintMode : null;
    }

    /**
     */
    @Override
    public void setSecondaryProgressTintList(ColorStateList tint) {
        if (UiConfig.MATERIALIZED) {
            super.setSecondaryProgressTintList(tint);
            return;
        }
        this.ensureDecorator();
        final SeekBarTintInfo tintInfo = mDecorator.getTintInfo();
        tintInfo.secondaryProgressTintList = tint;
        tintInfo.hasSecondaryProgressTintList = true;
        if (mProgressDrawable instanceof TintLayerDrawable) {
            this.applySecondaryProgressTint();
        } else {
            this.applyProgressTints();
        }
    }

    /**
     */
    @Nullable
    @Override
    public ColorStateList getSecondaryProgressTintList() {
        if (UiConfig.MATERIALIZED) {
            return super.getSecondaryProgressTintList();
        }
        this.ensureDecorator();
        return mDecorator.hasTintInfo() ? mDecorator.getTintInfo().secondaryProgressTintList : null;
    }

    /**
     */
    @Override
    public void setSecondaryProgressTintMode(PorterDuff.Mode tintMode) {
        if (UiConfig.MATERIALIZED) {
            super.setSecondaryProgressTintMode(tintMode);
            return;
        }
        this.ensureDecorator();
        final SeekBarTintInfo tintInfo = mDecorator.getTintInfo();
        tintInfo.secondaryProgressTintMode = tintMode;
        tintInfo.hasSecondaryProgressTintMode = true;
        if (mProgressDrawable instanceof TintLayerDrawable) {
            this.applySecondaryProgressTint();
        } else {
            this.applyProgressTints();
        }
    }

    /**
     */
    @Nullable
    @Override
    public PorterDuff.Mode getSecondaryProgressTintMode() {
        if (UiConfig.MATERIALIZED) {
            return super.getSecondaryProgressTintMode();
        }
        this.ensureDecorator();
        return mDecorator.hasTintInfo() ? mDecorator.getTintInfo().secondaryProgressTintMode : null;
    }

    /**
     */
    @Override
    public void setProgressBackgroundTintList(ColorStateList tint) {
        if (UiConfig.MATERIALIZED) {
            super.setProgressBackgroundTintList(tint);
            return;
        }
        this.ensureDecorator();
        final SeekBarTintInfo tintInfo = mDecorator.getTintInfo();
        tintInfo.progressBackgroundTintList = tint;
        tintInfo.hasProgressBackgroundTintList = true;
        if (mProgressDrawable instanceof TintLayerDrawable) {
            this.applyProgressBackgroundTint();
        } else {
            this.applyProgressTints();
        }
    }

    /**
     */
    @Nullable
    @Override
    public ColorStateList getProgressBackgroundTintList() {
        if (UiConfig.MATERIALIZED) {
            return super.getProgressBackgroundTintList();
        }
        this.ensureDecorator();
        return mDecorator.hasTintInfo() ? mDecorator.getTintInfo().progressBackgroundTintList : null;
    }

    /**
     */
    @Override
    public void setProgressBackgroundTintMode(PorterDuff.Mode tintMode) {
        if (UiConfig.MATERIALIZED) {
            super.setProgressBackgroundTintMode(tintMode);
            return;
        }
        this.ensureDecorator();
        final SeekBarTintInfo tintInfo = mDecorator.getTintInfo();
        tintInfo.progressBackgroundTintMode = tintMode;
        tintInfo.hasProgressBackgroundTintMode = true;
        if (mProgressDrawable instanceof TintLayerDrawable) {
            this.applyProgressBackgroundTint();
        } else {
            this.applyProgressTints();
        }
    }

    /**
     */
    @Nullable
    @Override
    public PorterDuff.Mode getProgressBackgroundTintMode() {
        if (UiConfig.MATERIALIZED) {
            return super.getProgressBackgroundTintMode();
        }
        this.ensureDecorator();
        return mDecorator.hasTintInfo() ? mDecorator.getTintInfo().progressBackgroundTintMode : null;
    }

    /**
     * Sets a ratio in which should be drawn tick marks of discrete interval of this seek bar if
     * the discrete mode is enabled by {@link #setDiscrete(boolean)}.
     * <p>
     * By default is this ratio set to {@code 0.2f} so there will be drawn 6 tick marks.
     * <p>
     * <b>Note</b>, that discrete interval has only informational character and do not force a user
     * to pick exactly value represented by one of interval's tick marks.
     *
     * @param ratio The desired interval ratio from the range {@code [0.0, 1.0]}.
     * @see R.attr#uiDiscreteIntervalRatio ui:uiDiscreteIntervalRatio
     * @see #getDiscreteIntervalRatio()
     */
    public void setDiscreteIntervalRatio(@FloatRange(from = 0, to = 1) float ratio) {
        if (mDiscreteIntervalRatio != ratio) {
            this.mDiscreteIntervalRatio = ratio;
            if (mProgressDrawable != null) {
                invalidate(mProgressDrawable.getBounds());
            }
        }
    }

    /**
     * Returns the ratio in which are drawn tick marks of discrete interval whenever is the discrete
     * mode enabled.
     *
     * @return Ratio for discrete interval from the range {@code [0.0, 1.0]}.
     * @see #setDiscreteIntervalRatio(float)
     * @see #isDiscrete()
     */
    @FloatRange(from = 0, to = 1)
    public float getDiscreteIntervalRatio() {
        return mDiscreteIntervalRatio;
    }

    /**
     * Sets a radius for the tick mark of a discrete interval. The discrete interval are drawn whenever
     * the discrete mode for this seek bar is enabled via {@link #setDiscrete(boolean)}.
     *
     * @param radius The desired radius for tick mark.
     * @see R.attr#uiDiscreteIntervalTickMarkRadius ui:uiDiscreteIntervalTickMarkRadius
     * @see #getDiscreteIntervalTickMarkRadius()
     */
    public void setDiscreteIntervalTickMarkRadius(@FloatRange(from = 0) float radius) {
        if (DISCRETE_INTERVAL_TICK_MARK_INFO.radius != radius) {
            DISCRETE_INTERVAL_TICK_MARK_INFO.radius = Math.max(0, radius);
            this.invalidateDiscreteIntervalsArea();
        }
    }

    /**
     * Returns the radius of the tick mark of a discrete interval.
     *
     * @return Tick mark radius.
     * @see #setDiscreteIntervalTickMarkRadius(float)
     */
    @FloatRange(from = 0)
    public float getDiscreteIntervalTickMarkRadius() {
        return DISCRETE_INTERVAL_TICK_MARK_INFO.radius;
    }

    /**
     * Sets a single color used to draw the discrete interval's tick mark.
     *
     * @param color The desired color.
     * @see R.attr#uiDiscreteIntervalTickMarkColor ui:uiDiscreteIntervalTickMarkColor
     */
    public void setDiscreteIntervalTickMarkColor(@ColorInt int color) {
        setDiscreteIntervalTickMarkColor(ColorStateList.valueOf(color));
    }

    /**
     * Sets the colors state list used to draw different states of the discrete interval's tick mark.
     *
     * @param colors The desired list of colors for the tick mark.
     * @see R.attr#uiDiscreteIntervalTickMarkColor ui:uiDiscreteIntervalTickMarkColor
     * @see #getDiscreteIntervalTickMarkColors()
     * @see #getDiscreteIntervalTickMarkCurrentColor()
     */
    public void setDiscreteIntervalTickMarkColor(@NonNull ColorStateList colors) {
        DISCRETE_INTERVAL_TICK_MARK_INFO.colors = colors;
        this.updateDiscreteIntervalTickMarksState(getDrawableState(), true);
    }

    /**
     * Returns the list of colors used to draw different states of the discrete interval's tick mark.
     *
     * @return Colors state list for interval's tick mark.
     * @see #setDiscreteIntervalTickMarkColor(android.content.res.ColorStateList)
     */
    @NonNull
    public ColorStateList getDiscreteIntervalTickMarkColors() {
        return DISCRETE_INTERVAL_TICK_MARK_INFO.colors;
    }

    /**
     * Returns the current color used to draw the discrete interval's tick mark.
     *
     * @return Current interval's tick mark color.
     */
    @ColorInt
    public int getDiscreteIntervalTickMarkCurrentColor() {
        return DISCRETE_INTERVAL_TICK_MARK_INFO.paint.getColor();
    }

    /**
     * Same as {@link #setDiscreteIndicator(android.graphics.drawable.Drawable)} for resource id.
     *
     * @param resId Resource id of the desired drawable for discrete indicator. May be {@code 0} to
     *              remove the current indicator.
     */
    public void setDiscreteIndicator(@DrawableRes int resId) {
        if (resId == 0) {
            setDiscreteIndicator(null);
        } else if (mDiscreteIndicatorRes != resId) {
            setDiscreteIndicator(ResourceUtils.getDrawable(getResources(), mDiscreteIndicatorRes = resId,
                    getContext().getTheme()));
        }
    }

    /**
     * Sets the drawable used to draw the discrete indicator in <b>discrete mode</b>.
     *
     * @param indicator The desired drawable for discrete indicator. May be {@code null} to clear
     *                  the current one.
     * @see R.attr#uiDiscreteIndicator ui:uiDiscreteIndicator
     * @see #setDiscreteIndicatorTintList(ColorStateList)
     * @see #setDiscreteIndicatorTintMode(PorterDuff.Mode)
     * @see #getDiscreteIndicator()
     * @see #setDiscrete(boolean)
     */
    public void setDiscreteIndicator(@Nullable Drawable indicator) {
        this.updateDiscreteIndicator(indicator);
    }

    /**
     * Updates current indicator to the specified one. If this seek bar has discrete mode enabled
     * ({@link #isDiscrete()}), the given indicator will be updated to scaleable drawable if it is
     * not yet.
     *
     * @param indicator The new indicator to update to.
     */
    private void updateDiscreteIndicator(Drawable indicator) {
        if (mDecorator.hasPrivateFlag(PFLAG_DISCRETE)) {
            indicator = mAnimations.makeDiscreteIndicatorScaleable(indicator,
                    Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL);
        }
        if (mDiscreteIndicator != indicator) {
            final boolean needUpdate;
            if (mDiscreteIndicator != null) {
                mDiscreteIndicator.setCallback(null);
                unscheduleDrawable(mDiscreteIndicator);
                needUpdate = true;
            } else {
                needUpdate = false;
            }
            if (indicator != null) {
                indicator.setCallback(this);
                indicator.setVisible(getVisibility() == VISIBLE, false);

                if (indicator.getIntrinsicWidth() != mDiscreteIndicatorWidth
                        || indicator.getIntrinsicHeight() != mDiscreteIndicatorHeight) {
                    this.mDiscreteIndicatorWidth = indicator.getIntrinsicWidth();
                    this.mDiscreteIndicatorHeight = indicator.getIntrinsicHeight();
                    requestLayout();
                }
            } else {
                this.mDiscreteIndicatorRes = 0;
                this.mDiscreteIndicatorWidth = mDiscreteIndicatorHeight = 0;
                requestLayout();
            }
            this.mDiscreteIndicator = indicator;
            this.applyDiscreteIndicatorTint();
            if (needUpdate) {
                this.updateDiscreteIndicatorPosition(getWidth(), getHeight());
                if (mDiscreteIndicator.isStateful()) {
                    mDiscreteIndicator.setState(getDrawableState());
                }
                this.invalidateDiscreteIndicatorArea();
            }
        }
    }

    /**
     * Returns the current discrete indicator's drawable.
     * <p>
     * <b>Note, that on pre {@link android.os.Build.VERSION_CODES#LOLLIPOP LOLLIPOP} Android versions
     * this method will return an instance of {@link TintDrawable TintDrawable} if tint has been applied
     * via {@link #setDiscreteIndicatorTintList(ColorStateList)}.</b>
     * <p>
     * The original wrapped indicator drawable can be obtained via {@link TintDrawable#getDrawable()}.
     *
     * @return Discrete indicator's drawable.
     * @see #setDiscreteIndicator(android.graphics.drawable.Drawable)
     */
    @Nullable
    public Drawable getDiscreteIndicator() {
        if (mDiscreteIndicator instanceof ScaleDrawable)
            return ((ScaleDrawable) mDiscreteIndicator).getDrawable();
        else
            return mDiscreteIndicator;
    }

    /**
     * Applies a tint to the discrete indicator, if specified. This call does not modify the current
     * tint mode, which is {@link android.graphics.PorterDuff.Mode#SRC_IN} by default.
     * <p>
     * Subsequent calls to {@link #setDiscreteIndicator(android.graphics.drawable.Drawable)} will
     * automatically mutate the drawable and apply the specified tint and tint mode using
     * {@link android.graphics.drawable.Drawable#setTintList(android.content.res.ColorStateList)}.
     *
     * @param tint The tint to apply, may be {@code null} to clear the current tint.
     * @see R.attr#uiDiscreteIndicatorTint ui:uiDiscreteIndicatorTint
     * @see #getDiscreteIndicatorTintList()
     * @see android.graphics.drawable.Drawable#setTintList(android.content.res.ColorStateList)
     */
    public void setDiscreteIndicatorTintList(@Nullable ColorStateList tint) {
        final SeekBarTintInfo tintInfo = mDecorator.getTintInfo();
        tintInfo.discreteIndicatorTintList = tint;
        tintInfo.hasDiscreteIndicatorTintList = true;
        this.applyDiscreteIndicatorTint();
    }

    /**
     * Returns the tint applied to the discrete indicator's drawable, if specified.
     *
     * @return The discrete indicator's drawable tint.
     * @see #setDiscreteIndicatorTintList(android.content.res.ColorStateList)
     */
    @Nullable
    public ColorStateList getDiscreteIndicatorTintList() {
        this.ensureDecorator();
        return mDecorator.hasTintInfo() ? mDecorator.getTintInfo().discreteIndicatorTintList : null;
    }

    /**
     * Specifies the blending mode used to apply the tint specified by {@link #setDiscreteIndicatorTintList(android.content.res.ColorStateList)}}
     * to the discrete indicator. The default mode is {@link android.graphics.PorterDuff.Mode#SRC_IN}.
     *
     * @param tintMode The blending mode used to apply the tint, may be {@code null} to clear the
     *                 current tint.
     * @see R.attr#uiDiscreteIndicatorTintMode ui:uiDiscreteIndicatorTintMode
     * @see #getDiscreteIndicatorTintMode()
     * @see android.graphics.drawable.Drawable#setTintMode(android.graphics.PorterDuff.Mode)
     */
    public void setDiscreteIndicatorTintMode(@Nullable PorterDuff.Mode tintMode) {
        final SeekBarTintInfo tintInfo = mDecorator.getTintInfo();
        tintInfo.discreteIndicatorTintMode = tintMode;
        tintInfo.hasDiscreteIndicatorTintMode = true;
        this.applyDiscreteIndicatorTint();
    }

    /**
     * Returns the blending mode used to apply the tint to the discrete indicator's drawable, if
     * specified.
     *
     * @return The discrete indicator's drawable blending mode used to apply the tint.
     * @see #setDiscreteIndicatorTintMode(android.graphics.PorterDuff.Mode)
     */
    @Nullable
    public PorterDuff.Mode getDiscreteIndicatorTintMode() {
        this.ensureDecorator();
        return mDecorator.hasTintInfo() ? mDecorator.getTintInfo().discreteIndicatorTintMode : null;
    }

    /**
     * Applies current discrete indicator tint from {@link Decorator#mTintInfo} to the current discrete
     * indicator's drawable.
     */
    @SuppressLint("NewApi")
    @SuppressWarnings("ConstantConditions")
    private void applyDiscreteIndicatorTint() {
        this.ensureDecorator();
        if (mDiscreteIndicator == null || !mDecorator.hasTintInfo()) {
            return;
        }
        final Drawable indicator = mDiscreteIndicator instanceof ScaleDrawable
                ? ((ScaleDrawable) mDiscreteIndicator).getDrawable()
                : mDiscreteIndicator;
        final SeekBarTintInfo tintInfo = mDecorator.getTintInfo();
        if ((!tintInfo.hasDiscreteIndicatorTintList && !tintInfo.hasDiscreteIndicatorTintMode)) {
            return;
        }
        if (UiConfig.MATERIALIZED) {
            this.mDiscreteIndicator = mDiscreteIndicator.mutate();
            if (tintInfo.hasDiscreteIndicatorTintList) {
                mDiscreteIndicator.setTintList(tintInfo.discreteIndicatorTintList);
            }
            if (tintInfo.hasDiscreteIndicatorTintMode) {
                mDiscreteIndicator.setTintMode(tintInfo.discreteIndicatorTintMode);
            }
            if (mDiscreteIndicator.isStateful()) {
                mDiscreteIndicator.setState(getDrawableState());
            }
            return;
        }
        final boolean isTintDrawable = indicator instanceof TintDrawable;
        final TintDrawable tintDrawable = isTintDrawable ? (TintDrawable) indicator : new TintDrawable(indicator);
        if (tintInfo.hasDiscreteIndicatorTintList) {
            tintDrawable.setTintList(tintInfo.discreteIndicatorTintList);
        }
        if (tintInfo.hasDiscreteIndicatorTintMode) {
            tintDrawable.setTintMode(tintInfo.discreteIndicatorTintMode);
        }
        if (isTintDrawable) {
            return;
        }
        this.mDiscreteIndicator = mDecorator.hasPrivateFlag(PFLAG_DISCRETE)
                ? mAnimations.makeDiscreteIndicatorScaleable(tintDrawable,
                        Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL)
                : tintDrawable;
        mDiscreteIndicator.setCallback(this);
        updateDiscreteIndicatorState(getDrawableState(), false);
    }

    /**
     * Sets a text color, size, and style for the discrete indicator's text from the specified
     * TextAppearance resource.
     *
     * @param resId Resource id of the desired TextAppearance style.
     * @see R.attr#uiDiscreteIndicatorTextAppearance ui:uiDiscreteIndicatorTextAppearance
     * @see #setDiscreteIndicatorTextSize(int, float)
     * @see #setDiscreteIndicatorTextColor(ColorStateList)
     * @see #setDiscreteIndicatorTypeface(Typeface)
     */
    public void setDiscreteIndicatorTextAppearance(@StyleRes int resId) {
        if (DISCRETE_INDICATOR_TEXT_INFO.fromTextAppearanceStyle(getContext(), resId)
                && DISCRETE_INDICATOR_TEXT_INFO.updatePaint(getDrawableState())) {
            this.invalidateDiscreteIndicatorArea();
        }
    }

    /**
     * Same as {@link #setDiscreteIndicatorTextSize(int, float)} in {@link TypedValue#COMPLEX_UNIT_SP}
     * and the specified <var>size</var>.
     *
     * @see #getDiscreteIndicatorTextSize()
     */
    public void setDiscreteIndicatorTextSize(float size) {
        setDiscreteIndicatorTextSize(TypedValue.COMPLEX_UNIT_SP, size);
    }

    /**
     * Sets a size for the discrete indicator's text to the given <var>unit</var> and <var>size</var>.
     *
     * @param unit The desired dimension unit. See {@link TypedValue} for possible units.
     * @param size The desired size in the specified unit.
     * @see #setDiscreteIndicatorTextSize(float)
     * @see #getDiscreteIndicatorTextSize()
     */
    public void setDiscreteIndicatorTextSize(int unit, float size) {
        setDiscreteIndicatorRawTextSize(TypedValue.applyDimension(unit, size, getResources().getDisplayMetrics()));
    }

    /**
     * Sets the raw text size for the Paint used to draw numbers graphics.
     *
     * @param size The desired raw size in pixels.
     */
    private void setDiscreteIndicatorRawTextSize(float size) {
        if (DISCRETE_INDICATOR_TEXT_INFO.updateTextSize(size)) {
            this.invalidateDiscreteIndicatorArea();
        }
    }

    /**
     * Returns the size of the discrete indicator's text.
     *
     * @return Size used when drawing discrete indicator's text graphics.
     * @see #setDiscreteIndicatorTextSize(int, float)
     * @see #setDiscreteIndicatorTextAppearance(int)
     */
    public float getDiscreteIndicatorTextSize() {
        return DISCRETE_INDICATOR_TEXT_INFO.paint.getTextSize();
    }

    /**
     * Sets a single color for the discrete indicator's text.
     *
     * @param color The desired color.
     * @see #setDiscreteIndicatorTextColor(ColorStateList)
     */
    public void setDiscreteIndicatorTextColor(@ColorInt int color) {
        setDiscreteIndicatorTextColor(ColorStateList.valueOf(color));
    }

    /**
     * Sets colors for the discrete indicator's text.
     *
     * @param colors The desired colors state list.
     * @see #setDiscreteIndicatorTextColor(int)
     * @see #getDiscreteIndicatorTextColors()
     * @see #getDiscreteIndicatorCurrentTextColor()
     */
    public void setDiscreteIndicatorTextColor(@NonNull ColorStateList colors) {
        if (DISCRETE_INDICATOR_TEXT_INFO.updateTextColor(colors, getDrawableState())) {
            this.invalidateDiscreteIndicatorArea();
        }
    }

    /**
     * Returns the colors for the discrete indicator's text.
     *
     * @return List of colors used when drawing discrete indicator's text graphics.
     * @see #setDiscreteIndicatorTextColor(android.content.res.ColorStateList)
     * @see #setDiscreteIndicatorTextColor(int)
     * @see #getDiscreteIndicatorCurrentTextColor()
     */
    @Nullable
    public ColorStateList getDiscreteIndicatorTextColors() {
        return DISCRETE_INDICATOR_TEXT_INFO.mAppearance.getTextColor();
    }

    /**
     * Returns the current color used to draw the discrete indicator's text.
     *
     * @return Current discrete indicator's text color.
     * @see #getDiscreteIndicatorTextColors()
     */
    @ColorInt
    public int getDiscreteIndicatorCurrentTextColor() {
        return DISCRETE_INDICATOR_TEXT_INFO.paint.getColor();
    }

    /**
     * Sets a typeface and style in which the discrete indicator's text should be displayed, and
     * turns on the fake bold and italic bits in the Paint if the Typeface that you provided does
     * not have all the bits in the style that you specified.
     *
     * @param typeface The desired typeface. May be {@code null} to create default one from the specified
     *                 <var>style</var>.
     * @param style    One of {@link Typeface#NORMAL}, {@link Typeface#BOLD}, {@link Typeface#ITALIC}
     *                 or {@link Typeface#BOLD_ITALIC}.
     * @see #setDiscreteIndicatorTypeface(Typeface)
     * @see #getDiscreteIndicatorTypeface()
     * @see #getDiscreteIndicatorTypefaceStyle()
     */
    public void setDiscreteIndicatorTypeface(@Nullable Typeface typeface, @TextAppearance.TextStyle int style) {
        if (DISCRETE_INDICATOR_TEXT_INFO.updateTypeface(typeface, style)) {
            this.invalidateDiscreteIndicatorArea();
        }
    }

    /**
     * Sets a typeface in which the discrete indicator's text should be displayed.
     * <p>
     * <b>Note</b>, that not all Typeface families actually have bold and italic variants, so you
     * may need to use {@link #setDiscreteIndicatorTypeface(Typeface, int)} to get the appearance that you actually
     * want.
     *
     * @param typeface The desired typeface.
     * @see #getDiscreteIndicatorTypeface()
     * @see #getDiscreteIndicatorTypefaceStyle()
     */
    public void setDiscreteIndicatorTypeface(@Nullable Typeface typeface) {
        if (DISCRETE_INDICATOR_TEXT_INFO.updateTypeface(typeface)) {
            this.invalidateDiscreteIndicatorArea();
        }
    }

    /**
     * Returns the typeface used to draw text of the discrete indicator.
     *
     * @return Discrete indicator's text typeface.
     * @see #setDiscreteIndicatorTypeface(Typeface, int)
     * @see #setDiscreteIndicatorTypeface(Typeface)
     * @see #getDiscreteIndicatorTypefaceStyle()
     * @see #setDiscreteIndicatorTextAppearance(int)
     */
    @Nullable
    public Typeface getDiscreteIndicatorTypeface() {
        return DISCRETE_INDICATOR_TEXT_INFO.paint.getTypeface();
    }

    /**
     * Returns the style of the typeface used to draw the discrete indicator's text.
     *
     * @return Typeface style.
     * @see #getDiscreteIndicatorTypeface()
     * @see #setDiscreteIndicatorTypeface(Typeface, int)
     */
    @TextAppearance.TextStyle
    @SuppressWarnings("ResourceType")
    public int getDiscreteIndicatorTypefaceStyle() {
        final Typeface typeface = DISCRETE_INDICATOR_TEXT_INFO.paint.getTypeface();
        return typeface != null ? typeface.getStyle() : Typeface.NORMAL;
    }

    /**
     * Sets a gravity to apply to the discrete indicator's text.
     *
     * @param gravity The desired gravity flags. One of {@link Gravity#TOP}, {@link Gravity#BOTTOM},
     *                {@link Gravity#START}, {@link Gravity#END}, {@link Gravity#CENTER_HORIZONTAL},
     *                {@link Gravity#CENTER_VERTICAL}, {@link Gravity#CENTER} or theirs relevant combination.
     * @see R.attr#uiDiscreteIndicatorTextGravity ui:uiDiscreteIndicatorTextGravity
     * @see #getDiscreteIndicatorTextGravity()
     * @see #setDiscreteIndicatorTextPadding(int, int, int, int)
     */
    public void setDiscreteIndicatorTextGravity(int gravity) {
        if (DISCRETE_INDICATOR_TEXT_INFO.gravity != gravity) {
            this.DISCRETE_INDICATOR_TEXT_INFO.gravity = gravity;
            this.invalidateDiscreteIndicatorArea();
        }
    }

    /**
     * Returns the gravity applied to the discrete indicator's text.
     *
     * @return Discrete indicator's text gravity.
     * @see #setDiscreteIndicatorTextGravity(int)
     */
    public int getDiscreteIndicatorTextGravity() {
        return DISCRETE_INDICATOR_TEXT_INFO.gravity;
    }

    /**
     * Sets a padding for the discrete indicator's text. The specified padding will be used to
     * position the indicator's text within the indicator's bounds, but will not cause the indicator
     * bounds to change.
     *
     * @param start  The desired relative start padding. Properly used according to {@link #getLayoutDirection()}.
     * @param top    The desired top padding.
     * @param end    The desired relative end padding. Properly used according to {@link #getLayoutDirection()}.
     * @param bottom The desired bottom padding.
     * @see R.attr#uiDiscreteIndicatorTextPaddingStart ui:uiDiscreteIndicatorTextPaddingStart
     * @see R.attr#uiDiscreteIndicatorTextPaddingTop ui:uiDiscreteIndicatorTextPaddingTop
     * @see R.attr#uiDiscreteIndicatorTextPaddingEnd ui:uiDiscreteIndicatorTextPaddingEnd
     * @see R.attr#uiDiscreteIndicatorTextPaddingBottom ui:uiDiscreteIndicatorTextPaddingBottom
     * @see #getDiscreteIndicatorTextPaddingStart()
     * @see #getDiscreteIndicatorTextPaddingTop()
     * @see #getDiscreteIndicatorTextPaddingEnd()
     * @see #getDiscreteIndicatorTextPaddingBottom()
     * @see #setDiscreteIndicatorTextGravity(int)
     */
    public void setDiscreteIndicatorTextPadding(int start, int top, int end, int bottom) {
        final Rect padding = DISCRETE_INDICATOR_TEXT_INFO.padding;
        if (padding.left != start || padding.top != top || padding.right != end || padding.bottom != bottom) {
            padding.left = start;
            padding.top = top;
            padding.right = end;
            padding.bottom = bottom;
            this.invalidateDiscreteIndicatorArea();
        }
    }

    /**
     * Returns the start padding of the discrete indicator's text.
     *
     * @return Indicator's text left padding if layout direction is {@link #LAYOUT_DIRECTION_LTR},
     * right padding if it is {@link #LAYOUT_DIRECTION_RTL}.
     * @see #setDiscreteIndicatorTextPadding(int, int, int, int)
     * @see #setDiscreteIndicatorTextGravity(int)
     */
    public int getDiscreteIndicatorTextPaddingStart() {
        return hasRTLDirection() ? DISCRETE_INDICATOR_TEXT_INFO.padding.right
                : DISCRETE_INDICATOR_TEXT_INFO.padding.left;
    }

    /**
     * Returns the top padding of the discrete indicator's text.
     *
     * @return Indicator's text top padding.
     * @see #setDiscreteIndicatorTextPadding(int, int, int, int)
     * @see #setDiscreteIndicatorTextGravity(int)
     */
    public int getDiscreteIndicatorTextPaddingTop() {
        return DISCRETE_INDICATOR_TEXT_INFO.padding.top;
    }

    /**
     * Returns the end padding of the discrete indicator's text.
     *
     * @return Indicator's text right padding if layout direction is {@link #LAYOUT_DIRECTION_LTR},
     * left padding if it is {@link #LAYOUT_DIRECTION_RTL}.
     * @see #setDiscreteIndicatorTextPadding(int, int, int, int)
     * @see #setDiscreteIndicatorTextGravity(int)
     */
    public int getDiscreteIndicatorTextPaddingEnd() {
        return hasRTLDirection() ? DISCRETE_INDICATOR_TEXT_INFO.padding.left
                : DISCRETE_INDICATOR_TEXT_INFO.padding.right;
    }

    /**
     * Returns the bottom padding of the discrete indicator's text.
     *
     * @return Indicator's text bottom padding.
     * @see #setDiscreteIndicatorTextPadding(int, int, int, int)
     * @see #setDiscreteIndicatorTextGravity(int)
     */
    public int getDiscreteIndicatorTextPaddingBottom() {
        return DISCRETE_INDICATOR_TEXT_INFO.padding.bottom;
    }

    /**
     */
    @Override
    public void setFont(@Nullable Font font) {
        this.ensureDecorator();
        mDecorator.setFont(font);
    }

    /**
     */
    @Override
    public void setFont(@NonNull String fontPath) {
        this.ensureDecorator();
        mDecorator.setFont(fontPath);
    }

    /**
     */
    @Override
    public void setTypeface(@Nullable Typeface typeface, @TextAppearance.TextStyle int style) {
        setDiscreteIndicatorTypeface(typeface, style);
    }

    /**
     */
    @Override
    public void setTypeface(@Nullable Typeface typeFace) {
        setDiscreteIndicatorTypeface(typeFace);
    }

    /**
     * Checks whether this view has specified {@link #LAYOUT_DIRECTION_RTL} as its layout direction
     * via {@link #setLayoutDirection(int)} or not.
     *
     * @return {@code True} if layout direction of this view is {@link #LAYOUT_DIRECTION_RTL}, false
     * if it is {@link #LAYOUT_DIRECTION_LTR}.
     */
    private boolean hasRTLDirection() {
        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1
                && getLayoutDirection() == LAYOUT_DIRECTION_RTL;
    }

    /**
     */
    @Override
    protected void drawableStateChanged() {
        super.drawableStateChanged();
        updateDrawablesState(false);
        if (!UiConfig.MATERIALIZED) {
            this.applyProgressTints();
            this.applyThumbTint();
        }
    }

    /**
     */
    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        this.previewDiscreteComponents(0, PREVIEW_DISCRETE_COMPONENTS_DURATION);
    }

    /**
     */
    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        mAnimations.cancel();
    }

    /**
     */
    @NonNull
    @Override
    public WidgetSizeAnimator animateSize() {
        this.ensureDecorator();
        return mDecorator.animateSize();
    }

    /**
     */
    @Override
    protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if (mDecorator.hasPrivateFlag(PFLAG_DISCRETE) && mDiscreteIndicatorHeight > 0) {
            // Measure extra space for discrete indicator.
            setMeasuredDimension(getMeasuredWidth(), getMeasuredHeight() + mDiscreteIndicatorHeight);
        }
    }

    /**
     */
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        this.ensureDecorator();
        mDecorator.onSizeChanged(w, h, oldw, oldh);
        if (mDecorator.hasPrivateFlag(PFLAG_DISCRETE) && mDiscreteIndicatorHeight > 0 && h != oldh) {
            this.updateTrackPosition();
            this.updateThumbPosition();
            this.updateDiscreteIndicatorPosition(getMeasuredWidth(), getMeasuredHeight());
        }
    }

    /**
     * Updates the current bounds of the progress track. This will move the track below the discrete
     * indicator if it is enabled for this SeekBarWidget.
     */
    private void updateTrackPosition() {
        if (mProgressDrawable == null) {
            return;
        }
        final Rect bounds = mProgressDrawable.getBounds();
        if (UiConfig.MATERIALIZED) {
            mProgressDrawable.setBounds(bounds.left, bounds.top + (mDiscreteIndicatorHeight / 2), bounds.right,
                    bounds.bottom + (mDiscreteIndicatorHeight / 2));
        } else {
            final int top = bounds.top + mDiscreteIndicatorHeight;
            mProgressDrawable.setBounds(bounds.left, top, bounds.right,
                    top + mProgressDrawable.getIntrinsicHeight());
        }
    }

    /**
     * Updates the current bounds of the thumb. This will move the thumb below the discrete indicator
     * if it is enabled for this SeekBarWidget.
     */
    private void updateThumbPosition() {
        if (mThumb == null) {
            return;
        }
        final Rect bounds = mThumb.getBounds();
        final int top = getPaddingTop() + mDiscreteIndicatorHeight;
        mThumb.setBounds(bounds.left, top, bounds.right, top + mThumb.getIntrinsicHeight());
    }

    /**
     * Updates the current bounds of the discrete indicator's drawable depends on the specified
     * <var>width</var> and <var>height</var> and the current value of progress.
     *
     * @param width  Current width of this view.
     * @param height Current height of this view.
     */
    private void updateDiscreteIndicatorPosition(int width, int height) {
        if (mDiscreteIndicator == null) {
            return;
        }
        final float progressRatio = getProgress() / (float) getMax();
        width -= getPaddingLeft() + getPaddingRight();
        final int left = Math.round(progressRatio * width);
        final int thumbHeight = mThumb != null ? mThumb.getIntrinsicHeight() : 0;
        final int top = getPaddingTop() + thumbHeight / 4;
        mDiscreteIndicator.setBounds(left, top, left + mDiscreteIndicatorWidth, top + mDiscreteIndicatorHeight);
    }

    /**
     * Like {@link #revealDiscreteComponents()} but this will show the discrete components only for
     * a the specified amount of time (thus just preview) and then the discrete components will be
     * automatically hided again.
     *
     * @param delay    Delay with which should be the reveal animation of discrete components started.
     * @param duration The duration for how long should be the discrete components previewed.
     */
    private void previewDiscreteComponents(long delay, long duration) {
        if (isEnabled() && mDecorator.hasPrivateFlag(PFLAG_DISCRETE)
                && mDecorator.hasPrivateFlag(PFLAG_DISCRETE_PREVIEW_ENABLED))
            mAnimations.previewDiscreteComponents(delay, duration);
    }

    /**
     * Reveals all discrete components in order to get this SeekBar (if discrete) to its discrete
     * state where such components should be visible with an animation.
     *
     * @see #concealDiscreteComponents()
     */
    private void revealDiscreteComponents() {
        if (isEnabled() && mDecorator.hasPrivateFlag(PFLAG_DISCRETE)) {
            mAnimations.revealDiscreteComponents();
        }
    }

    /**
     * Conceals all discrete components in order to revert this SeekBar (if discrete) back to its idle
     * state with an animation.
     *
     * @see #revealDiscreteComponents()
     */
    private void concealDiscreteComponents() {
        if (isEnabled() && mDecorator.hasPrivateFlag(PFLAG_DISCRETE))
            mAnimations.concealDiscreteComponents();
    }

    /**
     */
    @Override
    protected boolean verifyDrawable(Drawable who) {
        return who == mDiscreteIndicator || super.verifyDrawable(who);
    }

    /**
     * Updates state of all drawables and drawing related object of this view to the current state
     * of this view specified by {@link #getDrawableState()}.
     *
     * @param invalidate {@code True} to perform invalidation of this view, {@code false} otherwise.
     */
    private void updateDrawablesState(boolean invalidate) {
        final int[] state = getDrawableState();
        this.updateDiscreteIndicatorState(state, false);
        this.updateDiscreteIntervalTickMarksState(state, false);
        if (invalidate)
            invalidate();
    }

    /**
     * Updates the current state of the discrete indicator's graphics.
     *
     * @param state      The state according to which to update the graphics.
     * @param invalidate {@code True} to perform invalidation of this view, {@code false} otherwise.
     */
    @SuppressWarnings("CheckResult")
    private void updateDiscreteIndicatorState(int[] state, boolean invalidate) {
        if (mDiscreteIndicator != null && mDiscreteIndicator.isStateful()) {
            mDiscreteIndicator.setState(state);
        }
        DISCRETE_INDICATOR_TEXT_INFO.updatePaint(state);
        if (invalidate)
            this.invalidateDiscreteIndicatorArea();
    }

    /**
     * Updates the current state of the discrete interval's graphics.
     *
     * @param state      The state according to which to update the graphics.
     * @param invalidate {@code True} to perform invalidation of this view, {@code false} otherwise.
     */
    private void updateDiscreteIntervalTickMarksState(int[] state, boolean invalidate) {
        if (DISCRETE_INTERVAL_TICK_MARK_INFO.updatePaint(state) && invalidate) {
            this.invalidateDiscreteIntervalsArea();
        }
    }

    /**
     * Invalidates this view in area where the discrete indicator is at this time presented using
     * its current bounds.
     *
     * @see #invalidate(Rect)
     */
    private void invalidateDiscreteIndicatorArea() {
        if (mDiscreteIndicator != null)
            invalidate(mDiscreteIndicator.getBounds());
    }

    /**
     * Invalidates this view in area where the progress track is presented using its current bounds.
     *
     * @see #invalidate(Rect)
     */
    private void invalidateDiscreteIntervalsArea() {
        if (mProgressDrawable != null)
            invalidate(mProgressDrawable.getBounds());
    }

    /**
     */
    @Override
    @SuppressWarnings("NewApi")
    protected synchronized void onDraw(@NonNull Canvas canvas) {
        super.onDraw(canvas);
        if (mDecorator.hasPrivateFlag(PFLAG_DISCRETE) && mAnimations.shouldDraw()) {
            this.drawDiscreteInterval(canvas);
            this.drawDiscreteIndicator(canvas);
            if (mAnimations.areRunning() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                postInvalidateOnAnimation();
            }
        }
    }

    /**
     * Draws discrete interval, its thick marks depending on the current {@link #mDiscreteIntervalRatio}
     * value.
     *
     * @param canvas Canvas on which to draw discrete interval.
     */
    private void drawDiscreteInterval(Canvas canvas) {
        if (mDiscreteIntervalRatio == 0 || mProgressDrawable == null) {
            return;
        }

        final Rect trackBounds = mProgressDrawable.getBounds();
        final int trackLeft = getPaddingLeft();
        final int cy = trackBounds.centerY();
        float trackWidth = trackBounds.width();
        final float discreteInterval = mDiscreteIntervalRatio * trackWidth;
        trackWidth += DISCRETE_INTERVAL_TICK_MARK_INFO.radius;
        final Rect thumbBounds = mThumb != null ? mThumb.getBounds() : null;
        final int thumbOffset = getThumbOffset();

        float cx = 0;
        while (cx <= trackWidth) {
            // Ensure to not draw over thumb if it is not expected behaviour.
            final boolean isAtThumbPosition = thumbBounds != null
                    && trackLeft + cx >= thumbBounds.left + thumbOffset
                    && trackLeft + cx <= thumbBounds.right + thumbOffset;
            if (CAN_DRAW_DISCRETE_INTERVAL_OVER_THUMB || !isAtThumbPosition) {
                canvas.drawCircle(trackLeft + cx, cy, DISCRETE_INTERVAL_TICK_MARK_INFO.radius,
                        DISCRETE_INTERVAL_TICK_MARK_INFO.paint);
            }
            cx += discreteInterval;
        }
    }

    /**
     * Draws discrete indicator of this SeekBarWidget at its current position updated by
     * {@link #updateDiscreteIndicatorPosition(int, int)} according to the current progress.
     *
     * @param canvas Canvas on which to draw discrete indicator's drawable.
     */
    private void drawDiscreteIndicator(Canvas canvas) {
        if (mDiscreteIndicatorHeight == 0) {
            return;
        }
        // todo: draw according to LTR/RTL layout direction.
        mDiscreteIndicator.draw(canvas);

        // Draw current progress over indicator's graphics.
        final Rect indicatorBounds = mDiscreteIndicator.getBounds();
        final Paint textPaint = DISCRETE_INDICATOR_TEXT_INFO.paint;
        textPaint.getTextBounds("0", 0, 1, mRect);
        final float textSize = mRect.height();
        final Rect textPadding = DISCRETE_INDICATOR_TEXT_INFO.padding;
        final int absoluteTextGravity = WidgetGravity.getAbsoluteGravity(DISCRETE_INDICATOR_TEXT_INFO.gravity,
                ViewCompat.getLayoutDirection(this));

        final float textX, textY;
        // Resolve horizontal text position according to the requested gravity.
        switch (absoluteTextGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
        case Gravity.CENTER_HORIZONTAL:
            textPaint.setTextAlign(Paint.Align.CENTER);
            textX = indicatorBounds.centerX();
            break;
        case Gravity.RIGHT:
            textPaint.setTextAlign(Paint.Align.RIGHT);
            textX = indicatorBounds.right - textPadding.right;
            break;
        case Gravity.LEFT:
        default:
            textPaint.setTextAlign(Paint.Align.LEFT);
            textX = indicatorBounds.left + textPadding.left;
            break;
        }
        // Resolve vertical text position according to the requested gravity.
        switch (absoluteTextGravity & Gravity.VERTICAL_GRAVITY_MASK) {
        case Gravity.CENTER_VERTICAL:
            textY = indicatorBounds.centerY() + textSize / 2f;
            break;
        case Gravity.BOTTOM:
            textY = indicatorBounds.bottom - textPadding.bottom;
            break;
        case Gravity.TOP:
        default:
            textY = indicatorBounds.top + textSize + textPadding.top;
            break;
        }
        canvas.drawText(Integer.toString(getProgress()), textX, textY, textPaint);
    }

    /**
     */
    @NonNull
    @Override
    public Parcelable onSaveInstanceState() {
        final SavedState savedState = new SavedState(super.onSaveInstanceState());
        savedState.privateFlags = mDecorator.mPrivateFlags;
        return savedState;
    }

    /**
     */
    @Override
    public void onRestoreInstanceState(Parcelable state) {
        if (!(state instanceof SavedState)) {
            super.onRestoreInstanceState(state);
            return;
        }
        final SavedState savedState = (SavedState) state;
        super.onRestoreInstanceState(savedState.getSuperState());
        setDiscrete((savedState.privateFlags & PFLAG_DISCRETE) != 0);
        setDiscretePreviewEnabled((savedState.privateFlags & PFLAG_DISCRETE_PREVIEW_ENABLED) != 0);
    }

    /**
     * Inner classes ===============================================================================
     */

    /**
     * A {@link WidgetSavedState} implementation used to ensure that the state of {@link SeekBarWidget}
     * is properly saved.
     *
     * @author Martin Albedinsky
     */
    public static class SavedState extends WidgetSavedState {

        /**
         * Creator used to create an instance or array of instances of SavedState from {@link android.os.Parcel}.
         */
        public static final Creator<SavedState> CREATOR = new Creator<SavedState>() {
            /**
             */
            @Override
            public SavedState createFromParcel(@NonNull Parcel source) {
                return new SavedState(source);
            }

            /**
             */
            @Override
            public SavedState[] newArray(int size) {
                return new SavedState[size];
            }
        };

        /**
         */
        int privateFlags;

        /**
         * Creates a new instance of SavedState with the given <var>superState</var> to allow chaining
         * of saved states in {@link #onSaveInstanceState()} and also in {@link #onRestoreInstanceState(android.os.Parcelable)}.
         *
         * @param superState The super state obtained from {@code super.onSaveInstanceState()} within
         *                   {@code onSaveInstanceState()}.
         */
        protected SavedState(@NonNull Parcelable superState) {
            super(superState);
        }

        /**
         * Called from {@link #CREATOR} to create an instance of SavedState form the given parcel
         * <var>source</var>.
         *
         * @param source Parcel with data for the new instance.
         */
        protected SavedState(@NonNull Parcel source) {
            super(source);
            this.privateFlags = source.readInt();
        }

        /**
         */
        @Override
        public void writeToParcel(@NonNull Parcel dest, int flags) {
            super.writeToParcel(dest, flags);
            dest.writeInt(privateFlags);
        }
    }

    /**
     * This class holds all data necessary to tint all components of this view.
     */
    private static final class SeekBarTintInfo extends BackgroundTintInfo {

        /**
         * Color state list used to tint a specific states of the <b>primary progress</b> drawable.
         */
        ColorStateList primaryProgressTintList;

        /**
         * Flag indicating whether the {@link #primaryProgressTintList} has been set or not.
         */
        boolean hasPrimaryProgressTintList;

        /**
         * Blending mode used to apply tint to the <b>primary progress</b> drawable.
         */
        PorterDuff.Mode primaryProgressTintMode;

        /**
         * Flag indicating whether the {@link #primaryProgressTintMode} has been set or not.
         */
        boolean hasPrimaryProgressTintMode;

        /**
         * Color state list used to tint a specific states of the <b>secondary progress</b> drawable.
         */
        ColorStateList secondaryProgressTintList;

        /**
         * Flag indicating whether the {@link #secondaryProgressTintList} has been set or not.
         */
        boolean hasSecondaryProgressTintList;

        /**
         * Blending mode used to apply tint to the <b>secondary progress</b> drawable.
         */
        PorterDuff.Mode secondaryProgressTintMode;

        /**
         * Flag indicating whether the {@link #secondaryProgressTintMode} has been set or not.
         */
        boolean hasSecondaryProgressTintMode;

        /**
         * Color state list used to tint a specific states of the <b>progress background</b> drawable.
         */
        ColorStateList progressBackgroundTintList;

        /**
         * Flag indicating whether the {@link #progressBackgroundTintList} has been set or not.
         */
        boolean hasProgressBackgroundTintList;

        /**
         * Blending mode used to apply tint to the <b>progress background</b> drawable.
         */
        PorterDuff.Mode progressBackgroundTintMode;

        /**
         * Flag indicating whether the {@link #progressBackgroundTintMode} has been set or not.
         */
        boolean hasProgressBackgroundTintMode;

        /**
         * Color state list used to tint a specific states of the <b>discrete indicator</b> drawable.
         */
        ColorStateList discreteIndicatorTintList;

        /**
         * Blending mode used to apply tint to the <b>discrete indicator</b> drawable.
         */
        boolean hasDiscreteIndicatorTintList;

        /**
         * Blending mode used to apply tint to the <b>discrete indicator</b> drawable.
         */
        PorterDuff.Mode discreteIndicatorTintMode;

        /**
         * Flag indicating whether the {@link #discreteIndicatorTintMode} has been set or not.
         */
        boolean hasDiscreteIndicatorTintMode;
    }

    /**
     * Graphics info that holds all parameters necessary to draw progress text within discrete indicator.
     */
    private static final class DiscreteIndicatorTextInfo extends TextGraphicsInfo {

        /**
         * Flags determining where in the discrete indicator's area position the progress text.
         */
        int gravity = Gravity.CENTER;

        /**
         * Padding for the progress text.
         */
        final Rect padding;

        /**
         * Creates a new instance of DiscreteIndicatorTextInfo.
         */
        DiscreteIndicatorTextInfo() {
            super();
            this.padding = new Rect();
        }
    }

    /**
     * Graphics info that holds all parameters necessary to draw tick marks for discrete interval.
     */
    private static final class DiscreteIntervalTickMarkInfo extends ColorGraphicsInfo {

        /**
         * Radius of the tick marks of discrete interval.
         */
        float radius;

        /**
         * Creates a new instance of DiscreteIntervalTickMarkInfo.
         */
        DiscreteIntervalTickMarkInfo() {
            super();
        }
    }

    /**
     * Animations interface for this view.
     */
    private static abstract class Animations {

        /**
         * Duration for all animations related to discrete components.
         */
        static final long DISCRETE_COMPONENTS_ANIMATION_DURATION = 350;

        /**
         * Action to hide discrete components from the preview mode.
         */
        final Runnable HIDE_DISCRETE_COMPONENTS_FROM_PREVIEW = new Runnable() {

            /**
             */
            @Override
            public void run() {
                if (!view.isPressed() && onConcealDiscreteComponents()) {
                    discreteComponentsActive = false;
                }
            }
        };

        /**
         * View upon which will be animations performed.
         */
        final SeekBarWidget view;

        /**
         * Boolean flag indicating whether the preview of discrete components is active or not.
         */
        boolean discreteComponentsActive;

        /**
         * Current transformation value of the discrete components.
         */
        float transformation = 0;

        /**
         * Creates a new instance of Animations for the specified view.
         *
         * @param view The empty view for upon which to run animations.
         */
        Animations(SeekBarWidget view) {
            this.view = view;
        }

        /**
         * Returns a new instance of Animations implementation specific for the current animations
         * API capabilities.
         *
         * @param view The view upon which will the returned Animations object perform all requested
         *             animations.
         * @return New instance of Animations implementation.
         */
        static Animations get(SeekBarWidget view) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                return new LollipopAnimations(view);
            } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
                return new HoneyCombAnimations(view);
            }
            return new DefaultAnimations(view);
        }

        /**
         * Makes the given <var>thumb</var> drawable scaleable along a pivot determined by the specified
         * <var>gravity</var> flags.
         *
         * @see #makeDrawableScaleable(Drawable, int)
         */
        Drawable makeThumbScaleable(Drawable thumb, int gravity) {
            return makeDrawableScaleable(thumb, gravity);
        }

        /**
         * Makes the given <var>indicator</var> drawable scaleable along a pivot determined
         * by the specified <var>gravity</var> flags.
         *
         * @see #makeDrawableScaleable(Drawable, int)
         */
        Drawable makeDiscreteIndicatorScaleable(Drawable indicator, int gravity) {
            return makeDrawableScaleable(indicator, gravity);
        }

        /**
         * Wraps the given <var>drawable</var> into instance of {@link ScaleDrawable} if it is valid
         * and not ScaleDrawable yet.
         *
         * @param drawable The drawable to wrap and make scaleable.
         * @param gravity  The gravity determining a pivot along which can be the given drawable scaled.
         * @return Instance of ScaleDrawable with the specified drawable wrapped or {@code null}
         * if the given drawable was also {@code null}.
         */
        static Drawable makeDrawableScaleable(Drawable drawable, int gravity) {
            if (drawable == null || drawable instanceof ScaleDrawable)
                return drawable;
            final ScaleDrawable scaleDrawable = new ScaleDrawable(drawable, gravity, 1f, 1f);
            scaleDrawable.setLevel(MAX_LEVEL);
            return scaleDrawable;
        }

        /**
         * Like {@link #revealDiscreteComponents()} but this will reveal the discrete components
         * only temporarily and conceals them after the specified duration has been reached.
         *
         * @param delay    Delay with which should be the reveal animation started.
         * @param duration The duration for how long should be the discrete components previewed.
         */
        void previewDiscreteComponents(long delay, long duration) {
            view.removeCallbacks(HIDE_DISCRETE_COMPONENTS_FROM_PREVIEW);
            if (discreteComponentsActive) {
                view.postDelayed(HIDE_DISCRETE_COMPONENTS_FROM_PREVIEW, duration);
                return;
            }

            // Preview the discrete components for a while so a user can detect that the seek bar
            // is really in discrete mode.
            onRevealDiscreteComponents(delay);
            view.postDelayed(HIDE_DISCRETE_COMPONENTS_FROM_PREVIEW, duration);
            this.discreteComponentsActive = true;
        }

        /**
         * Reveals all discrete components with an animation if they are not active (visible) at this
         * time yet.
         */
        void revealDiscreteComponents() {
            if (!discreteComponentsActive) {
                onRevealDiscreteComponents(0);
                this.discreteComponentsActive = true;
            }
        }

        /**
         * Invoked whenever {@link #revealDiscreteComponents()} or {@link #previewDiscreteComponents(long, long)}
         * is called an the discrete components are not active (visible) at the time.
         *
         * @param delay The delay with which should be the reveal animation started.
         * @return {@code True} if the reveal animation for the discrete components has been started,
         * {@code false otherwise}.
         */
        abstract boolean onRevealDiscreteComponents(long delay);

        /**
         * Conceals all discrete components with an animation if they are active (visible) at this time.
         */
        void concealDiscreteComponents() {
            if (discreteComponentsActive) {
                view.removeCallbacks(HIDE_DISCRETE_COMPONENTS_FROM_PREVIEW);
                this.discreteComponentsActive = false;
                onConcealDiscreteComponents();
            }
        }

        /**
         * Invoked whenever {@link #concealDiscreteComponents()} is called an the discrete components
         * are active (visible) at the time.
         *
         * @return {@code True} if the conceal animation for the discrete components has been started,
         * {@code false otherwise}.
         */
        abstract boolean onConcealDiscreteComponents();

        /**
         * Specifies a transformation for the discrete components. Depends on the implementation,
         * this can change alpha value of some discrete components or theirs current scale for example.
         * <p>
         * This method can be used to animate revealing/concealing of the discrete components or theirs
         * immediate hiding/showing.
         *
         * @param transformation The desired transformation from the range {@code [0.0, 1.0]}.
         *                       Transformation {@code 0.0} means that discrete components will be
         *                       hided, {@code 1.0} means that they will be visible.
         */
        @Keep
        void setDiscreteTransformation(float transformation) {
            if (this.transformation != transformation) {
                this.transformation = transformation;
                // Scale up/down the discrete indicator and the thumb in a way when one is fully scaled
                // up the other is fully scaled down and reversed.
                setThumbScale(1 - transformation);
                setDiscreteIndicatorScale(transformation);

                // Fade in/out the text of discrete indicator during the indicator is at least 75% transformed/visible.
                if (transformation > 0.75) {
                    final int alpha = Math.round((transformation - 0.75f) / 0.25f * 255);
                    setDiscreteIndicatorTextAlpha(alpha);
                } else {
                    setDiscreteIndicatorTextAlpha(0);
                }

                // Fade in/out discrete interval.
                setDiscreteIntervalAlpha(Math.round(transformation * 255));
                invalidate();
            }
        }

        /**
         * Updates a scale level of the thumb's drawable.
         *
         * @param scale The scale value from the range {@code [0.0, 1.0]}.
         */
        void setThumbScale(float scale) {
            if (view.mThumb instanceof ScaleDrawable) {
                final int scaleLevel = Math.round(scale * MAX_LEVEL);
                view.mThumb.setLevel(Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN ? scaleLevel :
                // Correct scale level for pre JELLY_BEAN Android versions.
                // scaleLevel(10000) = scale(1.0) [expected scale(1.0)]
                // scaleLevel(5000)  = scale(0.0) [expected scale(0.5)]
                // scaleLevel(0)     = scale(1.0) [expected scale(0.0)]
                        scaleLevel + (int) ((10000 - scaleLevel) / 10000f * 5000));
            }
        }

        /**
         * Updates a scale level of the discrete indicator's drawable along with its text size.
         *
         * @param scale The scale value from the range {@code [0.0, 1.0]}.
         */
        void setDiscreteIndicatorScale(float scale) {
            updateDrawableScale(view.mDiscreteIndicator, scale);
            view.DISCRETE_INDICATOR_TEXT_INFO.paint
                    .setTextSize(view.DISCRETE_INDICATOR_TEXT_INFO.mAppearance.getTextSize() * scale);
        }

        /**
         * Updates a scale level of the given <var>drawable</var> according to the specified scale
         * value.
         *
         * @param drawable The drawable of which scale level to update.
         * @param scale    The scale value from the range {@code [0.0, 1.0]}.
         */
        private void updateDrawableScale(Drawable drawable, float scale) {
            if (drawable instanceof ScaleDrawable)
                drawable.setLevel(Math.round(scale * MAX_LEVEL));
        }

        /**
         * Specifies an alpha value for the graphics of the text of discrete indicator.
         *
         * @param alpha The desired alpha from the range {@code [0, 255]}.
         */
        void setDiscreteIndicatorTextAlpha(int alpha) {
            view.DISCRETE_INDICATOR_TEXT_INFO.paint.setAlpha(alpha);
        }

        /**
         * Specifies an alpha value for the graphics of the discrete interval.
         *
         * @param alpha The desired alpha from the range {@code [0, 255]}.
         */
        void setDiscreteIntervalAlpha(int alpha) {
            view.DISCRETE_INTERVAL_TICK_MARK_INFO.paint.setAlpha(alpha);
        }

        /**
         * Causes invalidation of the attached view.
         */
        final void invalidate() {
            view.invalidate();
        }

        /**
         * Checks whether components that are animated should be drawn or not.
         *
         * @return {@code True} to draw components animated by this object, {@code false} otherwise.
         */
        boolean shouldDraw() {
            return transformation > 0;
        }

        /**
         * Checks whether some animations are running or not.
         *
         * @return {@code True} if at least one animation is running at this time, {@code false} otherwise.
         */
        abstract boolean areRunning();

        /**
         * Cancels all running animations.
         */
        abstract void cancel();
    }

    /**
     * Default implementation of {@link Animations}.
     */
    private static final class DefaultAnimations extends Animations {

        /**
         * Animation used to reveal discrete components.
         */
        private final Animation revealDiscreteComponentsAnimation;

        /**
         * Animation used to conceal discrete components.
         */
        private final Animation concealDiscreteComponentsAnimation;

        /**
         * See {@link Animations#Animations(SeekBarWidget)}.
         */
        DefaultAnimations(SeekBarWidget view) {
            super(view);
            this.revealDiscreteComponentsAnimation = new DiscreteComponentsAnimation(this, 0.0f, 1.0f);
            revealDiscreteComponentsAnimation.setDuration(DISCRETE_COMPONENTS_ANIMATION_DURATION);

            this.concealDiscreteComponentsAnimation = new DiscreteComponentsAnimation(this, 1.0f, 0.0f);
            concealDiscreteComponentsAnimation.setDuration(DISCRETE_COMPONENTS_ANIMATION_DURATION);
        }

        /**
         */
        @Override
        boolean onRevealDiscreteComponents(long delay) {
            revealDiscreteComponentsAnimation.setStartOffset(delay);
            view.startAnimation(revealDiscreteComponentsAnimation);
            return false;
        }

        /**
         */
        @Override
        boolean onConcealDiscreteComponents() {
            view.startAnimation(concealDiscreteComponentsAnimation);
            return false;
        }

        /**
         */
        @Override
        boolean areRunning() {
            final Animation animation = view.getAnimation();
            return animation == revealDiscreteComponentsAnimation
                    || animation == concealDiscreteComponentsAnimation;
        }

        /**
         */
        @Override
        void cancel() {
            view.clearAnimation();
        }
    }

    /**
     * An {@link Animations} implementation used for post {@link android.os.Build.VERSION_CODES#HONEYCOMB HONEYCOMB}
     * Android versions.
     */
    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    private static class HoneyCombAnimations extends Animations {

        /**
         * Animator used to animate discrete components.
         */
        private final ObjectAnimator discreteComponentsAnimator;

        /**
         * See {@link Animations#Animations(SeekBarWidget)}.
         */
        HoneyCombAnimations(SeekBarWidget view) {
            super(view);
            this.discreteComponentsAnimator = ObjectAnimator.ofFloat(this, "discreteTransformation", 0, 0);
            this.discreteComponentsAnimator.setDuration(DISCRETE_COMPONENTS_ANIMATION_DURATION);
        }

        /**
         */
        @Override
        boolean onRevealDiscreteComponents(long delay) {
            if (transformation == 1)
                return false;
            discreteComponentsAnimator.setFloatValues(transformation, 1f);
            discreteComponentsAnimator.setStartDelay(delay);
            discreteComponentsAnimator.start();
            return true;
        }

        /**
         */
        @Override
        boolean onConcealDiscreteComponents() {
            if (transformation == 0)
                return false;
            discreteComponentsAnimator.setFloatValues(transformation, 0f);
            discreteComponentsAnimator.setStartDelay(0);
            discreteComponentsAnimator.start();
            return true;
        }

        /**
         */
        @Override
        boolean areRunning() {
            return discreteComponentsAnimator.isRunning();
        }

        /**
         */
        @Override
        void cancel() {
            if (discreteComponentsAnimator.isRunning()) {
                discreteComponentsAnimator.cancel();
            }
        }
    }

    /**
     * An {@link Animations} implementation used for post {@link android.os.Build.VERSION_CODES#LOLLIPOP LOLLIPOP}
     * Android versions.
     */
    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    private static final class LollipopAnimations extends HoneyCombAnimations {

        /**
         * See {@link HoneyCombAnimations#HoneyCombAnimations(SeekBarWidget)}.
         */
        LollipopAnimations(SeekBarWidget view) {
            super(view);
        }

        /**
         */
        @Override
        Drawable makeThumbScaleable(Drawable thumb, int gravity) {
            // fixme: Unfortunately on LOLLIPOP and higher the thumb wrapped into ScaleDrawable is
            // fixme: drawn by the framework with some alpha mask or whatever and that causes the
            // fixme: progress graphics not to be drawn behind the thumb, and also mThumb.setLevel(int)
            // fixme: with current scale level is not working so the thumb is not being scaled at all
            // fixme: because the thumb drawable on LOLLIPOP is actually the animated-selector and
            // fixme: it appears that such drawable cannot be scaled.
            return thumb;
        }
    }

    /**
     * An {@link Animation} implementation that can be used to animate transformation value of
     * discrete components.
     */
    private static final class DiscreteComponentsAnimation extends Animation {

        /**
         * Animations instance that can handle transformation change of discrete components.
         */
        final Animations animations;

        /**
         * Start transformation value from which to animate.
         */
        final float fromTransformation;

        /**
         * End transformation value to which to animate.
         */
        final float toTransformation;

        /**
         * Creates a new instance of DiscreteComponentsAnimation with the specified parameters.
         *
         * @param animations         The animations instance to which we can delegate animated transformation
         *                           change.
         * @param fromTransformation The transformation value from which to animate.
         * @param toTransformation   The transformation value to which to animate.
         */
        DiscreteComponentsAnimation(Animations animations, float fromTransformation, float toTransformation) {
            this.animations = animations;
            this.fromTransformation = fromTransformation;
            this.toTransformation = toTransformation;
        }

        /**
         */
        @Override
        protected void applyTransformation(float interpolatedTime, Transformation transformation) {
            final float trans = fromTransformation;
            animations.setDiscreteTransformation(trans + ((toTransformation - trans) * interpolatedTime));
        }
    }

    /**
     * Decorator implementation for this widget.
     */
    private final class Decorator extends FontDecorator<SeekBarWidget> {

        /**
         * See {@link WidgetDecorator#WidgetDecorator(View, int[])}.
         */
        Decorator(SeekBarWidget widget) {
            super(widget, R.styleable.Ui_SeekBar);
        }

        /**
         */
        @Override
        BackgroundTintInfo onCreateTintInfo() {
            return new SeekBarTintInfo();
        }

        /**
         */
        @NonNull
        @Override
        SeekBarTintInfo getTintInfo() {
            return (SeekBarTintInfo) super.getTintInfo();
        }

        /**
         */
        @Override
        @SuppressWarnings("ResourceType")
        void onProcessTintValues(Context context, TypedArray tintArray, int tintColor) {
            final SeekBarTintInfo tintInfo = getTintInfo();
            tintInfo.discreteIndicatorTintList = TintManager.createSeekBarThumbTintColors(getContext(), tintColor);
            if (tintArray.hasValue(R.styleable.Ui_SeekBar_uiDiscreteIndicatorTint)) {
                tintInfo.discreteIndicatorTintList = tintArray
                        .getColorStateList(R.styleable.Ui_SeekBar_uiDiscreteIndicatorTint);
            }
            tintInfo.discreteIndicatorTintMode = TintManager.parseTintMode(
                    tintArray.getInt(R.styleable.Ui_SeekBar_uiDiscreteIndicatorTintMode, 0),
                    PorterDuff.Mode.SRC_IN);
            if (UiConfig.MATERIALIZED) {
                if (tintArray.hasValue(R.styleable.Ui_SeekBar_uiThumbTint)) {
                    setThumbTintList(tintArray.getColorStateList(R.styleable.Ui_SeekBar_uiThumbTint));
                }
                if (tintArray.hasValue(R.styleable.Ui_SeekBar_uiProgressTint)) {
                    setProgressTintList(tintArray.getColorStateList(R.styleable.Ui_SeekBar_uiProgressTint));
                }
                if (tintArray.hasValue(R.styleable.Ui_SeekBar_uiProgressBackgroundTint)) {
                    setProgressBackgroundTintList(
                            tintArray.getColorStateList(R.styleable.Ui_SeekBar_uiProgressBackgroundTint));
                }
                if (tintArray.hasValue(R.styleable.Ui_SeekBar_uiBackgroundTint)) {
                    setBackgroundTintList(tintArray.getColorStateList(R.styleable.Ui_SeekBar_uiBackgroundTint));
                }
                if (tintArray.hasValue(R.styleable.Ui_SeekBar_uiThumbTintMode)) {
                    setThumbTintMode(TintManager.parseTintMode(
                            tintArray.getInt(R.styleable.Ui_SeekBar_uiThumbTintMode, 0), PorterDuff.Mode.SRC_IN));
                }
                if (tintArray.hasValue(R.styleable.Ui_SeekBar_uiProgressTintMode)) {
                    setProgressTintMode(TintManager.parseTintMode(
                            tintArray.getInt(R.styleable.Ui_SeekBar_uiProgressTintMode, 0),
                            PorterDuff.Mode.SRC_IN));
                }
                if (tintArray.hasValue(R.styleable.Ui_SeekBar_uiProgressBackgroundTintMode)) {
                    setProgressBackgroundTintMode(TintManager.parseTintMode(
                            tintArray.getInt(R.styleable.Ui_SeekBar_uiProgressBackgroundTintMode, 0),
                            PorterDuff.Mode.SRC_IN));
                }
                if (tintArray.hasValue(R.styleable.Ui_SeekBar_uiBackgroundTintMode)) {
                    setBackgroundTintMode(TintManager.parseTintMode(
                            tintArray.getInt(R.styleable.Ui_SeekBar_uiBackgroundTintMode, 0),
                            PorterDuff.Mode.SRC_IN));
                }
            } else {
                tintInfo.tintList = TintManager.createSeekBarThumbTintColors(getContext(), tintColor);
                if (tintArray.hasValue(R.styleable.Ui_SeekBar_uiThumbTint)) {
                    tintInfo.tintList = tintArray.getColorStateList(R.styleable.Ui_SeekBar_uiThumbTint);
                }
                tintInfo.primaryProgressTintList = TintManager.createSeekBarProgressTintColors(getContext(),
                        tintColor);
                if (tintArray.hasValue(R.styleable.Ui_SeekBar_uiProgressTint)) {
                    tintInfo.primaryProgressTintList = tintArray
                            .getColorStateList(R.styleable.Ui_SeekBar_uiProgressTint);
                }
                tintInfo.progressBackgroundTintList = TintManager
                        .createSeekBarProgressBackgroundTintColors(getContext(), tintColor);
                if (tintArray.hasValue(R.styleable.Ui_SeekBar_uiProgressBackgroundTint)) {
                    tintInfo.progressBackgroundTintList = tintArray
                            .getColorStateList(R.styleable.Ui_SeekBar_uiProgressBackgroundTint);
                }
                if (tintArray.hasValue(R.styleable.Ui_SeekBar_uiBackgroundTint)) {
                    tintInfo.backgroundTintList = tintArray
                            .getColorStateList(R.styleable.Ui_SeekBar_uiBackgroundTint);
                }
                tintInfo.tintMode = TintManager.parseTintMode(
                        tintArray.getInt(R.styleable.Ui_SeekBar_uiThumbTintMode, 0), PorterDuff.Mode.SRC_IN);
                tintInfo.primaryProgressTintMode = TintManager.parseTintMode(
                        tintArray.getInt(R.styleable.Ui_SeekBar_uiProgressTintMode, 0), PorterDuff.Mode.SRC_IN);
                tintInfo.progressBackgroundTintMode = TintManager.parseTintMode(
                        tintArray.getInt(R.styleable.Ui_SeekBar_uiProgressBackgroundTintMode, 0),
                        PorterDuff.Mode.SRC_IN);
                tintInfo.backgroundTintMode = TintManager.parseTintMode(
                        tintArray.getInt(R.styleable.Ui_SeekBar_uiBackgroundTintMode, 0),
                        tintInfo.backgroundTintList != null ? PorterDuff.Mode.SRC_IN : null);
            }
        }

        /**
         */
        @Override
        void onTintValuesProcessed() {
            final SeekBarTintInfo tintInfo = getTintInfo();
            // If there is no tint modes specified within style/xml do not tint at all.
            if (tintInfo.primaryProgressTintMode == null)
                tintInfo.primaryProgressTintList = null;
            if (tintInfo.secondaryProgressTintMode == null)
                tintInfo.secondaryProgressTintList = null;
            if (tintInfo.progressBackgroundTintMode == null)
                tintInfo.progressBackgroundTintList = null;
            if (tintInfo.discreteIndicatorTintMode == null)
                tintInfo.discreteIndicatorTintList = null;
            tintInfo.hasPrimaryProgressTintList = tintInfo.primaryProgressTintList != null;
            tintInfo.hasPrimaryProgressTintMode = tintInfo.primaryProgressTintMode != null;
            tintInfo.hasSecondaryProgressTintList = tintInfo.secondaryProgressTintList != null;
            tintInfo.hasSecondaryProgressTintMode = tintInfo.secondaryProgressTintMode != null;
            tintInfo.hasProgressBackgroundTintList = tintInfo.progressBackgroundTintList != null;
            tintInfo.hasProgressBackgroundTintMode = tintInfo.progressBackgroundTintMode != null;
            tintInfo.hasDiscreteIndicatorTintList = tintInfo.discreteIndicatorTintList != null;
            tintInfo.hasDiscreteIndicatorTintMode = tintInfo.discreteIndicatorTintMode != null;
            super.onTintValuesProcessed();
        }

        /**
         */
        @Override
        boolean shouldInvalidateTintInfo(@NonNull BackgroundTintInfo tintInfo) {
            final SeekBarTintInfo info = (SeekBarTintInfo) tintInfo;
            return !info.hasPrimaryProgressTintList && !info.hasPrimaryProgressTintMode
                    && !info.hasSecondaryProgressTintList && !info.hasSecondaryProgressTintMode
                    && !info.hasProgressBackgroundTintList && !info.hasProgressBackgroundTintMode
                    && !info.hasDiscreteIndicatorTintList && !info.hasDiscreteIndicatorTintMode
                    && super.shouldInvalidateTintInfo(tintInfo);
        }

        /**
         */
        @Override
        void superSetSelected(boolean selected) {
            SeekBarWidget.super.setSelected(selected);
        }

        /**
         */
        @Override
        @SuppressWarnings("deprecation")
        void superSetBackgroundDrawable(Drawable drawable) {
            SeekBarWidget.super.setBackgroundDrawable(drawable);
        }

        /**
         */
        @Override
        @TargetApi(Build.VERSION_CODES.LOLLIPOP)
        void superSetBackgroundTintList(ColorStateList tint) {
            SeekBarWidget.super.setBackgroundTintList(tint);
        }

        /**
         */
        @Override
        @TargetApi(Build.VERSION_CODES.LOLLIPOP)
        ColorStateList superGetBackgroundTintList() {
            return SeekBarWidget.super.getBackgroundTintList();
        }

        /**
         */
        @Override
        @TargetApi(Build.VERSION_CODES.LOLLIPOP)
        void superSetBackgroundTintMode(PorterDuff.Mode tintMode) {
            SeekBarWidget.super.setBackgroundTintMode(tintMode);
        }

        /**
         */
        @Override
        @TargetApi(Build.VERSION_CODES.LOLLIPOP)
        PorterDuff.Mode superGetBackgroundTintMode() {
            return SeekBarWidget.super.getBackgroundTintMode();
        }
    }
}