Как создать повторяющийся анимированный движущийся градиент, как неопределенный прогресс?

Фон

Android имеет стандартный ProgressBar со специальной анимацией, когда он неопределен. Существует также множество библиотек с таким количеством видов прогресса, которые доступны ( здесь).

Эта проблема

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

Имейте градиент от цвета X к цвету Y, который отображается горизонтально, и перемещается по координате X, чтобы цвета до X переходили в цвет Y.

Например (только иллюстрация), если у меня есть градиент синего <-> красного, от края до края, он будет выглядеть следующим образом:

enter image description here

Что я пробовал

Я пробовал некоторые решения, предлагаемые здесь в StackOverflow:

но, к сожалению, все они касаются стандартного представления ProgressBar для Android, что означает, что у него есть другой способ показать анимацию рисования.

Я также попытался найти что-то подобное на веб-сайте Android Arsenal, но, несмотря на то, что есть много хороших, я не мог найти такого.

Конечно, я мог бы просто анимировать 2 взгляда, каждый из которых имеет свой собственный градиент (одна противоположность другой), но я уверен, что есть лучший способ.

Вопрос

Можно ли использовать Drawable или анимацию, что делает градиент (или что-то еще) перемещаться таким образом (повторяя, конечно)?

Может быть, просто простирайтесь от ImageView и оживите туда?

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


РЕДАКТИРОВАТЬ:

Хорошо, я немного продвинулся, но я не уверен, что движение в порядке, и я думаю, что он не будет соответствовать скорости, как и следовало бы, в случае, если процессор немного занят, потому что он не учитывает капли кадров. Я сделал, чтобы нарисовать 2 GradientDrawables один рядом с другим, как таковой:

class HorizontalProgressView @JvmOverloads constructor(
        context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
    private val speedInPercentage = 1.5f
    private var xMovement: Float = 0.0f
    private val rightDrawable: GradientDrawable = GradientDrawable()
    private val leftDrawable: GradientDrawable = GradientDrawable()

    init {
        if (isInEditMode)
            setGradientColors(intArrayOf(Color.RED, Color.BLUE))
        rightDrawable.gradientType = GradientDrawable.LINEAR_GRADIENT;
        rightDrawable.orientation = GradientDrawable.Orientation.LEFT_RIGHT
        rightDrawable.shape = GradientDrawable.RECTANGLE;
        leftDrawable.gradientType = GradientDrawable.LINEAR_GRADIENT;
        leftDrawable.orientation = GradientDrawable.Orientation.RIGHT_LEFT
        leftDrawable.shape = GradientDrawable.RECTANGLE;
    }

    fun setGradientColors(colors: IntArray) {
        rightDrawable.colors = colors
        leftDrawable.colors = colors
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        val widthSize = View.MeasureSpec.getSize(widthMeasureSpec)
        val heightSize = View.MeasureSpec.getSize(heightMeasureSpec)
        rightDrawable.setBounds(0, 0, widthSize, heightSize)
        leftDrawable.setBounds(0, 0, widthSize, heightSize)
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        canvas.save()
        if (xMovement < width) {
            canvas.translate(xMovement, 0.0f)
            rightDrawable.draw(canvas)
            canvas.translate(-width.toFloat(), 0.0f)
            leftDrawable.draw(canvas)
        } else {
            //now the left one is actually on the right
            canvas.translate(xMovement - width, 0.0f)
            leftDrawable.draw(canvas)
            canvas.translate(-width.toFloat(), 0.0f)
            rightDrawable.draw(canvas)
        }
        canvas.restore()
        xMovement += speedInPercentage * width / 100.0f
        if (isInEditMode)
            return
        if (xMovement >= width * 2.0f)
            xMovement = 0.0f
        invalidate()
    }
}

использование:

    horizontalProgressView.setGradientColors(intArrayOf(Color.RED, Color.BLUE))

И результат (он хорошо работает, просто трудно редактировать видео):

enter image description here

Итак, теперь мой вопрос: что мне делать, чтобы он хорошо анимировал, даже если поток пользовательского интерфейса немного занят?

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

Ответы

Ответ 1

Я решил поставить ответ "пскинк" здесь, в Котлине (происхождение здесь). Я пишу это здесь только потому, что другие решения либо не работали, либо были обходными решениями вместо того, о чем я спрашивал.

class ScrollingGradient(private val pixelsPerSecond: Float) : Drawable(), Animatable, TimeAnimator.TimeListener {
    private val paint = Paint()
    private var x: Float = 0.toFloat()
    private val animator = TimeAnimator()

    init {
        animator.setTimeListener(this)
    }

    override fun onBoundsChange(bounds: Rect) {
        paint.shader = LinearGradient(0f, 0f, bounds.width().toFloat(), 0f, Color.WHITE, Color.BLUE, Shader.TileMode.MIRROR)
    }

