Spowolnienie kontrolera Viewpager w systemie Android

105

Czy istnieje sposób na spowolnienie prędkości przewijania za pomocą adaptera przeglądarki w systemie Android?


Wiesz, patrzyłem na ten kod. Nie mogę zrozumieć, co się mylę.

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

To niczego nie zmienia, a nie ma wyjątków?

Alex Kelly
źródło
spróbuj tego antoniocappiello.com/2015/10/31/…
Rushi Ayyappa
1
child.setNestedScrollingEnabled(false);pracował dla mnie
Pierre

Odpowiedzi:

230

Zacząłem od kodu HighFlyer, który rzeczywiście zmienił pole mScroller (co jest świetnym początkiem), ale nie pomógł wydłużyć czasu trwania przewijania, ponieważ ViewPager jawnie przekazuje czas trwania do mScroller, gdy żąda przewijania.

Rozszerzanie ViewPager nie działało, ponieważ nie można zastąpić ważnej metody (smoothScrollTo).

Skończyło się na naprawieniu tego, rozszerzając Scroller o ten kod:

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

I używając tego w ten sposób:

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

Zasadniczo zakodowałem czas trwania do 5 sekund i sprawiłem, że mój ViewPager go używa.

Mam nadzieję że to pomoże.

marmor
źródło
16
czy możesz mi powiedzieć, co to jest sInterpolator?
Shashank Degloorkar
10
„Interpolator” to szybkość, z jaką animacje są uruchamiane, np. Mogą przyspieszać, zwalniać i wiele więcej. Jest konstruktor bez interpolatora, myślę, że to powinno działać, więc po prostu spróbuj usunąć ten argument z wywołania. Jeśli to nie zadziała, dodaj: 'Interpolator sInterpolator = new AccelerateInterpolator ()'
marmor
8
Odkryłem, że DecelerateInterpolator działa lepiej w tym przypadku, utrzymuje prędkość początkowego machnięcia, a następnie powoli zwalnia.
Quint Stoffers
Cześć, czy masz jakiś pomysł, dlaczego scroller.setFixedDuration(5000);podaje błąd? Mówi się, że „Metoda setFixedDuration (int) jest nieokreślona”
jaibatrik
6
Jeśli używasz proguard, nie zapomnij dodać tej linii: -keep class android.support.v4. ** {*;} w przeciwnym razie otrzymasz zerowy wskaźnik, gdy getDeclaredField ()
GuilhE
61

Chciałem zrobić sam i osiągnąłem rozwiązanie (jednak wykorzystując refleksję). Jest podobny do przyjętego rozwiązania, ale używa tego samego interpolatora i zmienia tylko czas trwania na podstawie współczynnika. Musisz użyć a ViewPagerCustomDurationw swoim XML zamiast ViewPager, a następnie możesz to zrobić:

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

}

Mam nadzieję, że to komuś pomoże!

Oleg Vaskevich
źródło
3
To rozwiązanie podoba mi się bardziej niż zaakceptowana odpowiedź, ponieważ niestandardowy ViewPager to klasa drop-in z minimalnymi zmianami w istniejącym kodzie. Posiadanie niestandardowego przewijacza jako statycznej klasy wewnętrznej niestandardowego ViewPagera sprawia, że ​​jest on jeszcze bardziej hermetyzowany.
Emanuel Moecklin
1
Jeśli używasz proguard, nie zapomnij dodać tej linii: -keep class android.support.v4. ** {*;} w przeciwnym razie otrzymasz pusty wskaźnik, gdy getDeclaredField ()
GuilhE
Sugeruję aktualizację do trójskładnika, ponieważ nie chcesz pozwolić nikomu na przekazanie czynnika 0, to przerwie twój czas. Zaktualizować ostatni parametr, który przekazujesz do mScrollFactor> 0? (int) (duration * mScrollFactor): duration
portfoliobuilder
Sugeruję również, abyś sprawdził wartość null podczas ustawiania współczynnika przewijania w swojej klasie, ViewPagerCustomDuration. Tworzysz wystąpienie z mScroller jako obiektem zerowym, powinieneś potwierdzić, że nadal nie jest on pusty, próbując ustawić współczynnik przewijania dla tego obiektu.
portfoliobuilder
@portfoliobuilder prawidłowe komentarze, chociaż wydaje się, że wolisz bardziej defensywny styl programowania, który IMO bardziej pasuje do nieprzejrzystych bibliotek niż fragment kodu w SO. Ustawienie współczynnika przewijania na <= 0, a także wywołanie setScrollDurationFactor w widoku bez wystąpienia instancji to coś, czego większość deweloperów systemu Android powinna móc uniknąć bez sprawdzania środowiska uruchomieniowego.
Oleg Vaskevich
43

