Android a un ProgressBar standard avec une animation spéciale lorsqu'il est indéterminé. Il existe également de nombreuses bibliothèques de tant de types de vues de progression qui sont disponibles ( ici ).
Dans tout ce que j'ai cherché, je ne trouve pas de moyen de faire une chose très simple:
Avoir un dégradé de la couleur X à la couleur Y, qui s'affiche horizontalement, et se déplace en coordonnées X afin que les couleurs avant X passent à la couleur Y.
Par exemple (juste une illustration), si j'ai un dégradé de bleu <-> rouge, de bord en bord, ça irait comme tel:
J'ai essayé quelques solutions proposées ici sur StackOverflow:
mais malheureusement, ils concernent tous la vue standard ProgressBar d'Android, ce qui signifie qu'il a une manière différente de montrer l'animation du dessin.
J'ai également essayé de trouver quelque chose de similaire sur le site Android Arsenal, mais même s'il y en a beaucoup de Nice, je n'ai rien trouvé de tel.
Bien sûr, je pourrais simplement animer 2 vues moi-même, chacune ayant son propre gradient (l'un en face de l'autre), mais je suis sûr qu'il y a une meilleure façon.
Est-il possible d'utiliser un Drawable ou une animation de celui-ci, qui fait bouger un dégradé (ou autre) de cette façon (en répétant bien sûr)?
Peut-être simplement étendre ImageView et animer le dessinable là-bas?
Est-il également possible de définir la quantité de conteneur qui sera utilisée pour l'étirage répétitif? Je veux dire, dans l'exemple ci-dessus, cela pourrait être du bleu au rouge, de sorte que le bleu sera sur les bords et que la couleur rouge serait au milieu.
ÉDITER:
OK, j'ai fait un peu de progrès, mais je ne sais pas si le mouvement est correct, et je pense qu'il ne sera pas cohérent en vitesse comme il se doit, au cas où le CPU serait un peu occupé, car il ne tient pas compte des pertes de trames. Ce que j'ai fait, c'est de dessiner 2 GradientDrawables l'un à côté de l'autre, en tant que tels:
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()
}
}
usage:
horizontalProgressView.setGradientColors(intArrayOf(Color.RED, Color.BLUE))
Et le résultat (ça boucle bien, juste difficile d'éditer la vidéo):
Donc, ma question est maintenant, que dois-je faire pour m'assurer qu'il s'anime bien, même si le thread d'interface utilisateur est un peu occupé?
C'est juste que le invalidate
ne me semble pas un moyen fiable de le faire, seul. Je pense que cela devrait vérifier plus que cela. Peut-être pourrait-il utiliser une API d'animation à la place, avec interpolateur.
J'ai décidé de mettre la réponse "pskink" ici dans Kotlin (Origin here ). Je l'écris ici uniquement parce que les autres solutions ne fonctionnaient pas ou étaient des solutions de contournement au lieu de ce que je demandais.
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()
}
}
usage:
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>
L'idée derrière ma solution est relativement simple: afficher un FrameLayout
qui a deux vues enfant (un gradient début-fin et un gradient fin-début) et utiliser un ValueAnimator
pour animer les vues enfant ' translationX
attribut. Parce que vous ne faites aucun dessin personnalisé et que vous utilisez les utilitaires d'animation fournis par le framework, vous ne devriez pas avoir à vous soucier des performances de l'animation.
J'ai créé une sous-classe FrameLayout
personnalisée pour gérer tout cela pour vous. Tout ce que vous avez à faire est d'ajouter une instance de la vue à votre mise en page, comme ceci:
<com.example.MyHorizontalProgress
Android:layout_width="match_parent"
Android:layout_height="6dp"
app:animationDuration="2000"
app:gradientStartColor="#000"
app:gradientEndColor="#fff"/>
Vous pouvez personnaliser les couleurs du dégradé et la vitesse de l'animation directement à partir de XML.
Nous devons d'abord définir nos attributs personnalisés dans 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>
Et nous avons un fichier de ressources de mise en page pour gonfler nos deux vues animées:
<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>
Et voici le 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;
}
}
}
Le fonctionnement de Java est assez simple. Voici une description détaillée de ce qui se passe:
FrameLayout
AttributeSet
one
et two
(noms pas très créatifs, je sais)GradientDrawable
pour chaque vue enfant et appliquez-le comme arrière-planOnGlobalLayoutListener
pour configurer notre animationL'utilisation de OnGlobalLayoutListener
garantit que nous obtenons une valeur réelle pour la largeur de la barre de progression et garantit que nous ne commençons pas à animer tant que nous ne sommes pas disposés.
L'animation est également assez simple. Nous avons configuré un ValueAnimator
à répétition infinie qui émet des valeurs entre 0
Et 2 * width
. A chaque événement "update", notre updateListener
appelle setTranslationX()
sur nos vues enfants avec une valeur calculée à partir de la valeur "update" émise.
Et c'est tout! Faites-moi savoir si l'un des éléments ci-dessus n'est pas clair et je serai heureux de vous aider.
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();
}
});
Voici la vue des tests:
<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>
J'ai légèrement modifié le code du développeur Android, ce qui pourrait aider certaines personnes.
L'animation ne semblait pas se redimensionner correctement, j'ai donc corrigé cela, rendu la vitesse d'animation un peu plus facile à définir (en secondes au lieu de pixels) et déplacé le code init pour permettre l'incorporation directement dans la mise en page xml sans code dans votre activité .
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)
}
}
Layout xml (remplacez com.test.ScrollingProgressBar par l'emplacement du code ci-dessus)
<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"/>
Vous pouvez y parvenir si vous avez différents dessinables qui définissent les couleurs qui doivent apparaître comme barre de progression.
Utilisez 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>
Et dans votre Activity/xml, définissez-le comme une ressource d'arrière-plan pour votre barre de progression.
Ensuite, procédez comme suit
// 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();
Si nous prenons les dessinables respectifs de manière à couvrir respectivement les effets de dégradé bleu à rouge et rouge à bleu, les images que nous devons mentionner dans la liste d'animation comme color1, color2 etc.
Cette approche est similaire à la façon dont nous allons créer une image GIF avec plusieurs images statiques.