Ändern Sie die Dauer der ViewPager-Animation, wenn Sie programmgesteuert gleiten


73

Ich ändere die Folie mit dem folgenden Code:

viewPager.setCurrentItem(index++, true);

Aber es ändert sich zu schnell. Gibt es eine Möglichkeit, die Animationsgeschwindigkeit manuell einzustellen?


Sie sind sich nicht sicher, wie Ihre Suche aussah, aber sehen Sie sich diese Antwort hier auf SO an. Genau das sollten Sie suchen.
MH.

Eigentlich ist das meiner Lösung ziemlich ähnlich, obwohl ich einen Faktor anstelle der absoluten Dauer verwendet habe (da ViewPagerdie Dauer in a davon abhängen kann, wie viele Seiten Sie durchblättern).
Oleg Vaskevich

Antworten:


111

Ich wollte es selbst tun und habe eine Lösung gefunden (allerdings mit Reflexion). Ich habe es noch nicht getestet, aber es sollte funktionieren oder nur minimale Änderungen erfordern. Getestet auf Galaxy Nexus JB 4.2.1. 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 {
            Field scroller = ViewPager.class.getDeclaredField("mScroller");
            scroller.setAccessible(true);
            Field interpolator = ViewPager.class.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!


2
Ihre Lösung ist die schönste, die ich bisher gefunden habe, aber wenn die Zeile "scroller.set (this, mScroller)"; ausgeführt wird, erhalte ich die Fehlermeldung "IllegalArgumentException: ungültiger Wert für Feld". Ich denke, das liegt an der Verwendung einer ViewPager-Unterklasse ... irgendein Fix?
Elgui

Wie kann der var ViewPagerCustomDuration .mScroller verwendet werden? es scheint mit nichts verbunden zu sein ...
elgui

Ich werde gleich darauf eingehen. Es sollte möglich sein. Auch mScroller wird nur intern von verwendet ViewPager.
Oleg Vaskevich

Hmm, ich habe das getestet, um für mich zu arbeiten. Ich bin mir nicht sicher, warum du eine bekommst IllegalArgumentException.
Oleg Vaskevich

1
Erhalten eines Nullzeigerfehlers auf setScrollDurationFaktor dies, wenn proguard android aktiviert ist
aj0822ArpitJoshi

37

Ich habe eine bessere Lösung gefunden, basierend auf der Antwort von @ df778899 und der Android ValueAnimator-API . Es funktioniert gut ohne Reflexion und ist sehr flexibel. Es ist auch nicht erforderlich, einen benutzerdefinierten ViewPager zu erstellen und in das Paket android.support.v4.view einzufügen. Hier ist ein Beispiel:

private void animatePagerTransition(final boolean forward) {

    ValueAnimator animator = ValueAnimator.ofInt(0, viewPager.getWidth());
    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);
    if (viewPager.beginFakeDrag()) {
        animator.start();
    }
}

AKTUALISIEREN:

Nur überprüft, ob diese Lösung verwendet werden kann, um mehrere Seiten gleichzeitig zu wischen (zum Beispiel, wenn die erste Seite nach der letzten angezeigt werden soll). Dies ist ein leicht modifizierter Code, um die angegebene Seitenzahl zu verarbeiten:

private int oldDragPosition = 0;

private void animatePagerTransition(final boolean forward, int pageCount) {
    // if previous animation have not finished we can get exception
    if (pagerAnimation != null) {
        pagerAnimation.cancel();
    }
    pagerAnimation = getPagerTransitionAnimation(forward, pageCount);
    if (viewPager.beginFakeDrag()) {    // checking that started drag correctly
        pagerAnimation.start();
    }
}

private Animator getPagerTransitionAnimation(final boolean forward, int pageCount) {
    ValueAnimator animator = ValueAnimator.ofInt(0, viewPager.getWidth() - 1);
    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) {
            viewPager.endFakeDrag();
            oldDragPosition = 0;
            viewPager.beginFakeDrag();
        }
    });

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

        @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 / pageCount); // remove divider if you want to make each transition have the same speed as single page transition
    animator.setRepeatCount(pageCount);

    return animator;
}

Also macht es falsches Ziehen? scheint ein kluger Weg zu sein, aber was passiert, wenn der Benutzer während dieses Vorgangs mit dem Ziehen beginnt? Was ist auch, wenn ich möchte, dass es zur ersten Seite blättert (denn wenn es die letzte Seite erreicht, möchte ich es wieder auf die erste Seite bringen können)?
Android-Entwickler