Znalazłem lepsze rozwiązanie w oparciu o odpowiedź @ df778899 i Android ValueAnimator API . Działa dobrze bez refleksów i jest bardzo elastyczny. Nie ma również potrzeby tworzenia niestandardowego ViewPagera i umieszczania go w pakiecie android.support.v4.view. Oto przykład:

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
źródło
7
Myślę, że jest to niedoceniane. Wykorzystuje startFakeDragi endFakeDragma na celu zrobić więcej, niż jest dostępne po ViewPagerwyjęciu z pudełka. To działa świetnie dla mnie i jest dokładnie tym, czego szukałem
user3280133
1
To świetne rozwiązanie, dziękuję. Jedno małe ulepszenie: aby działało dla viewPagera z dopełnieniem, pierwsza linia powinna mieć postać ... ValueAnimator.ofInt (0, viewPager.getWidth () - (forward? ViewPager.getPaddingLeft (): viewPager.getPaddingRight () ));
DmitryO.
1
było to zdecydowanie najprostsze / skuteczne / odpowiedzialne rozwiązanie, ponieważ pomija refleksję i unika przypieczętowania naruszeń, takich jak rozwiązania powyżej
Ryan
2
@lobzic, gdzie dokładnie nazywasz swoją animatePagerTransition(final boolean forwardmetodę? W tym jestem zdezorientowany. Czy wywołujesz to w jednej z ViewPager.OnPageChangeListenermetod interfejsu? Lub gdzie indziej?
Sakiboy,
Żadnych brzydkich refleksów. Łatwo konfigurowalne. Bardzo dobra odpowiedź.
Oren
18

Jak widać w źródłach ViewPagera, czas trwania rzutowania kontrolowany przez obiekt mScroller. W dokumentacji możemy przeczytać:

Czas trwania przewijania można przekazać w konstruktorze i określa on maksymalny czas, jaki powinna zająć animacja przewijania

Więc jeśli chcesz kontrolować prędkość, możesz zmienić obiekt mScroller poprzez odbicie.

Powinieneś napisać coś takiego:

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
źródło
Jestem trochę zdezorientowany, jak to zrobić - konfiguruję ViewPager w pliku XML i przypisuję go do mPagera i setAdapter (mAdapter) ... Czy powinienem utworzyć klasę rozszerzającą ViewPager?
Alex Kelly
12
Powinieneś napisać coś takiego: setContentView (R.layout.main); mPager = (ViewPager) findViewById (R.id.view_pager); Pole mScroller = ViewPager.class.getDeclaredField ("mScroller"); mScroller.setAccessible (true); mScroller.set (mPager, scroller); // zainicjuj samodzielnie obiekt przewijania
HighFlyer,
Właśnie opublikowałem jeszcze jedną odpowiedź ... To nie działa ... Czy miałbyś trochę czasu, aby dać mi wskazówki, dlaczego nie?
Alex Kelly,
Po dokładniejszym przyjrzeniu się źródłom: znajdź mScroller.startScroll (sx, sy, dx, dy); w źródle ViewPager i spójrz na realizację Scrollera. startScroll wywołuje inne startScroll z parametrem DEFAULT_DURATION. Jest niemożliwe, aby inną wartość dla statycznego pola końcowego, więc jedna okazja lewo - przesłanianie smoothScrollTo z ViewPager
HighFlyer
16

To nie jest idealne rozwiązanie, nie możesz spowolnić prędkości, ponieważ jest to int. Ale jak dla mnie jest wystarczająco wolny i nie muszę używać refleksji.

Zwróć uwagę na pakiet, w którym znajduje się klasa. smoothScrollToma widoczność pakietu.

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
źródło
Zwróć uwagę, że pierwsza linia: "package android.support.v4.view;" jest obowiązkowe i powinieneś stworzyć taki pakiet w swoim katalogu głównym src. Jeśli tego nie zrobisz, Twój kod się nie skompiluje.
Elhanan Mishraky
3
@ElhananMishraky, dokładnie to miałem na myśli Notice the package where the class is.
pawelzieba
3
Czekaj, czekaj, czekaj, czekaj ... wuuuut? Możesz użyć sdk's package-local fields/ methods, jeśli po prostu sprawisz, że Android Studio pomyśli, że używasz tego samego pakietu? Czy jestem jedyną, która coś wącha sealing violation?
Bartek Lipiński
To naprawdę fajne rozwiązanie, ale też nie działało, ponieważ Interpolator używany przez ViewPager Scroller nie zachowuje się dobrze w tym przypadku. Dlatego połączyłem Twoje rozwiązanie z innymi za pomocą refleksji, aby uzyskać mScroller. W ten sposób mogę używać oryginalnego Scrollera, gdy użytkownik paginuje na strony, i mojego niestandardowego Scrollera, gdy programowo zmieniam strony za pomocą setCurrentItem ().
rolgalan
8

Wydaje się, że metody fakeDrag na ViewPager zapewniają alternatywne rozwiązanie.

Na przykład spowoduje to stronicowanie od pozycji 0 do 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 * countJest tam tylko po to, aby przyspieszyć przeciąganie)

