Как создать повторяющийся анимированный движущийся градиент, как неопределенный прогресс?
Фон
Android имеет стандартный ProgressBar со специальной анимацией, когда он неопределен. Существует также множество библиотек с таким количеством видов прогресса, которые доступны ( здесь).
Эта проблема
Во всем, что я искал, я не могу найти способ сделать очень простую вещь:
Имейте градиент от цвета X к цвету Y, который отображается горизонтально, и перемещается по координате X, чтобы цвета до X переходили в цвет Y.
Например (только иллюстрация), если у меня есть градиент синего <-> красного, от края до края, он будет выглядеть следующим образом:
Что я пробовал
Я пробовал некоторые решения, предлагаемые здесь в 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))
И результат (он хорошо работает, просто трудно редактировать видео):
Итак, теперь мой вопрос: что мне делать, чтобы он хорошо анимировал, даже если поток пользовательского интерфейса немного занят?
Это просто, что 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), чтобы он был динамическим.