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

Java tutorial

Introduction

Here is the source code for com.albedinsky.android.support.ui.widget.ViewPagerWidget.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.SuppressLint;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.ViewParent;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.animation.Interpolator;
import android.widget.Scroller;

import com.albedinsky.android.support.ui.PullController;
import com.albedinsky.android.support.ui.R;
import com.albedinsky.android.support.ui.UiConfig;
import com.albedinsky.android.support.ui.WidgetSizeAnimator;
import com.albedinsky.android.support.ui.graphics.TintOptions;
import com.albedinsky.android.support.ui.graphics.drawable.TintDrawable;
import com.albedinsky.android.support.ui.interpolator.ScrollerInterpolator;
import com.albedinsky.android.support.ui.widget.adapter.PagerAdapter;

/**
 * Extended version of {@link android.support.v4.view.ViewPager}. This updated ViewPager supports
 * <b>pull</b> feature and some additional features described below, like setting custom duration
 * for pages scroll by {@link #setPageScrollDuration(int)} or enabling swiping of multiple pages
 * on fling at once by {@link #setPageFlingSwipingEnabled(boolean)}.
 *
 * <h3>Pulling</h3>
 * This view can be pulled at its start and also at its end, using the {@link com.albedinsky.android.support.ui.PullController PullController}
 * to support this feature. The ViewPagerWidget is view with {@link Pullable#HORIZONTAL} orientation,
 * so its content can be pulled at the left or at the right by offsetting its current position using
 * {@link #offsetLeftAndRight(int)} method. The Xml attributes below can be used to customize pull
 * feature for this view:
 * <ul>
 * <li>{@link R.attr#uiPullMode uiPullMode}</li>
 * <li>{@link R.attr#uiPullDistanceFraction uiPullDistanceFraction}</li>
 * <li>{@link R.attr#uiPullDistance uiPullDistance}</li>
 * <li>{@link R.attr#uiPullCollapseDuration uiPullCollapseDuration}</li>
 * <li>{@link R.attr#uiPullCollapseDelay uiPullCollapseDelay}</li>
 * <li>{@link R.attr#uiPullMinVelocity uiPullMinVelocity}</li>
 * </ul>
 * See class overview of {@link com.albedinsky.android.support.ui.PullController PullController} for
 * additional info.
 *
 * <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 depends on its current size using the new animation
 * framework introduced in {@link android.os.Build.VERSION_CODES#HONEYCOMB HONEYCOMB} by
 * {@link android.animation.ObjectAnimator ObjectAnimator}s API.
 * <p>
 * Changing of fraction of X or Y is supported by 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 whole width of
 * such a view, an Xml file with ObjectAnimator will 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>
 * If this layout class is used as a root of view hierarchy, this can be especially used for fragment
 * transitions framework, where for {@link android.support.v4.app.FragmentTransaction FragmentTransaction},
 * used to change currently visible fragment by a new one, can be specified custom animations which
 * will change fragments by sliding them horizontally.
 *
 * <h3>Styling</h3>
 * The ViewPagerWidget view uses style specified by {@link R.attr#uiViewPagerStyle} within the current
 * theme to allow base set up. The Xml attributes below will be parsed from such a style:
 * <ul>
 * <li>{@link android.R.attr#background android:background}</li>
 * <li>{@link R.attr#uiPageMargin uiPageMargin}</li>
 * <li>{@link R.attr#uiPageMarginDrawable uiPageMarginDrawable}</li>
 * <li>{@link R.attr#uiPageSwipingEnabled uiPageSwipingEnabled}</li>
 * <li>{@link R.attr#uiPageFlingSwipingEnabled uiPageFlingSwipingEnabled}</li>
 * <li>{@link R.attr#uiPageFlingSwipingSensitivity uiPageFlingSwipingSensitivity}</li>
 * <li>{@link R.attr#uiPageScrollDuration uiPageScrollDuration}</li>
 * <li>{@link R.attr#uiPageScrollRelativeDurationEnabled uiPageScrollRelativeDurationEnabled}</li>
 * <li>{@link R.attr#uiCurrentPage uiCurrentPage}</li>
 * <li>{@link R.attr#uiOffScreenPageLimit uiOffScreenPageLimit}</li>
 * <li>{@link R.attr#uiPullEnabled uiPullEnabled}</li>
 * </ul>
 *
 * @author Martin Albedinsky
 */