df778899
źródło
4

używałem

DecelerateInterpolator ()

Oto przykład:

  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
źródło
2

Na podstawie przyjętego rozwiązania stworzyłem klasę kotlin z rozszerzeniem do przeglądania pagera. Cieszyć się! :)

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
źródło
1

Oto odpowiedź wykorzystująca zupełnie inne podejście. Ktoś mógłby powiedzieć, że to hacky; jednak nie używa refleksji i twierdziłbym, że zawsze będzie działać.

Wewnątrz znajdujemy następujący kod 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);

Oblicza czas trwania na podstawie kilku rzeczy. Widzisz coś, co możemy kontrolować? mAdapter.getPageWidth. Zaimplementujmy ViewPager.OnPageChangeListener w naszym adapterze . Zamierzamy wykryć przewijanie ViewPager i podać fałszywą wartość szerokości . Jeśli chcę, żeby trwanie byłok*100 , wrócę 1.0/(k-1)po getPageWidth. Poniżej znajduje się Kotlin impl tej części adaptera, który zmienia czas trwania na 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) {
    }

Nie zapomnij dodać adaptera jako OnPageChangedListener.

W moim konkretnym przypadku można bezpiecznie założyć, że użytkownik nie może przesuwać palcem, aby przeciągać między stronami. Jeśli musisz wspierać osiadanie po przeciągnięciu, musisz zrobić trochę więcej w swoich obliczeniach.

Jedną wadą jest to, że zależy to od tej zakodowanej na stałe 100wartości czasu trwania podstawowego w ViewPager. Jeśli to się zmieni, czas trwania zmieni się dzięki temu podejściu.

androidguy
źródło
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();

Chciałem rozwiązać ten sam problem, co wy wszyscy i to jest moje rozwiązanie tego samego problemu, ale myślę, że w ten sposób rozwiązanie jest bardziej elastyczne, ponieważ możesz zmienić czas trwania, jak chcesz, a także zmienić interpolację wartości według potrzeb aby osiągnąć różne efekty wizualne. Moje rozwiązanie przesuwa się ze strony „1” na stronę „2”, więc tylko w rosnących pozycjach, ale można je łatwo zmienić na malejące, wykonując „animProgress - lastFakeDrag” zamiast „lastFakeDrag - animProgress”. Myślę, że jest to najbardziej elastyczne rozwiązanie do wykonania tego zadania.

Wcierać
źródło