@androiddeveloper, ich habe diese Lösung mit NonSwipeableViewPager ( stackoverflow.com/a/9650884/2923816 ) verwendet. Ich habe also keine Probleme mit der Benutzerinteraktion beim falschen Ziehen. Wenn jedoch jemand die Wischgeste beibehalten muss, kann dies erreicht werden, indem ViewPager überschrieben und die Animation gesteuert wird, wenn der Benutzer den ViewPager berührt. Ich habe es nicht ausprobiert, aber ich denke, es ist möglich und sollte gut funktionieren. Für einen Loop-Pager sollten Sie den Animationscode anpassen, die Anpassungen hängen jedoch von der konkreten Implementierung des Loopings ab.
Lobzik

Über die Benutzerinteraktion, ok (da die Animation nicht so langsam ist, macht es mir nichts aus). Über das Scrollen zur ersten Seite verstehe ich nicht. Angenommen, es gibt nur 3 Seiten, und ich bin auf der letzten. Wie kann ich mit der von Ihnen festgelegten benutzerdefinierten Dauer zur ersten Seite scrollen? Würde es helfen, die Wiederholungseigenschaft des Animators zu verwenden?
Android-Entwickler

@androiddeveloper Ja, Sie können die Wiederholungseigenschaft verwenden. Wenn Sie zur gleichen Zeit wie beim üblichen Seitenübergang zur ersten Seite zurückkehren möchten, sollten Sie auch die Übergangsdauer für diese zurückkehrende Animation ändern.
Lobzik

Ich verstehe nicht. Können Sie bitte den Code ändern, um zu zeigen, wie das geht? Vielleicht haben Sie anstelle von "vorwärts" eine Ganzzahl, die angibt, wie viele Seiten noch übrig sind (und wenn sie negativ sind, gehen Sie rückwärts)?
Android-Entwickler