public class ViewPagerWidget extends ViewPager implements Widget, Pullable {

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

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

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

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

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

    /**
     * Flag indicating whether the swiping of pages on touch is enabled or not.
     */
    private static final int PFLAG_PAGE_SWIPING_ENABLED = 0x00008000;

    /**
     * Flag indicating whether the swiping of multiple pages on fling at once is enabled or not.
     */
    private static final int PFLAG_PAGE_FLING_SWIPING_ENABLED = 0x00010000;

    /**
     * Flag indicating whether scroll duration for pages scrolling should be computed as relative
     * depends on the count of pages to be scrolled or just fixed.
     */
    private static final int PFLAG_PAGE_SCROLL_RELATIVE_DURATION_ENABLED = 0x00020000;

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

    /**
     * Interpolator used for scrolling operations of this view.
     */
    private static final Interpolator SCROLLER_INTERPOLATOR = new ScrollerInterpolator();

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

    /**
     * This view's dimension.
     */
    private int mWidth, mHeight;

    /**
     * Animator used to animate size of this view.
     */
    private WidgetSizeAnimator mSizeAnimator;

    /**
     * Controller used to support pull feature for this view.
     */
    private PullController mPullController;

    /**
     * Index of the currently selected page.
     */
    private int mCurrentPage;

    /**
     * Set of private flags specified for this view.
     */
    private int mPrivateFlags = PrivateFlags.PFLAG_ALLOWS_DEFAULT_SELECTION | PFLAG_PAGE_SWIPING_ENABLED;

    /**
     * Duration for transition used to scroll between pages. If <b>negative</b>, the default duration
     * will be used.
     */
    private int mPageScrollDuration = -1;

    /**
     * Helper used to track velocity of fling to determine whether to initiate swipe of page or not.
     */
    private VelocityTracker mVelocityTracker;

    /**
     * Minimum sensitivity to initiate swipe of page on fling.
     */
    private float mPageFlingSwipingSensitivity = 6000;

    /**
     * Adapter with data set for this view pager.
     */
    private PagerAdapter mAdapter;

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

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

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

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

