Memperlambat kecepatan pengontrol Viewpager di android

105

Apakah ada cara untuk memperlambat kecepatan gulir dengan adaptor viewpager di android?


Anda tahu, saya telah melihat kode ini. Aku tidak tahu apa yang salah dong.

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

Itu tidak mengubah apa pun namun tidak ada pengecualian?

Alex Kelly
sumber
coba antoniocappiello.com
Rushi Ayyappa
1
child.setNestedScrollingEnabled(false);bekerja untuk saya
Pierre

Jawaban:

230

Saya sudah mulai dengan kode HighFlyer yang memang mengubah bidang mScroller (yang merupakan awal yang baik) tetapi tidak membantu memperpanjang durasi gulir karena ViewPager secara eksplisit meneruskan durasi ke mScroller saat meminta untuk menggulir.

Memperluas ViewPager tidak berfungsi karena metode penting (smoothScrollTo) tidak dapat diganti.

Saya akhirnya memperbaikinya dengan memperluas Scroller dengan kode ini:

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

Dan menggunakannya seperti ini:

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

Saya pada dasarnya telah melakukan hardcode durasinya menjadi 5 detik dan membuat ViewPager saya menggunakannya.

Semoga ini membantu.

marmor
sumber
16
bisakah kamu memberitahuku apa itu sInterpolator ??
Shashank Degloorkar
10
'Interpolator' adalah kecepatan di mana animasi dijalankan, misalnya animasi dapat dipercepat, diperlambat, dan banyak lagi. Ada konstruktor tanpa interpolator, saya pikir itu seharusnya berfungsi, jadi coba hapus argumen itu dari panggilan. Jika itu tidak berhasil, tambahkan: 'Interpolator sInterpolator = new AccelerateInterpolator ()'
marmor
8
Saya menemukan DecelerateInterpolator berfungsi lebih baik dalam kasus ini, ia mempertahankan kecepatan gesekan awal dan kemudian perlahan melambat.
Quint Stoffers
Hai, apakah Anda tahu mengapa scroller.setFixedDuration(5000);memberikan kesalahan? Dikatakan "Metode setFixedDuration (int) tidak ditentukan"
jaibatrik
6
Jika Anda menggunakan proguard, jangan lupa untuk menambahkan baris ini: -keep class android.support.v4. ** {*;} jika tidak, Anda akan mendapatkan pointer null saat getDeclaredField ()
GuilhE
61

Saya ingin melakukannya sendiri dan telah mencapai solusi (menggunakan refleksi, bagaimanapun). Ini mirip dengan solusi yang diterima tetapi menggunakan interpolator yang sama dan hanya mengubah durasi berdasarkan suatu faktor. Anda perlu menggunakan a ViewPagerCustomDurationdi XML Anda, bukan ViewPager, lalu Anda bisa melakukan ini:

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

}

Semoga ini bisa membantu seseorang!

Oleg Vaskevich
sumber
3
Saya menyukai solusi ini lebih baik daripada jawaban yang diterima karena ViewPager kustom adalah kelas drop-in dengan sedikit perubahan pada kode yang ada. Memiliki penggulung khusus sebagai kelas dalam statis dari ViewPager khusus membuatnya semakin terenkapsulasi.
Emanuel Moecklin
1
Jika Anda menggunakan proguard, jangan lupa untuk menambahkan baris ini: -keep class android.support.v4. ** {*;} jika tidak, Anda akan mendapatkan pointer null saat getDeclaredField ()
GuilhE
Saya sarankan memperbarui ke terner, karena Anda tidak ingin mengizinkan siapa pun lewat dalam faktor 0, ini akan merusak durasi waktu Anda. Perbarui parameter terakhir yang Anda berikan ke mScrollFactor> 0? (int) (durasi * mScrollFactor): durasi
portfoliobuilder
Saya juga menyarankan agar Anda melakukan pemeriksaan null saat mengatur faktor gulir di kelas Anda, ViewPagerCustomDuration. Anda membuat instance dengan mScroller sebagai objek null, Anda harus mengonfirmasi bahwa itu masih bukan null saat mencoba menyetel faktor gulir pada objek itu.
portfoliobuilder
Komentar valid @portfoliobuilder, meskipun tampaknya Anda menyukai gaya pemrograman yang lebih defensif yang menurut IMO lebih cocok untuk pustaka buram daripada cuplikan di SO. Menyetel faktor gulir ke <= 0 serta memanggil setScrollDurationFactor pada tampilan non-instantiated adalah sesuatu yang seharusnya dapat dihindari oleh sebagian besar pengembang Android tanpa pemeriksaan waktu proses.
Oleg Vaskevich
43

