Повільна швидкість контролера Viewpager в android


104

Чи є спосіб уповільнити швидкість прокрутки за допомогою адаптера переглядача в 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();
} 

Це нічого нічого не змінило, але винятки не трапляються?



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 краще працює в цьому випадку, він підтримує швидкість початкового прогону, а потім повільно сповільнюється.
Квінт Стофферс

Привіт, у вас є ідеї, чому scroller.setFixedDuration(5000);дає помилку? У ньому написано "Метод setFixedDuration (int) не визначений"
jaibatrik

6
Якщо ви використовуєте proguard, не забудьте додати цей рядок: -keep class android.support.v4. ** {*;} в іншому випадку ви отримаєте нульовий покажчик, коли getDeclaredField ()
GuilhE

61

Я хотів зробити сам і досяг рішення (проте, використовуючи роздуми). Це схоже на прийняте рішення, але використовує той самий інтерполятор і змінює тривалість лише на основі коефіцієнта. Вам потрібно використовувати a 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 клас android.support.v4. ** {*;} в іншому випадку ви отримаєте нульовий покажчик, коли getDeclaredField ()
GuilhE

Я пропоную оновити до тернару, оскільки ви не хочете, щоб хтось міг переходити в коефіцієнт 0, це порушить вашу тривалість часу. Оновіть кінцевий параметр, який ви передаєте, до mScrollFactor> 0? (int) (тривалість * mScrollFactor): тривалість
портфоліо

Я також пропоную вам зробити нульову перевірку під час встановлення коефіцієнта прокрутки у вашому класі ViewPagerCustomDuration. Якщо ви використовуєте mScroller як нульовий об'єкт, вам слід підтвердити, що він все ще не є нульовим при спробі встановити на цьому об'єкті коефіцієнт прокрутки.
портфоліо

@portfoliobuilder дійсні коментарі, хоча, здається, ви віддаєте перевагу більш оборонному стилю програмування, який IMO більше підходить для непрозорих бібліотек, ніж фрагмент SO. Встановлення коефіцієнта прокрутки <= 0, а також виклик setScrollDurationFactor на неінтензований вигляд - це те, що більшості розробників Android повинно бути в змозі уникнути цього без перевірки часу виконання.
Олег Васкевич

43

Я знайшов краще рішення на основі відповіді @ df778899 та API ValueAnimator Android . Він чудово працює без роздумів і дуже гнучкий. Також немає необхідності робити власні програми 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методів інтерфейсу? Або десь ще?
Сакібой

Ніяких потворних відбитків не хакує. Легко налаштовується Дуже гарна відповідь.
Орен

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 

Я трохи розгублений, як це зробити - я встановлюю ViewPager у XML-файл і призначив його mPager, і setAdapter (mAdapter) ... Чи повинен я створити клас, який розширює ViewPager?
Алекс Келлі

12
Вам слід написати щось подібне: setContentView (R.layout.main); mPager = (ViewPager) findViewById (R.id.view_pager); Поле mScroller = ViewPager.class.getDeclaredField ("mScroller"); mScroller.setAccessible (вірно); mScroller.set (mPager, скроллер); // ініціалізуйте об’єкт прокрутки самостійно
HighFlyer

Я щойно опублікував ще одну відповідь ... Це не працює ... Ви хотіли б трохи часу дати мені вказівку, чому ні?
Алекс Келлі

Після ретельнішого огляду джерел: знайдіть mScroller.startScroll (sx, sy, dx, dy); у джерелі ViewPager та подивіться на реалізацію Scroller. startScroll викликає інший startScroll за допомогою параметра DEFAULT_DURATION. Неможливо встановити ще одне значення для статичного остаточного поля, тому залишилася одна можливість - переосмиснути гладкеScrollTo з 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);
    }
}

Зауважте, що перший рядок: "пакет android.support.v4.view;" є обов'язковим і вам слід створити такий пакунок у вашому корінні src. Якщо ви цього не зробите, код не збирається.
Ельханан Мішракі

3
@ElhananMishraky точно, саме з цим я мав на увазі Notice the package where the class is.
pawelzieba

3
Чекати, чекати, чекати, чекати ... вуаут? Ви можете використовувати sdk package-local fields/ methods, якщо ви просто змусили Android Studio вважати, що ви використовуєте той самий пакет? Я єдиний, хто пахне sealing violation?
Bartek Lipinski

Це дійсно приємне рішення, але воно не спрацювало і для мене, оскільки інтерполятор, який використовується ViewPager Scroller, не веде себе добре в цьому випадку. Тому я поєднав ваше рішення з іншими, використовуючи роздуми, щоб отримати mScroller. Таким чином, я можу використовувати оригінальний скроллер, коли користувач робить сторінки, і власний користувальницький скроллер, коли я програматично змінюю сторінки за допомогою setCurrentItem ().
rolgalan

8

Методи підробленої програми на 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Якраз там, щоб підвищити швидкість перетягування в міру його просування)


4

Я звик

DelerateInterpolator ()

Ось приклад:

  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. Далі йде Котлін impl цієї частини адаптера, який перетворює тривалість на 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". Я думаю, що це найбільш гнучке рішення для виконання цього завдання.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.