Verlangsamung der Geschwindigkeit des Viewpager-Controllers in Android

104

Gibt es eine Möglichkeit, die Bildlaufgeschwindigkeit mit dem Viewpager-Adapter in Android zu verlangsamen?


Weißt du, ich habe mir diesen Code angesehen. Ich kann nicht herausfinden, was ich falsch mache.

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

Es ändert sich nichts, aber es treten keine Ausnahmen auf?

Alex Kelly
quelle
versuchen Sie dies antoniocappiello.com/2015/10/31/…
Rushi Ayyappa
1
child.setNestedScrollingEnabled(false);arbeitete für mich
Pierre

Antworten:

230

Ich habe mit HighFlyers Code begonnen, der zwar das mScroller-Feld geändert hat (was ein guter Anfang ist), aber nicht dazu beigetragen hat, die Dauer des Bildlaufs zu verlängern, da ViewPager die Dauer explizit an den mScroller übergibt, wenn er zum Bildlauf aufgefordert wird.

Das Erweitern von ViewPager hat nicht funktioniert, da die wichtige Methode (oothScrollTo) nicht überschrieben werden kann.

Am Ende habe ich das Problem behoben, indem ich Scroller mit folgendem Code erweitert habe:

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

Und so verwenden:

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

Grundsätzlich habe ich die Dauer auf 5 Sekunden fest codiert und meinen ViewPager dazu gebracht, sie zu verwenden.

Hoffe das hilft.

Marmor
quelle
16
Kannst du mir bitte sagen, was ein Interpolator ist?
Shashank Degloorkar
10
'Interpolator' ist die Geschwindigkeit, mit der Animationen ausgeführt werden, z. B. können sie beschleunigen, verlangsamen und vieles mehr. Es gibt einen Konstruktor ohne Interpolator. Ich denke, das sollte funktionieren. Versuchen Sie also einfach, dieses Argument aus dem Aufruf zu entfernen. Wenn das nicht funktioniert, fügen Sie hinzu: 'Interpolator sInterpolator = new AccelerateInterpolator ()'
marmor
8
Ich habe festgestellt, dass DecelerateInterpolator in diesem Fall besser funktioniert. Es behält die Geschwindigkeit des anfänglichen Wischens bei und bremst dann langsam ab.
Quint Stoffers
Hallo, hast du eine Idee, warum scroller.setFixedDuration(5000);ein Fehler auftritt? Es heißt "Die Methode setFixedDuration (int) ist undefiniert"
jaibatrik
6
Wenn Sie Proguard verwenden, vergessen Sie nicht, diese Zeile hinzuzufügen: -keep class android.support.v4. ** {*;} Andernfalls erhalten Sie einen Nullzeiger, wenn getDeclaredField ()
GuilhE
61

Ich wollte es selbst tun und habe eine Lösung gefunden (allerdings mit Reflexion). Es ähnelt der akzeptierten Lösung, verwendet jedoch denselben Interpolator und ändert die Dauer nur basierend auf einem Faktor. Sie müssen ein ViewPagerCustomDurationin Ihrem XML anstelle von verwenden ViewPager, und dann können Sie dies tun:

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

}

Hoffe das hilft jemandem!

Oleg Vaskevich
quelle
3
Diese Lösung gefällt mir besser als die akzeptierte Antwort, da der benutzerdefinierte ViewPager eine Drop-In-Klasse mit minimalen Änderungen am vorhandenen Code ist. Wenn Sie den benutzerdefinierten Scroller als statische innere Klasse des benutzerdefinierten ViewPagers verwenden, wird er noch gekapselter.
Emanuel Moecklin
1
Wenn Sie Proguard verwenden, vergessen Sie nicht, diese Zeile hinzuzufügen: -keep class android.support.v4. ** {*;} Andernfalls erhalten Sie einen Nullzeiger, wenn getDeclaredField ()
GuilhE
Ich schlage vor, auf ein Ternär zu aktualisieren, da Sie nicht zulassen möchten, dass jemand einen 0-Faktor übergibt. Dadurch wird Ihre Zeitdauer unterbrochen. Aktualisieren Sie den letzten Parameter, den Sie an mScrollFactor> 0 übergeben. (int) (Dauer * mScrollFactor): Dauer
Portfoliobuilder
Ich schlage außerdem vor, dass Sie eine Nullprüfung durchführen, wenn Sie den Bildlauffaktor in Ihrer Klasse ViewPagerCustomDuration festlegen. Wenn Sie mit mScroller als Nullobjekt instanziieren, sollten Sie bestätigen, dass es immer noch nicht Null ist, wenn Sie versuchen, den Bildlauffaktor für dieses Objekt festzulegen.
Portfoliobuilder
@portfoliobuilder gültige Kommentare, obwohl Sie anscheinend einen defensiveren Programmierstil bevorzugen, der IMO eher für undurchsichtige Bibliotheken als für ein Snippet auf SO geeignet ist. Das Setzen des Bildlauffaktors auf <= 0 sowie das Aufrufen von setScrollDurationFactor in einer nicht instanziierten Ansicht sollten die meisten Android-Entwickler vermeiden können, auf Laufzeitprüfungen zu verzichten.
Oleg Vaskevich
43

