1 package com.android.mail.ui; 2 3 import android.animation.ValueAnimator; 4 import android.content.Context; 5 import android.content.res.TypedArray; 6 import android.graphics.Canvas; 7 import android.graphics.Paint; 8 import android.graphics.drawable.GradientDrawable; 9 import android.util.AttributeSet; 10 import android.view.View; 11 import android.view.animation.Interpolator; 12 13 import com.android.mail.R; 14 15 /** 16 * Procedurally-drawn version of a horizontal indeterminate progress bar. Draws faster and more 17 * frequently (by making use of the animation timer), requires minimal memory overhead, and allows 18 * some configuration via attributes: 19 * <ul> 20 * <li>barColor (color attribute for the bar's solid color) 21 * <li>barHeight (dimension attribute for the height of the solid progress bar) 22 * <li>detentWidth (dimension attribute for the width of each transparent detent in the bar) 23 * </ul> 24 * <p> 25 * This progress bar has no intrinsic height, so you must declare it with one explicitly. (It will 26 * use the given height as the bar's shadow height.) 27 */ 28 public class ButteryProgressBar extends View { 29 30 private final GradientDrawable mShadow; 31 private final ValueAnimator mAnimator; 32 33 private final Paint mPaint = new Paint(); 34 35 private final int mBarColor; 36 private final int mSolidBarHeight; 37 private final int mSolidBarDetentWidth; 38 39 private final float mDensity; 40 41 private int mSegmentCount; 42 43 /** 44 * The baseline width that the other constants below are optimized for. 45 */ 46 private static final int BASE_WIDTH_DP = 300; 47 /** 48 * A reasonable animation duration for the given width above. It will be weakly scaled up and 49 * down for wider and narrower widths, respectively-- the goal is to provide a relatively 50 * constant detent velocity. 51 */ 52 private static final int BASE_DURATION_MS = 500; 53 /** 54 * A reasonable number of detents for the given width above. It will be weakly scaled up and 55 * down for wider and narrower widths, respectively. 56 */ 57 private static final int BASE_SEGMENT_COUNT = 5; 58 59 private static final int DEFAULT_BAR_HEIGHT_DP = 4; 60 private static final int DEFAULT_DETENT_WIDTH_DP = 3; 61 62 public ButteryProgressBar(Context c) { 63 this(c, null); 64 } 65 66 public ButteryProgressBar(Context c, AttributeSet attrs) { 67 super(c, attrs); 68 69 mDensity = c.getResources().getDisplayMetrics().density; 70 71 final TypedArray ta = c.obtainStyledAttributes(attrs, R.styleable.ButteryProgressBar); 72 try { 73 mBarColor = ta.getColor(R.styleable.ButteryProgressBar_barColor, 74 c.getResources().getColor(android.R.color.holo_blue_light)); 75 mSolidBarHeight = ta.getDimensionPixelSize( 76 R.styleable.ButteryProgressBar_barHeight, 77 Math.round(DEFAULT_BAR_HEIGHT_DP * mDensity)); 78 mSolidBarDetentWidth = ta.getDimensionPixelSize( 79 R.styleable.ButteryProgressBar_detentWidth, 80 Math.round(DEFAULT_DETENT_WIDTH_DP * mDensity)); 81 } finally { 82 ta.recycle(); 83 } 84 85 mAnimator = new ValueAnimator(); 86 mAnimator.setFloatValues(1.0f, 2.0f); 87 mAnimator.setRepeatCount(ValueAnimator.INFINITE); 88 mAnimator.setInterpolator(new ExponentialInterpolator()); 89 mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 90 91 @Override 92 public void onAnimationUpdate(ValueAnimator animation) { 93 invalidate(); 94 } 95 96 }); 97 98 mPaint.setColor(mBarColor); 99 100 mShadow = new GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM, 101 new int[]{(mBarColor & 0x00ffffff) | 0x22000000, 0}); 102 } 103 104 @Override 105 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 106 if (changed) { 107 final int w = getWidth(); 108 109 mShadow.setBounds(0, mSolidBarHeight, w, getHeight() - mSolidBarHeight); 110 111 final float widthMultiplier = w / mDensity / BASE_WIDTH_DP; 112 // simple scaling by width is too aggressive, so dampen it first 113 final float durationMult = 0.3f * (widthMultiplier - 1) + 1; 114 final float segmentMult = 0.1f * (widthMultiplier - 1) + 1; 115 mAnimator.setDuration((int) (BASE_DURATION_MS * durationMult)); 116 mSegmentCount = (int) (BASE_SEGMENT_COUNT * segmentMult); 117 } 118 } 119 120 @Override 121 protected void onDraw(Canvas canvas) { 122 if (!mAnimator.isStarted()) { 123 return; 124 } 125 126 mShadow.draw(canvas); 127 128 final float val = (Float) mAnimator.getAnimatedValue(); 129 130 final int w = getWidth(); 131 // Because the left-most segment doesn't start all the way on the left, and because it moves 132 // towards the right as it animates, we need to offset all drawing towards the left. This 133 // ensures that the left-most detent starts at the left origin, and that the left portion 134 // is never blank as the animation progresses towards the right. 135 final int offset = w >> mSegmentCount - 1; 136 // segments are spaced at half-width, quarter, eighth (powers-of-two). to maintain a smooth 137 // transition between segments, we used a power-of-two interpolator. 138 for (int i = 0; i < mSegmentCount; i++) { 139 final float l = val * (w >> (i + 1)); 140 final float r = (i == 0) ? w + offset : l * 2; 141 canvas.drawRect(l + mSolidBarDetentWidth - offset, 0, r - offset, mSolidBarHeight, 142 mPaint); 143 } 144 } 145 146 @Override 147 protected void onVisibilityChanged(View changedView, int visibility) { 148 super.onVisibilityChanged(changedView, visibility); 149 150 if (visibility == VISIBLE) { 151 start(); 152 } else { 153 stop(); 154 } 155 } 156 157 private void start() { 158 if (mAnimator == null) { 159 return; 160 } 161 mAnimator.start(); 162 } 163 164 private void stop() { 165 if (mAnimator == null) { 166 return; 167 } 168 mAnimator.cancel(); 169 } 170 171 private static class ExponentialInterpolator implements Interpolator { 172 173 @Override 174 public float getInterpolation(float input) { 175 return (float) Math.pow(2.0, input) - 1; 176 } 177 178 } 179 180 } 181