Ralentissement de la vitesse du contrôleur Viewpager sous Android

105

Existe-t-il un moyen de ralentir la vitesse de défilement avec l'adaptateur viewpager dans Android?


Vous savez, j'ai regardé ce code. Je ne peux pas comprendre ce que je me trompe.

try{ 
    Field mScroller = mPager.getClass().getDeclaredField("mScroller"); 
    mScroller.setAccessible(true); 
    Scroller scroll = new Scroller(cxt);
    Field scrollDuration = scroll.getClass().getDeclaredField("mDuration");
    scrollDuration.setAccessible(true);
    scrollDuration.set(scroll, 1000);
    mScroller.set(mPager, scroll);
}catch (Exception e){
    Toast.makeText(cxt, "something happened", Toast.LENGTH_LONG).show();
} 

Cela ne change rien mais aucune exception ne se produit?

Alex Kelly
la source
essayez ceci antoniocappiello.com/2015/10/31/…
Rushi Ayyappa
1
child.setNestedScrollingEnabled(false);travaillé pour moi
Pierre

Réponses:

230

J'ai commencé avec le code de HighFlyer qui a en effet changé le champ mScroller (ce qui est un bon début) mais n'a pas aidé à prolonger la durée du défilement car ViewPager transmet explicitement la durée au mScroller lors de la demande de défilement.

L'extension de ViewPager ne fonctionnait pas car la méthode importante (smoothScrollTo) ne peut pas être remplacée.

J'ai fini par résoudre ce problème en étendant Scroller avec ce code:

public class FixedSpeedScroller extends Scroller {

    private int mDuration = 5000;

    public FixedSpeedScroller(Context context) {
        super(context);
    }

    public FixedSpeedScroller(Context context, Interpolator interpolator) {
        super(context, interpolator);
    }

    public FixedSpeedScroller(Context context, Interpolator interpolator, boolean flywheel) {
        super(context, interpolator, flywheel);
    }


    @Override
    public void startScroll(int startX, int startY, int dx, int dy, int duration) {
        // Ignore received duration, use fixed one instead
        super.startScroll(startX, startY, dx, dy, mDuration);
    }

    @Override
    public void startScroll(int startX, int startY, int dx, int dy) {
        // Ignore received duration, use fixed one instead
        super.startScroll(startX, startY, dx, dy, mDuration);
    }
}

Et en l'utilisant comme ceci:

try {
    Field mScroller;
    mScroller = ViewPager.class.getDeclaredField("mScroller");
    mScroller.setAccessible(true); 
    FixedSpeedScroller scroller = new FixedSpeedScroller(mPager.getContext(), sInterpolator);
    // scroller.setFixedDuration(5000);
    mScroller.set(mPager, scroller);
} catch (NoSuchFieldException e) {
} catch (IllegalArgumentException e) {
} catch (IllegalAccessException e) {
}

J'ai fondamentalement codé la durée à 5 secondes et j'ai fait en sorte que mon ViewPager l'utilise.

J'espère que cela t'aides.

marmor
la source
16
pouvez-vous me dire ce qu'est sInterpolator?
Shashank Degloorkar
10
«Interpolator» est la vitesse à laquelle les animations sont exécutées, par exemple elles peuvent accélérer, ralentir et bien plus encore. Il y a un constructeur sans interpolateur, je pense que cela devrait fonctionner, alors essayez simplement de supprimer cet argument de l'appel. Si cela ne fonctionne pas, ajoutez: 'Interpolator sInterpolator = new AccelerateInterpolator ()'
marmor
8
J'ai trouvé que DecelerateInterpolator fonctionnait mieux dans ce cas, il maintient la vitesse du balayage initial, puis décélère lentement.
Quint Stoffers
Salut, avez-vous une idée de pourquoi scroller.setFixedDuration(5000);donne une erreur? Il dit "La méthode setFixedDuration (int) est indéfinie"
jaibatrik
6
Si vous utilisez proguard, n'oubliez pas d'ajouter cette ligne: -keep class android.support.v4. ** {*;} sinon vous obtiendrez un pointeur nul lorsque getDeclaredField ()
GuilhE
61