Ich habe eine bessere Lösung gefunden, basierend auf der Antwort @ df778899 und der Android ValueAnimator-API . Es funktioniert gut ohne Reflexion und ist sehr flexibel. Außerdem müssen Sie keinen benutzerdefinierten ViewPager erstellen und in das Paket android.support.v4.view einfügen. Hier ist ein Beispiel:

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
quelle
7
Ich denke das wird unterschätzt. Es nutzt startFakeDragund endFakeDragdie dazu gedacht sind, mehr zu tun, als im ViewPagerLieferumfang enthalten ist. Dies funktioniert gut für mich und ist genau das, wonach ich gesucht habe
user3280133
1
Das ist eine großartige Lösung, danke. Eine kleine Verbesserung: Damit es für einen viewPager mit Auffüllung funktioniert, sollte die erste Zeile ... ValueAnimator.ofInt (0, viewPager.getWidth () - (forward? ViewPager.getPaddingLeft (): viewPager.getPaddingRight () ));
DmitryO.
1
Dies war bei weitem die einfachste / effizienteste / haftbarste Lösung, da sie Reflexionen außer Acht lässt und Siegelverletzungen wie die oben genannten Lösungen vermeidet
Ryan
2
@lobzic, wo genau nennst du deine animatePagerTransition(final boolean forwardMethode? Darüber bin ich verwirrt. Nennen Sie es in einer der ViewPager.OnPageChangeListenerSchnittstellenmethoden? Oder irgendwo anders?
Sakiboy
Keine hässlichen Reflexionshacks. Einfach zu konfigurieren. Eine sehr gute Antwort.
Oren
18

Wie Sie sehen in ViewPager Quellen, die Dauer von mScroller Objekt Fling gesteuert. In der Dokumentation können wir lesen:

Die Dauer des Bildlaufs kann im Konstruktor übergeben werden und gibt die maximale Zeit an, die die Bildlaufanimation dauern soll

Wenn Sie also die Geschwindigkeit steuern möchten, können Sie das mScroller-Objekt über die Reflexion ändern.

Sie sollten so etwas schreiben:

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 
Überflieger
quelle
Ich bin etwas verwirrt darüber, wie das geht - ich richte den ViewPager in der XML-Datei ein und habe ihn einem mPager zugewiesen und setAdapter (mAdapter) ... Soll ich eine Klasse erstellen, die ViewPager erweitert?
Alex Kelly
12
Sie sollten ungefähr so ​​schreiben: setContentView (R.layout.main); mPager = (ViewPager) findViewById (R.id.view_pager); Feld mScroller = ViewPager.class.getDeclaredField ("mScroller"); mScroller.setAccessible (true); mScroller.set (mPager, Scroller); // Scroller-Objekt selbst initialisieren
HighFlyer
Ich habe gerade noch eine Antwort gepostet ... Es funktioniert nicht ... Hätten Sie etwas Zeit, mir Anweisungen zu geben, warum nicht?
Alex Kelly
Nach genauerer Betrachtung der Quellen: Finden Sie mScroller.startScroll (sx, sy, dx, dy); in der ViewPager-Quelle und sehen Sie sich die Scroller-Realisierung an. startScroll ruft andere startScroll mit dem Parameter DEFAULT_DURATION auf. Seine unmöglich einen anderen Wert für die statische letzte Feld zu machen, so eine Gelegenheit links - Überschreibung smoothScrollTo von ViewPager
Highflyer
16

Dies ist keine perfekte Lösung, Sie können die Geschwindigkeit nicht verlangsamen, weil es eine ist int. Aber für mich ist es langsam genug und ich muss keine Reflexion verwenden.

Beachten Sie das Paket, in dem sich die Klasse befindet. smoothScrollTohat Paketsichtbarkeit.

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
quelle
Beachten Sie, dass die erste Zeile: "package android.support.v4.view;" ist obligatorisch und Sie sollten ein solches Paket in Ihrem src-Stammverzeichnis erstellen. Wenn Sie dies nicht tun, wird Ihr Code nicht kompiliert.
Elhanan Mishraky
3
@ ElhananMishraky genau, das habe ich gemeint Notice the package where the class is.
Pawelzieba
3
Warten Sie, warten Sie, warten Sie, warten Sie ... wuuuut? Sie können sdk's package-local fields/ verwenden methods, wenn Sie Android Studio nur glauben lassen, dass Sie dasselbe Paket verwenden? Bin ich der einzige, der etwas riecht sealing violation?
Bartek Lipinski
Dies ist eine wirklich schöne Lösung, aber sie hat auch bei mir nicht funktioniert, da sich der vom ViewPager Scroller verwendete Interpolator in diesem Fall nicht gut verhält. Also habe ich Ihre Lösung mit den anderen kombiniert, indem ich Reflexion verwendet habe, um den mScroller zu erhalten. Auf diese Weise kann ich den ursprünglichen Scroller verwenden, wenn der Benutzer paginiert, und meinen benutzerdefinierten Scroller, wenn ich Seiten programmgesteuert mit setCurrentItem () ändere.
Rolgalan
8

Die fakeDrag-Methoden in ViewPager scheinen eine alternative Lösung zu bieten.

Zum Beispiel wird von Seite 0 bis 1 gewechselt:

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

(Das count * countist nur da, um das Ziehen zu beschleunigen)

df778899
quelle
4

Ich habe benutzt

DecelerateInterpolator ()

Hier ist das Beispiel:

  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
quelle
2

Basierend auf der akzeptierten Lösung habe ich eine Kotlin-Klasse mit der Erweiterung für View Pager erstellt. Genießen! :) :)

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
quelle
1

