com.albedinsky.android.support.ui.widget.BaseProgressBar.java Source code

Java tutorial

Introduction

Here is the source code for com.albedinsky.android.support.ui.widget.BaseProgressBar.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.support.ui.widget;

import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.util.Pools;
import android.util.AttributeSet;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;

import com.albedinsky.android.support.ui.R;
import com.albedinsky.android.support.ui.UiConfig;
import com.albedinsky.android.support.ui.graphics.drawable.ProgressDrawable;
import com.albedinsky.android.support.ui.view.ViewWidget;

import java.util.ArrayList;
import java.util.List;

/**
 * A {@link android.view.View} implementation which represents a base container for {@link ProgressDrawable}
 * to draw a progress or an indeterminate graphics and primary to handle logic for which the
 * ProgressDrawable does not have enough capacity, like starting and stopping animations.
 * <p>
 * The BaseProgressBar supports base logic to properly present the attached ProgressDrawable like
 * measuring based on the size of the drawable and also its drawing. This class also handles base
 * management, like starting/stopping of indeterminate animations for the progress drawable based on
 * its current mode (determinate or indeterminate), other modes need to be managed by a specific
 * implementation of the BaseProgressBar class.
 * <p>
 * The ProgressDrawable can be specified by {@link #setDrawable(ProgressDrawable)} and can be accessed
 * via {@link #getDrawable()} which allows some customizations of the progress drawable's appearance.
 * There are also provided (delegated) some methods for direct access to the drawable like, {@link #setMode(int)}
 * {@link #setProgress(int)} or {@link #startIndeterminate(boolean)} and {@link #stopIndeterminate(boolean, boolean)}.
 *
 * <h3>Tinting</h3>
 * Tinting of this view is supported via extended tint API of the {@link ProgressDrawable}. For this
 * purpose, the BaseProgressBar class provides (delegates) following methods:
 * <ul>
 * <li>{@link #setProgressTintList(android.content.res.ColorStateList)}</li>
 * <li>{@link #setProgressTintMode(android.graphics.PorterDuff.Mode)}</li>
 * <li>{@link #setIndeterminateTintList(android.content.res.ColorStateList)}</li>
 * <li>{@link #setIndeterminateTintMode(android.graphics.PorterDuff.Mode)}</li>
 * <li>{@link #setProgressBackgroundTintList(android.content.res.ColorStateList)}</li>
 * <li>{@link #setProgressBackgroundTintMode(android.graphics.PorterDuff.Mode)}</li>
 * </ul>
 * See {@link ProgressDrawable ProgressDrawable's class} overview for additional info about tinting
 * process.
 *
 * <h3>Styling</h3>
 * <ul>
 * <li>{@link android.R.attr#progress android:progress}</li>
 * <li>{@link android.R.attr#max android:max}</li>
 * <li>{@link android.R.attr#thickness android:thickness}</li>
 * <li>{@link R.attr#uiColorProgress uiColorProgress}</li>
 * <li>{@link R.attr#uiColorsProgress uiColorsProgress}</li>
 * <li>{@link R.attr#uiMultiColored uiMultiColored}</li>
 * <li>{@link R.attr#uiColorProgressBackground uiColorProgressBackground}</li>
 * <li>{@link R.attr#uiRounded uiRounded}</li>
 * <li>{@link R.attr#uiIndeterminateSpeed uiIndeterminateSpeed}</li>
 * <li>{@link R.attr#uiProgressTint uiProgressTint}</li>
 * <li>{@link R.attr#uiProgressTintMode uiProgressTintMode}</li>
 * <li>{@link R.attr#uiIndeterminateTint uiIndeterminateTint}</li>
 * <li>{@link R.attr#uiIndeterminateTintMode uiIndeterminateTintMode}</li>
 * <li>{@link R.attr#uiProgressBackgroundTint uiProgressBackgroundTint}</li>
 * <li>{@link R.attr#uiProgressBackgroundTintMode uiProgressBackgroundTintMode}</li>
 * <li>{@link R.attr#uiBackgroundTint uiBackgroundTint} [pre LOLLIPOP]</li>
 * <li>{@link R.attr#uiBackgroundTintMode uiBackgroundTintMode} [pre LOLLIPOP]</li>
 * </ul>
 *
 * @author Martin Albedinsky
 */