J'ai voulu faire moi-même et j'ai trouvé une solution (en utilisant la réflexion, cependant). Elle est similaire à la solution acceptée mais utilise le même interpolateur et ne modifie la durée qu'en fonction d'un facteur. Vous devez utiliser a ViewPagerCustomDurationdans votre XML au lieu de ViewPager, puis vous pouvez le faire:

ViewPagerCustomDuration vp = (ViewPagerCustomDuration) findViewById(R.id.myPager);
vp.setScrollDurationFactor(2); // make the animation twice as slow

ViewPagerCustomDuration.java:

import android.content.Context;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.animation.Interpolator;

import java.lang.reflect.Field;

public class ViewPagerCustomDuration extends ViewPager {

    public ViewPagerCustomDuration(Context context) {
        super(context);
        postInitViewPager();
    }

    public ViewPagerCustomDuration(Context context, AttributeSet attrs) {
        super(context, attrs);
        postInitViewPager();
    }

    private ScrollerCustomDuration mScroller = null;

    /**
     * Override the Scroller instance with our own class so we can change the
     * duration
     */
    private void postInitViewPager() {
        try {
            Class<?> viewpager = ViewPager.class;
            Field scroller = viewpager.getDeclaredField("mScroller");
            scroller.setAccessible(true);
            Field interpolator = viewpager.getDeclaredField("sInterpolator");
            interpolator.setAccessible(true);

            mScroller = new ScrollerCustomDuration(getContext(),
                    (Interpolator) interpolator.get(null));
            scroller.set(this, mScroller);
        } catch (Exception e) {
        }
    }

    /**
     * Set the factor by which the duration will change
     */
    public void setScrollDurationFactor(double scrollFactor) {
        mScroller.setScrollDurationFactor(scrollFactor);
    }

}

ScrollerCustomDuration.java:

import android.annotation.SuppressLint;
import android.content.Context;
import android.view.animation.Interpolator;
import android.widget.Scroller;

public class ScrollerCustomDuration extends Scroller {

    private double mScrollFactor = 1;

    public ScrollerCustomDuration(Context context) {
        super(context);
    }

    public ScrollerCustomDuration(Context context, Interpolator interpolator) {
        super(context, interpolator);
    }

    @SuppressLint("NewApi")
    public ScrollerCustomDuration(Context context, Interpolator interpolator, boolean flywheel) {
        super(context, interpolator, flywheel);
    }

    /**
     * Set the factor by which the duration will change
     */
    public void setScrollDurationFactor(double scrollFactor) {
        mScrollFactor = scrollFactor;
    }

    @Override
    public void startScroll(int startX, int startY, int dx, int dy, int duration) {
        super.startScroll(startX, startY, dx, dy, (int) (duration * mScrollFactor));
    }

}

J'espère que cela aide quelqu'un!

Oleg Vaskevich
la source
3
J'aime cette solution mieux que la réponse acceptée car le ViewPager personnalisé est une classe à accès direct avec des modifications minimes du code existant. Le fait d'avoir le scroller personnalisé en tant que classe interne statique du ViewPager personnalisé le rend encore plus encapsulé.
Emanuel Moecklin
1
Si vous utilisez proguard, n'oubliez pas d'ajouter cette ligne: -keep class android.support.v4. ** {*;} sinon vous obtiendrez un pointeur nul lorsque getDeclaredField ()
GuilhE
Je suggère de passer à un ternaire, car vous ne voulez permettre à personne de passer un facteur 0, cela interrompra votre durée. Mettre à jour le paramètre final que vous transmettez à mScrollFactor> 0? (int) (duration * mScrollFactor): duration
portfoliobuilder
Je vous suggère également de faire une vérification nulle lors de la définition du facteur de défilement dans votre classe, ViewPagerCustomDuration. Vous instanciez avec mScroller en tant qu'objet nul, vous devez confirmer qu'il n'est toujours pas nul lorsque vous essayez de définir le facteur de défilement sur cet objet.
portfoliobuilder
@portfoliobuilder commentaires valides, bien qu'il semble que vous préfériez un style de programmation plus défensif qui convient mieux à l'OMI pour les bibliothèques opaques qu'un extrait de code sur SO. Définir le facteur de défilement sur <= 0 et appeler setScrollDurationFactor sur une vue non instanciée est quelque chose que la plupart des développeurs Android devraient pouvoir éviter de faire sans vérifications d'exécution.
Oleg Vaskevich
43

