Android에서 Viewpager 컨트롤러의 속도 저하


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);나를 위해 일했습니다
Pierre

답변:


230

나는 실제로 mScroller 필드를 변경 한 HighFlyer의 코드로 시작했지만 (훌륭한 시작) 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의 정적 내부 클래스로 사용하면 더욱 캡슐화됩니다.
Emanuel Moecklin 2014

1
proguard를 사용하는 경우 다음 줄을 추가하는 것을 잊지 마십시오 : -keep class android.support.v4. ** {*;} 그렇지 않으면 getDeclaredField ()
GuilhE

아무도 0 요소를 통과하는 것을 원하지 않기 때문에 삼항으로 업데이트하는 것이 좋습니다. 이렇게하면 시간이 중단됩니다. mScrollFactor> 0에 전달하는 최종 매개 변수를 업데이트 하시겠습니까? (int) (duration * mScrollFactor) : duration
Portfoliobuilder

또한 ViewPagerCustomDuration 클래스에서 스크롤 요소를 설정할 때 null 검사를 수행하는 것이 좋습니다. mScroller를 null 개체로 인스턴스화하면 해당 개체에 스크롤 요소를 설정하려고 할 때 여전히 null이 아닌지 확인해야합니다.
Portfoliobuilder

@portfoliobuilder 유효한 주석, SO에 대한 스 니펫보다 불투명 한 라이브러리에 더 적합한 IMO 인 방어적인 프로그래밍 스타일을 선호하는 것 같습니다. 스크롤 팩터를 <= 0으로 설정하고 인스턴스화되지 않은 뷰에서 setScrollDurationFactor를 호출하는 것은 대부분의 Android 개발자가 런타임 검사없이 수행하는 것을 피할 수 있어야합니다.
Oleg Vaskevich

43

@ df778899의 답변Android ValueAnimator API를 기반으로 더 나은 솔루션을 찾았습니다 . 반사없이 잘 작동하며 매우 유연합니다. 또한 사용자 정의 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 ()-(forward? viewPager.getPaddingLeft () : viewPager.getPaddingRight () ));
DmitryO.

1
이것은 반사를 무시하고 위의 솔루션과 같은 씰링 위반을 방지하기 때문에 가장 간단하고 효율적이며 신뢰할 수있는 솔루션이었습니다
Ryan

2
@lobzic, 정확히 어디에서 animatePagerTransition(final boolean forward메서드 를 호출 합니까? 그것이 내가 혼란스러워하는 것입니다. ViewPager.OnPageChangeListener인터페이스 메소드 중 하나에서 호출 합니까? 아니면 다른 곳?
Sakiboy

추악한 반사 해킹이 없습니다. 쉽게 구성 할 수 있습니다. 아주 좋은 대답입니다.
Oren

18

당신이 할 수있는대로 ViewPager 소스에서 있듯이 mScroller 개체에 의해 제어되는 플링 기간입니다. 에서 documantation 우리는 읽을 수 있습니다 :

스크롤 기간은 생성자에 전달 될 수 있으며 스크롤 애니메이션에 걸리는 최대 시간을 지정합니다.

따라서 속도를 제어하려면 반사를 통해 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 

이 작업을 수행하는 방법에 대해 약간 혼란 스럽습니다. XML 파일에 ViewPager를 설정하고 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 (true); mScroller.set (mPager, scroller); // 스크롤러 객체를 직접 초기화
HighFlyer 2011

방금 답변을 하나 더 게시했습니다 ... 작동하지 않습니다 ... 왜 안되는지에 대한 지침을 제공 할 시간이 있습니까?
Alex Kelly

소스를 좀 더 자세히 살펴본 후 : find mScroller.startScroll (sx, sy, dx, dy); ViewPager 소스에서 Scroller 실현을 살펴보십시오. startScroll은 DEFAULT_DURATION 매개 변수를 사용하여 다른 startScroll을 호출합니다. 정적 최종 필드에 대해 다른 값을 만드는 것은 불가능하므로 한 가지 기회가 남았습니다. ViewPager의 smoothScrollTo 재정의
HighFlyer 2011

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

첫 번째 줄 : "package android.support.v4.view;" 필수이며 src 루트에 이러한 패키지를 만들어야합니다. 그렇게하지 않으면 코드가 컴파일되지 않습니다.
Elhanan Mishraky

3
@ElhananMishraky 정확히, 그것이 내가 의미하는 바입니다 Notice the package where the class is.
pawelzieba 2014 년

3
잠깐, 잠깐, 잠깐만 ... Android Studio에서 동일한 패키지를 사용하고 있다고 생각하면 sdk의 package-local fields/를 사용할 수 methods있습니까? 나만 냄새를 맡는 sealing violation건가요?
Bartek Lipinski 2015

이것은 정말 좋은 솔루션이지만 ViewPager Scroller에서 사용하는 Interpolator가이 경우에 잘 작동하지 않기 때문에 나에게도 효과가 없었습니다. 그래서 mScroller를 얻기 위해 리플렉션을 사용하여 다른 솔루션과 솔루션을 결합했습니다. 이런 식으로 사용자가 페이지를 매길 때 원래 Scroller를 사용하고 setCurrentItem ()을 사용하여 프로그래밍 방식으로 페이지를 변경할 때 사용자 지정 Scroller를 사용할 수 있습니다.
rolgalan

8

ViewPager의 fakeDrag 메서드는 대체 솔루션을 제공하는 것 같습니다.

예를 들어 이것은 항목 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

나는 사용했다

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가 스크롤되는시기를 감지 하고 width에 가짜 값을 제공합니다 . 나는 시간이 원하는 경우 k*100, 그때 돌아갑니다 1.0/(k-1)위해 getPageWidth. 다음은 기간을 400으로 바꾸는 어댑터의이 부분의 Kotlin impl입니다.

    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로 추가하는 것을 잊지 마십시오.

내 특별한 경우는 사용자가 페이지 사이를 스 와이프하여 드래그 할 수 없다고 안전하게 가정 할 수 있습니다. 드래그 후 정착을 지원해야하는 경우 계산에서 약간 더 많은 작업을 수행해야합니다.

한 가지 단점은 이것이 100ViewPager의 하드 코딩 된 기본 기간 값에 따라 달라진다는 것입니다 . 변경되면이 접근 방식으로 기간이 변경됩니다.


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"로 스 와이프하므로 증가하는 위치에서만 가능하지만 "lastFakeDrag-animProgress"대신 "animProgress-lastFakeDrag"를 수행하여 감소 위치로 이동하도록 쉽게 변경할 수 있습니다. 이 작업을 수행하는 데 가장 유연한 솔루션이라고 생각합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.