Rallentamento della velocità del controller Viewpager in Android


105

C'è un modo per rallentare la velocità di scorrimento con l'adattatore viewpager in Android?


Sai, ho guardato questo codice. Non riesco a capire cosa sto sbagliando.

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

Non cambia nulla eppure non si verificano eccezioni?



1
child.setNestedScrollingEnabled(false);ha lavorato per me
Pierre

Risposte:


230

Ho iniziato con il codice di HighFlyer che ha effettivamente cambiato il campo mScroller (che è un ottimo inizio) ma non ha aiutato a estendere la durata dello scorrimento perché ViewPager passa esplicitamente la durata a mScroller quando richiede di scorrere.

L'estensione di ViewPager non ha funzionato poiché il metodo importante (smoothScrollTo) non può essere ignorato.

Ho finito per risolvere questo problema estendendo Scroller con questo codice:

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

E usandolo in questo modo:

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

Fondamentalmente ho codificato la durata a 5 secondi e l'ho fatto utilizzare dal mio ViewPager.

Spero che questo ti aiuti.


16
puoi per favore dirmi cos'è sInterpolator ??
Shashank Degloorkar

10
"Interpolator" è la velocità con cui vengono eseguite le animazioni, ad esempio possono accelerare, decelerare e molto altro ancora. C'è un costruttore senza un interpolatore, penso che dovrebbe funzionare, quindi prova semplicemente a rimuovere quell'argomento dalla chiamata. Se non funziona, aggiungi: "Interpolator sInterpolator = new AccelerateInterpolator ()"
marmor

8
Ho scoperto che DecelerateInterpolator funziona meglio in questo caso, mantiene la velocità dello swipe iniziale e poi rallenta lentamente.
Quint Stoffers

Ciao, hai idea del perché scroller.setFixedDuration(5000);dà un errore? Dice "Il metodo setFixedDuration (int) non è definito"
jaibatrik

6
Se stai usando proguard, non dimenticare di aggiungere questa riga: -keep class android.support.v4. ** {*;} altrimenti otterrai un puntatore nullo quando getDeclaredField ()
GuilhE

61

Volevo fare da solo e ho raggiunto una soluzione (usando la riflessione, tuttavia). È simile alla soluzione accettata ma utilizza lo stesso interpolatore e modifica solo la durata in base a un fattore. Devi usare un ViewPagerCustomDurationnel tuo XML invece di ViewPager, e poi puoi farlo:

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

}

Spero che questo aiuti qualcuno!


3
Mi piace questa soluzione meglio della risposta accettata perché il ViewPager personalizzato è una classe drop-in con modifiche minime al codice esistente. Avere lo scroller personalizzato come classe interna statica del ViewPager personalizzato lo rende ancora più incapsulato.
Emanuel Moecklin

1
Se stai usando proguard, non dimenticare di aggiungere questa riga: -keep class android.support.v4. ** {*;} altrimenti otterrai un puntatore nullo quando getDeclaredField ()
GuilhE

Suggerisco l'aggiornamento a un ternario, perché non vuoi permettere a nessuno di passare un fattore 0, questo interromperà la durata del tuo tempo. Aggiornare il parametro finale che stai passando a mScrollFactor> 0? (int) (duration * mScrollFactor): duration
portfoliobuilder

Suggerisco anche di eseguire un controllo nullo quando si imposta il fattore di scorrimento nella propria classe, ViewPagerCustomDuration. Crea un'istanza con mScroller come oggetto nullo, dovresti confermare che non è ancora nullo quando provi a impostare il fattore di scorrimento su quell'oggetto.
portfoliobuilder

@portfoliobuilder commenti validi, anche se sembra che preferisci uno stile di programmazione più difensivo che sia IMO più adatto per le librerie opache rispetto a uno snippet su SO. L'impostazione del fattore di scorrimento su <= 0 e la chiamata a setScrollDurationFactor su una vista non istanziata è qualcosa che la maggior parte degli sviluppatori Android dovrebbe essere in grado di evitare di fare senza i controlli di runtime.
Oleg Vaskevich

43

Ho trovato una soluzione migliore, basata sulla risposta di @ df778899 e sull'API ValueAnimator di Android . Funziona bene senza riflessi ed è molto flessibile. Inoltre, non è necessario creare ViewPager personalizzato e inserirlo nel pacchetto android.support.v4.view. Ecco un esempio:

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

7
Penso che questo sia sottovalutato. Fa uso di startFakeDrage endFakeDragche dovrebbero essere per fare di più di quanto sia disponibile ViewPagerfuori dagli schemi. Questo funziona alla grande per me ed è esattamente quello che stavo cercando
user3280133

1
È un'ottima soluzione, grazie. Un piccolo miglioramento, però: per farlo funzionare per un viewPager con padding, la prima riga dovrebbe essere ... ValueAnimator.ofInt (0, viewPager.getWidth () - (forward? ViewPager.getPaddingLeft (): viewPager.getPaddingRight () ));
DmitryO.

1
questa era di gran lunga la soluzione più semplice / efficiente / responsabile poiché ignora la riflessione ed evita di sigillare violazioni come le soluzioni precedenti
Ryan

