web-dev-qa-db-fra.com

Comment créer un dégradé en mouvement animé répétitif dessinable, comme une progression indéterminée?

Contexte

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

Le problème

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:

enter image description here

Ce que j'ai essayé

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.

La question

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

enter image description here

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.

23
android developer

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>
4
android developer

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.

Le code

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:

  • Gonflez notre ressource de mise en page, en ajoutant nos deux enfants à animer dans le FrameLayout
  • Lisez la durée de l'animation et les valeurs des couleurs dans AttributeSet
  • Trouver les vues enfant one et two (noms pas très créatifs, je sais)
  • Créez un GradientDrawable pour chaque vue enfant et appliquez-le comme arrière-plan
  • Utilisez un OnGlobalLayoutListener pour configurer notre animation

L'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.

4
Ben P.
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>
0
artkoenig

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"/>
0
Twisted89

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.

0
venkatesh venkey