Замедление скорости работы контроллера Viewpager в android

105

Есть ли способ замедлить скорость прокрутки с помощью адаптера просмотра в Android?


Вы знаете, я смотрел этот код. Я не могу понять, в чем я ошибаюсь.

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

Ничего не меняет, но исключений нет?

Алекс Келли
источник
попробуйте это antoniocappiello.com/2015/10/31/…
Руши Айяппа
1
child.setNestedScrollingEnabled(false);работал у меня
Пьер

Ответы:

230

Я начал с кода HighFlyer, который действительно изменил поле mScroller (что является отличным началом), но не помог увеличить продолжительность прокрутки, потому что ViewPager явно передает длительность mScroller при запросе прокрутки.

Расширение ViewPager не сработало, поскольку важный метод (smoothScrollTo) нельзя переопределить.

В итоге я исправил это, расширив Scroller с помощью этого кода:

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

И используя это так:

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

Я в основном жестко задал продолжительность до 5 секунд и заставил свой ViewPager использовать его.

Надеюсь это поможет.

мрамор
источник
16
Подскажите пожалуйста, что такое sInterpolator ??
Shashank Degloorkar
10
«Интерполятор» - это скорость, с которой запускаются анимации, например, они могут ускоряться, замедляться и многое другое. Есть конструктор без интерполятора, я думаю, он должен работать, поэтому просто попробуйте удалить этот аргумент из вызова. Если это не сработает, добавьте: 'Interpolator sInterpolator = new AccelerateInterpolator ()'
marmor
8
Я обнаружил, что DecelerateInterpolator работает лучше в этом случае, он поддерживает скорость первоначального смахивания, а затем медленно замедляется.
Quint Stoffers
Привет, ты хоть представляешь, почему scroller.setFixedDuration(5000);выдает ошибку? Он говорит: «Метод setFixedDuration (int) не определен»
jaibatrik
6
Если вы используете proguard, не забудьте добавить эту строку: -keep class android.support.v4. ** {*;} иначе вы получите нулевой указатель, когда getDeclaredField ()
GuilhE
61

Я хотел сделать сам и нашел решение (однако, используя отражение). Это похоже на принятое решение, но использует тот же интерполятор и изменяет продолжительность только в зависимости от фактора. Вам нужно использовать ViewPagerCustomDurationв своем XML вместо ViewPager, и тогда вы можете сделать это:

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

}

Надеюсь, это кому-то поможет!

Олег Васкевич
источник
3
Мне это решение нравится больше, чем принятый ответ, потому что настраиваемый ViewPager - это добавляемый класс с минимальными изменениями в существующем коде. Наличие настраиваемого скроллера в качестве статического внутреннего класса настраиваемого ViewPager делает его еще более инкапсулированным.
Эмануэль Моеклин
1
Если вы используете proguard, не забудьте добавить эту строку: -keep class android.support.v4. ** {*;} в противном случае вы получите нулевой указатель при getDeclaredField ()
GuilhE
Я предлагаю перейти на троичный, потому что вы не хотите позволять кому-либо переходить с коэффициентом 0, это нарушит вашу продолжительность времени. Обновить последний параметр, который вы передаете в mScrollFactor> 0? (INT) (длительность * mScrollFactor): продолжительность
portfoliobuilder
Я также предлагаю выполнить нулевую проверку при установке коэффициента прокрутки в вашем классе ViewPagerCustomDuration. Вы создаете экземпляр с mScroller как нулевой объект, вы должны подтвердить, что он по-прежнему не равен нулю при попытке установить коэффициент прокрутки для этого объекта.
portfoliobuilder
@portfoliobuilder допустимые комментарии, хотя кажется, что вы предпочитаете более защитный стиль программирования, который IMO больше подходит для непрозрачных библиотек, чем фрагмент кода на SO. Установка коэффициента прокрутки на <= 0, а также вызов setScrollDurationFactor в представлении без создания экземпляра - это то, чего большинство разработчиков Android должны иметь возможность избежать без проверок во время выполнения.
Олег Васкевич
43