public abstract class BaseProgressBar<D extends ProgressDrawable> extends ViewWidget
        implements ProgressDrawable.AnimationCallback, ProgressDrawable.ExplodeAnimationCallback {

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

    /**
     * Listener which can receive callbacks about <b>started</b> or <b>stopped</b> animation session
     * of progress drawable.
     */
    public static interface OnProgressAnimationListener {

        /**
         * Invoked whenever a new animation session is started for the specified progress <var>drawable</var>.
         *
         * @param progressBar A progress bar to which is the specified drawable attached to.
         * @param drawable    The progress drawable for which has been requested new animation session
         *                    by {@link ProgressDrawable#start()} and the drawable has been before that
         *                    call in the idle mode.
         */
        public void onStarted(@NonNull BaseProgressBar progressBar, @NonNull ProgressDrawable drawable);

        /**
         * Invoked whenever the current animation sessions is stopped for the specified progress
         * <var>drawable</var>.
         *
         * @param progressBar A progress bar to which is the specified drawable attached to.
         * @param drawable    The progress drawable for which has been stopped its current animation
         *                    sessions by {@link ProgressDrawable#stop()} or {@link ProgressDrawable#stop(boolean)}
         *                    and the drawable has been before that call in the animation mode.
         */
        public void onStopped(@NonNull BaseProgressBar progressBar, @NonNull ProgressDrawable drawable);
    }

    /**
     * Listener which can receive callbacks about <b>exploded</b> and <b>imploded</b> thickness of
     * progress drawable.
     */
    public static interface OnProgressExplodeAnimationListener {

        /**
         * Invoked whenever an explosion of the specified progress <var>drawable</var> is finished.
         *
         * @param progressBar A progress bar to which is the specified drawable attached to.
         * @param drawable    The progress drawable for which has been explosion of its thickness
         *                    finished after {@link ProgressDrawable#explode()} has been called upon the
         *                    drawable.
         */
        public void onExploded(@NonNull BaseProgressBar progressBar, @NonNull ProgressDrawable drawable);

        /**
         * Invoked whenever an implosion of the specified progress <var>drawable</var> is finished.
         *
         * @param progressBar A progress bar to which is the specified drawable attached to.
         * @param drawable    The progress drawable for which has been implosion of its thickness
         *                    finished after {@link ProgressDrawable#implode()} has been called upon the
         *                    drawable.
         */
        public void onImploded(@NonNull BaseProgressBar progressBar, @NonNull ProgressDrawable drawable);
    }

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

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

    /**
     * Flag indicating whether the output trough log-cat is enabled or not.
     */
    // private static final boolean LOG_ENABLED = true;

    /**
     * Flag indicating whether the debug output trough log-cat is enabled or not.
     */
    // private static final boolean DEBUG_ENABLED = true;

    /**
     * Flag copied from {@link ProgressDrawable#DETERMINATE} for better access.
     */
    private static final int DETERMINATE = ProgressDrawable.DETERMINATE;

    /**
     * Flag copied from {@link ProgressDrawable#INDETERMINATE} for better access.
     */
    private static final int INDETERMINATE = ProgressDrawable.INDETERMINATE;

    /**
     * Delay for posting of an accessibility events from this view.
     */
    private static final long ACCESSIBILITY_EVENT_DELAY = 200;

    /**
     * Flag indicating whether {@link #mRefreshProgressRunnable} has been posted or not.
     */
    private static final int PFLAG_REFRESH_PROGRESS_POSTED = 0x00008000;

    /**
     * Flag indicating whether an indeterminate animation should be stopped after the progress
     * drawable has been imploded.
     */
    private static final int PFLAG_STOP_INDETERMINATE_AFTER_IMPLOSION = 0x00010000;

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

    /**
     * Lock used for synchronized operations.
     */
    static final Object LOCK = new Object();

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

    /**
     * An application resources.
     */
    final Resources mResources;

    /**
     * Drawable used to draw the progress.
     */
    D mDrawable;

    /**
     * Current mode of this progress bar determining the drawing behaviour of the progress drawable.
     */
    int mMode;

    /**
     * Progress drawable's dimension.
     */
    private int mDrawableWidth, mDrawableHeight;

    /**
     * Set of private flags specific for this widget.
     */
    private int mPrivateFlags = PrivateFlags.PFLAG_ALLOWS_DEFAULT_SELECTION;

    /**
     * Maximum allowed value of progress which can be set to this progress bar.
     */
    int mMax = ProgressDrawable.MAX_PROGRESS;

    /**
     * Id of the UI thread used to check if a specific method call is on UI thread or not.
     */
    final long mUiThreadId;

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

    /**
     * Animation callback delegate for the current ProgressDrawable.
     */
    private OnProgressAnimationListener mProgressAnimationListener;

    /**
     * Explode animation callback delegate for the current ProgressDrawable.
     */
    private OnProgressExplodeAnimationListener mProgressExplodeAnimationListener;

    /**
     * Data used when tinting components of this view.
     */
    private TintInfo mTintInfo;

    /**
     * Task used to refresh progress from the background thread.
     */
    private RefreshProgressRunnable mRefreshProgressRunnable;

    /**
     * Task used to post an accessibility event for the changed progress.
     */
    private AccessibilityEventSender mAccessibilityEventSender;

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

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

    /**
     * Same as {@link #BaseProgressBar(android.content.Context, android.util.AttributeSet, int)}
     * with <code>0</code> as attribute for default style.
     */
    public BaseProgressBar(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    /**
     * Creates a new instance of BaseProgressBar within the given <var>context</var>.
     *
     * @param context      Context in which will be this 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.
     */
    public BaseProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.mResources = context.getResources();
        this.mUiThreadId = Thread.currentThread().getId();

        onAttachDrawable();
        if (mDrawable == null) {
            throw new IllegalArgumentException("No progress drawable has been attached.");
        }

        /**
         * Process attributes.
         */
        final TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.Ui_Widget_ProgressBar,
                defStyleAttr, 0);
        if (typedArray != null) {
            this.processTintValues(context, typedArray);

            final int n = typedArray.getIndexCount();
            for (int i = 0; i < n; i++) {
                final int index = typedArray.getIndex(i);
                if (index == R.styleable.Ui_Widget_ProgressBar_uiColorProgress) {
                    mDrawable.setColor(typedArray.getColor(index, mDrawable.getColor()));
                } else if (index == R.styleable.Ui_Widget_ProgressBar_uiColorsProgress) {
                    final int colorsResId = typedArray.getResourceId(index, -1);
                    if (colorsResId > 0) {
                        mDrawable.setColors(mResources.getIntArray(colorsResId));
                    }
                } else if (index == R.styleable.Ui_Widget_ProgressBar_uiMultiColored) {
                    mDrawable.setMultiColored(typedArray.getBoolean(index, mDrawable.isMultiColored()));
                } else if (index == R.styleable.Ui_Widget_ProgressBar_uiColorProgressBackground) {
                    mDrawable.setBackgroundColor(typedArray.getInt(index, Color.TRANSPARENT));
                } else if (index == R.styleable.Ui_Widget_ProgressBar_android_thickness) {
                    mDrawable.setThickness(typedArray.getDimensionPixelSize(index, 0));
                } else if (index == R.styleable.Ui_Widget_ProgressBar_uiRounded) {
                    mDrawable.setRounded(typedArray.getBoolean(index, false));
                } else if (index == R.styleable.Ui_Widget_ProgressBar_uiIndeterminateSpeed) {
                    mDrawable.setIndeterminateSpeed(typedArray.getFloat(index, 1));
                }
            }
        }

        this.applyProgressTint();
        this.applyIndeterminateTint();
        this.applyProgressBackgroundTint();
    }

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

    /**
     * Public --------------------------------------------------------------------------------------
     */

    /**
     * Same as {@link #startIndeterminate(boolean)} with <var>explode</var> flag set to {@code false}.
     */
    public void startIndeterminate() {
        startIndeterminate(false);
    }

    /**
     * Starts an indeterminate animation session (if not running already) for the progress
     * drawable with graphics explosion if requested. Use this method to start the indeterminate
     * animation if you stopped it before by {@link #stopIndeterminate()} or
     * {@link #stopIndeterminate(boolean, boolean)}.
     * <p>
     * This method groups {@link ProgressDrawable#start()} and {@link ProgressDrawable#explode()} for
     * better usability.
     *
     * @param explode {@code True} if the drawable's graphics should be also exploded, {@code false}
     *                otherwise.
     * @see #stopIndeterminate(boolean, boolean)
     * @see ProgressDrawable#setExploded(boolean)
     */
    public void startIndeterminate(boolean explode) {
        if (mDrawable != null && mMode != DETERMINATE && !mDrawable.isRunning()
                && hasPrivateFlag(PrivateFlags.PFLAG_ATTACHED_TO_WINDOW)) {
            if (explode) {
                mDrawable.setExploded(false);
                mDrawable.explode();
            }
            mDrawable.start();
        }
    }

    /**
     * Same as {@link #stopIndeterminate(boolean, boolean)} with <var>immediate</var> flag set to
     * {@code false} and <var>implode</var> flag set to {@code false}.
     */
    public void stopIndeterminate() {
        stopIndeterminate(false, false);
    }

    /**
     * Stops the current (if running) indeterminate animation session for the progress drawable with
     * graphics implosion if requested. Use this method if you want to stop the indeterminate animation.
     * <p>
     * This method groups {@link ProgressDrawable#stop(boolean)} and {@link ProgressDrawable#implode()}
     * for better usability.
     * <p>
     * <b>Note</b>, that if implosion of the indeterminate graphics if requested, the indeterminate
     * animation will be stopped immediately after the implode animation finishes.
     *
     * @param immediate {@code True} to stop the indeterminate animation immediately, {@code false}
     *                  to let the drawable finish drawing of the indeterminate graphics for more
     *                  natural user experience.
     * @param implode   {@code True} if the drawable's graphics should be also imploded, {@code false}
     *                  otherwise.
     */
    public void stopIndeterminate(boolean immediate, boolean implode) {
        if (mDrawable != null && mMode != DETERMINATE && mDrawable.isRunning()) {
            if (implode) {
                this.updatePrivateFlags(PFLAG_STOP_INDETERMINATE_AFTER_IMPLOSION, true);
                if (!immediate) {
                    mDrawable.stop(false);
                }
                mDrawable.setExploded(true);
                mDrawable.implode();
                return;
            }
            mDrawable.stop(immediate);
        }
    }

    /**
     * Delegate method for {@link ProgressDrawable#explode()}.
     */
    public void explodeProgress() {
        if (mDrawable != null) {
            mDrawable.setExploded(false);
            mDrawable.explode();
        }
    }

    /**
     * Delegate method for {@link ProgressDrawable#implode()}.
     */
    public void implodeProgress() {
        if (mDrawable != null) {
            mDrawable.setExploded(true);
            mDrawable.implode();
        }
    }

    /**
     * Delegate method for {@link ProgressDrawable#setExploded(boolean)}.
     */
    public void setProgressExploded(boolean exploded) {
        if (mDrawable != null) {
            mDrawable.setExploded(exploded);
        }
    }

    /**
     * Delegate method for {@link ProgressDrawable#isExploded()}.
     */
    public boolean isProgressExploded() {
        return mDrawable != null && mDrawable.isExploded();
    }

    /**
     */
    @Override
    public void jumpDrawablesToCurrentState() {
        super.jumpDrawablesToCurrentState();
        if (mDrawable != null) {
            mDrawable.jumpToCurrentState();
        }
    }

    /**
     */
    @Override
    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public void drawableHotspotChanged(float x, float y) {
        super.drawableHotspotChanged(x, y);
        if (mDrawable != null) {
            mDrawable.setHotspot(x, y);
        }
    }

    /**
     */
    @Override
    public void onStarted(@NonNull ProgressDrawable drawable) {
        if (mProgressAnimationListener != null) {
            mProgressAnimationListener.onStarted(this, drawable);
        }
    }

    /**
     */
    @Override
    public void onStopped(@NonNull ProgressDrawable drawable) {
        if (mProgressAnimationListener != null) {
            mProgressAnimationListener.onStopped(this, drawable);
        }
    }

    /**
     */
    @Override
    public void onExploded(@NonNull ProgressDrawable drawable) {
        if (mProgressExplodeAnimationListener != null) {
            mProgressExplodeAnimationListener.onExploded(this, drawable);
        }
    }

    /**
     */
    @Override
    public void onImploded(@NonNull ProgressDrawable drawable) {
        if (hasPrivateFlag(PFLAG_STOP_INDETERMINATE_AFTER_IMPLOSION)) {
            this.updatePrivateFlags(PFLAG_STOP_INDETERMINATE_AFTER_IMPLOSION, false);
            drawable.stop(true);
        }
        if (mProgressExplodeAnimationListener != null) {
            mProgressExplodeAnimationListener.onImploded(this, drawable);
        }
    }

    /**
     */
    @Override
    public void onInitializeAccessibilityEvent(@NonNull AccessibilityEvent event) {
        super.onInitializeAccessibilityEvent(event);
        event.setClassName(BaseProgressBar.class.getName());
        event.setItemCount(mMax);
        event.setCurrentItemIndex(mProgress);
    }

    /**
     */
    @Override
    public void onInitializeAccessibilityNodeInfo(@NonNull AccessibilityNodeInfo info) {
        super.onInitializeAccessibilityNodeInfo(info);
        info.setClassName(BaseProgressBar.class.getName());
    }

    /**
     * Getters + Setters ---------------------------------------------------------------------------
     */
    /**
     * Registers a callback to be invoked whenever a new animation session is <b>started</b> or
     * the current one is <b>stopped</b> for the progress drawable attached to this progress bar.
     *
     * @param listener Listener callback.
     */
    public void setOnProgressAnimationListener(@NonNull OnProgressAnimationListener listener) {
        this.mProgressAnimationListener = listener;
    }

    /**
     * Removes the current OnProgressAnimationListener callback if any.
     */
    public void removeOnProgressAnimationListener() {
        this.mProgressAnimationListener = null;
    }

    /**
     * Registers a callback to be invoked whenever <b>explode</b> or <b>implode</b> animation is finished
     * for the progress drawable attached to this progress bar.
     *
     * @param listener Listener callback.
     */
    public void setOnProgressExplodeAnimationListener(@NonNull OnProgressExplodeAnimationListener listener) {
        this.mProgressExplodeAnimationListener = listener;
    }

    /**
     * Removes the current OnProgressExplodeAnimationListener callback if any.
     */
    public void removeOnProgressExplodeAnimationListener() {
        this.mProgressExplodeAnimationListener = null;
    }

    /**
     * Applies a tint to the progress graphics of the drawable, if specified. This call does not modify
     * the current tint mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
     * <p>
     * Subsequent calls to {@link #setDrawable(ProgressDrawable)} will automatically mutate the drawable
     * and apply the specified tint and tint mode using {@link ProgressDrawable#setProgressTintList(ColorStateList)}.
     *
     * @param tint The tint to apply, may be {@code null} to clear the current tint.
     * @see #getProgressTintList()
     * @see ProgressDrawable#setProgressTintList(ColorStateList)
     */
    public void setProgressTintList(@Nullable ColorStateList tint) {
        this.ensureTintInfo();
        mTintInfo.progressTintList = tint;
        mTintInfo.hasProgressTintList = true;
        this.applyProgressTint();
    }

    /**
     * Returns the tint applied to the progress graphics of the progress drawable, if specified.
     *
     * @return The progress graphics tint.
     * @see #setProgressTintList(android.content.res.ColorStateList)
     */
    @Nullable
    public ColorStateList getProgressTintList() {
        return mTintInfo != null ? mTintInfo.progressTintList : null;
    }

    /**
     * Specifies the blending mode used to apply the tint specified by {@link #setProgressTintList(ColorStateList)}}
     * to the progress graphics of the progress drawable. The default mode is {@link PorterDuff.Mode#SRC_IN}.
     *
     * @param tintMode The blending mode used to apply the tint, may be {@code null} to clear the
     *                 current tint.
     * @see #getProgressTintMode()
     * @see ProgressDrawable#setProgressTintMode(PorterDuff.Mode)
     */
    public void setProgressTintMode(@Nullable PorterDuff.Mode tintMode) {
        this.ensureTintInfo();
        mTintInfo.progressTintMode = tintMode;
        mTintInfo.hasProgressTintMode = true;
        this.applyProgressTint();
    }

    /**
     * Returns the blending mode used to apply the tint to the progress graphics of the progress
     * drawable, if specified.
     *
     * @return The progress graphics blending mode used to apply the tint.
     * @see #setProgressTintMode(android.graphics.PorterDuff.Mode)
     */
    @Nullable
    public PorterDuff.Mode getProgressTintMode() {
        return mTintInfo != null ? mTintInfo.tintMode : null;
    }

    /**
     * Applies a tint to the indeterminate graphics of the drawable, if specified. This call does not
     * modify the current tint mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
     * <p>
     * Subsequent calls to {@link #setDrawable(ProgressDrawable)} will automatically mutate the drawable
     * and apply the specified tint and tint mode using {@link ProgressDrawable#setIndeterminateTintList(ColorStateList)}.
     *
     * @param tint The tint to apply, may be {@code null} to clear the current tint.
     * @see #getIndeterminateTintList()
     * @see ProgressDrawable#setIndeterminateTintList(ColorStateList)
     */
    public void setIndeterminateTintList(@Nullable ColorStateList tint) {
        this.ensureTintInfo();
        mTintInfo.indeterminateTintList = tint;
        mTintInfo.hasIndeterminateTintList = true;
        this.applyIndeterminateTint();
    }

    /**
     * Returns the tint applied to the indeterminate graphics of the progress drawable, if specified.
     *
     * @return The progress graphics tint.
     * @see #setIndeterminateTintList(android.content.res.ColorStateList)
     */
    @Nullable
    public ColorStateList getIndeterminateTintList() {
        return mTintInfo != null ? mTintInfo.indeterminateTintList : null;
    }

    /**
     * Specifies the blending mode used to apply the tint specified by {@link #setIndeterminateTintList(ColorStateList)}}
     * to the indeterminate graphics of the progress drawable. The default mode is {@link PorterDuff.Mode#SRC_IN}.
     *
     * @param tintMode The blending mode used to apply the tint, may be {@code null} to clear the
     *                 current tint.
     * @see #getIndeterminateTintMode()
     * @see ProgressDrawable#setIndeterminateTintMode(PorterDuff.Mode)
     */
    public void setIndeterminateTintMode(@Nullable PorterDuff.Mode tintMode) {
        this.ensureTintInfo();
        mTintInfo.indeterminateTintMode = tintMode;
        mTintInfo.hasIndeterminateTintMode = true;
        this.applyIndeterminateTint();
    }

    /**
     * Returns the blending mode used to apply the tint to the indeterminate graphics of the progress
     * drawable, if specified.
     *
     * @return The indeterminate graphics blending mode used to apply the tint.
     * @see #setIndeterminateTintMode(android.graphics.PorterDuff.Mode)
     */
    @Nullable
    public PorterDuff.Mode getIndeterminateTintMode() {
        return mTintInfo != null ? mTintInfo.indeterminateTintMode : null;
    }

    /**
     * Applies a tint to the background graphics of the drawable, if specified. This call does not
     * modify the current tint mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
     * <p>
     * Subsequent calls to {@link #setDrawable(ProgressDrawable)} will automatically mutate the drawable
     * and apply the specified tint and tint mode using {@link ProgressDrawable#setBackgroundTintList(ColorStateList)}.
     *
     * @param tint The tint to apply, may be {@code null} to clear the current tint.
     * @see #getProgressBackgroundTintList()
     * @see ProgressDrawable#setBackgroundTintList(ColorStateList)
     */
    public void setProgressBackgroundTintList(@Nullable ColorStateList tint) {
        if (UiConfig.LOLLIPOP) {
            super.setBackgroundTintList(tint);
            return;
        }
        this.ensureTintInfo();
        mTintInfo.backgroundTintList = tint;
        mTintInfo.hasBackgroundTintList = true;
        this.applyProgressBackgroundTint();
    }

    /**
     * Returns the tint applied to the background graphics of the progress drawable, if specified.
     *
     * @return The background graphics tint.
     * @see #setProgressBackgroundTintList(android.content.res.ColorStateList)
     */
    @Nullable
    public ColorStateList getProgressBackgroundTintList() {
        if (UiConfig.LOLLIPOP) {
            return super.getBackgroundTintList();
        }
        return mTintInfo != null ? mTintInfo.backgroundTintList : null;
    }

    /**
     * Specifies the blending mode used to apply the tint specified by {@link #setProgressBackgroundTintList(ColorStateList)}}
     * to the background graphics of the progress drawable. The default mode is {@link PorterDuff.Mode#SRC_IN}.
     *
     * @param tintMode The blending mode used to apply the tint, may be {@code null} to clear the
     *                 current tint.
     * @see #getProgressBackgroundTintMode()
     * @see ProgressDrawable#setBackgroundTintMode(android.graphics.PorterDuff.Mode)
     */
    public void setProgressBackgroundTintMode(@Nullable PorterDuff.Mode tintMode) {
        if (UiConfig.LOLLIPOP) {
            super.setBackgroundTintMode(tintMode);
            return;
        }
        this.ensureTintInfo();
        mTintInfo.backgroundTintMode = tintMode;
        mTintInfo.hasBackgroundTinMode = true;
        this.applyProgressBackgroundTint();
    }

    /**
     * Returns the blending mode used to apply the tint to the background graphics of the progress
     * drawable, if specified.
     *
     * @return The background graphics blending mode used to apply the tint.
     * @see #setProgressBackgroundTintMode(android.graphics.PorterDuff.Mode)
     */
    @Nullable
    public PorterDuff.Mode getProgressBackgroundTintMode() {
        if (UiConfig.LOLLIPOP) {
            return super.getBackgroundTintMode();
        }
        return mTintInfo != null ? mTintInfo.backgroundTintMode : null;
    }

    /**
     * Sets the drawable used to draw a progress or an indeterminate graphics of this progress bar.
     * Whether this progress bar draws the progress or indeterminate graphics depends on its current
     * mode specified by {@link #setMode(int)}.
     * <p>
     * <b>Note</b>, that the specified drawable and its appearance can be updated directly by accessing
     * it, using {@link #getDrawable()}, but there are some methods which are delegated by this progress
     * bar to its attached drawable and should be called upon this progress bar like {@link #setProgress(int)}
     * or {@link #setMode(int)}. See {@link BaseProgressBar class} overview for more info.
     *
     * @param drawable The desired progress drawable. Can be {@code null} to clear the current drawable.
     */
    public void setDrawable(@Nullable D drawable) {
        if (mDrawable != drawable) {
            if (mDrawable != null) {
                mDrawable.setCallback(null);
                mDrawable.setAnimationCallback(null);
                mDrawable.setExplodeAnimationCallback(null);
                unscheduleDrawable(mDrawable);
            }

            if (drawable != null) {
                drawable.setCallback(this);
                drawable.setVisible(getVisibility() == View.VISIBLE, false);
                if (mDrawableWidth != drawable.getIntrinsicWidth()
                        || mDrawableHeight != drawable.getIntrinsicHeight()) {
                    this.mDrawableWidth = drawable.getIntrinsicWidth();
                    this.mDrawableHeight = drawable.getIntrinsicHeight();
                    requestLayout();
                }
            } else {
                mDrawableWidth = mDrawableHeight = 0;
                requestLayout();
            }

            if ((mDrawable = drawable) != null) {
                onSetUpDrawable(mDrawable);
            }
        }
    }

    /**
     * Returns the drawable used to draw a progress or an indeterminate graphics of this progress bar
     * depends on its current mode.
     *
     * @return An instance of {@link ProgressDrawable} or {@code null} if the drawable has been
     * removed by passing {@code null} to {@link #setDrawable(ProgressDrawable)}.
     * @see #setDrawable(ProgressDrawable)
     * @see #setMode(int)
     */
    @Nullable
    public D getDrawable() {
        return mDrawable;
    }

    /**
     * Delegate method for {@link com.albedinsky.android.support.ui.graphics.drawable.ProgressDrawable#setMode(int)}.
     * <p>
     * Sets the progress mode for this progress bar.
     */
    public void setMode(int mode) {
        if (mMode != mode) {
            changeMode(mode);
        }
    }

    /**
     * Returns the current progress mode of this progress bar.
     *
     * @return Current progress mode.
     * @see #setMode(int)
     */
    public int getMode() {
        if (mDrawable != null) {
            this.mMode = mDrawable.getMode();
        }
        return mMode;
    }

    /**
     * Restart the current mode. This will stop all running progress animations (if any) and starts
     * them again.
     * <p>
     * <b>Note</b>, that none <b>INDETERMINATE</b> mode, this will also clear the current progress
     * and will set it to {@code 0}.
     */
    public void restartMode() {
        if (hasPrivateFlag(PrivateFlags.PFLAG_ATTACHED_TO_WINDOW)) {
            onRestartMode(mMode);
        }
    }

    /**
     * Sets the current value of progress displayed by this progress bar. Does nothing if the current
     * mode is <b>INDETERMINATE</b>.
     * <p>
     * <b>Note</b>, that it is allowed to call this method also from the background thread.
     *
     * @param progress The desired progress value. Should be from the range {@code [0, getMax()]}.
     * @see #getProgress()
     * @see #getMax()
     * @see #setMode(int)
     */
    public synchronized void setProgress(int progress) {
        if (mMode != INDETERMINATE && mProgress != progress && progress >= 0 && progress <= mMax) {
            this.refreshProgress(android.R.id.progress, mProgress = progress);
        }
    }

    /**
     * Returns the current value of progress displayed by this progress bar.
     *
     * @return Current progress value from the range {@code [0, getMax()]} or {@code 0} if the
     * current mode is <b>INDETERMINATE</b>.
     * @see #setProgress(int)
     */
    public synchronized int getProgress() {
        if (mDrawable != null) {
            this.mProgress = mDrawable.getProgress();
        }
        return mMode != INDETERMINATE ? mProgress : 0;
    }

    /**
     * Delegate method for {@link ProgressDrawable#setMax(int)}.
     */
    public synchronized void setMax(int max) {
        if (mMax != max) {
            this.mMax = max;
            if (mDrawable != null) {
                mDrawable.setMax(max);
            }
            this.refreshProgress(android.R.id.progress, mProgress);
        }
    }

    /**
     * Delegate method for {@link ProgressDrawable#getMax()}.
     */
    public int getMax() {
        return mMax;
    }

    /**
     */
    @Override
    public void setVisibility(int visibility) {
        final boolean changed = visibility != getVisibility();
        super.setVisibility(visibility);
        if (changed && mDrawable != null) {
            handleVisibilityChange(visibility);
        }
    }

    /**
     */
    @Override
    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
    public void setLayoutDirection(int layoutDirection) {
        super.setLayoutDirection(layoutDirection);
        if (mDrawable != null) {
            mDrawable.setLayoutDirection(getLayoutDirection());
        }
    }

    /**
     * Protected -----------------------------------------------------------------------------------
     */

    /**
     */
    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        this.updatePrivateFlags(PrivateFlags.PFLAG_ATTACHED_TO_WINDOW, true);
        onRestartMode(mMode);
    }

    /**
     */
    @Override
    protected void onDetachedFromWindow() {
        stopIndeterminate(false, true);
        this.updatePrivateFlags(PrivateFlags.PFLAG_ATTACHED_TO_WINDOW, false);
        super.onDetachedFromWindow();
    }

    /**
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        final int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int width = mDrawable.getIntrinsicWidth();
        int height = mDrawable.getIntrinsicHeight();

        // Take into count also padding.
        width += getPaddingLeft() + getPaddingRight();
        height += getPaddingTop() + getPaddingBottom();

        switch (widthMode) {
        case MeasureSpec.AT_MOST:
            width = Math.min(width, widthSize);
            break;
        case MeasureSpec.EXACTLY:
            width = widthSize;
            break;
        }

        switch (heightMode) {
        case MeasureSpec.AT_MOST:
            height = Math.min(height, heightSize);
            break;
        case MeasureSpec.EXACTLY:
            height = heightSize;
            break;
        }

        // Check also against minimum size.
        width = Math.max(width, getSuggestedMinimumWidth());
        height = Math.max(height, getSuggestedMinimumHeight());
        setMeasuredDimension(width, height);
    }

    /**
     */
    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        this.updateDrawableBounds();
    }

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

    /**
     */
    @Override
    protected void drawableStateChanged() {
        super.drawableStateChanged();
        if (mDrawable != null) {
            mDrawable.setState(getDrawableState());
        }
    }

    /**
     */
    @Override
    protected void onVisibilityChanged(@NonNull View changedView, int visibility) {
        super.onVisibilityChanged(changedView, visibility);
        if (mDrawable != null) {
            handleVisibilityChange(visibility);
        }
    }

    /**
     */
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (mDrawable != null && mDrawable.isVisible()) {
            mDrawable.draw(canvas);
        }
    }

    /**
     */
    @NonNull
    @Override
    protected Parcelable onSaveInstanceState() {
        final SavedState savedState = new SavedState(super.onSaveInstanceState());
        savedState.mode = mMode;
        savedState.progress = mProgress;
        return savedState;
    }

    /**
     */
    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        if (!(state instanceof SavedState)) {
            super.onRestoreInstanceState(state);
            return;
        }

        final SavedState savedState = (SavedState) state;
        super.onRestoreInstanceState(savedState.getSuperState());
        changeMode(savedState.mode);
        setProgress(savedState.progress);
    }

    /**
     * Invoked to attach progress drawable to this progress bar. This is invoked when this progress
     * bar is being first time created before parsing of any values from AttributeSet.
     */
    abstract void onAttachDrawable();

    /**
     * Attaches the specified <var>drawable</var> to this progress bar. The given drawable will be
     * used to draw progress or indeterminate graphics of this progress bar.
     *
     * @param drawable The desired drawable to be attached.
     */
    final void attachDrawable(D drawable) {
        setDrawable(drawable);
    }

    /**
     * Handles change in visibility of this view. This will stop all animations if the specified
     * <var>visibility</var> flag is not {@link #VISIBLE}, otherwise it will start indeterminate
     * animations if the current mode is not <b>DETERMINATE</b>.
     *
     * @param visibility The current visibility flag.
     */
    void handleVisibilityChange(int visibility) {
        switch (mMode) {
        case INDETERMINATE:
            if (visibility != VISIBLE) {
                stopIndeterminate(false, true);
            } else {
                startIndeterminate(false);
            }
            postInvalidate();
            break;
        }
    }

    /**
     * Changes the current mode of this progress bar. This will invoke {@link #onPreModeChange(int)}
     * to allow perform some actions before the mode will be changed and than {@link #onModeChange(int)}
     * will be invoked.
     *
     * @param mode The desired mode to be changed.
     */
    void changeMode(int mode) {
        onPreModeChange(mode);
        if (mDrawable != null) {
            mDrawable.setMode(mode);
        }
        onModeChange(mMode = mode);
    }

    /**
     * Invoked from {@link #changeMode(int)} before the requested mode is changed.
     *
     * @param mode The new mode which will be changed.
     */
    void onPreModeChange(int mode) {
        switch (mode) {
        case DETERMINATE:
            stopIndeterminate(false, true);
            break;
        }
    }

    /**
     * Invoked from {@link #changeMode(int)} after the requested mode has been changed.
     *
     * @param mode The changed mode.
     */
    void onModeChange(int mode) {
        switch (mode) {
        case INDETERMINATE:
            startIndeterminate(false);
            break;
        }
    }

    /**
     * Invoked to restart the current mode.
     *
     * @param mode The current mode that should be restarted.
     */
    void onRestartMode(int mode) {
        if (mDrawable != null && mDrawable.isRunning()) {
            mDrawable.stop(true);
        }

        switch (mode) {
        case DETERMINATE:
            setProgress(0);
            break;
        case INDETERMINATE:
            startIndeterminate(false);
            break;
        }
    }

    /**
     * Called right after the new progress drawable has been attached/set to this progress bar to
     * set up its initial parameters.
     *
     * @param drawable The attached progress drawable to set up.
     */
    void onSetUpDrawable(@NonNull D drawable) {
        drawable.setMax(mMax);
        drawable.setMode(mMode);
        drawable.setProgress(mProgress);
        drawable.setAnimationCallback(this);
        drawable.setExplodeAnimationCallback(this);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
            drawable.setLayoutDirection(getLayoutDirection());
        }
        this.applyProgressTint();
        this.applyIndeterminateTint();
    }

    /**
     * Refreshes the current progress value displayed by this progress bar with respect to UI thread,
     * so this can be also called from the background thread.
     * <p>
     * If called from the UI thread, {@link #onRefreshProgress(int, int, boolean)} will be called
     * immediately, otherwise to refresh progress will be posted runnable.
     *
     * @param id       One of {@link android.R.id#progress} or {@link android.R.id#secondaryProgress}.
     * @param progress The progress value to be refreshed.
     */
    final synchronized void refreshProgress(int id, int progress) {
        if (mUiThreadId == Thread.currentThread().getId()) {
            onRefreshProgress(id, progress, true);
            return;
        }

        this.ensureRefreshProgressRunnable();
        final RefreshData refreshData = RefreshData.obtain(id, progress);
        mRefreshProgressRunnable.refreshData.add(refreshData);

        if (hasPrivateFlag(PrivateFlags.PFLAG_ATTACHED_TO_WINDOW)
                && !hasPrivateFlag(PFLAG_REFRESH_PROGRESS_POSTED)) {
            post(mRefreshProgressRunnable);
            this.updatePrivateFlags(PFLAG_REFRESH_PROGRESS_POSTED, true);
        }
    }

    /**
     * Invoked directly from {@link #refreshProgress(int, int)} if such a method has been called
     * from the UI thread, otherwise this is invoked from the posted refresh runnable.
     *
     * @param id       One of {@link android.R.id#progress} or {@link android.R.id#secondaryProgress}.
     * @param progress The progress value to be refreshed.
     * @param notify   {@code True} if this call should be dispatched also as accessibility event,
     *                 {@code false} otherwise.
     */
    synchronized void onRefreshProgress(int id, int progress, boolean notify) {
        if (id == android.R.id.progress) {
            if (mDrawable != null) {
                mDrawable.setProgress(progress);
            } else {
                invalidate();
            }

            if (notify) {
                scheduleAccessibilityEventSender();
            }
        }
    }

    /**
     * Private -------------------------------------------------------------------------------------
     */

    /**
     * Updates a bounds of the current progress drawable according to the current size of this
     * progress bar within layout.
     */
    private void updateDrawableBounds() {
        if (mDrawable != null) {
            mDrawable.setBounds(0, 0, getMeasuredWidth(), getMeasuredHeight());
        }
    }

    /**
     * Schedules an accessibility event for the changed/selected progress value.
     */
    private void scheduleAccessibilityEventSender() {
        if (mAccessibilityEventSender == null) {
            mAccessibilityEventSender = new AccessibilityEventSender();
        } else {
            removeCallbacks(mAccessibilityEventSender);
        }
        postDelayed(mAccessibilityEventSender, ACCESSIBILITY_EVENT_DELAY);
    }

    /**
     * Ensures that the runnable task to refresh progress is initialized.
     */
    private void ensureRefreshProgressRunnable() {
        if (mRefreshProgressRunnable == null) {
            this.mRefreshProgressRunnable = new RefreshProgressRunnable();
        }
    }

    /**
     * Ensures that the tint info object is initialized.
     */
    private void ensureTintInfo() {
        if (mTintInfo == null) {
            this.mTintInfo = new TintInfo();
        }
    }

    /**
     * Called from the constructor to process tint values for this view.
     *
     * @param context    The context passed to constructor.
     * @param typedArray TypedArray obtained for styleable attributes specific for this view.
     */
    @SuppressWarnings("All")
    private void processTintValues(Context context, TypedArray typedArray) {
        this.ensureTintInfo();

        // Get tint colors.
        if (typedArray.hasValue(R.styleable.Ui_Widget_ProgressBar_uiProgressTint)) {
            mTintInfo.progressTintList = typedArray
                    .getColorStateList(R.styleable.Ui_Widget_ProgressBar_uiProgressTint);
        }
        if (typedArray.hasValue(R.styleable.Ui_Widget_ProgressBar_uiIndeterminateTint)) {
            mTintInfo.indeterminateTintList = typedArray
                    .getColorStateList(R.styleable.Ui_Widget_ProgressBar_uiIndeterminateTint);
        }
        if (typedArray.hasValue(R.styleable.Ui_Widget_ProgressBar_uiProgressBackgroundTint)) {
            mTintInfo.backgroundTintList = typedArray
                    .getColorStateList(R.styleable.Ui_Widget_ProgressBar_uiProgressBackgroundTint);
        }

        // Get tint modes.
        mTintInfo.progressTintMode = TintManager.parseTintMode(
                typedArray.getInt(R.styleable.Ui_Widget_ProgressBar_uiProgressTintMode, 0), PorterDuff.Mode.SRC_IN);
        mTintInfo.indeterminateTintMode = TintManager.parseTintMode(
                typedArray.getInt(R.styleable.Ui_Widget_ProgressBar_uiIndeterminateTintMode, 0),
                PorterDuff.Mode.SRC_IN);
        mTintInfo.backgroundTintMode = TintManager.parseTintMode(
                typedArray.getInt(R.styleable.Ui_Widget_ProgressBar_uiProgressBackgroundTintMode, 0),
                PorterDuff.Mode.SRC_IN);

        // If there is no tint mode specified within style/xml do not tint at all.
        if (mTintInfo.backgroundTintMode == null) {
            mTintInfo.backgroundTintList = null;
        }
        if (mTintInfo.progressTintMode == null) {
            mTintInfo.progressTintList = null;
        }
        if (mTintInfo.indeterminateTintMode == null) {
            mTintInfo.indeterminateTintList = null;
        }

        mTintInfo.hasBackgroundTintList = mTintInfo.backgroundTintList != null;
        mTintInfo.hasBackgroundTinMode = mTintInfo.backgroundTintMode != null;
        mTintInfo.hasProgressTintList = mTintInfo.progressTintList != null;
        mTintInfo.hasProgressTintMode = mTintInfo.progressTintMode != null;
        mTintInfo.hasIndeterminateTintList = mTintInfo.indeterminateTintList != null;
        mTintInfo.hasIndeterminateTintMode = mTintInfo.indeterminateTintMode != null;
    }

    /**
     * Applies current tint from {@link #mTintInfo} to the progress graphics of the current progress
     * drawable.
     *
     * @return {@code True} if the tint has been applied or cleared, {@code false} otherwise.
     */
    private boolean applyProgressTint() {
        this.applyProgressBackgroundTint();
        if (mTintInfo == null || (!mTintInfo.hasProgressTintList && !mTintInfo.hasProgressTintMode)
                || mDrawable == null) {
            return false;
        }

        mDrawable.mutate();
        if (mTintInfo.hasProgressTintList) {
            mDrawable.setProgressTintList(mTintInfo.progressTintList);
        }
        if (mTintInfo.hasProgressTintMode) {
            mDrawable.setProgressTintMode(mTintInfo.progressTintMode);
        }
        return true;
    }

    /**
     * Applies current tint from {@link #mTintInfo} to the indeterminate graphics of the current progress
     * drawable.
     *
     * @return {@code True} if the tint has been applied or cleared, {@code false} otherwise.
     */
    private boolean applyIndeterminateTint() {
        this.applyProgressBackgroundTint();
        if (mTintInfo == null || (!mTintInfo.hasIndeterminateTintList && !mTintInfo.hasIndeterminateTintMode)
                || mDrawable == null) {
            return false;
        }

        mDrawable.mutate();
        if (mTintInfo.hasIndeterminateTintList) {
            mDrawable.setIndeterminateTintList(mTintInfo.indeterminateTintList);
        }
        if (mTintInfo.hasIndeterminateTintMode) {
            mDrawable.setIndeterminateTintMode(mTintInfo.indeterminateTintMode);
        }
        return true;
    }

    /**
     * Applies current tint from {@link #mTintInfo} to the progress background graphics of the current
     * progress drawable.
     *
     * @return {@code True} if the tint has been applied or cleared, {@code false} otherwise.
     */
    private boolean applyProgressBackgroundTint() {
        if (mTintInfo == null || (!mTintInfo.hasBackgroundTintList && !mTintInfo.hasBackgroundTinMode)
                || mDrawable == null) {
            return false;
        }

        mDrawable.mutate();
        if (mTintInfo.hasBackgroundTintList) {
            mDrawable.setBackgroundTintList(mTintInfo.backgroundTintList);
        }
        if (mTintInfo.hasBackgroundTinMode) {
            mDrawable.setBackgroundTintMode(mTintInfo.backgroundTintMode);
        }
        return true;
    }

    /**
     * Updates the current private flags.
     *
     * @param flag Value of the desired flag to add/remove to/from the current private flags.
     * @param add  Boolean flag indicating whether to add or remove the specified <var>flag</var>.
     */
    @SuppressWarnings("unused")
    private void updatePrivateFlags(int flag, boolean add) {
        if (add) {
            this.mPrivateFlags |= flag;
        } else {
            this.mPrivateFlags &= ~flag;
        }
    }

    /**
     * Returns a boolean flag indicating whether the specified <var>flag</var> is contained within
     * the current private flags or not.
     *
     * @param flag Value of the flag to check.
     * @return {@code True} if the requested flag is contained, {@code false} otherwise.
     */
    @SuppressWarnings("unused")
    private boolean hasPrivateFlag(int flag) {
        return (mPrivateFlags & flag) != 0;
    }

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

    /**
     * A {@link android.view.View.BaseSavedState BaseSavedState} implementation used to ensure the
     * state of {@link com.albedinsky.android.support.ui.widget.BaseProgressBar} is properly saved.
     *
     * @author Martin Albedinsky
     */
    static final class SavedState extends BaseSavedState {

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

        /**
         * 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 mode, progress;

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

        /**
         * Creates a new instance 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 A super state obtained from {@code super.onSaveInstanceState()} within
         *                   {@code onSaveInstanceState()}.
         */
        SavedState(@Nullable Parcelable superState) {
            super(superState);
        }

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

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

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

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

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

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

        /**
         * Blending mode used to apply tint to the <b>progress</b> graphics of the progress drawable.
         */
        PorterDuff.Mode progressTintMode;

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

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

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

        /**
         * Blending mode used to apply tint to the <b>indeterminate</b> graphics of the progress drawable.
         */
        PorterDuff.Mode indeterminateTintMode;

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

    /**
     * Class holding all data necessary to properly refresh progress value from the background thread.
     */
    private static final class RefreshData {

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

        /**
         * Pool of RefreshData objects for better performance.
         */
        static final Pools.SynchronizedPool<RefreshData> POOL = new Pools.SynchronizedPool<>(25);

        /**
         * Id of progress to refresh.
         */
        int id;

        /**
         * Progress value to be refreshed.
         */
        int progress;

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

        /**
         * Obtains an instance of RefreshData from the pool or creates a new instance if the pool
         * is currently empty.
         *
         * @param id       The id of progress for which to create the new data. One of {@link android.R.id#progress}
         *                 or {@link android.R.id#secondaryProgress}.
         * @param progress Value of progress to refresh.
         * @return Acquired or new instance of RefreshData object with the specified data.
         */
        static RefreshData obtain(int id, int progress) {
            RefreshData data = POOL.acquire();
            if (data == null) {
                data = new RefreshData();
            }
            data.id = id;
            data.progress = progress;
            return data;
        }

        /**
         * Recycles this refresh data object by releasing it from the current refresh data pool.
         */
        void recycle() {
            POOL.release(this);
        }
    }

    /**
     * Task used to refresh current progress value from the background thread.
     */
    private final class RefreshProgressRunnable implements Runnable {

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

        /**
         * List of refresh data to be processed to properly refresh progress.
         */
        final List<RefreshData> refreshData = new ArrayList<>();

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

        /**
         */
        @Override
        public void run() {
            synchronized (LOCK) {
                if (!refreshData.isEmpty()) {
                    for (RefreshData data : refreshData) {
                        onRefreshProgress(data.id, data.progress, true);
                        data.recycle();
                    }
                    refreshData.clear();
                }
                updatePrivateFlags(PFLAG_REFRESH_PROGRESS_POSTED, false);
            }
        }
    }

    /**
     * Task used to post an accessibility event for the changed progress.
     */
    private class AccessibilityEventSender implements Runnable {

        /**
         */
        @Override
        public void run() {
            sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
        }
    }
}