    override fun draw(canvas: Canvas) {
        canvas.clipRect(bounds)
        canvas.translate(x, 0f)
        canvas.drawPaint(paint)
    }

    override fun setAlpha(alpha: Int) {}

    override fun setColorFilter(colorFilter: ColorFilter?) {}

    override fun getOpacity(): Int = PixelFormat.TRANSLUCENT

    override fun start() {
        animator.start()
    }

    override fun stop() {
        animator.cancel()
    }

    override fun isRunning(): Boolean = animator.isRunning

    override fun onTimeUpdate(animation: TimeAnimator, totalTime: Long, deltaTime: Long) {
        x = pixelsPerSecond * totalTime / 1000
        invalidateSelf()
    }
}

использование:

MainActivity.kt

    val px = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 200f, resources.getDisplayMetrics())
    progress.indeterminateDrawable = ScrollingGradient(px)

activity_main.xml

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
    android:layout_height="match_parent" android:gravity="center" android:orientation="vertical"
    tools:context=".MainActivity">

    <ProgressBar
        android:id="@+id/progress" style="?android:attr/progressBarStyleHorizontal" android:layout_width="200dp"
        android:layout_height="20dp" android:indeterminate="true"/>
</LinearLayout>

Ответ 2

Идея моего решения относительно проста: отобразите FrameLayout, у которого есть два дочерних представления (начальный градиент и градиент конечного старта), и используйте ValueAnimator для анимации атрибута translationX child views. Поскольку вы не выполняете какой-либо пользовательский чертеж, а потому, что используете утилиты анимации, предоставляемые инфраструктурой, вам не нужно беспокоиться о производительности анимации.

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

<com.example.MyHorizontalProgress
    android:layout_width="match_parent"
    android:layout_height="6dp"
    app:animationDuration="2000"
    app:gradientStartColor="#000"
    app:gradientEndColor="#fff"/>

Вы можете настроить цвета градиента и скорость анимации непосредственно из XML.

Код

Сначала нам нужно определить наши пользовательские атрибуты в res/values/attrs.xml:

<declare-styleable name="MyHorizontalProgress">
    <attr name="animationDuration" format="integer"/>
    <attr name="gradientStartColor" format="color"/>
    <attr name="gradientEndColor" format="color"/>
</declare-styleable>

И у нас есть файл ресурсов макета для раздувания двух наших анимированных просмотров:

<merge xmlns:android="http://schemas.android.com/apk/res/android">

    <View
        android:id="@+id/one"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

    <View
        android:id="@+id/two"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</merge>

И здесь Java:

public class MyHorizontalProgress extends FrameLayout {

    private static final int DEFAULT_ANIMATION_DURATION = 2000;
    private static final int DEFAULT_START_COLOR = Color.RED;
    private static final int DEFAULT_END_COLOR = Color.BLUE;

    private final View one;
    private final View two;

    private int animationDuration;
    private int startColor;
    private int endColor;

    private int laidOutWidth;

    public MyHorizontalProgress(Context context, AttributeSet attrs) {
        super(context, attrs);

        inflate(context, R.layout.my_horizontal_progress, this);
        readAttributes(attrs);

        one = findViewById(R.id.one);
        two = findViewById(R.id.two);

        ViewCompat.setBackground(one, new GradientDrawable(LEFT_RIGHT, new int[]{ startColor, endColor }));
        ViewCompat.setBackground(two, new GradientDrawable(LEFT_RIGHT, new int[]{ endColor, startColor }));

        getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {

            @Override
            public void onGlobalLayout() {
                laidOutWidth = MyHorizontalProgress.this.getWidth();

                ValueAnimator animator = ValueAnimator.ofInt(0, 2 * laidOutWidth);
                animator.setInterpolator(new LinearInterpolator());
                animator.setRepeatCount(ValueAnimator.INFINITE);
                animator.setRepeatMode(ValueAnimator.RESTART);
                animator.setDuration(animationDuration);
                animator.addUpdateListener(updateListener);
                animator.start();

                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                    getViewTreeObserver().removeOnGlobalLayoutListener(this);
                }
                else {
                    getViewTreeObserver().removeGlobalOnLayoutListener(this);
                }
            }
        });
    }

    private void readAttributes(AttributeSet attrs) {
        TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.MyHorizontalProgress);
        animationDuration = a.getInt(R.styleable.MyHorizontalProgress_animationDuration, DEFAULT_ANIMATION_DURATION);
        startColor = a.getColor(R.styleable.MyHorizontalProgress_gradientStartColor, DEFAULT_START_COLOR);
        endColor = a.getColor(R.styleable.MyHorizontalProgress_gradientEndColor, DEFAULT_END_COLOR);
        a.recycle();
    }

    private ValueAnimator.AnimatorUpdateListener updateListener = new ValueAnimator.AnimatorUpdateListener() {

        @Override
        public void onAnimationUpdate(ValueAnimator valueAnimator) {
            int offset = (int) valueAnimator.getAnimatedValue();
            one.setTranslationX(calculateOneTranslationX(laidOutWidth, offset));
            two.setTranslationX(calculateTwoTranslationX(laidOutWidth, offset));
        }
    };

    private int calculateOneTranslationX(int width, int offset) {
        return (-1 * width) + offset;
    }

    private int calculateTwoTranslationX(int width, int offset) {
        if (offset <= width) {
            return offset;
        }
        else {
            return (-2 * width) + offset;
        }
    }
}

