Disminución de la velocidad del controlador Viewpager en Android

104

¿Hay alguna forma de reducir la velocidad de desplazamiento con el adaptador de visor en Android?


Sabes, he estado mirando este código. No puedo entender qué estoy haciendo mal.

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

¿No cambia nada pero no se producen excepciones?

Alex Kelly
fuente
probar este antoniocappiello.com/2015/10/31/...
Rushi Ayyappa
1
child.setNestedScrollingEnabled(false);trabajó para mí
Pierre

Respuestas:

230

Comencé con el código de HighFlyer que de hecho cambió el campo mScroller (que es un gran comienzo) pero no ayudó a extender la duración del desplazamiento porque ViewPager pasa explícitamente la duración al mScroller al solicitar el desplazamiento.

La extensión de ViewPager no funcionó porque el método importante (smoothScrollTo) no se puede anular.

Terminé arreglando esto extendiendo Scroller con este código:

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

Y usándolo así:

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

Básicamente, codifiqué la duración en 5 segundos e hice que mi ViewPager la usara.

Espero que esto ayude.

marmor
fuente
16
¿Puedes decirme qué es sInterpolator?
Shashank Degloorkar
10
'Interpolador' es la velocidad a la que se ejecutan las animaciones, por ejemplo, pueden acelerar, desacelerar y mucho más. Hay un constructor sin interpolador, creo que debería funcionar, así que simplemente intente eliminar ese argumento de la llamada. Si eso no funciona, agregue: 'Interpolator sInterpolator = new AccelerateInterpolator ()'
marmor
8
Descubrí que DecelerateInterpolator funciona mejor en este caso, mantiene la velocidad del deslizamiento inicial y luego desacelera lentamente.
Quint Stoffers
Hola, ¿tienes alguna idea de por qué scroller.setFixedDuration(5000);da un error? Dice "El método setFixedDuration (int) no está definido"
jaibatrik
6
Si está utilizando proguard, no olvide agregar esta línea: -keep class android.support.v4. ** {*;} de lo contrario, obtendrá un puntero nulo cuando getDeclaredField ()
GuilhE
61

Quería hacerlo yo mismo y he logrado una solución (sin embargo, usando la reflexión). Es similar a la solución aceptada, pero utiliza el mismo interpolador y solo cambia la duración según un factor. Necesita usar un ViewPagerCustomDurationen su XML en lugar de ViewPager, y luego puede hacer esto:

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

}

¡Espero que esto ayude a alguien!

Oleg Vaskevich
fuente
3
Me gusta más esta solución que la respuesta aceptada porque el ViewPager personalizado es una clase directa con cambios mínimos en el código existente. Tener el desplazador personalizado como una clase interna estática del ViewPager personalizado lo hace aún más encapsulado.
Emanuel Moecklin
1
Si está utilizando proguard, no olvide agregar esta línea: -keep class android.support.v4. ** {*;} de lo contrario, obtendrá un puntero nulo cuando getDeclaredField ()
GuilhE
Sugiero actualizar a un ternario, porque no desea permitir que nadie pase un factor 0, esto romperá la duración del tiempo. ¿Actualizar el parámetro final que está pasando a mScrollFactor> 0? (int) (duration * mScrollFactor): duración
portfoliobuilder
También sugiero que haga una verificación nula al configurar el factor de desplazamiento en su clase, ViewPagerCustomDuration. Si crea una instancia con mScroller como un objeto nulo, debe confirmar que aún no es nulo cuando intente establecer el factor de desplazamiento en ese objeto.
portfoliobuilder
@portfoliobuilder comentarios válidos, aunque parece que favorece un estilo de programación más defensivo que, en mi opinión, es más adecuado para bibliotecas opacas que un fragmento de SO. Establecer el factor de desplazamiento en <= 0, así como llamar a setScrollDurationFactor en una vista no instanciada es algo que la mayoría de los desarrolladores de Android deberían poder evitar sin verificaciones en tiempo de ejecución.
Oleg Vaskevich
43

