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

Java tutorial

Introduction

Here is the source code for com.albedinsky.android.support.ui.widget.ScrollViewWidget.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.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.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.ScrollView;

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;

/**
 * Extended version of {@link android.widget.ScrollView}. This updated ScrollView supports <b>pull</b>
 * feature and also setting of {@link OnScrollChangeListener} to listen for changes in scroll of this
 * view class.
 *
 * <h3>Tinting</h3>
 * <b>Tinting of the background is supported by this widget for the versions below LOLLIPOP.</b>
 *
 * <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 ScrollViewWidget is view with {@link Pullable#VERTICAL} orientation,
 * so its content can be pulled at the top or at the bottom by offsetting its current position using
 * {@link #offsetTopAndBottom(int)} method. The Xml attributes below can be used to customize pull
 * feature for this view:
 * <ul>
 * <li>{@link R.attr#uiPullEnabled uiPullEnabled}</li>
 * <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>
 * <ul>
 * <li>{@link R.attr#uiPullEnabled uiPullEnabled}</li>
 * </ul>
 *
 * @author Martin Albedinsky
 */
public class ScrollViewWidget extends ScrollView implements Widget, Pullable {

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

    /**
     * Listener which receives callback about change in the scroll of the {@link ScrollView} class.
     *
     * @author Martin Albedinsky
     */
    public static interface OnScrollChangeListener {

        /**
         * Invoked whenever a change in scroll occurs within the given <var>scrollView</var>.
         *
         * @param scrollView    The scroll view within which has scroll changed.
         * @param horizontal    Current horizontal scroll position.
         * @param vertical      Current vertical scroll position.
         * @param oldHorizontal Old (before change) horizontal scroll position.
         * @param oldVertical   Old (before change) vertical scroll position.
         */
        public void onScrollChanged(@NonNull ScrollView scrollView, int horizontal, int vertical, int oldHorizontal,
                int oldVertical);
    }

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

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

    /**
     * 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;

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

    /**
     * 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;

    /**
     * Callback to be invoked whenever {@link #onScrollChanged(int, int, int, int)} occurs.
     */
    private OnScrollChangeListener mScrollListener;

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

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

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

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

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

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

            this.processTintValues(context, typedArray);
            final int n = typedArray.getIndexCount();
            for (int i = 0; i < n; i++) {
                int index = typedArray.getIndex(i);
                if (index == R.styleable.Ui_Widget_ScrollView_uiPullEnabled) {
                    setPullEnabled(typedArray.getBoolean(index, false));
                }
            }
            typedArray.recycle();
        }

        this.applyBackgroundTint();
    }

    /**
     * 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(ScrollViewWidget.class.getName());
    }

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

    /**
     */
    @Override
    public boolean onInterceptTouchEvent(@NonNull MotionEvent event) {
        return (hasPrivateFlag(PrivateFlags.PFLAG_PULL_ENABLED) && mPullController.shouldInterceptTouchEvent(event))
                || super.onInterceptTouchEvent(event);
    }

    /**
     */
    @Override
    public boolean onTouchEvent(@NonNull MotionEvent event) {
        return (hasPrivateFlag(PrivateFlags.PFLAG_PULL_ENABLED) && mPullController.processTouchEvent(event))
                || super.onTouchEvent(event);
    }

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

    /**
     * Registers a callback to be invoked whenever {@link #onScrollChanged(int, int, int, int)} is
     * invoked within this scroll view.
     *
     * @param listener Listener callback.
     */
    public void setOnScrollChangeListener(@NonNull OnScrollChangeListener listener) {
        this.mScrollListener = listener;
    }

    /**
     * Removes the current OnScrollChangeListener if any.
     */
    public void removeOnScrollChangeListener() {
        this.mScrollListener = null;
    }

    /**
     */
    @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 VERTICAL;
    }

    /**
     */
    @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 -----------------------------------------------------------------------------------
     */

    /**
     * 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 onScrollChanged(int l, int t, int oldl, int oldt) {
        if (mScrollListener != null) {
            mScrollListener.onScrollChanged(this, l, t, oldl, oldt);
        }
    }

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

    /**
     */
    @Override
    protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
        super.onOverScrolled(scrollX, scrollY, clampedX, clampedY);
        if ((mPrivateFlags & PrivateFlags.PFLAG_PULL_ENABLED) != 0) {
            mPullController.dispatchOverScroll(scrollX, scrollY, clampedX, clampedY);
        }
    }

    /**
     * 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_ScrollView_uiBackgroundTint)) {
            mTintInfo.backgroundTintList = typedArray
                    .getColorStateList(R.styleable.Ui_Widget_ScrollView_uiBackgroundTint);
        }

        // Get tint modes.
        mTintInfo.backgroundTintMode = TintManager.parseTintMode(
                typedArray.getInt(R.styleable.Ui_Widget_ScrollView_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;
    }

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

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