Я нашел лучшее решение, основанное на ответе @ df778899 и API Android ValueAnimator . Он отлично работает без отражений и очень гибкий. Также нет необходимости создавать собственный ViewPager и помещать его в пакет android.support.v4.view. Вот пример:

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();
}
лобзик
источник
7
Я думаю, это недооценивается. Он использует startFakeDragи endFakeDragпредназначены для того, чтобы делать больше, чем доступно ViewPagerиз коробки. Это отлично работает для меня, и это именно то, что я искал
user3280133
1
Это отличное решение, спасибо. Однако одно небольшое улучшение: чтобы он работал для viewPager с заполнением, первая строка должна быть ... ValueAnimator.ofInt (0, viewPager.getWidth () - (вперед? ViewPager.getPaddingLeft (): viewPager.getPaddingRight () ));
ДмитрийО.
1
это было, безусловно, самое простое / эффективное / ответственное решение, так как оно игнорирует отражение и избегает нарушений герметизации, подобных приведенным выше решениям
Райан
2
@lobzic, где именно вы называете свой animatePagerTransition(final boolean forwardметод? Вот что меня смущает. Вы вызываете это в одном из ViewPager.OnPageChangeListenerметодов интерфейса? Или еще где-нибудь?
Sakiboy
Никаких уродливых хаков с отражением. Легко настраивается. Очень хороший ответ.
Oren
18

Как вы можете видеть в источниках ViewPager, длительность переброски контролируется объектом mScroller. В документации можно прочитать:

Продолжительность прокрутки может быть передана в конструктор и указывает максимальное время, которое должна занимать анимация прокрутки.

Итак, если вы хотите контролировать скорость, вы можете изменить объект mScroller через отражение.

Вы должны написать что-то вроде этого:

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
источник
Я немного запутался в том, как это сделать - я настраиваю ViewPager в XML-файле, и я назначил его mPager и setAdapter (mAdapter) ... Следует ли мне создать класс, расширяющий ViewPager?
Alex Kelly
12
Вы должны написать что-то вроде этого: setContentView (R.layout.main); mPager = (ViewPager) findViewById (R.id.view_pager); Поле mScroller = ViewPager.class.getDeclaredField («mScroller»); mScroller.setAccessible (правда); mScroller.set (mPager, скроллер); // инициализировать объект скроллера самостоятельно
HighFlyer
Я только что отправил еще один ответ ... Он не работает ... У вас будет время, чтобы дать мне направление, почему бы и нет?
Alex Kelly
После более внимательного просмотра исходников: найдите mScroller.startScroll (sx, sy, dx, dy); в исходном коде ViewPager и взгляните на реализацию Scroller. startScroll вызывает другой startScroll с параметром DEFAULT_DURATION. Невозможно сделать другое значение для static final поля, поэтому осталась одна возможность - переопределить smoothScrollTo в ViewPager
HighFlyer
16

Это не идеальное решение, вы не можете уменьшить скорость, потому что это int. Но для меня это достаточно медленно, и мне не нужно использовать отражение.

Обратите внимание на пакет, в котором находится класс. smoothScrollToимеет видимость пакета.

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
источник
Обратите внимание, что первая строка: «package android.support.v4.view;» является обязательным, и вы должны создать такой пакет в корне src. Если вы этого не сделаете, ваш код не будет компилироваться.
Эльханан Мишраки 01
3
@ElhananMishraky, именно это я имел в виду Notice the package where the class is.
pawelzieba 02
3
Подожди, подожди, подожди ... ууууут? Вы можете использовать sdk's package-local fields/ methods, если вы просто заставляете Android Studio думать, что используете тот же пакет? Я единственный, кто нюхает sealing violation?
Bartek Lipinski
Это действительно хорошее решение, но оно не сработало и для меня, поскольку Interpolator, используемый ViewPager Scroller, в этом случае ведет себя не очень хорошо. Итак, я объединил ваше решение с другими, используя отражение, чтобы получить mScroller. Таким образом, я могу использовать исходный Scroller, когда пользователь разбивает страницы, и мой собственный Scroller, когда я меняю страницы программно с помощью setCurrentItem ().
rolgalan
8

Методы fakeDrag в ViewPager, похоже, предлагают альтернативное решение.

Например, это будет страница с пункта 0 до 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 * countПросто чтобы ускорить перетаскивание по мере его продвижения)

df778899
источник
4

я использовал

DecelerateInterpolator ()

Вот пример:

  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();
            }
Младен Раконжак
источник
2

На основе принятого решения я создал класс kotlin с расширением для просмотра пейджера. Наслаждайтесь! :)

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

}
Януш Хайн
источник
1

Вот ответ с использованием совершенно другого подхода. Кто-то может сказать, что в этом есть хакерское чувство; однако он не использует отражение, и я бы сказал, что он всегда будет работать.

Находим внутри следующий код 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);

Он рассчитывает продолжительность на основе нескольких вещей. Видите что-нибудь, что мы можем контролировать? mAdapter.getPageWidth. Реализуем ViewPager.OnPageChangeListener в нашем адаптере . Мы собираемся обнаружить, когда ViewPager прокручивается, и дать фиктивное значение ширине . Если я хочу, чтобы длительность быть k*100, то я вернусь 1.0/(k-1)к getPageWidth. Ниже приведен код Kotlin для этой части адаптера, который превращает длительность в 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) {
    }

Не забудьте добавить адаптер как OnPageChangedListener.

В моем конкретном случае можно с уверенностью предположить, что пользователь не может перемещаться между страницами свайпом. Если вам нужно поддерживать оседание после перетаскивания, вам нужно сделать немного больше в своих расчетах.

Одним из недостатков является то, что это зависит от жестко 100заданного значения базовой продолжительности в ViewPager. Если это изменится, то при таком подходе изменится и ваша продолжительность.

андроид
источник
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();

Я хотел решить ту же проблему, что и все вы, и это мое решение для той же проблемы, но я думаю, что таким образом решение будет более гибким, поскольку вы можете изменить продолжительность, как вам нравится, а также изменить интерполяцию значений по своему желанию. для достижения различных визуальных эффектов. Мое решение перемещается со страницы «1» на страницу «2», поэтому только в увеличивающихся позициях, но может быть легко изменено для уменьшения позиций, выполняя «animProgress - lastFakeDrag» вместо «lastFakeDrag - animProgress». Считаю, что это наиболее гибкое решение для выполнения этой задачи.

Вбивать в голову
источник