如何创建一个重复的动画移动渐变可绘制对象,如不确定的进度?

How to create a repeating animated moving gradient drawable, like an indeterminate progress?

背景

Android 有一个标准的 ProgressBar,在不确定时带有特殊动画。还有很多可用的进度视图库 (here)。

问题

在我搜索过的所有内容中,我找不到一种方法来做一件非常简单的事情:

具有从颜色 X 到颜色 Y 的渐变,水平显示,并在 X 坐标中移动,以便 X 之前的颜色变为颜色 Y。

例如(只是一个例子),如果我有一个蓝色<->红色的渐变,从边到边,它会是这样的:

我试过的

我已经尝试了 Whosebug 上提供的一些解决方案:

但遗憾的是它们都是关于 Android 的标准 ProgressBar 视图,这意味着它有不同的方式来显示可绘制对象的动画。

我也试过在Android阿森纳网站上找到类似的东西,但是尽管有很多不错的东西,但我找不到这样的东西。

当然,我可以自己制作 2 个视图的动画,每个视图都有自己的渐变(一个与另一个相反),但我确信有更好的方法。

问题

是否可以使用 Drawable 或其动画,使渐变(或其他任何东西)以这种方式移动(当然是重复)?

也许只是从 ImageView 扩展并为那里的可绘制对象设置动画?

是否也可以设置容器的多少用于重复绘制?我的意思是,在上面的例子中,它可以从蓝色到红色,这样蓝色就会在边缘,红色就会在中间。


编辑:

好的,我有一点进步,但我不确定运动是否可以,而且我认为它不会像应该的那样保持速度一致,以防万一CPU有点忙,因为它没有考虑掉帧。我所做的是一个接一个地绘制 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))

结果(它确实循环很好,只是很难编辑视频):

所以我现在的问题是,即使 UI 线程有点忙,我应该怎么做才能确保动画效果良好?

只是 invalidate 对我来说似乎不是一个可靠的方法,一个人。我认为它应该检查更多。也许它可以使用一些动画 API 来代替 interpolator 。

我的解决方案背后的想法相对简单:显示具有两个 child 视图(start-end 渐变和 end-start 渐变)的 FrameLayout 并使用ValueAnimator 为 child 视图的 translationX 属性设置动画。因为您没有进行任何自定义绘图,并且因为您使用的是 framework-provided 动画实用程序,所以您不必担心动画性能。

我创建了一个自定义 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 的工作原理非常简单。这是正在发生的事情的step-by-step:

  • 膨胀我们的布局资源,将我们的两个 to-be-animated children 添加到 FrameLayout
  • AttributeSet
  • 中读取动画持续时间和颜色值
  • 找到 onetwo child 视图(不是很有创意的名字,我知道)
  • 为每个 child 视图创建一个 GradientDrawable 并将其应用为背景
  • 使用 OnGlobalLayoutListener 设置我们的动画

OnGlobalLayoutListener 的使用确保我们获得进度条宽度的真实值,并确保我们在布局之前不会开始动画。

动画也很简单。我们设置了一个 infinitely-repeating ValueAnimator 来发出介于 02 * width 之间的值。在每个 "update" 事件中,我们的 updateListener 在我们的 child 视图上调用 setTranslationX(),并根据发出的 "update" 值计算出一个值。

就是这样!如果以上任何内容不清楚,请告诉我,我很乐意提供帮助。

为了性能,我会扩展 ProgressBar class 并自己覆盖 onDraw 方法。然后在 Paint 中绘制一个具有适当渐变的 Rect : Canvas's drawRect method where you specify coordinates and the Paint

这是一个很好的 android 开始自定义绘图的输入: Custom drawing by Android

这里是自定义绘图视图的简单开始示例: Simple example using 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) 方法来设置变量进度并使视图无效:

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)方法让它是动态的。

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>

如果你有不同的drawable,你可以实现它,它定义了需要显示为进度条的颜色。

使用AnimationDrawableanimation_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();

如果我们以从蓝色到红色覆盖的方式获取相应的drawable 和红色到蓝色的渐变效果分别是那些我们必须在动画列表中提到的图像,如颜色 1、颜色 2 等

这种方法类似于我们用多个静态图像制作 GIF 图像的方法。

我决定将“pskink”答案放在 Kotlin 中(来源 )。我把它写在这里只是因为其他解决方案要么不起作用,要么是解决方法而不是我问的问题。

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>

我已经稍微修改了'android 开发人员的代码,这可能会对某些人有所帮助。

动画似乎没有正确调整大小,所以我已经修复了这个问题,使动画速度更容易设置(秒而不是基于像素)并重新定位初始化代码以允许直接嵌入到布局中xml Activity.

中没有代码

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 {

    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()
    }

    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)
    }
}

布局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"/>