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.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); } } }