Diminuindo a velocidade do controlador Viewpager no Android

104

Existe alguma maneira de diminuir a velocidade de rolagem com o adaptador do viewpager no Android?


Você sabe, estive olhando este código. Não consigo descobrir o que estou fazendo de errado.

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

Isso não muda nada, mas nenhuma exceção ocorre?

Alex Kelly
fonte
tente este antoniocappiello.com/2015/10/31/…
Rushi Ayyappa
1
child.setNestedScrollingEnabled(false);trabalhou para mim
Pierre

Respostas:

230

Comecei com o código de HighFlyer, que de fato mudou o campo mScroller (o que é um ótimo começo), mas não ajudou a estender a duração da rolagem porque o ViewPager passa explicitamente a duração para o mScroller ao solicitar a rolagem.

Estender o ViewPager não funcionou porque o método importante (smoothScrollTo) não pode ser substituído.

Acabei corrigindo isso estendendo o Scroller com 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);
    }
}

E usando assim:

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

Basicamente, codifiquei a duração para 5 segundos e fiz meu ViewPager usá-la.

Espero que isto ajude.

marmor
fonte
16
Você pode me dizer o que é sInterpolator ??
Shashank Degloorkar
10
'Interpolador' é a taxa na qual as animações são executadas, por exemplo, elas podem acelerar, desacelerar e muito mais. Há um construtor sem um interpolador, acho que deve funcionar, então simplesmente tente remover esse argumento da chamada. Se isso não funcionar, adicione: 'Interpolator sInterpolator = new AccelerateInterpolator ()'
marmor
8
Descobri que DecelerateInterpolator funciona melhor neste caso, ele mantém a velocidade do toque inicial e, em seguida, desacelera lentamente.
Quint Stoffers de
Olá, tem ideia do porque scroller.setFixedDuration(5000);dá erro? Diz "The method setFixedDuration (int) is undefined"
jaibatrik
6
Se você estiver usando o proguard, não se esqueça de adicionar esta linha: -keep class android.support.v4. ** {*;} caso contrário, você obterá um ponteiro nulo quando getDeclaredField ()
GuilhE
61

Eu queria fazer sozinho e encontrei uma solução (usando reflexão, no entanto). É semelhante à solução aceita, mas usa o mesmo interpolador e apenas altera a duração com base em um fator. Você precisa usar um ViewPagerCustomDurationem seu XML em vez de ViewPager, e então você pode fazer isso:

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 isso ajude alguém!

Oleg Vaskevich
fonte
3
Gosto mais dessa solução do que da resposta aceita porque o ViewPager personalizado é uma classe drop-in com alterações mínimas no código existente. Ter o scroller personalizado como uma classe interna estática do ViewPager personalizado o torna ainda mais encapsulado.
Emanuel Moecklin
1
Se você estiver usando o proguard, não se esqueça de adicionar esta linha: -keep class android.support.v4. ** {*;} caso contrário, você obterá um ponteiro nulo quando getDeclaredField ()
GuilhE
Sugiro atualizar para um ternário, pois você não quer permitir que ninguém passe no fator 0, isso vai quebrar o seu tempo de duração. Atualizar o parâmetro final que você está passando para mScrollFactor> 0? (int) (duração * mScrollFactor): duração
portfólio de
Eu também sugiro que você faça uma verificação nula ao definir o fator de rolagem em sua classe, ViewPagerCustomDuration. Você instancia o mScroller como um objeto nulo, você deve confirmar que ainda não é nulo ao tentar definir o fator de rolagem nesse objeto.
portfólio de
Comentários válidos do @portfoliobuilder, embora pareça que você favorece um estilo de programação mais defensivo que seja IMO mais adequado para bibliotecas opacas do que um snippet em SO. Definir o fator de rolagem como <= 0, bem como chamar setScrollDurationFactor em uma visualização não instanciada, é algo que a maioria dos desenvolvedores Android deve evitar fazer sem verificações de tempo de execução.
Oleg Vaskevich
43

