Java tutorial
/* * ================================================================================================= * 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.HorizontalScrollView; 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.HorizontalScrollView}. 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 HorizontalScrollViewWidget 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#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> * <objectAnimator xmlns:android="http://schemas.android.com/apk/res/android" * android:propertyName="fractionX" * android:valueFrom="0.0" * android:valueTo="1.0" * android:duration="300"/> * </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 HorizontalScrollViewWidget extends HorizontalScrollView implements Widget, Pullable { /** * Interface =================================================================================== */ /** * Listener which receives callback about change in the scroll of the {@link HorizontalScrollView} * 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 HorizontalScrollView scrollView, int horizontal, int vertical, int oldHorizontal, int oldVertical); } /** * Constants =================================================================================== */ /** * Log TAG. */ // private static final String TAG = "HorizontalScrollViewWidget"; /** * 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 #HorizontalScrollViewWidget(android.content.Context, android.util.AttributeSet)} without * attributes. */ public HorizontalScrollViewWidget(Context context) { this(context, null); } /** * Same as {@link #HorizontalScrollViewWidget(android.content.Context, android.util.AttributeSet, int)} * with {@link android.R.attr#horizontalScrollViewStyle} as attribute for default style. */ public HorizontalScrollViewWidget(Context context, AttributeSet attrs) { this(context, attrs, android.R.attr.horizontalScrollViewStyle); } /** * Creates a new instance of HorizontalScrollViewWidget 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 HorizontalScrollViewWidget(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); /** * Process attributes. */ final TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.Ui_Widget_HorizontalScrollView, 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_HorizontalScrollView_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(HorizontalScrollViewWidget.class.getName()); } /** */ @Override public void onInitializeAccessibilityNodeInfo(@NonNull AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); info.setClassName(HorizontalScrollViewWidget.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 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 ----------------------------------------------------------------------------------- */ /** * 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_HorizontalScrollView_uiBackgroundTint)) { mTintInfo.backgroundTintList = typedArray .getColorStateList(R.styleable.Ui_Widget_HorizontalScrollView_uiBackgroundTint); } // Get tint modes. mTintInfo.backgroundTintMode = TintManager.parseTintMode( typedArray.getInt(R.styleable.Ui_Widget_HorizontalScrollView_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 =============================================================================== */ }