J'ai trouvé une meilleure solution, basée sur la réponse de @ df778899 et l' API Android ValueAnimator . Cela fonctionne bien sans réflexion et est très flexible. De plus, il n'est pas nécessaire de créer un ViewPager personnalisé et de le placer dans le package android.support.v4.view. Voici un exemple:

private void animatePagerTransition(final boolean forward) {

    ValueAnimator animator = ValueAnimator.ofInt(0, viewPager.getWidth() - ( forward ? viewPager.getPaddingLeft() : viewPager.getPaddingRight() ));
    animator.addListener(new Animator.AnimatorListener() {
        @Override
        public void onAnimationStart(Animator animation) {
        }

        @Override
        public void onAnimationEnd(Animator animation) {
            viewPager.endFakeDrag();
        }

        @Override
        public void onAnimationCancel(Animator animation) {
            viewPager.endFakeDrag();
        }

        @Override
        public void onAnimationRepeat(Animator animation) {
        }
    });

    animator.setInterpolator(new AccelerateInterpolator());
    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

        private int oldDragPosition = 0;

        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            int dragPosition = (Integer) animation.getAnimatedValue();
            int dragOffset = dragPosition - oldDragPosition;
            oldDragPosition = dragPosition;
            viewPager.fakeDragBy(dragOffset * (forward ? -1 : 1));
        }
    });

    animator.setDuration(AppConstants.PAGER_TRANSITION_DURATION_MS);
    viewPager.beginFakeDrag();
    animator.start();
}
lobzik
la source
7
Je pense que c'est sous-estimé. Il utilise startFakeDraget endFakeDragest censé être pour faire plus que ce qui est disponible dans ViewPagerla boîte. Cela fonctionne très bien pour moi et c'est exactement ce que je recherchais
user3280133
1
C'est une excellente solution, merci. Une petite amélioration, cependant: pour le faire fonctionner pour un viewPager avec padding, la première ligne devrait être ... ValueAnimator.ofInt (0, viewPager.getWidth () - (forward? ViewPager.getPaddingLeft (): viewPager.getPaddingRight () ));
DmitryO.
1
c'était de loin la solution la plus simple / efficace / responsable car elle ne tient pas compte de la réflexion et évite de sceller les violations comme les solutions ci
Ryan
2
@lobzic, Où appelez-vous exactement votre animatePagerTransition(final boolean forwardméthode? C'est ce dont je suis confus. L'appelez-vous dans l'une des ViewPager.OnPageChangeListenerméthodes d'interface? Ou ailleurs?
Sakiboy
Pas de hacks de réflexion laids. Facilement configurable. Une très bonne réponse.
Oren
18

Comme vous pouvez le voir dans les sources de ViewPager, la durée du fling est contrôlée par l'objet mScroller. Dans la documentation, nous pouvons lire:

La durée du défilement peut être passée dans le constructeur et spécifie le temps maximum que l'animation de défilement doit prendre

Ainsi, si vous souhaitez contrôler la vitesse, vous pouvez modifier l'objet mScroller par réflexion.

Vous devriez écrire quelque chose comme ceci:

setContentView(R.layout.main);
mPager = (ViewPager)findViewById(R.id.view_pager);
Field mScroller = ViewPager.class.getDeclaredField("mScroller");   
mScroller.setAccessible(true);
mScroller.set(mPager, scroller); // initialize scroller object by yourself 
HighFlyer
la source
Je suis un peu confus sur la façon de faire cela - je configure le ViewPager dans le fichier XML et je l'ai assigné à un mPager, et setAdapter (mAdapter) ... Dois-je créer une classe qui étend ViewPager?
Alex Kelly
12
Vous devriez écrire quelque chose comme ceci: setContentView (R.layout.main); mPager = (ViewPager) findViewById (R.id.view_pager); Champ mScroller = ViewPager.class.getDeclaredField ("mScroller"); mScroller.setAccessible (vrai); mScroller.set (mPager, scroller); // initialiser l'objet de défilement par vous
HighFlyer
Je viens de poster une autre réponse ... Cela ne fonctionne pas ... Auriez-vous le temps de me dire pourquoi?
Alex Kelly
Après un examen plus attentif des sources: trouvez mScroller.startScroll (sx, sy, dx, dy); dans ViewPager source et regardez la réalisation de Scroller. startScroll appelle un autre startScroll avec le paramètre DEFAULT_DURATION. Il est impossible de créer une autre valeur pour le champ final statique, il reste donc une opportunité - remplacer smoothScrollTo de ViewPager
HighFlyer
16

Ce n'est pas la solution parfaite, vous ne pouvez pas ralentir la vitesse car c'est un int. Mais pour moi, c'est assez lent et je n'ai pas besoin d'utiliser la réflexion.

Notez le package où se trouve la classe. smoothScrollToa une visibilité sur les packages.

package android.support.v4.view;

import android.content.Context;
import android.util.AttributeSet;

public class SmoothViewPager extends ViewPager {
    private int mVelocity = 1;

    public SmoothViewPager(Context context) {
        super(context);
    }

    public SmoothViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    void smoothScrollTo(int x, int y, int velocity) {
        //ignore passed velocity, use one defined here
        super.smoothScrollTo(x, y, mVelocity);
    }
}
pawelzieba
la source
Notez que la première ligne: "package android.support.v4.view;" est obligatoire et vous devez créer un tel paquet à votre racine src. Si vous ne le faites pas, votre code ne sera pas compilé.
Elhanan Mishraky
3
@ElhananMishraky exactement, c'est ce que je voulais dire Notice the package where the class is.
pawelzieba
3
Attendez, attendez, attendez, attendez ... wuuuut? Vous pouvez utiliser sdk's package-local fields/ methods, si vous faites simplement croire à Android Studio que vous utilisez le même package? Suis-je le seul à en sentir sealing violation?
Bartek Lipinski
C'est une très bonne solution, mais cela ne fonctionnait pas non plus pour moi puisque l'interpolateur utilisé par le ViewPager Scroller ne se comporte pas bien dans ce cas. J'ai donc combiné votre solution avec les autres en utilisant la réflexion pour obtenir le mScroller. De cette façon, je peux utiliser le Scroller d'origine lorsque l'utilisateur pagine, et mon Scroller personnalisé lorsque je change de page par programmation avec setCurrentItem ().
rolgalan
8

Les méthodes fakeDrag sur ViewPager semblent fournir une solution alternative.

Par exemple, cela va page de l'élément 0 à 1:

rootView.setOnClickListener(new OnClickListener() {
  @Override
  public void onClick(View v) {
      ViewPager pager = (ViewPager) getActivity().findViewById(R.id.pager);
      //pager.setCurrentItem(1, true);
      pager.beginFakeDrag();
      Handler handler = new Handler();
      handler.post(new PageTurner(handler, pager));
  }
});


private static class PageTurner implements Runnable {
  private final Handler handler;
  private final ViewPager pager;
  private int count = 0;

  private PageTurner(Handler handler, ViewPager pager) {
    this.handler = handler;
    this.pager = pager;
  }

  @Override
  public void run() {
    if (pager.isFakeDragging()) {
      if (count < 20) {
        count++;
        pager.fakeDragBy(-count * count);
        handler.postDelayed(this, 20);
      } else {
        pager.endFakeDrag();
      }
    }
  }
}

(Le count * countest juste là pour accélérer la vitesse de traînée au fur et à mesure)

df778899
la source
4

j'ai utilisé

DécélérerInterpolateur ()

Voici l'exemple:

  mViewPager = (ViewPager) findViewById(R.id.container);
            mViewPager.setAdapter(mSectionsPagerAdapter);
            Field mScroller = null;
            try {
                mScroller = ViewPager.class.getDeclaredField("mScroller");
                mScroller.setAccessible(true);
                Scroller scroller = new Scroller(this, new DecelerateInterpolator());
                mScroller.set(mViewPager, scroller);
            } catch (NoSuchFieldException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
Mladen Rakonjac
la source
2

Sur la base de la solution acceptée, j'ai créé une classe kotlin avec une extension pour afficher le pager. Prendre plaisir! :)

class ViewPageScroller : Scroller {

    var fixedDuration = 1500 //time to scroll in milliseconds

    constructor(context: Context) : super(context)

    constructor(context: Context, interpolator: Interpolator) : super(context, interpolator)

    constructor(context: Context, interpolator: Interpolator, flywheel: Boolean) : super(context, interpolator, flywheel)


    override fun startScroll(startX: Int, startY: Int, dx: Int, dy: Int, duration: Int) {
        // Ignore received duration, use fixed one instead
        super.startScroll(startX, startY, dx, dy, fixedDuration)
    }

    override fun startScroll(startX: Int, startY: Int, dx: Int, dy: Int) {
        // Ignore received duration, use fixed one instead
        super.startScroll(startX, startY, dx, dy, fixedDuration)
    }
}

fun ViewPager.setViewPageScroller(viewPageScroller: ViewPageScroller) {
    try {
        val mScroller: Field = ViewPager::class.java.getDeclaredField("mScroller")
        mScroller.isAccessible = true
        mScroller.set(this, viewPageScroller)
    } catch (e: NoSuchFieldException) {
    } catch (e: IllegalArgumentException) {
    } catch (e: IllegalAccessException) {
    }

}
Janusz Hain
la source
1

Voici une réponse utilisant une approche totalement différente. Quelqu'un pourrait dire que cela a une impression de piratage; cependant, il n'utilise pas la réflexion, et je dirais que cela fonctionnera toujours.

On retrouve le code suivant à l'intérieur ViewPager.smoothScrollTo:

    if (velocity > 0) {
        duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
    } else {
        final float pageWidth = width * mAdapter.getPageWidth(mCurItem);
        final float pageDelta = (float) Math.abs(dx) / (pageWidth + mPageMargin);
        duration = (int) ((pageDelta + 1) * 100);
    }
    duration = Math.min(duration, MAX_SETTLE_DURATION);

Il calcule la durée en fonction de plusieurs éléments. Vous voyez tout ce que nous pouvons contrôler? mAdapter.getPageWidth. Implémentons ViewPager.OnPageChangeListener dans notre adaptateur . Nous allons détecter quand le ViewPager défile et donner une fausse valeur pour la largeur . Si je veux que la durée soit k*100, alors je reviendrai 1.0/(k-1)pour getPageWidth. Ce qui suit est Kotlin impl de cette partie de l'adaptateur qui transforme la durée en 400:

    var scrolling = false
    override fun getPageWidth(position: Int): Float {
        return if (scrolling) 0.333f else 1f
    }

    // OnPageChangeListener to detect scroll state
    override fun onPageScrollStateChanged(state: Int) {
        scrolling = state == ViewPager.SCROLL_STATE_SETTLING
    }

    override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
    }

    override fun onPageSelected(position: Int) {
    }

N'oubliez pas d'ajouter l'adaptateur en tant que OnPageChangedListener.

Mon cas particulier peut supposer en toute sécurité que l'utilisateur ne peut pas glisser pour faire glisser entre les pages. Si vous devez prendre en charge la stabilisation après un glissement, vous devez en faire un peu plus dans votre calcul.

Un inconvénient est que cela dépend de cette 100valeur de durée de base codée en dur dans ViewPager. Si cela change, vos durées changent avec cette approche.

androidguy
la source
0
                binding.vpTour.beginFakeDrag();
                lastFakeDrag = 0;
                ValueAnimator va = ValueAnimator.ofInt(0, binding.vpTour.getWidth());
                va.setDuration(1000);
                va.addUpdateListener(animation -> {
                    if (binding.vpTour.isFakeDragging()) {
                        int animProgress = (Integer) animation.getAnimatedValue();
                        binding.vpTour.fakeDragBy(lastFakeDrag - animProgress);
                        lastFakeDrag = animProgress;
                    }
                });
                va.addListener(new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationEnd(Animator animation) {
                        super.onAnimationEnd(animation);
                        if (binding.vpTour.isFakeDragging()) {
                            binding.vpTour.endFakeDrag();
                        }
                    }
                });
                va.start();

Je voulais résoudre le même problème que vous tous et c'est ma solution pour le même problème, mais je pense que de cette façon, la solution est plus flexible car vous pouvez modifier la durée comme vous le souhaitez et également modifier l'interpolation des valeurs comme vous le souhaitez pour obtenir différents effets visuels. Ma solution glisse de la page "1" à la page "2", donc uniquement dans des positions croissantes, mais peut facilement être modifiée pour aller dans des positions décroissantes en faisant "animProgress - lastFakeDrag" au lieu de "lastFakeDrag - animProgress". Je pense que c'est la solution la plus flexible pour effectuer cette tâche.

Insister sur
la source