Как работает Java, довольно просто. Здесь шаг за шагом, что происходит:

  • Наполните наш ресурс компоновки, добавив двух наших анимированных детей в FrameLayout
  • Прочитайте продолжительность анимации и значения цвета из AttributeSet
  • Найдите one и two дочерних представления (я знаю не очень творческие имена)
  • Создайте GradientDrawable для каждого дочернего представления и примените его как фоновый
  • Используйте OnGlobalLayoutListener для настройки нашей анимации

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

Анимация довольно проста. Мы создаем бесконечно повторяющийся ValueAnimator который испускает значения между 0 и 2 * width. В каждом событии "update" наш updateListener вызывает setTranslationX() в наших дочерних представлениях со значением, вычисленным из испускаемого значения "обновления".

И это! Дайте мне знать, если какой-либо из вышеперечисленных вопросов неясен, и я буду рад помочь.

Ответ 3

final View bar = view.findViewById(R.id.progress);
final GradientDrawable background = new GradientDrawable(GradientDrawable.Orientation.LEFT_RIGHT, new int[]{Color.BLUE, Color.RED, Color.BLUE, Color.RED});
bar.setBackground(background);
bar.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
    @Override
    public void onLayoutChange(final View v, final int left, final int top, final int right, final int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
        background.setBounds(-2 * v.getWidth(), 0, v.getWidth(), v.getHeight());
        ValueAnimator animation = ValueAnimator.ofInt(0, 2 * v.getWidth());
        animation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                background.setBounds(-2 * v.getWidth() + (int) animation.getAnimatedValue(), 0, v.getWidth() + (int) animation.getAnimatedValue(), v.getHeight());
            }
        });
        animation.setRepeatMode(ValueAnimator.RESTART);
        animation.setInterpolator(new LinearInterpolator());
        animation.setRepeatCount(ValueAnimator.INFINITE);
        animation.setDuration(3000);
        animation.start();
    }
});

Это представление для тестирования:

<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_gravity="center" >

    <View
        android:id="@+id/progress"
        android:layout_width="match_parent"
        android:layout_height="40dp"/>

</FrameLayout>

Ответ 4

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

Использовать AnimationDrawable animation_list

 <animation-list android:id="@+id/selected" android:oneshot="false">
    <item android:drawable="@drawable/color1" android:duration="50" />
    <item android:drawable="@drawable/color2" android:duration="50" />
    <item android:drawable="@drawable/color3" android:duration="50" />
    <item android:drawable="@drawable/color4" android:duration="50" />
    -----
    -----
 </animation-list>

и в вашем Activity/xml задайте это как фоновый ресурс для вашей панели прогресса.

то сделайте следующее

// Get the background, which has been compiled to an AnimationDrawable object.
 AnimationDrawable frameAnimation = (AnimationDrawable)prgressBar.getBackground();

 // Start the animation (looped playback by default).
 frameAnimation.start();

если мы возьмем соответствующие чертежи таким образом, чтобы покрывать эффекты от синего до красного и красного до синего градиента соответственно тем изображениям, которые мы должны упомянуть в списке анимации, как color1, color2 и т.д.

Этот подход похож на то, как мы создадим изображение GIF с несколькими статическими изображениями.

Ответ 5

Я немного изменил код Android-разработчика, который может помочь некоторым людям.

Похоже, размер анимации не изменился должным образом, поэтому я исправил это, немного упростив настройку скорости анимации (в секундах, а не на основе пикселей), и переместил код инициализации, чтобы разрешить встраивание прямо в XML-макет без кода в вашей деятельности.,

ScrollingProgressBar.kt

package com.test

import android.content.Context
import android.util.AttributeSet
import android.widget.ProgressBar
import android.animation.TimeAnimator
import android.graphics.*
import android.graphics.drawable.Animatable
import android.graphics.drawable.Drawable

class ScrollingGradient : Drawable(), Animatable, TimeAnimator.TimeListener {

    private val paint = Paint()
    private var x: Float = 0.toFloat()
    private val animator = TimeAnimator()
    private var pixelsPerSecond: Float = 0f
    private val animationTime: Int = 2

    init {
        animator.setTimeListener(this)
    }