2
@lobzic, dove chiami esattamente il tuo animatePagerTransition(final boolean forwardmetodo? Questo è ciò di cui sono confuso. Lo chiami in uno dei ViewPager.OnPageChangeListenermetodi di interfaccia? O da qualche altra parte?
Sakiboy

Nessun brutto trucco di riflessione. Facilmente configurabile. Un'ottima risposta.
Oren

18

Come puoi vedere nei sorgenti di ViewPager, durata del fling controllata dall'oggetto mScroller. Nella documentazione possiamo leggere:

La durata dello scorrimento può essere passata nel costruttore e specifica il tempo massimo che dovrebbe richiedere l'animazione di scorrimento

Quindi, se vuoi controllare la velocità, puoi cambiare l'oggetto mScroller tramite la riflessione.

Dovresti scrivere qualcosa del genere:

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 

Sono un po 'confuso su come farlo: sto configurando ViewPager nel file XML e l'ho assegnato a un mPager e setAdapter (mAdapter) ... Devo creare una classe che estende ViewPager?
Alex Kelly

12
Dovresti scrivere qualcosa del genere: setContentView (R.layout.main); mPager = (ViewPager) findViewById (R.id.view_pager); Campo mScroller = ViewPager.class.getDeclaredField ("mScroller"); mScroller.setAccessible (true); mScroller.set (mPager, scroller); // inizializza l'oggetto scroller da solo
HighFlyer

Ho appena postato un'altra risposta ... Non funziona ... Avresti un po 'di tempo per darmi indicazioni sul perché no?
Alex Kelly

Dopo uno sguardo più attento ai sorgenti: trova mScroller.startScroll (sx, sy, dx, dy); nel sorgente ViewPager e dai un'occhiata alla realizzazione di Scroller. startScroll chiama un altro startScroll con il parametro DEFAULT_DURATION. È impossibile creare un altro valore per il campo finale statico, quindi è rimasta un'opportunità: ignorare smoothScrollTo di ViewPager
HighFlyer

16

Questa non è una soluzione perfetta, non puoi rallentare la velocità perché è un file int. Ma per me è abbastanza lento e non devo usare la riflessione.

Notare il pacchetto in cui si trova la classe. smoothScrollToha visibilità sul pacchetto.

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

Nota che la prima riga: "package android.support.v4.view;" è obbligatorio e dovresti creare tale pacchetto nella tua root src. Se non lo farai, il tuo codice non verrà compilato.
Elhanan Mishraky

3
@ElhananMishraky esattamente, questo è ciò che intendevo con Notice the package where the class is.
pawelzieba

3
Aspetta, aspetta, aspetta, aspetta ... wuuuut? Puoi usare sdk package-local fields/ methods, se fai in modo che Android Studio pensi che stai usando lo stesso pacchetto? Sono l'unico che ne sta annusando un po ' sealing violation?
Bartek Lipinski

Questa è davvero una bella soluzione, ma non ha funzionato nemmeno per me poiché l'Interpolator utilizzato dallo scroller ViewPager non si comporta bene in questo caso. Quindi ho combinato la tua soluzione con le altre usando la riflessione per ottenere mScroller. In questo modo, posso utilizzare lo scroller originale quando l'utente impagina e il mio scroller personalizzato quando cambio pagine a livello di codice con setCurrentItem ().
rolgalan

8

I metodi fakeDrag su ViewPager sembrano fornire una soluzione alternativa.

Ad esempio, questo passerà dall'elemento 0 all'elemento 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();
      }
    }
  }
}

count * countlì solo per aumentare la velocità di trascinamento mentre procede)


4

ho usato

DecelerateInterpolator ()

Ecco l'esempio:

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

2

Sulla base della soluzione accettata ho creato la classe kotlin con estensione per view pager. Godere! :)

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

}

1

Ecco una risposta utilizzando un approccio completamente diverso. Qualcuno potrebbe dire che questo ha un aspetto hacky; tuttavia, non utilizza la riflessione e direi che funzionerà sempre.

All'interno troviamo il seguente codice 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);

Calcola la durata in base a poche cose. Vedi qualcosa che possiamo controllare? mAdapter.getPageWidth. Implementiamo ViewPager.OnPageChangeListener nel nostro adattatore . Rileveremo quando ViewPager sta scorrendo e forniremo un valore falso per la larghezza . Se voglio che la durata sia k*100, tornerò 1.0/(k-1)per getPageWidth. Quello che segue è Kotlin impl di questa parte dell'adattatore che trasforma la durata in 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) {
    }

Non dimenticare di aggiungere l'adattatore come OnPageChangedListener.

Il mio caso particolare può tranquillamente presumere che l'utente non possa scorrere per trascinare tra le pagine. Se devi supportare la stabilizzazione dopo un trascinamento, devi fare un po 'di più nel tuo calcolo.

Uno svantaggio è che questo dipende da quel 100valore di durata di base hardcoded in ViewPager. Se ciò cambia, le tue durate cambiano con questo approccio.


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

Volevo risolvere lo stesso problema di tutti voi e questa è la mia soluzione per lo stesso problema, ma penso che in questo modo la soluzione sia più flessibile in quanto puoi cambiare la durata come preferisci e anche cambiare l'interpolazione dei valori come desideri per ottenere diversi effetti visivi. La mia soluzione scorre dalla pagina "1" alla pagina "2", quindi solo in posizioni crescenti, ma può essere facilmente modificata per andare in posizioni decrescenti facendo "animProgress - lastFakeDrag" invece di "lastFakeDrag - animProgress". Penso che questa sia la soluzione più flessibile per eseguire questa operazione.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.