Saya telah menemukan solusi yang lebih baik, berdasarkan jawaban @ df778899 dan Android ValueAnimator API . Ini bekerja dengan baik tanpa refleksi dan sangat fleksibel. Juga tidak perlu membuat ViewPager kustom dan memasukkannya ke dalam paket android.support.v4.view. Berikut ini contohnya:

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
sumber
7
Saya pikir ini diremehkan. Itu memanfaatkan startFakeDragdan endFakeDragyang dimaksudkan untuk melakukan lebih dari yang tersedia di ViewPagerluar kotak. Ini bekerja dengan baik untuk saya dan persis seperti yang saya cari
user3280133
1
Itu solusi yang bagus, terima kasih. Namun, ada satu peningkatan kecil: agar berfungsi untuk viewPager dengan padding, baris pertama harus ... ValueAnimator.ofInt (0, viewPager.getWidth () - (forward? ViewPager.getPaddingLeft (): viewPager.getPaddingRight () ));
DmitryO.
1
Sejauh ini, ini adalah solusi paling sederhana / efisien / bertanggung jawab karena mengabaikan refleksi dan menghindari pelanggaran penyegelan seperti solusi di atas
Ryan
2
@lobzic, Di mana tepatnya Anda menyebut animatePagerTransition(final boolean forwardmetode Anda ? Itulah yang membuat saya bingung. Apakah Anda menyebutnya dalam salah satu ViewPager.OnPageChangeListenermetode antarmuka? Atau di tempat lain?
Sakiboy
Tidak ada peretasan refleksi yang jelek. Dapat dikonfigurasi dengan mudah. Jawaban yang sangat bagus.
Oren
18

Seperti yang Anda lihat di sumber ViewPager, durasi lemparan dikontrol oleh objek mScroller. Dalam dokumentasi kita dapat membaca:

Durasi gulir dapat diteruskan dalam konstruktor dan menentukan waktu maksimum yang harus diambil animasi gulir

Jadi, jika Anda ingin mengontrol kecepatan, Anda dapat mengubah objek mScroller melalui refleksi.

Anda harus menulis sesuatu seperti ini:

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 
Seorang yg penuh ambisi
sumber
Saya agak bingung bagaimana melakukan ini - Saya menyiapkan ViewPager dalam file XML dan saya menetapkannya ke mPager, dan setAdapter (mAdapter) ... Haruskah saya membuat kelas yang memperluas ViewPager?
Alex Kelly
12
Anda harus menulis sesuatu seperti ini: setContentView (R.layout.main); mPager = (ViewPager) findViewById (R.id.view_pager); Bidang mScroller = ViewPager.class.getDeclaredField ("mScroller"); mScroller.setAccessible (true); mScroller.set (mPager, scroller); // inisialisasi objek penggulung sendiri
HighFlyer
Saya baru saja memposting satu jawaban lagi ... Ini tidak berhasil ... Apakah Anda punya waktu untuk memberi saya arahan mengapa tidak?
Alex Kelly
Setelah lebih cermat melihat sumber: temukan mScroller.startScroll (sx, sy, dx, dy); di sumber ViewPager dan lihat realisasi Scroller. startScroll memanggil startScroll lain dengan parameter DEFAULT_DURATION. Tidak mungkin membuat nilai lain untuk bidang final statis, jadi satu peluang tersisa - timpa smoothScrollTo dari ViewPager
HighFlyer
16

Ini bukan solusi sempurna, Anda tidak bisa membuat kecepatan lebih lambat karena ini adalah int. Tapi bagi saya itu cukup lambat dan saya tidak perlu menggunakan refleksi.

Perhatikan paket tempat kelas tersebut berada. smoothScrollTomemiliki visibilitas paket.

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.dll
sumber
Perhatikan bahwa baris pertama: "package android.support.v4.view;" bersifat wajib dan Anda harus membuat paket seperti itu di root src Anda. Jika Anda tidak mau melakukannya, kode Anda tidak akan bisa dikompilasi.
Elhanan Mishraky
3
@ElhananMishraky tepatnya, itulah yang saya maksud Notice the package where the class is.
pawelzieba
3
Tunggu, tunggu, tunggu, tunggu ... wuuuut? Anda dapat menggunakan sdk's package-local fields/ methods, jika Anda membuat Android Studio mengira Anda menggunakan paket yang sama? Apakah saya satu-satunya yang mencium beberapa sealing violation?
Bartek Lipinski
Ini adalah solusi yang sangat bagus, tetapi tidak berhasil untuk saya karena Interpolator yang digunakan oleh ViewPager Scroller tidak berfungsi dengan baik dalam kasus ini. Jadi saya menggabungkan solusi Anda dengan yang lain menggunakan refleksi untuk mendapatkan mScroller. Dengan cara ini, saya dapat menggunakan Scroller asli saat pengguna melakukan paginasi, dan Scroller kustom saya saat mengubah halaman secara terprogram dengan setCurrentItem ().
rolgalan
8

Metode fakeDrag di ViewPager tampaknya memberikan solusi alternatif.

Misalnya ini akan halaman dari item 0 ke 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 * countHanya ada di sana untuk mempercepat seret saat berjalan)