Hier ist eine Antwort mit einem ganz anderen Ansatz. Jemand könnte sagen, dass dies ein hackiges Gefühl hat; Es wird jedoch keine Reflexion verwendet, und ich würde argumentieren, dass es immer funktionieren wird.

Wir finden den folgenden Code darin 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);

Es berechnet die Dauer anhand einiger Dinge. Sehen Sie etwas, das wir kontrollieren können? mAdapter.getPageWidth. Lassen Sie uns ViewPager.OnPageChangeListener in unserem Adapter implementieren . Wir werden erkennen, wann der ViewPager einen Bildlauf durchführt, und einen falschen Wert für die Breite angeben . Wenn ich die Dauer sein soll k*100, dann will ich wieder 1.0/(k-1)für getPageWidth. Das Folgende ist Kotlin Impl dieses Teils des Adapters, der die Dauer in 400 umwandelt:

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

Vergessen Sie nicht, den Adapter als OnPageChangedListener hinzuzufügen.

In meinem speziellen Fall kann davon ausgegangen werden, dass der Benutzer nicht wischen kann, um zwischen Seiten zu ziehen. Wenn Sie das Abwickeln nach einem Ziehen unterstützen müssen, müssen Sie bei Ihrer Berechnung etwas mehr tun.

Ein Nachteil ist, dass dies von dem fest 100codierten Wert für die Basisdauer in ViewPager abhängt. Wenn sich dies ändert, ändert sich Ihre Dauer mit diesem Ansatz.

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

Ich wollte das gleiche Problem wie Sie alle lösen und dies ist meine Lösung für das gleiche Problem, aber ich denke, auf diese Weise ist die Lösung flexibler, da Sie die Dauer nach Belieben ändern und auch die Interpolation der Werte nach Wunsch ändern können verschiedene visuelle Effekte zu erzielen. Meine Lösung wischt von Seite "1" zu Seite "2", also nur in zunehmenden Positionen, kann aber leicht geändert werden, um in abnehmende Positionen zu wechseln, indem "animProgress - lastFakeDrag" anstelle von "lastFakeDrag - animProgress" ausgeführt wird. Ich denke, dies ist die flexibelste Lösung für die Ausführung dieser Aufgabe.

Einreiben
quelle