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