12
 public class PresentationViewPager extends ViewPager {

    public static final int DEFAULT_SCROLL_DURATION = 250;
    public static final int PRESENTATION_MODE_SCROLL_DURATION = 1000;

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

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

    public void setDurationScroll(int millis) {
        try {
            Class<?> viewpager = ViewPager.class;
            Field scroller = viewpager.getDeclaredField("mScroller");
            scroller.setAccessible(true);
            scroller.set(this, new OwnScroller(getContext(), millis));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public class OwnScroller extends Scroller {

        private int durationScrollMillis = 1;

        public OwnScroller(Context context, int durationScroll) {
            super(context, new DecelerateInterpolator());
            this.durationScrollMillis = durationScroll;
        }

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

6

Eine bessere Lösung besteht darin, einfach auf die privaten Felder zuzugreifen, indem Sie die Klasse im Support Package erstellen. BEARBEITEN Dies ist an die MAX_SETTLE_DURATIONvon der ViewPagerKlasse festgelegten 600 ms gebunden .

package android.support.v4.view;

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

public class SlowViewPager extends ViewPager {

    // The speed of the scroll used by setCurrentItem()
    private static final int VELOCITY = 200;

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

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

    @Override
    void setCurrentItemInternal(int item, boolean smoothScroll, boolean always) {
        setCurrentItemInternal(item, smoothScroll, always, VELOCITY);
    }

}

Sie können dann natürlich ein benutzerdefiniertes Attribut hinzufügen, damit dies über XML festgelegt werden kann.


Danke, das scheint zu funktionieren, aber selbst wenn VELOCITY auf 1 gesetzt ist, ist die Schriftrolle meiner Meinung nach immer noch ziemlich schnell. Ich wünschte, ich könnte sie weiter verlangsamen.
Oliver Pearmain

@ HaggleLad Ja, leider ist mein Ansatz an die maximale Animationsdauer von 600 ms gebunden. Wenn es länger dauern soll, müssen Sie eine andere Methode verwenden. Ich werde meine Antwort aktualisieren, um dies zu notieren.
Paul Burke

8
Ist setCurrentItemInternal keine private Methode?
Marty Miller

2
@MartyMiller Ja, aber wenn die Support-Bibliothek Teil Ihres Projekts ist, können Sie eine Klasse in ihrem Paket erstellen.
Paul Burke

1
Das Bearbeiten der Support-Bibliothek ist keine gültige Methode. Wenn ich mich für ein Upgrade entschieden habe, funktioniert dieser Code nicht. In meinem persönlichen Fall habe ich auch keinen Quellcode für die Support-Bibliothek (und bearbeite ihn nicht, wenn ich ihn habe)
Ahmed Adel Ismail

0

Hier ist mein Code, der in Librera Reader verwendet wird

public class MyViewPager extends ViewPager {

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

    private void initMyScroller() {
        try {
            Class<?> viewpager = ViewPager.class;
            Field scroller = viewpager.getDeclaredField("mScroller");
            scroller.setAccessible(true);
            scroller.set(this, new MyScroller(getContext())); // my liner scroller

            Field mFlingDistance = viewpager.getDeclaredField("mFlingDistance");
            mFlingDistance.setAccessible(true);
            mFlingDistance.set(this, Dips.DP_10);//10 dip

            Field mMinimumVelocity = viewpager.getDeclaredField("mMinimumVelocity");
            mMinimumVelocity.setAccessible(true);
            mMinimumVelocity.set(this, 0); //0 velocity

        } catch (Exception e) {
            LOG.e(e);
        }

    }

    public class MyScroller extends Scroller {
        public MyScroller(Context context) {
            super(context, new LinearInterpolator()); // my LinearInterpolator
        }

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

 }

0

Ich habe die Version von Cicero Moura verwendet, um eine Kotlin-Klasse zu erstellen, die ab Android 10 immer noch perfekt funktioniert.

import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.animation.DecelerateInterpolator
import android.widget.Scroller
import androidx.viewpager.widget.ViewPager

class CustomViewPager(context: Context, attrs: AttributeSet) :
        ViewPager(context, attrs) {


    private companion object {
        const val DEFAULT_SPEED = 1000
    }

    init {
        setScrollerSpeed(DEFAULT_SPEED)
    }

    var scrollDuration = DEFAULT_SPEED
        set(millis) {
            setScrollerSpeed(millis)
        }

    private fun setScrollerSpeed(millis: Int) {
        try {
            ViewPager::class.java.getDeclaredField("mScroller")
                    .apply {
                        isAccessible = true
                        set(this@CustomViewPager, OwnScroller(millis))
                    }

        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    inner class OwnScroller(private val durationScrollMillis: Int) : Scroller(context, AccelerateDecelerateInterpolator()) {
        override fun startScroll(startX: Int, startY: Int, dx: Int, dy: Int, duration: Int) {
            super.startScroll(startX, startY, dx, dy, durationScrollMillis)
        }
    }
}

Initialisierung aus der Aktivitätsklasse:

viewPager.apply {
    scrollDuration = 2000
    adapter = pagerAdapter
}

-2

Nachdem ich meinen ganzen Tag verschwendet hatte, fand ich eine Lösung, die offscreenPageLimit auf total no setzte. der Seite.

SetOffScreenLimit (page.length) speichert alle Ansichten im Speicher, um eine konstante Länge der ViewPager-Bildläufe zu gewährleisten. Dies stellt jedoch ein Problem für alle Animationen dar, bei denen die Funktion View.requestLayout aufgerufen wird (z. B. für Animationen, bei denen Änderungen am Rand oder an den Grenzen vorgenommen werden). Es macht sie sehr langsam (gemäß Romain Guy), weil alle Ansichten, die sich im Speicher befinden, ebenfalls ungültig werden. Deshalb habe ich verschiedene Methoden ausprobiert, um die Dinge reibungslos zu gestalten, aber das Überschreiben von requestLayout und anderen ungültigen Methoden verursacht viele andere Probleme.

Ein guter Kompromiss besteht darin, das Off-Screen-Limit dynamisch so zu ändern, dass die meisten Bildläufe zwischen den Seiten sehr flüssig sind, während sichergestellt wird, dass alle In-Page-Animationen reibungslos sind, indem die Ansichten beim Benutzer entfernt werden. Dies funktioniert sehr gut, wenn Sie nur 1 oder 2 Ansichten haben, die andere Ansichten aus dem Speicher entfernen müssen.

*** Verwenden Sie diese Option, wenn keine Lösung funktioniert, da durch Festlegen des Offset-Limits alle Fragmente gleichzeitig geladen werden

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.