    override fun onBoundsChange(bounds: Rect) {
        paint.shader = LinearGradient(0f, 0f, bounds.width().toFloat(), 0f, Color.parseColor("#00D3D3D3"), Color.parseColor("#CCD3D3D3"), Shader.TileMode.MIRROR)
        pixelsPerSecond = ((bounds.right - bounds.left) / animationTime).toFloat()
    }

    override fun draw(canvas: Canvas) {
        canvas.clipRect(bounds)
        canvas.translate(x, 0f)
        canvas.drawPaint(paint)
    }

    override fun setAlpha(alpha: Int) {}

    override fun setColorFilter(colorFilter: ColorFilter?) {}

    override fun getOpacity(): Int = PixelFormat.TRANSLUCENT

    override fun start() {
        animator.start()
    }

    override fun stop() {
        animator.cancel()
    }

    override fun isRunning(): Boolean = animator.isRunning

    override fun onTimeUpdate(animation: TimeAnimator, totalTime: Long, deltaTime: Long) {
        x = pixelsPerSecond * totalTime / 1000
        invalidateSelf()
    }
}

class ScrollingProgressBar : ProgressBar {


    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        this.indeterminateDrawable.setBounds(this.left, this.top, this.right, this.bottom)
    }

    constructor(context: Context) : super(context)

    constructor(context: Context, attrs: AttributeSet) : super(context, attrs)

    constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle)

    init {
        this.indeterminateDrawable = ScrollingGradient()
    }
}

Макет xml (замените com.test.ScrollingProgressBar на расположение кода выше)

<com.test.ScrollingProgressBar
        android:id="@+id/progressBar1"
        android:background="#464646"
        style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="match_parent"
        android:layout_height="80dp"
        android:gravity="center"
        android:indeterminateOnly="true"/>

Ответ 6

для повышения производительности я бы расширил класс ProgressBar и переопределил метод onDraw самостоятельно. Затем нарисуйте Rect с правильным градиентом в методе Paint: Canvas drawRect, где вы указываете координаты и Paint

Вот хороший ввод для Android, чтобы начать собственное рисование: Пользовательское рисование на Android

А вот простой начальный пример пользовательского вида чертежа: Простой пример с использованием onDraw

Итак, в коде, что-то вроде этого будет делать для статического градиента:

public class MyView extends View {
    private int color1 = 0, color2 = 1;
    private LinearGradient linearGradient = new LinearGradient(0,0,0,0,color1,color2, Shader.TileMode.REPEAT);
    Paint p;
    public MyView(Context context) {
        super(context);
    }

    @Override
    protected synchronized void onDraw(Canvas canvas) {
        p = new Paint();
        p.setDither(true);
        p.setShader(linearGradient);
        canvas.drawRect(0,0,getWidth(),getHeight(),p);
    }

    @Override
    protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        linearGradient = new LinearGradient(0,heightMeasureSpec/2, widthMeasureSpec,heightMeasureSpec/2,color1,color2, Shader.TileMode.REPEAT);
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
}

Вы можете поиграть с LinearGradient другим конструктором, чтобы получить желаемый эффект (принимает список точек, вам, вероятно, понадобится 3 из них, один из которых посередине дает прогресс). Вы можете реализовать прогресс с переменной в вашем представлении. Метод onMeasure позволяет мне адаптироваться к виду, изменяя его размер. Вы можете создать метод setProgress (float progress), который устанавливает переменную progress и делает недействительным View:

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Shader;
import android.view.View;

public class MyProgressBar extends View {

private int myWidth = 0, myHeight = 0;
private int[] myColors = new int[]{0,1};
private float[] myPositions = new float[]{0.0f,0.0f,1.0f};

private LinearGradient myLinearGradient = new LinearGradient(0,0,myWidth,myHeight/2,myColors,myPositions, Shader.TileMode.REPEAT);
private Paint myPaint = new Paint();

public MyProgressBar(Context context) {
    super(context);
    myPaint.setDither(true);
}

@Override
protected synchronized void onDraw(Canvas canvas) {
    myPaint.setShader(myLinearGradient);
    canvas.drawRect(0,0,getWidth(),getHeight(),p);
}

@Override
protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    myWidth = widthMeasureSpec;
    myHeight = heightMeasureSpec;
    myLinearGradient = new LinearGradient(0,0,myWidth,myHeight/2,myColors,myPositions, Shader.TileMode.REPEAT);
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
// progress must be a percentage, a float between 0.0f and 1.0f
public void setProgress(float progress) {
    myPositions[1] = progress;
    myLinearGradient = new LinearGradient(0,0,myWidth,myHeight/2,myColors,myPositions, Shader.TileMode.REPEAT);
    this.invalidate();
}
}

Конечно, вы должны использовать метод setProgress (progress), чтобы он был динамическим.