    /**
     * Creates a new instance of ViewPagerWidget 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 ViewPagerWidget(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs);
        /**
         * Process attributes.
         */
        final TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.Ui_Widget_ViewPager,
                defStyleAttr, 0);
        if (typedArray != null) {
            this.ensurePullController();
            mPullController.setUpFromAttrs(context, attrs, defStyleAttr);

            final int n = typedArray.getIndexCount();
            for (int i = 0; i < n; i++) {
                final int index = typedArray.getIndex(i);
                if (index == R.styleable.Ui_Widget_ViewPager_android_background) {
                    int resID = typedArray.getResourceId(index, -1);
                    if (resID != -1) {
                        setBackgroundResource(resID);
                    } else {
                        setBackgroundColor(typedArray.getColor(0, Color.TRANSPARENT));
                    }
                } else if (index == R.styleable.Ui_Widget_ViewPager_uiPageMargin) {
                    setPageMargin(typedArray.getDimensionPixelSize(index, 0));
                } else if (index == R.styleable.Ui_Widget_ViewPager_uiPageMarginDrawable) {
                    setPageMarginDrawable(typedArray.getDrawable(index));
                } else if (index == R.styleable.Ui_Widget_ViewPager_uiPageSwipingEnabled) {
                    updatePrivateFlags(PFLAG_PAGE_SWIPING_ENABLED, typedArray.getBoolean(index, true));
                } else if (index == R.styleable.Ui_Widget_ViewPager_uiPageFlingSwipingEnabled) {
                    updatePrivateFlags(PFLAG_PAGE_FLING_SWIPING_ENABLED, typedArray.getBoolean(index, false));
                } else if (index == R.styleable.Ui_Widget_ViewPager_uiPageFlingSwipingSensitivity) {
                    this.mPageFlingSwipingSensitivity = Math.max(0,
                            typedArray.getFloat(index, mPageFlingSwipingSensitivity));
                } else if (index == R.styleable.Ui_Widget_ViewPager_uiCurrentPage) {
                    this.mCurrentPage = typedArray.getInteger(index, 0);
                } else if (index == R.styleable.Ui_Widget_ViewPager_uiOffScreenPageLimit) {
                    setOffscreenPageLimit(typedArray.getInt(index, getOffscreenPageLimit()));
                } else if (index == R.styleable.Ui_Widget_ViewPager_uiPageScrollDuration) {
                    this.mPageScrollDuration = typedArray.getInteger(index, mPageScrollDuration);
                } else if (index == R.styleable.Ui_Widget_ViewPager_uiPageScrollRelativeDurationEnabled) {
                    updatePrivateFlags(PFLAG_PAGE_SCROLL_RELATIVE_DURATION_ENABLED,
                            typedArray.getBoolean(index, false));
                } else if (index == R.styleable.Ui_Widget_ViewPager_uiPullEnabled) {
                    setPullEnabled(typedArray.getBoolean(index, false));
                }
            }
        }

        // Override default scroller so we can use custom durations for scroll if requested.
        this.mScroller = new WidgetScroller(context, SCROLLER_INTERPOLATOR);
    }

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

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

    /**
     */
    @NonNull
    @Override
    public WidgetSizeAnimator animateSize() {
        return (mSizeAnimator != null) ? mSizeAnimator : (mSizeAnimator = new WidgetSizeAnimator(this));
    }

    /**
     */
    @Override
    public void onInitializeAccessibilityEvent(@NonNull AccessibilityEvent event) {
        super.onInitializeAccessibilityEvent(event);
        event.setClassName(ViewPagerWidget.class.getName());
    }

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

    /**
     */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        if (!hasPrivateFlag(PFLAG_PAGE_SWIPING_ENABLED)) {
            return false;
        }

        if (hasPrivateFlag(PrivateFlags.PFLAG_PULL_ENABLED) && mPullController.shouldInterceptTouchEvent(event)) {
            this.requestParentDisallowInterceptTouchEvent(true);
            return true;
        }
        return super.onInterceptTouchEvent(event);
    }

    /**
     */
    @Override
    public boolean onTouchEvent(@NonNull MotionEvent event) {
        if (!hasPrivateFlag(PFLAG_PAGE_SWIPING_ENABLED)) {
            return false;
        }

        if (hasPrivateFlag(PrivateFlags.PFLAG_PULL_ENABLED) && mPullController.processTouchEvent(event)) {
            this.requestParentDisallowInterceptTouchEvent(true);
            return true;
        }

        if (hasPrivateFlag(PFLAG_PAGE_FLING_SWIPING_ENABLED)) {
            this.ensureVelocityTracker();
            mVelocityTracker.addMovement(event);
            switch (event.getActionMasked()) {
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                mVelocityTracker.computeCurrentVelocity(UiConfig.VELOCITY_UNITS);
                final float xVelocity = mVelocityTracker.getXVelocity();
                if (Math.abs(xVelocity) > mPageFlingSwipingSensitivity) {
                    super.onTouchEvent(event);
                    this.handleFling(xVelocity);
                    return true;
                }
            }
        }
        return super.onTouchEvent(event);
    }

    /**
     * Getters + Setters ---------------------------------------------------------------------------
     */

    /**
     */
    @Override
    public void setAdapter(PagerAdapter adapter) {
        super.setAdapter(mAdapter = adapter);
    }

    /**
     */
    @Override
    public void setCurrentItem(int item) {
        if (mPageScrollDuration >= 0) {
            setCurrentItem(item, computePageScrollDuration(item));
            return;
        }
        this.resetPageScrollDuration();
        super.setCurrentItem(item);
    }

    /**
     */
    @Override
    public void setCurrentItem(int item, boolean smoothScroll) {
        if (mPageScrollDuration >= 0 && smoothScroll) {
            setCurrentItem(item, computePageScrollDuration(item));
            return;
        }
        super.setCurrentItem(item, smoothScroll);
    }

    /**
     * Same as {@link #setCurrentItem(int)}, but this will use the specified <var>duration</var> for
     * scroll. <b>Note</b>, that this duration will be used only for this particular call, if you
     * want to use a specific duration for all {@link #setCurrentItem(int)} related calls, set your
     * desired duration by {@link #setPageScrollDuration(int)}.
     *
     * @param item     A position of page to scroll to.
     * @param duration The desired scroll duration used to scroll to the specified page in milliseconds.
     */
    public void setCurrentItem(int item, int duration) {
        this.usePageScrollDuration(duration);
        super.setCurrentItem(item, duration > 0);
        this.resetPageScrollDuration();
    }

    /**
     * Sets the scroller for this view pager used to apply and animate scroll to the pages of this
     * pager.
     *
     * @param scroller The desired scroller to be used by this pager.
     */
    public void setScroller(@NonNull Scroller scroller) {
        this.mScroller = scroller;
    }

    /**
     * Sets a flag indicating whether the swiping of pages on a user's touch/drag is enabled or not.
     * This enables/disables only interaction for the user, calls to {@link #setCurrentItem(int)} and
     * similar methods will still be working.
     *
     * @param enabled {@code True} if page swiping should be enabled, {@code false} otherwise.
     */
    public void setPageSwipingEnabled(boolean enabled) {
        updatePrivateFlags(PFLAG_PAGE_SWIPING_ENABLED, enabled);
    }

    /**
     * Returns a flag indicating whether the swiping of pages is enabled for a user or not.
     *
     * @return {@code True} if page swiping is enabled, {@code false} otherwise.
     * @see #setPageSwipingEnabled(boolean)
     */
    public boolean isPageSwipingEnabled() {
        return hasPrivateFlag(PFLAG_PAGE_SWIPING_ENABLED);
    }

    /**
     * Sets a flag indicating whether the swiping of multiple pages on a user's fling is enabled or
     * not. Enabling this feature means, that the user can by fling scroll multiple pages at once.
     * How many pages will be scrolled at fling depends on the velocity of a specific fling. Sensitivity
     * for the fling can be set by {@link #setPageFlingSwipingSensitivity(float)}.
     *
     * @param enabled {@code True} if page swiping on fling should be enabled, {@code false} otherwise.
     */
    public void setPageFlingSwipingEnabled(boolean enabled) {
        updatePrivateFlags(PFLAG_PAGE_FLING_SWIPING_ENABLED, enabled);
    }

    /**
     * Returns a flag indicating whether the swiping of multiple pages on fling is enabled or not.
     *
     * @return {@code True} if page swiping on fling is enabled, {@code false} otherwise.
     * @see #setPageFlingSwipingEnabled(boolean)
     */
    public boolean isPageFlingSwipingEnabled() {
        return hasPrivateFlag(PFLAG_PAGE_FLING_SWIPING_ENABLED);
    }

    /**
     * Sets a sensitivity for the velocity of a user's fling used to determine whether to handle a
     * particular fling to swipe multiple pages at once or not.
     * <p>
     * <b>Note</b>, that velocity of fling is computed in {@link UiConfig#VELOCITY_UNITS}.
     *
     * @param sensitivity The desired sensitivity. The bigger sensitivity is, the less pages will
     *                    be scrolled at once and in reverse.
     * @see #setPageFlingSwipingEnabled(boolean)
     */
    public void setPageFlingSwipingSensitivity(float sensitivity) {
        this.mPageFlingSwipingSensitivity = Math.max(0, sensitivity);
    }

    /**
     * Returns the current sensitivity for the fling velocity used to determine whether to handle
     * a user's fling or not.
     * <p>
     * Default value: <b>6000</b>
     *
     * @return Swiping sensitivity.
     */
    public float getPageFlingSwipingSensitivity() {
        return mPageFlingSwipingSensitivity;
    }

    /**
     * Sets the duration for scroll used whenever {@link #setCurrentItem(int)} or similar methods
     * are called upon this view pager.
     *
     * @param duration The desired scroll duration in milliseconds. Pass here {@code -1} to clear
     *                 the current one, so the default one will be used.
     */
    public void setPageScrollDuration(int duration) {
        this.mPageScrollDuration = duration;
    }

    /**
     * Returns the current duration used for scrolling of pages of this pager.
     *
     * @return Scroll duration in milliseconds.
     */
    public int getPageScrollDuration() {
        return mPageScrollDuration;
    }

    /**
     * Sets a flag indicating whether the duration of page scroll should be computed as a relative
     * one depends on the count of pages to be scrolled, or as a fixed one regardless how many pages
     * will be scrolled.
     * <p>
     * If the relative duration is enabled, the scroll duration requested by {@link #setPageScrollDuration(int)}
     * is used as a base, so a duration for the current page scroll is computed like so:
     * <b>{@code Math.abs(getCurrentItem() - position) * pageScrollDuration}</b>
     *
     * @param enabled {@code True} if relative duration of page scroll should be enabled, {@code false}
     *                otherwise.
     */
    public void setPageScrollRelativeDurationEnabled(boolean enabled) {
        updatePrivateFlags(PFLAG_PAGE_SCROLL_RELATIVE_DURATION_ENABLED, enabled);
    }

    /**
     * Returns a flag indicating whether the duration of pages scroll is computed as a relative one
     * or not.
     *
     * @return {@code True} if duration of page scroll is computed as relative one, {@code false} as
     * fixed one.
     */
    public boolean isPageScrollRelativeDurationEnabled() {
        return hasPrivateFlag(PFLAG_PAGE_SCROLL_RELATIVE_DURATION_ENABLED);
    }

    /**
     */
    @Override
    public void setPullEnabled(boolean enabled) {
        this.updatePrivateFlags(PrivateFlags.PFLAG_PULL_ENABLED, enabled);
        if (enabled) {
            this.ensurePullController();
        }
    }

    /**
     */
    @Override
    public boolean isPullEnabled() {
        return hasPrivateFlag(PrivateFlags.PFLAG_PULL_ENABLED);
    }

    /**
     */
    @NonNull
    @Override
    public PullController getPullController() {
        this.ensurePullController();
        return mPullController;
    }

    /**
     */
    @Override
    public int getOrientation() {
        return HORIZONTAL;
    }

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

    /**
     */
    @Override
    @SuppressLint("NewApi")
    public void setBackgroundTintList(@Nullable ColorStateList tint) {
        if (UiConfig.LOLLIPOP) {
            super.setBackgroundTintList(tint);
            return;
        }
        this.ensureTintInfo();
        mTintInfo.backgroundTintList = tint;
        mTintInfo.hasBackgroundTintList = true;
        this.applyBackgroundTint();
    }

    /**
     */
    @Nullable
    @Override
    @SuppressLint("NewApi")
    public ColorStateList getBackgroundTintList() {
        if (UiConfig.LOLLIPOP) {
            return super.getBackgroundTintList();
        }
        return mTintInfo != null ? mTintInfo.backgroundTintList : null;
    }

    /**
     */
    @Override
    @SuppressLint("NewApi")
    public void setBackgroundTintMode(@Nullable PorterDuff.Mode tintMode) {
        if (UiConfig.LOLLIPOP) {
            super.setBackgroundTintMode(tintMode);
            return;
        }
        this.ensureTintInfo();
        mTintInfo.backgroundTintMode = tintMode;
        mTintInfo.hasBackgroundTinMode = true;
        this.applyBackgroundTint();
    }

    /**
     */
    @Nullable
    @Override
    @SuppressLint("NewApi")
    public PorterDuff.Mode getBackgroundTintMode() {
        if (UiConfig.LOLLIPOP) {
            return super.getBackgroundTintMode();
        }
        return mTintInfo != null ? mTintInfo.backgroundTintMode : null;
    }

    /**
     */
    @Override
    public void setFractionX(float fraction) {
        setX(mWidth > 0 ? (getLeft() + (fraction * mWidth)) : OUT_OF_SCREEN);
    }

    /**
     */
    @Override
    public float getFractionX() {
        return (mWidth > 0) ? (getLeft() + (getX() / mWidth)) : 0;
    }

    /**
     */
    @Override
    public void setFractionY(float fraction) {
        setY(mHeight > 0 ? (getTop() + (fraction * mHeight)) : OUT_OF_SCREEN);
    }

    /**
     */
    @Override
    public float getFractionY() {
        return (mHeight > 0) ? (getTop() + (getY() / mHeight)) : 0;
    }

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

    /**
     */
    @Override
    public void setSelected(boolean selected) {
        if (hasPrivateFlag(PrivateFlags.PFLAG_ALLOWS_DEFAULT_SELECTION)) {
            setSelectionState(selected);
        }
    }

    /**
     */
    @Override
    public void setSelectionState(boolean selected) {
        super.setSelected(selected);
    }

    /**
     */
    @Override
    public void setAllowDefaultSelection(boolean allow) {
        this.updatePrivateFlags(PrivateFlags.PFLAG_ALLOWS_DEFAULT_SELECTION, allow);
    }

    /**
     */
    @Override
    public boolean allowsDefaultSelection() {
        return this.hasPrivateFlag(PrivateFlags.PFLAG_ALLOWS_DEFAULT_SELECTION);
    }

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

    /**
     */
    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        setCurrentItem(mCurrentPage);
    }

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

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

    /**
     */
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        this.mWidth = w;
        this.mHeight = h;
    }

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

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

    /**
     * Called from the constructor to process tint values for this view. <b>Note</b>, that for
     * {@link android.os.Build.VERSION_CODES#LOLLIPOP LOLLIPOP} is this call ignored.
     *
     * @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) {
        // Do not handle for LOLLIPOP.
        if (UiConfig.LOLLIPOP) {
            return;
        }

        this.ensureTintInfo();

        // Get tint colors.
        if (typedArray.hasValue(R.styleable.Ui_Widget_TextView_uiBackgroundTint)) {
            mTintInfo.backgroundTintList = typedArray
                    .getColorStateList(R.styleable.Ui_Widget_TextView_uiBackgroundTint);
        }

        // Get tint modes.
        mTintInfo.backgroundTintMode = TintManager.parseTintMode(
                typedArray.getInt(R.styleable.Ui_Widget_TextView_uiBackgroundTintMode, 0),
                mTintInfo.backgroundTintList != null ? PorterDuff.Mode.SRC_IN : null);

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

        mTintInfo.hasBackgroundTintList = mTintInfo.backgroundTintList != null;
        mTintInfo.hasBackgroundTinMode = mTintInfo.backgroundTintMode != null;
    }

    /**
     * Applies current background tint from {@link #mTintInfo} to the current background drawable.
     * <b>Note</b>, that for {@link android.os.Build.VERSION_CODES#LOLLIPOP LOLLIPOP} is this call
     * ignored.
     *
     * @return {@code True} if the tint has been applied or cleared, {@code false} otherwise.
     */
    @SuppressWarnings("deprecation")
    private boolean applyBackgroundTint() {
        final Drawable drawable = getBackground();
        if (UiConfig.LOLLIPOP || mTintInfo == null
                || (!mTintInfo.hasBackgroundTintList && !mTintInfo.hasBackgroundTinMode) || drawable == null) {
            return false;
        }

        final TintOptions tintOptions = new TintOptions().tintList(mTintInfo.backgroundTintList)
                .tintMode(mTintInfo.backgroundTintMode);

        if (drawable instanceof TintDrawable) {
            if (!tintOptions.applyable()) {
                drawable.setCallback(null);
                drawable.clearColorFilter();
                super.setBackgroundDrawable(((TintDrawable) drawable).getDrawable());
            } else {
                ((TintDrawable) drawable).setTintOptions(tintOptions);
            }
            return true;
        }

        if (!tintOptions.applyable()) {
            drawable.clearColorFilter();
            return true;
        }

        final TintDrawable tintDrawable = new TintDrawable(drawable);
        tintDrawable.setTintOptions(tintOptions);
        super.setBackgroundDrawable(tintDrawable);
        tintDrawable.attachCallback();
        return true;
    }

    /**
     * Handles fling event performed by a user with the specified <var>velocity</var>. This will
     * compute how many pages should be scrolled and than will call {@link #setCurrentItem(int)}
     * for the computed page position.
     *
     * @param velocity The velocity with which has been fling performed.
     */
    private void handleFling(float velocity) {
        if (mAdapter == null) {
            return;
        }

        int scrollPages = Math.round(Math.abs(velocity) / mPageFlingSwipingSensitivity);
        if (velocity > 0) {
            setCurrentItem(Math.max(0, getCurrentItem() - scrollPages));
        } else {
            setCurrentItem(Math.min(mAdapter.getCount() - 1, getCurrentItem() + scrollPages));
        }
    }

    /**
     * Updates the scroll duration for the current scroller (if instance of {@link WidgetScroller}),
     * so this duration will be used when {@link WidgetScroller#startScroll(int, int, int, int, int)}
     * will be next time called upon this scroller.
     *
     * @param duration The desired scroll duration in milliseconds. Pass here {@code -1} to clear
     *                 the current one, so the default one will be used.
     */
    private void usePageScrollDuration(int duration) {
        if (mScroller instanceof WidgetScroller) {
            ((WidgetScroller) mScroller).setScrollDuration(duration);
        }
    }

    /**
     * Resets the current scroll duration for pages.
     */
    private void resetPageScrollDuration() {
        usePageScrollDuration(-1);
    }

    /**
     * Computes scroll duration for page at the specified <var>position</var>. The computed duration
     * will depends on if the relative duration is enabled or not by {@link #setPageScrollRelativeDurationEnabled(boolean)}.
     *
     * @param position The position of page for which to compute scroll duration.
     * @return Computed scroll duration in milliseconds.
     */
    private int computePageScrollDuration(int position) {
        if (hasPrivateFlag(PFLAG_PAGE_SCROLL_RELATIVE_DURATION_ENABLED)) {
            return Math.abs(getCurrentItem() - position) * mPageScrollDuration;
        }
        return mPageScrollDuration;
    }

    /**
     * Ensures that the {@link #mPullController} is initialized.
     */
    private void ensurePullController() {
        if (mPullController == null) {
            this.mPullController = new PullController(this);
        }
    }

    /**
     * Ensures that the {@link #mVelocityTracker} is initialized.
     */
    private void ensureVelocityTracker() {
        if (mVelocityTracker == null) {
            this.mVelocityTracker = VelocityTracker.obtain();
        }
    }

    /**
     * Requests the current parent to disallow intercepting of touch event by {@link ViewParent#requestDisallowInterceptTouchEvent(boolean)}.
     *
     * @param disallow {@code True} to disallow, {@code false} otherwise.
     */
    private void requestParentDisallowInterceptTouchEvent(boolean disallow) {
        final ViewParent parent = getParent();
        if (parent != null) {
            parent.requestDisallowInterceptTouchEvent(disallow);
        }
    }

    /**
     * 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 ===============================================================================
     */
}