/* * Copyright (C) 2013 Google Inc. * Licensed to The Android Open Source Project. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.mail.ui; import android.animation.ValueAnimator; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.drawable.GradientDrawable; import android.util.AttributeSet; import android.view.View; import android.view.animation.Interpolator; import com.android.mail.R; /** * Procedurally-drawn version of a horizontal indeterminate progress bar. Draws faster and more * frequently (by making use of the animation timer), requires minimal memory overhead, and allows * some configuration via attributes: * *

* This progress bar has no intrinsic height, so you must declare it with one explicitly. (It will * use the given height as the bar's shadow height.) */ public class ButteryProgressBar extends View { private final GradientDrawable mShadow; private final ValueAnimator mAnimator; private final Paint mPaint = new Paint(); private final int mBarColor; private final int mSolidBarHeight; private final int mSolidBarDetentWidth; private final float mDensity; private int mSegmentCount; /** * The baseline width that the other constants below are optimized for. */ private static final int BASE_WIDTH_DP = 300; /** * A reasonable animation duration for the given width above. It will be weakly scaled up and * down for wider and narrower widths, respectively-- the goal is to provide a relatively * constant detent velocity. */ private static final int BASE_DURATION_MS = 500; /** * A reasonable number of detents for the given width above. It will be weakly scaled up and * down for wider and narrower widths, respectively. */ private static final int BASE_SEGMENT_COUNT = 5; private static final int DEFAULT_BAR_HEIGHT_DP = 4; private static final int DEFAULT_DETENT_WIDTH_DP = 3; public ButteryProgressBar(Context c) { this(c, null); } public ButteryProgressBar(Context c, AttributeSet attrs) { super(c, attrs); mDensity = c.getResources().getDisplayMetrics().density; final TypedArray ta = c.obtainStyledAttributes(attrs, R.styleable.ButteryProgressBar); try { mBarColor = ta.getColor(R.styleable.ButteryProgressBar_barColor, c.getResources().getColor(android.R.color.holo_blue_light)); mSolidBarHeight = ta.getDimensionPixelSize( R.styleable.ButteryProgressBar_barHeight, Math.round(DEFAULT_BAR_HEIGHT_DP * mDensity)); mSolidBarDetentWidth = ta.getDimensionPixelSize( R.styleable.ButteryProgressBar_detentWidth, Math.round(DEFAULT_DETENT_WIDTH_DP * mDensity)); } finally { ta.recycle(); } mAnimator = new ValueAnimator(); mAnimator.setFloatValues(1.0f, 2.0f); mAnimator.setRepeatCount(ValueAnimator.INFINITE); mAnimator.setInterpolator(new ExponentialInterpolator()); mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { invalidate(); } }); mPaint.setColor(mBarColor); mShadow = new GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM, new int[]{(mBarColor & 0x00ffffff) | 0x22000000, 0}); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { if (changed) { final int w = getWidth(); mShadow.setBounds(0, mSolidBarHeight, w, getHeight() - mSolidBarHeight); final float widthMultiplier = w / mDensity / BASE_WIDTH_DP; // simple scaling by width is too aggressive, so dampen it first final float durationMult = 0.3f * (widthMultiplier - 1) + 1; final float segmentMult = 0.1f * (widthMultiplier - 1) + 1; mAnimator.setDuration((int) (BASE_DURATION_MS * durationMult)); mSegmentCount = (int) (BASE_SEGMENT_COUNT * segmentMult); } } @Override protected void onDraw(Canvas canvas) { if (!mAnimator.isStarted()) { return; } mShadow.draw(canvas); final float val = (Float) mAnimator.getAnimatedValue(); final int w = getWidth(); // Because the left-most segment doesn't start all the way on the left, and because it moves // towards the right as it animates, we need to offset all drawing towards the left. This // ensures that the left-most detent starts at the left origin, and that the left portion // is never blank as the animation progresses towards the right. final int offset = w >> mSegmentCount - 1; // segments are spaced at half-width, quarter, eighth (powers-of-two). to maintain a smooth // transition between segments, we used a power-of-two interpolator. for (int i = 0; i < mSegmentCount; i++) { final float l = val * (w >> (i + 1)); final float r = (i == 0) ? w + offset : l * 2; canvas.drawRect(l + mSolidBarDetentWidth - offset, 0, r - offset, mSolidBarHeight, mPaint); } } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); start(); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); stop(); } @Override protected void onVisibilityChanged(View changedView, int visibility) { super.onVisibilityChanged(changedView, visibility); if (visibility == VISIBLE) { start(); } else { stop(); } } private void start() { if (getVisibility() != VISIBLE) { return; } mAnimator.start(); } private void stop() { mAnimator.cancel(); } private static class ExponentialInterpolator implements Interpolator { @Override public float getInterpolation(float input) { return (float) Math.pow(2.0, input) - 1; } } }