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