На момент написания статьи я работаю с отличными ребятами в Novoda над приложением для трансляции видео для телевидения в Великобритании Channel 4. Один из элементов дизайна, которые мне приходилось реализовывать был бесконечный ProgressBar в стиле Material Design. Для Android Lollipop и выше создание подобного дизайна не составляет труда, но вот поддержка устройств более ранних версий ОС стала для нас испытанием. В этой статье мы рассмотрим решение данной проблемы.

Для начала посмотрим, как работает на Lollipop бесконечный ProgressBar:



В то время как стиль виджета выглядел довольно легко реализуем, то корень проблемы лежал в анимации с неопределенным временем. Короткая линия направляется с лева на право, но длина ее варьируется на протяжении путешествия.

Первым делом, я решил сделать бекпорт существующего решения. Но возникли кое-какие трудности с реализацией, так как там использовался AnimatedVectorDrawable, который не доступен до Lollipop. Решение, которое я нашел, немного коварно, но все же дало мне на удивление очень похожий результат.

Решение включает в себя создание нашей собственной реализации ProgressBar (что является наследником стандартного ProgressBar) который полностью обходит стандартную логику неопределенного времени и реализует свою собственную на основе стандартной первичного и вторичного поведения, что уже встроены в ProgressBar. Трюк заключается в методе его обработки — сначала задний план, потом второй прогресс и затем первый. Если задний план и первичный прогресс одного цвета, а вторичный прогресс другого цвета, то создается эффект изменения длины.

Посмотрим на примере для лучшего понимания. Если мы поставим цвет заднего плана светло-зеленым, вторичный прогресс средне-зеленым, а первичный прогресс — темно-зеленым, то получим такой результат:

image

Тем не менее, если мы установим первичный цвет такой же как и задний план, то получим нужный эффект:

image

Мы можем настраивать начальные и конечные точки просто задавая значение 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.

Исходный код используемый в статье доступен по ссылке.

Комментарии (10)


  1. FLashM
    06.11.2015 21:41
    +1

    С лева на право, говорите?


  1. withkittens
    07.11.2015 16:12
    +1

    Ещё различие в том, что у нативного прогрессбара две полосы: одна медленная, вторая быстрая, которая как бы не успевает догнать первую:


    1. withkittens
      07.11.2015 17:01
      +1

      Я объясняю ещё хуже, чем рисую, но попробую донести свою мысль:

      Заголовок спойлера


      1. AStefanovskiy
        07.11.2015 17:56

        Да, похоже на правду.


        1. withkittens
          07.11.2015 18:01
          +2

          А ещё есть же замечательный Polymer Project, оттуда можно стянуть параметры анимации ;)


    1. Antares19
      08.11.2015 04:37
      +1

      Кстати где-нибудь говорилось — есть ли в этом какой-то скрытый смысл кроме интересного визуального решения? (ну может по исследования в таком виде ожидание проходит веселее:)


  1. grishkaa
    07.11.2015 20:29
    +1

    Есть ещё вот такая библиотека, которая реализует горизонтальные и круглые прогрессбары в стиле material. Как раз недавно использовал у себя.


  1. cmepthuk
    08.11.2015 21:23

    Было бы здорово, если бы простая анимация в теле статьи была всё таки гифками, а не ифреймами на youtube. Быстрее, легче выше, сильнее. Понимаю, что это лишь перевод и этот комментарий скорее просто возглас в пустоту, но пользователей на мобильных гаджетах сограниченным запасом ресурсов и по сей день достаточно :)


    1. AStefanovskiy
      08.11.2015 23:17

      Сделал в виде гифок.


      1. cmepthuk
        08.11.2015 23:56

        Отлично! Большое человеческое спасибо за отзывчивость!