df778899
sumber
4

Saya telah menggunakan

DecelerateInterpolator ()

Berikut contohnya:

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

Berdasarkan solusi yang diterima saya telah membuat kelas kotlin dengan ekstensi untuk tampilan halaman. Nikmati! :)

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

Inilah jawaban menggunakan pendekatan yang sama sekali berbeda. Seseorang mungkin mengatakan ini memiliki perasaan hacky; namun, itu tidak menggunakan refleksi, dan saya berpendapat bahwa itu akan selalu berhasil.

Kami menemukan kode berikut di dalam 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);

Ini menghitung durasi berdasarkan beberapa hal. Lihat apa saja yang bisa kita kendalikan? mAdapter.getPageWidth. Mari mengimplementasikan ViewPager.OnPageChangeListener di adaptor kita . Kami akan mendeteksi saat ViewPager menggulir, dan memberikan nilai palsu untuk lebar . Jika saya ingin durasi untuk menjadi k*100, maka saya akan kembali 1.0/(k-1)untuk getPageWidth. Berikut adalah implikasi Kotlin dari bagian adaptor ini yang mengubah durasinya menjadi 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) {
    }

Jangan lupa untuk menambahkan adaptor sebagai OnPageChangedListener.

Kasus khusus saya dapat dengan aman mengasumsikan bahwa pengguna tidak dapat menggeser untuk menyeret antar halaman. Jika Anda harus mendukung penyelesaian setelah tarik, maka Anda perlu melakukan sedikit lebih banyak dalam perhitungan Anda.

Satu kelemahannya adalah hal ini bergantung pada 100nilai durasi dasar hardcode di ViewPager. Jika itu berubah, maka durasi Anda berubah dengan pendekatan ini.

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

Saya ingin menyelesaikan masalah yang sama seperti Anda semua dan ini adalah solusi saya untuk masalah yang sama, tetapi saya pikir dengan cara ini solusinya lebih fleksibel karena Anda dapat mengubah durasi sesuka Anda dan juga mengubah interpolasi nilai sesuai keinginan untuk mencapai efek visual yang berbeda. Solusi saya menggesek dari halaman "1" ke halaman "2", jadi hanya dalam posisi yang meningkat, tetapi dapat dengan mudah diubah untuk masuk ke posisi yang menurun dengan melakukan "animProgress - lastFakeDrag" daripada "lastFakeDrag - animProgress". Saya rasa ini adalah solusi paling fleksibel untuk melakukan tugas ini.

Rubin
sumber