Synchronizing two ViewPagers using OnPageChangeListener

后端 未结 6 2009
北海茫月
北海茫月 2020-12-08 22:57

I\'m trying to synchronize two ViewPagers, as apparently have quite a lot of people before me, and I\'ve got as far as this:

private ViewPager m         


        
相关标签:
6条回答
  • 2020-12-08 23:31

    I have Solved the problem without utilizing the Listener. So you can use the listener for some other stuff and will make code look cleaner.

    I know its a question asked a long ago. I was searching for a solution and solved it myself.

    This is how my Custom ViewPager code is

    public class CustomPager extends ViewPager {
    CustomPager mCustomPager;
    private boolean forSuper;
    
    public CustomPager(Context context) {
        super(context);
    }
    
    public CustomPager(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    
    @Override
    public boolean onInterceptTouchEvent(MotionEvent arg0) {
        if (!forSuper) {
    
            mCustomPager.forSuper(true);
            mCustomPager.onInterceptTouchEvent(arg0);
            mCustomPager.forSuper(false);
        }
        return super.onInterceptTouchEvent(arg0);
    }
    
    @Override
    public boolean onTouchEvent(MotionEvent arg0) {
        if (!forSuper) {
            mCustomPager.forSuper(true);
            mCustomPager.onTouchEvent(arg0);
            mCustomPager.forSuper(false);
        }
        return super.onTouchEvent(arg0);
    }
    
    public void setViewPager(CustomPager customPager) {
        mCustomPager = customPager;
    }
    
    public void forSuper(boolean forSuper) {
        this.forSuper = forSuper;
    }
    
    @Override
    public void setCurrentItem(int item, boolean smoothScroll) {
        if (!forSuper) {
            mCustomPager.forSuper(true);
            mCustomPager.setCurrentItem(item, smoothScroll);
            mCustomPager.forSuper(false);
        }
        super.setCurrentItem(item, smoothScroll);
    }
    
    @Override
    public void setCurrentItem(int item) {
        if (!forSuper) {
            mCustomPager.forSuper(true);
            mCustomPager.setCurrentItem(item);
            mCustomPager.forSuper(false);
        }
        super.setCurrentItem(item);
    
    }
    
    }
    

    And you have to set pagers like this before adapter is set and just after getting reference using the ID.

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        backPager=(CustomPager) findViewById(R.id.BackPager);
        frontPager = (CustomPager) findViewById(R.id.frontPager);
        frontPager.setViewPager(backPager);
        backPager.setViewPager(frontPager);
        backPager.setAdapter(your Adapter);
        frontPager.setAdapter(your Adapter);
    }
    

    Must set ViewPagers before assigning adapter.

    0 讨论(0)
  • 2020-12-08 23:33

    All the Answers are approximately currect in some situation. Here I am giving one more answer which will use to slide both the ViewPagers simultaneously whether there size is same or not:

    viewPagerBanner.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
        private int scrollState = ViewPager.SCROLL_STATE_IDLE;
        // Indicates that the pager is in an idle, settled state. 
        // The current page is fully in view and no animation is in progress.
    
        @Override
        public void onPageScrolled(int position, float positionOffset, 
                                   int positionOffsetPixels) {
            if (scrollState == ViewPager.SCROLL_STATE_IDLE) {
                return;
            }
            viewPagerTitle.scrollTo(viewPagerBanner.getScrollX()*
                                    viewPagerTitle.getWidth()/
                                    viewPagerBanner.getWidth(), 0);
            // We are not interested in Y axis position
        }
    
        @Override
        public void onPageSelected(int position) {}
    
        @Override
        public void onPageScrollStateChanged(int state) {
            scrollState = state;
            if (state == ViewPager.SCROLL_STATE_IDLE) {
                viewPagerTitle.setCurrentItem(viewPagerBanner.getCurrentItem(), false);
            }
        }
    });
    
    0 讨论(0)
  • 2020-12-08 23:35

    This does everything right except it sometimes misses very quick flicks on the slaved view. For some reason including fake drag events during the settling phase causes real problems, though.

    private ViewPager mNavPager;
    
    private ViewPager mMainPager;
    
    private final OnPageChangeListener mNavPagerListener = new OnPageChangeListener() {
    
        private int mLastScrollPosition;
        private int mLastPagePosition;
    
        @Override
        public void onPageSelected(int position) {
            mLastPagePosition = position;
        }
    
        @Override
        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
            if(mMainPager.isFakeDragging()) {
                int absoluteOffsetPixels = positionOffsetPixels;
                if(mLastPagePosition!=position) {
                    absoluteOffsetPixels += (position - mLastPagePosition) * mMainPager.getWidth();
                    mLastPagePosition = position;
                }
                Log.d(TAG, "fake nav drag by " + (mLastScrollPosition - absoluteOffsetPixels));
                mMainPager.fakeDragBy(mLastScrollPosition - absoluteOffsetPixels);
                mLastScrollPosition = positionOffsetPixels;
            }
        }
    
        @Override
        public void onPageScrollStateChanged(int state) {
            if(!mNavPager.isFakeDragging()) {
                switch(state) {
                case ViewPager.SCROLL_STATE_DRAGGING:
                    if(!mMainPager.isFakeDragging())
                        mMainPager.beginFakeDrag();
                    break;
                case ViewPager.SCROLL_STATE_SETTLING:
                case ViewPager.SCROLL_STATE_IDLE:
                    if(mMainPager.isFakeDragging()) {
                        mMainPager.endFakeDrag();
                        mLastScrollPosition = 0;
                    }
                    break;
                }
            }
        }
    };
    
    private OnPageChangeListener mMainPagerListener = new OnPageChangeListener() {
    
        private int mLastScrollPosition;
        private int mLastPagePosition;
    
        @Override
        public void onPageSelected(int position) {
            mLastPagePosition = position;
        }
    
        @Override
        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
            if(mNavPager.isFakeDragging()) {
                int absoluteOffsetPixels = positionOffsetPixels;
                if(mLastPagePosition!=position) {
                    absoluteOffsetPixels += (position - mLastPagePosition) * mMainPager.getWidth();
                    mLastPagePosition = position;
                }
                Log.d(TAG, "fake nav drag by " + (mLastScrollPosition - absoluteOffsetPixels));
                mNavPager.fakeDragBy(mLastScrollPosition - absoluteOffsetPixels);
                mLastScrollPosition = positionOffsetPixels;
            }
        }
    
        @Override
        public void onPageScrollStateChanged(int state) {
            if(!mMainPager.isFakeDragging()) {
                switch(state) {
                case ViewPager.SCROLL_STATE_DRAGGING:
                    if(!mNavPager.isFakeDragging())
                        mNavPager.beginFakeDrag();
                    break;
                case ViewPager.SCROLL_STATE_SETTLING:
                case ViewPager.SCROLL_STATE_IDLE:
                    if(mNavPager.isFakeDragging()) {
                        mNavPager.endFakeDrag();
                        mLastScrollPosition = 0;
                    }
                    break;
                }
            }
        }
    };
    

    EDIT

    I now believe this to be impossible without some fairly substantial custom code. The reason is essentially that both ViewPagers have a VelocityTracker inside, which controls scrolling. Since the MotionEvents being passed in may not be passed to the VelocityTracker at the same relative times for each pager, the trackers will occasionally reach different conclusions about how to react.

    However, it is possible to use a modified PagerTitleStrip to get precise tracking of a ViewPager, and to transfer touch events captured by the strip directly to the ViewPager.

    The source for PagerTitleStrip is here.

    Broadly, what needs to be done to make this work is as follows: replace mPrevText, mCurrText and mNextText with views of the type you want to use; remove the onAttachedToWindow() and onDetachedFromWindow() functions; remove calls to the PagerAdapter that deal with dataset observers, and add an OnTouchListener to the modified strip that fake drags the main pager. You'll also need to add the modified title strip as an OnPageChangeListener to the ViewPager since the internal listeners aren't visible outside the package.

    Is this a gigantic pain? Yes. But it works. I will write it up in more detail soon.

    0 讨论(0)
  • 2020-12-08 23:37

    I solved this problem in a much easier (and cleaner) way using the OnPageChangeListener:

    mViewPager1.setOnPageChangeListener(new ViewPager.OnPageChangeListener() {
    
          private int mScrollState = ViewPager.SCROLL_STATE_IDLE;
    
          @Override
          public void onPageScrolled(final int position, final float positionOffset, final int positionOffsetPixels) {
            if (mScrollState == ViewPager.SCROLL_STATE_IDLE) {
              return;
            }
            mViewPager2.scrollTo(mViewPager1.getScrollX(), mViewPager2.getScrollY());
          }
    
          @Override
          public void onPageSelected(final int position) {
    
          }
    
          @Override
          public void onPageScrollStateChanged(final int state) {
            mScrollState = state;
            if (state == ViewPager.SCROLL_STATE_IDLE) {
              mViewPager2.setCurrentItem(mViewPager1.getCurrentItem(), false);
            }
          }
    });
    
    mViewPager2.setOnPageChangeListener(new ViewPager.OnPageChangeListener() {
    
          private int mScrollState = ViewPager.SCROLL_STATE_IDLE;
    
          @Override
          public void onPageScrolled(final int position, final float positionOffset, final int positionOffsetPixels) {
            if (mScrollState == ViewPager.SCROLL_STATE_IDLE) {
              return;
            }
            mViewPager1.scrollTo(mViewPager2.getScrollX(), mViewPager1.getScrollY());
          }
    
          @Override
          public void onPageSelected(final int position) {
    
          }
    
          @Override
          public void onPageScrollStateChanged(final int state) {
            mScrollState = state;
            if (state == ViewPager.SCROLL_STATE_IDLE) {
              mViewPager1.setCurrentItem(mViewPager2.getCurrentItem(), false);
            }
          }
    });
    
    0 讨论(0)
  • 2020-12-08 23:44

    With help from all other answers I've created a Class so it easily can be implemented.

        public class OnPageChangeListernerSync implements ViewPager.OnPageChangeListener {
    
        private ViewPager master;
        private ViewPager slave;
        private int mScrollState = ViewPager.SCROLL_STATE_IDLE;
    
        public OnPageChangeListernerSync(ViewPager master, ViewPager slave){
            this.master = master;
            this.slave = slave;
        }
    
        @Override
        public void onPageScrolled(final int position, final float positionOffset, final int positionOffsetPixels) {
            if (mScrollState == ViewPager.SCROLL_STATE_IDLE) {
                return;
            }
            this.slave.scrollTo(this.master.getScrollX()*
                    this.slave.getWidth()/
                    this.master.getWidth(), 0);
        }
    
        @Override
        public void onPageSelected(final int position) {
    
        }
    
        @Override
        public void onPageScrollStateChanged(final int state) {
            mScrollState = state;
            if (state == ViewPager.SCROLL_STATE_IDLE) {
                this.slave.setCurrentItem(this.master
                        .getCurrentItem(), false);
            }
        }
    }
    

    ...

    // In your activity
    this.upperPager.addOnPageChangeListener(new OnPageChangeListernerSync(this.upperPager, this.lowerPager));
    
    this.lowerPager.addOnPageChangeListener(new OnPageChangeListernerSync(this.lowerPager, this.upperPager));
    
    0 讨论(0)
  • 2020-12-08 23:47

    I had a similar issue, also needed to combine my syncing with animations in PageTransformer.

    Originally posted this answer here: https://stackoverflow.com/a/43638796/2867886

    But I will paste it here for convenience.

    The solution that worked best for me was to pass MotionEvent in OnTouchListener between ViewPager instances. Tried fake dragging but it was always laggy and buggy - I needed a smooth, parallax-like effect.

    So, my advice is to implement a View.OnTouchListener. The MotionEvent has to be scaled to compensate for the difference in width.

    public class SyncScrollOnTouchListener implements View.OnTouchListener {
    
    private final View syncedView;
    
    public SyncScrollOnTouchListener(@NonNull View syncedView) {
        this.syncedView = syncedView;
    }
    
    @Override
    public boolean onTouch(View view, MotionEvent motionEvent) {
        MotionEvent syncEvent = MotionEvent.obtain(motionEvent);
        float width1 = view.getWidth();
        float width2 = syncedView.getWidth();
    
        //sync motion of two view pagers by simulating a touch event
        //offset by its X position, and scaled by width ratio
        syncEvent.setLocation(syncedView.getX() + motionEvent.getX() * width2 / width1,
                motionEvent.getY());
        syncedView.onTouchEvent(syncEvent);
        return false;
    }
    }
    

    Then set it to your ViewPager

        sourcePager.setOnTouchListener(new SyncScrollOnTouchListener(targetPager));
    

    Note that this solution will only work if both pagers have the same orientation. If you need it to work for different orientations - adjust syncEvent Y coordinate instead of X.

    There is one more issue that we need to take into account - minimum fling speed and distance that can cause just one pager to change page.

    It can be easily fixed by adding an OnPageChangeListener to our pager

    sourcePager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
            @Override
            public void onPageScrolled(int position, float positionOffset,
                                       int positionOffsetPixels) {
                //no-op
            }
    
            @Override
            public void onPageSelected(int position) {
                targetPager.setCurrentItem(position, true);
            }
    
            @Override
            public void onPageScrollStateChanged(int state) {
                //no-op
            }
        }); 
    
    0 讨论(0)
提交回复
热议问题