Eu encontrei uma solução melhor, com base na resposta de @ df778899 e na API Android ValueAnimator . Funciona bem sem reflexão e é muito flexível. Além disso, não há necessidade de tornar o ViewPager personalizado e colocá-lo no pacote android.support.v4.view. Aqui está um exemplo:

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
fonte
7
Eu acho que isso é subestimado. Ele usa startFakeDrage endFakeDragdestina-se a fazer mais do que o disponível ViewPagerfora da caixa. Isso funciona muito bem para mim e é exatamente o que eu estava procurando
user3280133
1
Essa é uma ótima solução, obrigado. No entanto, uma pequena melhoria: para fazê-lo funcionar para um viewPager com preenchimento, a primeira linha deve ser ... ValueAnimator.ofInt (0, viewPager.getWidth () - (forward? ViewPager.getPaddingLeft (): viewPager.getPaddingRight () ));
DmitryO.
1
esta foi de longe a solução mais simples / eficiente / responsável, uma vez que desconsidera a reflexão e evita violações de vedação como as soluções acima
Ryan
2
@lobzic, onde exatamente você chama seu animatePagerTransition(final boolean forwardmétodo? É sobre isso que estou confuso. Você o chama em um dos ViewPager.OnPageChangeListenermétodos de interface? Ou em outro lugar?
Sakiboy
Sem cortes de reflexão feios. Facilmente configurável. Uma resposta muito boa.
Oren
18

Como você pode ver nas fontes do ViewPager, a duração do fling controlada pelo objeto mScroller. Em documantation podemos ler:

A duração da rolagem pode ser passada no construtor e especifica o tempo máximo que a animação de rolagem deve levar

Portanto, se você quiser controlar a velocidade, pode alterar o objeto mScroller por meio de reflexão.

Você deve escrever algo assim:

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
fonte
Estou um pouco confuso sobre como fazer isso - estou configurando o ViewPager no arquivo XML e atribuí-lo a um mPager e setAdapter (mAdapter) ... Devo criar uma classe que estende ViewPager?
Alex Kelly
12
Você deve escrever algo assim: setContentView (R.layout.main); mPager = (ViewPager) findViewById (R.id.view_pager); Campo mScroller = ViewPager.class.getDeclaredField ("mScroller"); mScroller.setAccessible (true); mScroller.set (mPager, scroller); // inicializar o objeto scroller por você mesmo
HighFlyer
Acabei de postar mais uma resposta ... Não está funcionando ... Você teria algum tempo para me orientar sobre por que não?
Alex Kelly
Após uma análise mais cuidadosa das fontes: encontre mScroller.startScroll (sx, sy, dx, dy); na fonte do ViewPager e dê uma olhada na realização do Scroller. startScroll chama outro startScroll com o parâmetro DEFAULT_DURATION. É impossível criar outro valor para o campo final estático, então sobrou uma oportunidade - substituir smoothScrollTo de ViewPager
HighFlyer
16

Esta não é a solução perfeita, você não pode tornar a velocidade mais lenta porque é um int. Mas, para mim, é lento o suficiente e não preciso usar reflexão.

Observe o pacote onde está a classe. smoothScrollTotem visibilidade de pacote.

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
fonte
Observe que a primeira linha: "package android.support.v4.view;" é obrigatório e você deve criar tal pacote em sua raiz src. Se você não fizer isso, seu código não será compilado.
Elhanan Mishraky 01 de
3
@ElhananMishraky exatamente, é o que eu quis dizer com Notice the package where the class is.
pawelzieba
3
Espere, espere, espere, espere ... wuuuut? Você pode usar sdk's package-local fields/ methods, se apenas fizer o Android Studio pensar que está usando o mesmo pacote? Eu sou o único que está sentindo algum cheiro sealing violation?
Bartek Lipinski
Esta é uma solução muito boa, mas também não funcionou para mim, já que o Interpolator usado pelo ViewPager Scroller não se comporta bem neste caso. Portanto, combinei sua solução com as outras usando reflexão para obter o mScroller. Dessa forma, posso usar o Scroller original quando o usuário pagina, e meu Scroller personalizado quando mudo as páginas programaticamente com setCurrentItem ().
rolgalan
8

Os métodos fakeDrag no ViewPager parecem fornecer uma solução alternativa.

Por exemplo, esta página vai do item 0 a 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();
      }
    }
  }
}

(O count * countestá lá apenas para acelerar o arrasto à medida que avança)

df778899
fonte
4

Eu tenho usado

DecelerateInterpolator ()

Aqui está o exemplo:

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

Com base na solução aceita, criei uma classe kotlin com extensão para visualização de pager. Aproveitar! :)

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

Aqui está uma resposta usando uma abordagem totalmente diferente. Alguém pode dizer que isso tem uma sensação de hacker; no entanto, não usa reflexão e eu diria que sempre funcionará.

Encontramos o seguinte código dentro 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);

Ele calcula a duração com base em algumas coisas. Vê algo que podemos controlar? mAdapter.getPageWidth. Vamos implementar ViewPager.OnPageChangeListener em nosso adaptador . Vamos detectar quando o ViewPager está rolando e fornecer um valor falso para a largura . Se eu quiser que a duração seja k*100, então voltarei 1.0/(k-1)para getPageWidth. A seguir está o implemento Kotlin dessa parte do adaptador que transforma a duração em 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) {
    }

Não se esqueça de adicionar o adaptador como OnPageChangedListener.

Meu caso particular pode assumir com segurança que o usuário não pode deslizar para arrastar entre as páginas. Se você tiver que suportar a sedimentação após um arrasto, precisará fazer um pouco mais de cálculo.

Uma desvantagem é que isso depende desse 100valor de duração base codificado no ViewPager. Se isso mudar, então suas durações mudam com essa abordagem.

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

Queria resolver o mesmo problema que todos vocês e esta é a minha solução para o mesmo problema, mas acho que desta forma a solução é mais flexível, pois você pode alterar a duração como quiser e também alterar a interpolação dos valores conforme desejado para obter diferentes efeitos visuais. Minha solução desliza da página "1" para a página "2", portanto, apenas em posições crescentes, mas pode ser facilmente alterada para ir em posições decrescentes fazendo "animProgress - lastFakeDrag" em vez de "lastFakeDrag - animProgress". Acho que esta é a solução mais flexível para realizar esta tarefa.

Esfregar
fonte