package com.beardedhen.androidbootstrap; import android.animation.Animator; import android.animation.ValueAnimator; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.Rect; import android.graphics.RectF; import android.os.Bundle; import android.os.Parcelable; import android.support.annotation.ColorInt; import android.support.annotation.NonNull; import android.util.AttributeSet; import android.view.View; import android.view.ViewParent; import android.view.animation.DecelerateInterpolator; import android.view.animation.LinearInterpolator; import com.beardedhen.androidbootstrap.api.attributes.BootstrapBrand; import com.beardedhen.androidbootstrap.api.defaults.DefaultBootstrapBrand; import com.beardedhen.androidbootstrap.api.defaults.DefaultBootstrapSize; import com.beardedhen.androidbootstrap.api.view.BootstrapBrandView; import com.beardedhen.androidbootstrap.api.view.BootstrapSizeView; import com.beardedhen.androidbootstrap.api.view.ProgressView; import com.beardedhen.androidbootstrap.api.view.RoundableView; import com.beardedhen.androidbootstrap.utils.ColorUtils; import com.beardedhen.androidbootstrap.utils.DimenUtils; import java.io.Serializable; import static android.graphics.Bitmap.Config.ARGB_8888; /** * BootstrapProgressBar displays determinate progress to the user, and is colored with BootstrapBrands. * Striped effects and progress update animations are supported out of the box. * * Its possible to group multiple together in an {@link com.beardedhen.androidbootstrap.BootstrapProgressBarGroup BootstrapProgressBarGroup} to give the appearance of a <a href="http://getbootstrap.com/components/#progress-stacked">stacked</a> progressbar. */ public class BootstrapProgressBar extends View implements ProgressView, BootstrapBrandView, RoundableView, BootstrapSizeView, Animator.AnimatorListener, ValueAnimator.AnimatorUpdateListener { private static final String TAG = "com.beardedhen.androidbootstrap.AwesomeTextView"; private static final long UPDATE_ANIM_MS = 300; private static final int STRIPE_ALPHA = 150; private static final long STRIPE_CYCLE_MS = 1500; private Paint progressPaint; private Paint stripePaint; private Paint bgPaint; private Paint textPaint; private int userProgress; private int drawnProgress; private int maxProgress; private boolean striped; private boolean animated; private boolean rounded; //used for progressbarGroup so that only the currect corners will be rounded private boolean canRoundLeft = true; private boolean canRoundRight = true; private ValueAnimator progressAnimator; private Paint tilePaint; private final float baselineHeight = DimenUtils.pixelsFromDpResource(getContext(), R.dimen.bootstrap_progress_bar_height); private BootstrapBrand bootstrapBrand; private Canvas progressCanvas; private Bitmap progressBitmap; private Bitmap stripeTile; private float bootstrapSize; private boolean showPercentage; public BootstrapProgressBar(Context context) { super(context); initialise(null); } public BootstrapProgressBar(Context context, AttributeSet attrs) { super(context, attrs); initialise(attrs); } public BootstrapProgressBar(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initialise(attrs); } private void initialise(AttributeSet attrs) { ValueAnimator.setFrameDelay(15); // attempt 60fps tilePaint = new Paint(); progressPaint = new Paint(); progressPaint.setStyle(Paint.Style.FILL); progressPaint.setAntiAlias(true); stripePaint = new Paint(); stripePaint.setStyle(Paint.Style.FILL); stripePaint.setAntiAlias(true); textPaint = new Paint(); textPaint.setStyle(Paint.Style.FILL); textPaint.setAntiAlias(true); textPaint.setColor(ColorUtils.resolveColor(android.R.color.black, getContext())); textPaint.setTextSize(DimenUtils.pixelsFromSpResource(getContext(), R.dimen.bootstrap_progress_bar_default_font_size)); bgPaint = new Paint(); bgPaint.setStyle(Paint.Style.FILL); bgPaint.setColor(ColorUtils.resolveColor(R.color.bootstrap_gray_light, getContext())); // get attributes TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.BootstrapProgressBar); try { this.animated = a.getBoolean(R.styleable.BootstrapProgressBar_animated, false); this.rounded = a.getBoolean(R.styleable.BootstrapProgressBar_roundedCorners, false); this.striped = a.getBoolean(R.styleable.BootstrapProgressBar_striped, false); this.showPercentage = a.getBoolean(R.styleable.BootstrapProgressBar_bootstrapshowPercentage, false); this.userProgress = a.getInt(R.styleable.BootstrapProgressBar_bootstrapProgress, 0); this.maxProgress = a.getInt(R.styleable.BootstrapProgressBar_bootstrapMaxProgress, 100); int typeOrdinal = a.getInt(R.styleable.BootstrapProgressBar_bootstrapBrand, -1); int sizeOrdinal = a.getInt(R.styleable.BootstrapProgressBar_bootstrapSize, -1); this.bootstrapSize = DefaultBootstrapSize.fromAttributeValue(sizeOrdinal).scaleFactor(); this.bootstrapBrand = DefaultBootstrapBrand.fromAttributeValue(typeOrdinal); this.drawnProgress = userProgress; } finally { a.recycle(); } textPaint.setColor(bootstrapBrand.defaultTextColor(getContext())); textPaint.setTextSize((DimenUtils.pixelsFromSpResource(getContext(), R.dimen.bootstrap_button_default_font_size)) * this.bootstrapSize ); updateBootstrapState(); setProgress(this.userProgress); setMaxProgress(this.maxProgress); } @Override public Parcelable onSaveInstanceState() { Bundle bundle = new Bundle(); bundle.putParcelable(TAG, super.onSaveInstanceState()); bundle.putInt(KEY_USER_PROGRESS, userProgress); bundle.putInt(KEY_DRAWN_PROGRESS, drawnProgress); bundle.putBoolean(KEY_STRIPED, striped); bundle.putBoolean(KEY_ANIMATED, animated); bundle.putBoolean(RoundableView.KEY, rounded); bundle.putFloat(BootstrapSizeView.KEY, bootstrapSize); bundle.putSerializable(BootstrapBrand.KEY, bootstrapBrand); return bundle; } @Override public void onRestoreInstanceState(Parcelable state) { if (state instanceof Bundle) { Bundle bundle = (Bundle) state; Serializable brand = bundle.getSerializable(BootstrapBrand.KEY); if (brand instanceof BootstrapBrand) { bootstrapBrand = (BootstrapBrand) brand; } this.userProgress = bundle.getInt(KEY_USER_PROGRESS); this.drawnProgress = bundle.getInt(KEY_DRAWN_PROGRESS); this.striped = bundle.getBoolean(KEY_STRIPED); this.animated = bundle.getBoolean(KEY_ANIMATED); this.rounded = bundle.getBoolean(RoundableView.KEY); this.bootstrapSize = bundle.getFloat(BootstrapSizeView.KEY); state = bundle.getParcelable(TAG); } super.onRestoreInstanceState(state); updateBootstrapState(); setProgress(userProgress); } private int getStripeColor(@ColorInt int color) { return Color.argb(STRIPE_ALPHA, Color.red(color), Color.green(color), Color.blue(color)); } /** * Starts an animation which moves the progress bar from one value to another, in response to * a call to setProgress(). Animation update callbacks allow the interpolator value to be used * to calculate the current progress value, which is stored in a temporary variable. The view is * then invalidated. * <p/> * If this method is called when a progress update animation is already running, the previous * animation will be cancelled, and the currently drawn progress recorded. A new animation will * then be started from the last drawn point. */ private void startProgressUpdateAnimation() { clearAnimation(); progressAnimator = ValueAnimator.ofFloat(drawnProgress, userProgress); progressAnimator.setDuration(UPDATE_ANIM_MS); progressAnimator.setRepeatCount(0); progressAnimator.setRepeatMode(ValueAnimator.RESTART); progressAnimator.setInterpolator(new DecelerateInterpolator()); progressAnimator.addUpdateListener(this); // start striped animation after progress update if needed progressAnimator.addListener(this); progressAnimator.start(); } @Override public void onAnimationUpdate(ValueAnimator animation) { drawnProgress = (int) ((float) animation.getAnimatedValue()); invalidate(); } @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationEnd(Animator animation) { startStripedAnimationIfNeeded(); // start striped animation after progress update } @Override public void onAnimationCancel(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { } /** * Starts an infinite animation cycle which provides the visual effect of stripes moving * backwards. The current system time is used to offset tiled bitmaps of the progress background, * producing the effect that the stripes are moving backwards. */ private void startStripedAnimationIfNeeded() { if (!striped || !animated) { return; } clearAnimation(); progressAnimator = ValueAnimator.ofFloat(0, 0); progressAnimator.setDuration(UPDATE_ANIM_MS); progressAnimator.setRepeatCount(ValueAnimator.INFINITE); progressAnimator.setRepeatMode(ValueAnimator.RESTART); progressAnimator.setInterpolator(new LinearInterpolator()); progressAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { invalidate(); } }); progressAnimator.start(); } /* * Custom Measuring/Drawing */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // restrict view to default progressbar height int width = MeasureSpec.getSize(widthMeasureSpec); int height = MeasureSpec.getSize(heightMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); switch (heightMode) { case MeasureSpec.EXACTLY: break; case MeasureSpec.AT_MOST: // prefer default height, if not all available use as much as possible float desiredHeight = (baselineHeight * bootstrapSize); height = (height > desiredHeight) ? (int) desiredHeight : height; break; default: height = (int) (baselineHeight * bootstrapSize); break; } setMeasuredDimension(width, height); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { if (h != oldh) { stripeTile = null; // dereference cached bitmap } super.onSizeChanged(w, h, oldw, oldh); } @Override protected void onDraw(Canvas canvas) { float w = getWidth(); float h = getHeight(); if (w <= 0 || h <= 0) { return; } if (progressBitmap == null) { progressBitmap = Bitmap.createBitmap((int) w, (int) h, ARGB_8888); } if (progressCanvas == null) { progressCanvas = new Canvas(progressBitmap); } progressCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); float ratio = (drawnProgress / (float) maxProgress); int lineEnd = (int) (w * ratio); float offset = 0; float offsetFactor = (System.currentTimeMillis() % STRIPE_CYCLE_MS) / (float) STRIPE_CYCLE_MS; if (striped && animated) { // determine offset for current animation frame of progress bar offset = (h * 2) * offsetFactor; } if (striped) { // draw a regular striped bar if (stripeTile == null) { stripeTile = createTile(h, stripePaint, progressPaint); } float start = 0 - offset; while (start < lineEnd) { // FIXME progressCanvas.drawBitmap(stripeTile, start, 0, tilePaint); start += stripeTile.getWidth(); } } else { // draw a filled bar progressCanvas.drawRect(0, 0, lineEnd, h, progressPaint); } progressCanvas.drawRect(lineEnd, 0, w, h, bgPaint); // draw bg float corners = rounded ? h / 2 : 0; Bitmap round = createRoundedBitmap(progressBitmap, corners, canRoundRight, canRoundLeft); canvas.drawBitmap(round, 0, 0, tilePaint); if(showPercentage) { String percent = getProgress() + "%"; int xPos = (lineEnd / 2); xPos = xPos - (int) (textPaint.measureText(percent) / 2); int yPos = (int) ((canvas.getHeight() / 2) - ((textPaint.descent() + textPaint.ascent()) / 2)); //((textPaint.descent() + textPaint.ascent()) / 2) is the distance from the baseline to the center. canvas.drawText(percent, xPos, yPos, textPaint); } } /** * Creates a Bitmap which is a tile of the progress bar background * * @param h the view height * @return a bitmap of the progress bar background */ private static Bitmap createTile(float h, Paint stripePaint, Paint progressPaint) { Bitmap bm = Bitmap.createBitmap((int) h * 2, (int) h, ARGB_8888); Canvas tile = new Canvas(bm); float x = 0; Path path = new Path(); path.moveTo(x, 0); path.lineTo(x, h); path.lineTo(h, h); tile.drawPath(path, stripePaint); // draw striped triangle path.reset(); path.moveTo(x, 0); path.lineTo(x + h, h); path.lineTo(x + (h * 2), h); path.lineTo(x + h, 0); tile.drawPath(path, progressPaint); // draw progress parallelogram x += h; path.reset(); path.moveTo(x, 0); path.lineTo(x + h, 0); path.lineTo(x + h, h); tile.drawPath(path, stripePaint); // draw striped triangle (completing tile) return bm; } /** * Creates a rounded bitmap with transparent corners, from a square bitmap. * See <a href="http://stackoverflow.com/questions/4028270">StackOverflow</a> * * @param bitmap the original bitmap * @param cornerRadius the radius of the corners * @param roundRight if you should round the corners on the right, note that if set to true and cornerRadius == 0 it will create a square * @param roundLeft if you should round the corners on the right, note that if set to true and cornerRadius == 0 it will create a square * @return a rounded bitmap */ private static Bitmap createRoundedBitmap(Bitmap bitmap, float cornerRadius, boolean roundRight, boolean roundLeft) { Bitmap roundedBitmap = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), ARGB_8888); Canvas canvas = new Canvas(roundedBitmap); final Paint paint = new Paint(); final Rect frame = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()); // final Rect frameLeft = new Rect(0, 0, bitmap.getWidth() /2, bitmap.getHeight()); // final Rect frameRight = new Rect(bitmap.getWidth() /2, bitmap.getHeight(), bitmap.getWidth(), bitmap.getHeight()); final Rect leftRect = new Rect(0, 0, bitmap.getWidth() / 2, bitmap.getHeight()); final Rect rightRect = new Rect(bitmap.getWidth() / 2, 0, bitmap.getWidth(), bitmap.getHeight()); // prepare canvas for transfer paint.setAntiAlias(true); paint.setColor(0xFFFFFFFF); paint.setStyle(Paint.Style.FILL); canvas.drawARGB(0, 0, 0, 0); canvas.drawRoundRect(new RectF(frame), cornerRadius, cornerRadius, paint); if (!roundLeft){ canvas.drawRect(leftRect, paint); } if (!roundRight){ canvas.drawRect(rightRect, paint); } // draw bitmap paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); canvas.drawBitmap(bitmap, frame, frame, paint); return roundedBitmap; } private void updateBootstrapState() { int color = bootstrapBrand.defaultFill(getContext()); progressPaint.setColor(color); stripePaint.setColor(getStripeColor(color)); invalidateDrawCache(); invalidate(); } private void invalidateDrawCache() { stripeTile = null; progressBitmap = null; progressCanvas = null; } /* * Getters/Setters */ @SuppressLint("DefaultLocale") @Override public void setProgress(int progress) { if (getParent() instanceof BootstrapProgressBarGroup){ this.userProgress = 0; setMaxProgress(progress); }else { if (progress < 0 || progress > maxProgress) { throw new IllegalArgumentException( String.format("Invalid value '%d' - progress must be an integer in the range 0-%d", progress, maxProgress)); } } this.userProgress = progress; if (animated) { startProgressUpdateAnimation(); } else { this.drawnProgress = progress; invalidate(); } ViewParent parent = getParent(); if (parent != null) { if (parent instanceof BootstrapProgressBarGroup) { BootstrapProgressBarGroup parentGroup = (BootstrapProgressBarGroup) parent; parentGroup.onProgressChanged(this); } } } @Override public int getProgress() { return userProgress; } @Override public void setStriped(boolean striped) { this.striped = striped; invalidate(); startStripedAnimationIfNeeded(); } @Override public boolean isStriped() { return striped; } @Override public void setAnimated(boolean animated) { this.animated = animated; invalidate(); startStripedAnimationIfNeeded(); } @Override public boolean isAnimated() { return animated; } @Override public void setBootstrapBrand(@NonNull BootstrapBrand bootstrapBrand) { this.bootstrapBrand = bootstrapBrand; textPaint.setColor(bootstrapBrand.defaultTextColor(getContext())); updateBootstrapState(); } @NonNull @Override public BootstrapBrand getBootstrapBrand() { return bootstrapBrand; } @Override public void setRounded(boolean rounded) { this.rounded = rounded; updateBootstrapState(); } @Override public boolean isRounded() { return rounded; } @Override public float getBootstrapSize() { return bootstrapSize; } @Override public void setBootstrapSize(float bootstrapSize) { this.bootstrapSize = bootstrapSize; textPaint.setTextSize((DimenUtils.pixelsFromSpResource(getContext(), R.dimen.bootstrap_progress_bar_default_font_size)) * this.bootstrapSize ); requestLayout(); updateBootstrapState(); } @Override public void setBootstrapSize(DefaultBootstrapSize bootstrapSize) { setBootstrapSize(bootstrapSize.scaleFactor()); } /** * * @return int, the max progress. */ public int getMaxProgress() { return maxProgress; } /** * Used for settings the maxprogress. Also check if currentProgress is smaller than newMaxProgress. * @param newMaxProgress the maxProgress value */ public void setMaxProgress(int newMaxProgress) { if (getProgress() <= newMaxProgress) { maxProgress = newMaxProgress; } else { throw new IllegalArgumentException( String.format("MaxProgress cant be smaller than the current progress %d<%d", getProgress(), newMaxProgress)); } invalidate(); BootstrapProgressBarGroup parent = (BootstrapProgressBarGroup) getParent(); } void setCornerRounding(boolean left, boolean right){ canRoundLeft = left; canRoundRight = right; } boolean getCornerRoundingLeft(){ return canRoundLeft; } boolean getCornerRoundingRight(){ return canRoundRight; } }