Encontré una mejor solución, basada en la respuesta de @ df778899 y la API ValueAnimator de Android . Funciona bien sin reflejos y es muy flexible. Además, no es necesario crear ViewPager personalizado y ponerlo en el paquete android.support.v4.view. Aquí hay un ejemplo:

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
fuente
7
Creo que esto está subestimado. Hace uso de startFakeDragy endFakeDragque están destinados a hacer más de lo que está disponible ViewPagerde fábrica. Esto funciona muy bien para mí y es exactamente lo que estaba buscando
user3280133
1
Esa es una gran solución, gracias. Sin embargo, una pequeña mejora: para que funcione para un viewPager con relleno, la primera línea debería ser ... ValueAnimator.ofInt (0, viewPager.getWidth () - (forward? ViewPager.getPaddingLeft (): viewPager.getPaddingRight () ));
DmitryO.
1
esta fue, con mucho, la solución más simple / eficiente / responsable, ya que ignora la reflexión y evita sellar violaciones como las soluciones anteriores
Ryan
2
@lobzic, ¿Dónde llamas exactamente a tu animatePagerTransition(final boolean forwardmétodo? Eso es lo que me confunde. ¿Lo llama en uno de los ViewPager.OnPageChangeListenermétodos de interfaz? ¿O en otro sitio?
Sakiboy
No hay trucos de reflexión feos. Fácilmente configurable. Muy buena respuesta.
Oren
18

Como puede ver en las fuentes de ViewPager, la duración de la aventura controlada por el objeto mScroller. En la documentación podemos leer:

La duración del desplazamiento se puede pasar en el constructor y especifica el tiempo máximo que debe tomar la animación de desplazamiento.

Entonces, si desea controlar la velocidad, puede cambiar el objeto mScroller a través de la reflexión.

Deberías escribir algo como esto:

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
fuente
Estoy un poco confundido sobre cómo hacer esto: estoy configurando ViewPager en el archivo XML y lo asigné a un mPager y setAdapter (mAdapter) ... ¿Debo crear una clase que amplíe ViewPager?
Alex Kelly
12
Debería escribir algo como esto: setContentView (R.layout.main); mPager = (ViewPager) findViewById (R.id.view_pager); Campo mScroller = ViewPager.class.getDeclaredField ("mScroller"); mScroller.setAccessible (verdadero); mScroller.set (mPager, desplazamiento); // inicializa el objeto de desplazamiento por ti mismo
HighFlyer
Acabo de publicar una respuesta más ... No funciona ... ¿Tendría algo de tiempo para darme instrucciones sobre por qué no?
Alex Kelly
Después de mirar más detenidamente las fuentes: busque mScroller.startScroll (sx, sy, dx, dy); en la fuente de ViewPager y observe la realización de Scroller. startScroll llama a otro startScroll con el parámetro DEFAULT_DURATION. Es imposible hacer otro valor para el campo final estático, por lo que queda una oportunidad: anule smoothScrollTo de ViewPager
HighFlyer
16

Esta no es la solución perfecta, no puede hacer que la velocidad sea más lenta porque es un int. Pero para mí es lo suficientemente lento y no tengo que usar la reflexión.

Observe el paquete donde está la clase. smoothScrollTotiene visibilidad de paquete.

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
fuente
Tenga en cuenta que la primera línea: "paquete android.support.v4.view;" es obligatorio y debe crear dicho paquete en su src root. Si no lo hace, su código no se compilará.
Elhanan Mishraky
3
@ElhananMishraky exactamente, eso es lo que quise decir con Notice the package where the class is.
pawelzieba
3
Espera, espera, espera, espera ... ¿wuuuut? Puede usar sdk's package-local fields/ methods, si solo hace que Android Studio crea que está usando el mismo paquete. ¿Soy el único que huele un poco sealing violation?
Bartek Lipinski
Esta es una solución realmente agradable, pero tampoco funcionó para mí, ya que el interpolador utilizado por ViewPager Scroller no se comporta bien en este caso. Así que combiné su solución con las demás usando la reflexión para obtener el mScroller. De esta manera, puedo usar el Scroller original cuando el usuario pagina y mi Scroller personalizado cuando cambio de página mediante programación con setCurrentItem ().
rolgalan
8

Los métodos fakeDrag en ViewPager parecen proporcionar una solución alternativa.

Por ejemplo, esto pasará del elemento 0 al 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();
      }
    }
  }
}

(El count * countestá ahí para hacer que el arrastre se acelere a medida que avanza)

df778899
fuente
4

He usado

DecelerateInterpolator ()

Aquí está el ejemplo:

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

Según la solución aceptada, he creado la clase kotlin con extensión para el paginador de vista. ¡Disfrutar! :)

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

Aquí hay una respuesta que utiliza un enfoque completamente diferente. Alguien podría decir que esto tiene una sensación de hacky; sin embargo, no utiliza la reflexión y yo diría que siempre funcionará.

Encontramos el siguiente código en su interior 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);

Calcula la duración basándose en algunas cosas. ¿Ves algo que podamos controlar? mAdapter.getPageWidth. Implementemos ViewPager.OnPageChangeListener en nuestro adaptador . Vamos a detectar cuando ViewPager se está desplazando y daremos un valor falso para el ancho . Si quiero que la duración sea k*100, entonces voy a volver 1.0/(k-1)a getPageWidth. Lo siguiente es Kotlin impl de esta parte del adaptador que convierte la duración 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) {
    }

No olvide agregar el adaptador como OnPageChangedListener.

Mi caso particular puede asumir con seguridad que el usuario no puede deslizar para arrastrar entre páginas. Si tiene que soportar el asentamiento después de un arrastre, entonces necesita hacer un poco más en su cálculo.

Una desventaja es que esto depende de ese 100valor de duración base codificado en ViewPager. Si eso cambia, entonces sus duraciones cambian con este enfoque.

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

Quería resolver el mismo problema que todos ustedes y esta es mi solución para el mismo problema, pero creo que de esta manera la solución es más flexible ya que puede cambiar la duración como desee y también cambiar la interpolación de los valores como desee. para lograr diferentes efectos visuales. Mi solución pasa de la página "1" a la página "2", por lo que solo en posiciones crecientes, pero se puede cambiar fácilmente para ir en posiciones decrecientes haciendo "animProgress - lastFakeDrag" en lugar de "lastFakeDrag - animProgress". Creo que esta es la solución más flexible para realizar esta tarea.

Frotar
fuente