[Перевод] Material ProgressBar для pre-Lollipop
На момент написания статьи я работаю с отличными ребятами в Novoda над приложением для трансляции видео для телевидения в Великобритании Channel 4. Один из элементов дизайна, которые мне приходилось реализовывать был бесконечный ProgressBar в стиле Material Design. Для Android Lollipop и выше создание подобного дизайна не составляет труда, но вот поддержка устройств более ранних версий ОС стала для нас испытанием. В этой статье мы рассмотрим решение данной проблемы.
Для начала посмотрим, как работает на Lollipop бесконечный ProgressBar
:
В то время как стиль виджета выглядел довольно легко реализуем, то корень проблемы лежал в анимации с неопределенным временем. Короткая линия направляется с лева на право, но длина ее варьируется на протяжении путешествия.
Первым делом, я решил сделать бекпорт существующего решения. Но возникли кое-какие трудности с реализацией, так как там использовался AnimatedVectorDrawable
, который не доступен до Lollipop. Решение, которое я нашел, немного коварно, но все же дало мне на удивление очень похожий результат.
Решение включает в себя создание нашей собственной реализации ProgressBar
(что является наследником стандартного ProgressBar
) который полностью обходит стандартную логику неопределенного времени и реализует свою собственную на основе стандартной первичного и вторичного поведения, что уже встроены в ProgressBar
. Трюк заключается в методе его обработки — сначала задний план, потом второй прогресс и затем первый. Если задний план и первичный прогресс одного цвета, а вторичный прогресс другого цвета, то создается эффект изменения длины.
Посмотрим на примере для лучшего понимания. Если мы поставим цвет заднего плана светло-зеленым, вторичный прогресс средне-зеленым, а первичный прогресс — темно-зеленым, то получим такой результат:
Тем не менее, если мы установим первичный цвет такой же как и задний план, то получим нужный эффект:
Мы можем настраивать начальные и конечные точки просто задавая значение secondaryProgress и значение ProgressBar соответственно.
А теперь посмотрим, как же мы все-таки это реализовали в коде:
public class MaterialProgressBar extends ProgressBar {
private static final int INDETERMINATE_MAX = 1000;
private static final String SECONDARY_PROGRESS = "secondaryProgress";
private static final String PROGRESS = "progress";
private Animator animator = null;
private final int duration;
public MaterialProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.MaterialProgressBar, defStyleAttr, 0);
int backgroundColour;
int progressColour;
try {
backgroundColour = ta.getColor(R.styleable.MaterialProgressBar_backgroundColour, 0);
progressColour = ta.getColor(R.styleable.MaterialProgressBar_progressColour, 0);
int defaultDuration = context.getResources().getInteger(android.R.integer.config_mediumAnimTime);
duration = ta.getInteger(R.styleable.MaterialProgressBar_duration, defaultDuration);
} finally {
ta.recycle();
}
Resources resources = context.getResources();
setProgressDrawable(resources.getDrawable(android.R.drawable.progress_horizontal));
createIndeterminateProgressDrawable(backgroundColour, progressColour);
setMax(INDETERMINATE_MAX);
super.setIndeterminate(false);
this.setIndeterminate(true);
}
private void createIndeterminateProgressDrawable(@ColorInt int backgroundColour, @ColorInt int progressColour) {
LayerDrawable layerDrawable = (LayerDrawable) getProgressDrawable();
if (layerDrawable != null) {
layerDrawable.mutate();
layerDrawable.setDrawableByLayerId(android.R.id.background, createShapeDrawable(backgroundColour));
layerDrawable.setDrawableByLayerId(android.R.id.progress, createClipDrawable(backgroundColour));
layerDrawable.setDrawableByLayerId(android.R.id.secondaryProgress, createClipDrawable(progressColour));
}
}
private Drawable createClipDrawable(@ColorInt int colour) {
ShapeDrawable shapeDrawable = createShapeDrawable(colour);
return new ClipDrawable(shapeDrawable, Gravity.START, ClipDrawable.HORIZONTAL);
}
private ShapeDrawable createShapeDrawable(@ColorInt int colour) {
ShapeDrawable shapeDrawable = new ShapeDrawable();
setColour(shapeDrawable, colour);
return shapeDrawable;
}
private void setColour(ShapeDrawable drawable, int colour) {
Paint paint = drawable.getPaint();
paint.setColor(colour);
}
.
.
.
}
Основной метод здесь — createIndeterminateProgressDrawable()
который заменяет слой в LayerDrawable
(который будет обрабатываться как ProgressBar
) с подходящими цветами.
Еще хотелось бы отметить то, что мы жестко прописываем это, как ProgressBar
с неопределенным временем в конструкторе – для того чтобы сохранить простоту и легкость понимания примера. На самом деле существует дополнительный код для переключения стандартного ProgressBar
в режим с неопределенным временем.
Теперь мы можем прорисовывать сегмент, но как его анимировать? Это на удивление просто — мы анимируем прогресс и вторичный прогресс значением ProgressBar, но используя разные интерполяторы:
public class MaterialProgressBar extends ProgressBar {
.
.
.
@Override
public synchronized void setIndeterminate(boolean indeterminate) {
if (isStarted()) {
return;
}
animator = createIndeterminateAnimator();
animator.setTarget(this);
animator.start();
}
private boolean isStarted() {
return animator != null && animator.isStarted();
}
private Animator createIndeterminateAnimator() {
AnimatorSet set = new AnimatorSet();
Animator progressAnimator = getAnimator(SECONDARY_PROGRESS, new DecelerateInterpolator());
Animator secondaryProgressAnimator = getAnimator(PROGRESS, new AccelerateInterpolator());
set.playTogether(progressAnimator, secondaryProgressAnimator);
set.setDuration(duration);
return set;
}
@NonNull
private ObjectAnimator getAnimator(String propertyName, Interpolator interpolator) {
ObjectAnimator progressAnimator = ObjectAnimator.ofInt(this, propertyName, 0, INDETERMINATE_MAX);
progressAnimator.setInterpolator(interpolator);
progressAnimator.setDuration(duration);
progressAnimator.setRepeatMode(ValueAnimator.RESTART);
progressAnimator.setRepeatCount(ValueAnimator.INFINITE);
return progressAnimator;
}
}
Сделав наш ProgressBar
чуть больше обычного, и немного замедлив анимацию мы увидим следующее:
Все же вернем его к нормальным размерам и скоростям и сравним со стандартным виджетом ProgressBar неопределенного времени, реализованным в Lollipop:
Конечно же, они не являются идентичными — реализация в Lollipop имеет анимацию на секунду короче. Не смотря на это, данная реализация достаточно близка к исходной чтобы быть использованной на устройствах до Lollipop.
Исходный код используемый в статье доступен по ссылке.