Статьи

Сделайте ваш индикатор прогресса более плавным в Android

Если вы используете приложение Android Gmail, вы, вероятно, заметили, что индикатор выполнения немного настроен. Я не говорю о обновлении, но о неопределенном индикаторе выполнения, который появляется сразу после. Эта неопределенная прорисовка намного плавнее, чем обычно.

В этом посте я покажу вам способ воспроизвести этот плавный неопределенный горизонтальный индикатор выполнения. Вот результат: в первом индикаторе выполнения используется неопределенная отрисовка по умолчанию, в то время как остальные все настраиваемые.

Очевидно, есть проблема с размером видео, поэтому вы можете нажать на эту ссылку, чтобы увидеть его на Youtube.

Как работает анимация по умолчанию

Прежде всего, вам нужно будет использовать стиль горизонтального индикатора выполнения: Widget.ProgressBar.Horizontalдля устройств до ICS и Widget.Holo.ProgressBar.Horizontalдля устройств после ICS. Это дает нам два важных параметра: indeterminateOnly=falseи indeterminateDrawable.

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

<animation-list xmlns:android="http://schemas.android.com/apk/res/android" android:oneshot="false">
    <item android:drawable="@drawable/progressbar_indeterminate_holo1" android:duration="50" />
    <item android:drawable="@drawable/progressbar_indeterminate_holo2" android:duration="50" />
    <item android:drawable="@drawable/progressbar_indeterminate_holo3" android:duration="50" />
    <item android:drawable="@drawable/progressbar_indeterminate_holo4" android:duration="50" />
    <item android:drawable="@drawable/progressbar_indeterminate_holo5" android:duration="50" />
    <item android:drawable="@drawable/progressbar_indeterminate_holo6" android:duration="50" />
    <item android:drawable="@drawable/progressbar_indeterminate_holo7" android:duration="50" />
    <item android:drawable="@drawable/progressbar_indeterminate_holo8" android:duration="50" />
</animation-list>

Это простая анимация. Вот почему у нас не может быть гладкости, которую мы хотим. Нам просто нужно сделать настраиваемый неопределенный объект рисования, который динамически рисует линии вместо рисования растровых изображений.

Пользовательский неопределенный Drawable

Вот моя оригинальная идея (не впечатляйтесь моими графическими навыками):

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

Примечание : мои математические навыки немного плохи, так что, возможно, я говорю глупости, не стесняйтесь комментировать 🙂

Вот код, который я использовал.

int i = 0;
// we loop to draw lines until we reach the max width
while (prev < width) {
  float value = (float) Math.expm1(++i + offset);
    canvas.drawLine(prev, centerY, prev + value - mSeparatorWidth, centerY, mPaint);
    prev = value + prev;
}

Я использовал функцию expm1 (которая возвращает exp (x) — 1), но вы можете попробовать любую растущую функцию. Мы могли бы представить функцию, которая умножается на две предыдущие части каждый раз, или какую-то последовательность Фибоначчи.

Вот результат для кода выше:

Я мог бы остановиться здесь, но я хотел пойти дальше и дать нашему модулю возможность полностью настраиваться.

Используйте интерполяторы!

Мне всегда нравились отношения между этими кривыми и анимацией. Это позволяет разработчикам радикально изменить свою анимацию в одну строку. Вы можете заставить его отскочить, перескочить, … просто изменив интерполятор. Я использовал здесь интерполяторы, чтобы изменить внешний вид анимации.

Диаграмма лучше слов

Хорошо. Видя это, код будет лучше, чем мой график. Извини за это.

В основном, мы устанавливаем несколько разделов. Интерполятор даст нам соотношение длины каждой секции. Анимация выполняется смещением, увеличенным в каждом кадре.

int width = mBounds.width() + mSeparatorWidth;
int centerY = mBounds.centerY();
float xSectionWidth = 1f / mSectionsCount;

//line before the first section
int offset = (int) (Math.abs(mInterpolator.getInterpolation(mCurrentOffset) - mInterpolator.getInterpolation(0)) * width);
if (offset > 0) {
    if (offset > mSeparatorWidth) {
        canvas.drawLine(0, centerY, offset - mSeparatorWidth, centerY, mPaint);
    }
}

int prev;
int end;
int spaceLength;
for (int i = 0; i < mSectionsCount; ++i) {
    float xOffset = xSectionWidth * i + mCurrentOffset;
    prev = (int) (mInterpolator.getInterpolation(xOffset) * width);
    float ratioSectionWidth =
            Math.abs(mInterpolator.getInterpolation(xOffset) -
                    mInterpolator.getInterpolation(Math.min(xOffset + xSectionWidth, 1f)));

    //separator between each piece of line                
    int sectionWidth = (int) (width * ratioSectionWidth);
    if (sectionWidth + prev < width)
        spaceLength = Math.min(sectionWidth, mSeparatorWidth);
    else
      spaceLength = 0;
    int drawLength = sectionWidth > spaceLength ? sectionWidth - spaceLength : 0;
    end = prev + drawLength;

    if (end > prev) {
        canvas.drawLine(prev, centerY, end, centerY, mPaint);
    }
}

Вот результат с некоторыми основными интерполяторами:

Извините за задержку в GIF, образец apk скоро будет доступен, так что следите за обновлениями;)

Ограничения : Ваш интерполятор должен быть монотонной функцией!

Библиотека

Я сделал небольшую библиотеку доступной на Github ! не стесняйтесь, чтобы раскошелиться и сделать запросы тянуть!