I have a ViewPager with some views. I'd like to go to the first one after right swiping on the last one.
I tried
#Override
public Fragment getItem(int arg0) {
int i = arg0 % fragmentList.size();
return fragmentList.get(i);
}
#Override
public int getCount() {
return fragmentList.size()+1;
}
But I got an error
E/AndroidRuntime(22912): java.lang.IllegalStateException: Fragment already added: RubricFragment{4136cd80 #1 id=0x7f06000c android:switcher:2131099660:0}
One possibility is setting up the screens like this:
C' A B C A'
C' looks just like C, but when you scroll to there, it switches you to the real C.
A' looks just like A, but when you scroll to there, it switches you to the real A.
I would do this by implementing onPageScrollStateChanged like so:
#Override
public void onPageScrollStateChanged (int state) {
if (state == ViewPager.SCROLL_STATE_IDLE) {
int curr = viewPager.getCurrentItem();
int lastReal = viewPager.getAdapter().getCount() - 2;
if (curr == 0) {
viewPager.setCurrentItem(lastReal, false);
} else if (curr > lastReal) {
viewPager.setCurrentItem(1, false);
}
}
}
Note that this calls the alternate form of setCurrentItem and passes false to cause the jump to happen instantly rather than as a smooth scroll.
There are two main drawbacks I see to this. Firstly, upon reaching either end the user has to let the scrolling settle before they can go further. Secondly, it means having a second copy of all of the views in your first and last page. Depending on how resource-heavy your screens are, that may rule out this technique as a possible solution.
Note also that since the view pager doesn't let clicks go through to underlying controls until after the scrolling has settled, it's probably fine to not set up clicklisteners and the like for the A' and C' fragments.
Edit: Having now implemented this myself, there's another pretty major drawback. When it switches from A' to A or C' to C, the screen flickers for a moment, at least on my current test device.
I would create a dummy page at the end of the ViewPager.
Then I use this code to go to the first page when the user scroll to the dummy page. I know it's far from perfect :D
#Override
public void onPageScrolled(int position, float arg1, int arg2) {
if (position >= NUM_PAGE-1) {
mViewPager.setCurrentItem(0, true);
}
}
My solution is based on benkc, but first and last page scroll animation are disabled, and when pages "scrolled" to real page, scroll animation is enable again, this scheme can solve the first drawback.
but my ViewPager.setCurrentItem(position, false) result is still have scroll animation, so i implements animation which is too fast to seen.
the fast scrolling animation like this, don't mind the comment, just my code didn't use these method:
public class FixedSpeedScroller extends Scroller {
private int mDuration = 0;
public FixedSpeedScroller(Context context) {
super(context);
}
#Override
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
super.startScroll(startX, startY, dx, dy, mDuration);
}
#Override
public void startScroll(int startX, int startY, int dx, int dy) {
super.startScroll(startX, startY, dx, dy, mDuration);
}
}
and use this method to viewpager's activity
private Scroller scroller;
private void setViewPagerScroll(boolean instant) {
try {
Field mScroller = null;
mScroller = ViewPager.class.getDeclaredField("mScroller");
mScroller.setAccessible(true);
if (scroller == null) {
scroller = (Scroller) mScroller.get(mViewPager);
}
FixedSpeedScroller fss = new FixedSpeedScroller(mViewPager.getContext());
mScroller.set(mViewPager, instant ? fss : scroller);
} catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException e) {
e.printStackTrace();
}
}
and modify onPageScrollStateChanged like this, only first page or last page (i have 5 pages) would change animation to fast scrolling, otherwise has normal scrolling:
public void onPageScrollStateChanged(int state) {
if (state == ViewPager.SCROLL_STATE_IDLE) {
if (position == 0) {
setViewPagerScroll(true);
mViewPager.setCurrentItem(3);
} else if (position == 4) {
setViewPagerScroll(true);
mViewPager.setCurrentItem(1);
} else {
setViewPagerScroll(false);
}
}
}
FixedSpeedScroller references is here: http://blog.csdn.net/ekeuy/article/details/12841409
Kotlin Version:
Initailize the variables
private var mCurrentPosition = 0
private var mScrollState = 0
private lateinit var mImageViewPager: ViewPager
onCreate:
mImageViewPager = findViewById<View>(R.id.pager) as ViewPager
mImageViewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
override fun onPageScrollStateChanged(state: Int) {
handleScrollState(state)
mScrollState = state
}
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
}
override fun onPageSelected(position: Int) {
mCurrentPosition = position
}
})
functions outside onCreate:
private fun handleScrollState(state: Int) {
if (state == ViewPager.SCROLL_STATE_IDLE) {
if (!isScrollStateSettling()){
val lastPosition: Int = mImageViewPager.getAdapter()?.getCount()!! - 1
if (mCurrentPosition == 0) {
mImageViewPager.setCurrentItem(lastPosition , false)
} else if (mCurrentPosition == lastPosition) {
mImageViewPager.setCurrentItem(0 , false)
}
}
}
}
private fun isScrollStateSettling(): Boolean {
return mScrollState == ViewPager.SCROLL_STATE_SETTLING
}
this should do the job without dummy pages:
private boolean isFirstOrLastPage;
private int currentPageIndex = 0;
#Override
public void onPageScrolled(int arg0, float arg1, int arg2) {
if(currentPageIndex!=arg0){
isFirstOrLastPage = false;
return;
}
if((arg0==0 || arg0==PAGES.size()-1) && arg1 == 0 && arg2 == 0){
if(isFirstOrLastPage){
//DO SOMETHING
}else{
isFirstOrLastPage = true;
}
}
}
#Override
public void onPageSelected(int arg0) {
currentPageIndex = arg0;
}
this works, the accepted answer no good because there is a lag when the loop happens:
#Override
public int getCount() {
return Integer.MAX_VALUE;
}
#Override
public CharSequence getPageTitle(int position) {
String title = mTitleList.get(position % mActualTitleListSize);
return title;
}
#Override
public Object instantiateItem(ViewGroup container, int position) {
int virtualPosition = position % mActualTitleListSize;
return super.instantiateItem(container, virtualPosition);
}
#Override
public void destroyItem(ViewGroup container, int position, Object object) {
int virtualPosition = position % mActualTitleListSize;
super.destroyItem(container, virtualPosition, object);
}
answer taken from here : ViewPager as a circular queue / wrapping
Related
I have a viewpager with left and right arrows. I want to hide left arrow when the first position of a ViewPager and hide right arrow when the last position of a ViewPager visible.
I have referred this solution: How to get position of first and last position of viewpager?
In my view pager I have 3 visible pages at a time. For this I have used getPageWidth to 0.33f :
But it did not work as expected for me.
My code :
vpPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
#Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
#Override
public void onPageSelected(int position) {
if (position == (vpPager.getAdapter().getCount())-1) {
right.setVisibility(View.INVISIBLE);
} else {
right.setVisibility(View.VISIBLE);
}
if (position == 0) {
left.setVisibility(View.INVISIBLE);
} else {
left.setVisibility(View.VISIBLE);
}
}
#Override
public void onPageScrollStateChanged(int state) {
}
});
In XML I have set visibility gone for both arrows.
Any help will be appreciatable. Thank you!
To find out current page of ViewPager use ViewPager.getCurrentItem()
Here is a code sample
mViewPager.addOnPageChangeListener(new CustomViewPager.OnPageChangeListener() {
#Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
#Override
public void onPageSelected(int position) {
int currentItem = vpPager.getCurrentItem();
if (currentItem == (vpPager.getAdapter().getCount()-1)) {
right.setVisibility(View.INVISIBLE);
} else {
right.setVisibility(View.VISIBLE);
}
if (currentItem == 0) {
left.setVisibility(View.INVISIBLE);
} else {
left.setVisibility(View.VISIBLE);
}
}
#Override
public void onPageScrollStateChanged(int state) {
}
});
In order to make it work, I would use onPageScrolled instead of onPageSelected since the later one is not called right after the ViewPager is initialised and you can keep the buttons visible in the xml file. It would look like this:
mViewPager.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
#Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
super.onPageScrolled(position, positionOffset, positionOffsetPixels);
if (position == 0) {
left.setVisibility(View.INVISIBLE);
} else {
left.setVisibility(View.VISIBLE);
}
if (position == adapter.getCount() - 1) {
right.setVisibility(View.INVISIBLE);
} else {
right.setVisibility(View.VISIBLE);
}
}
});
The next and previous buttons are set to work like this:
left.setOnClickListener((v) -> mViewPager.setCurrentItem(mViewPager.getCurrentItem() - 1, true));
right.setOnClickListener((v) -> mViewPager.setCurrentItem(mViewPager.getCurrentItem() + 1, true));
Also, using onPageScrolled you can create a nice fade-in/fade-out animation for the buttons to stop them from appearing suddenly on the screen.
The following solution is working fine with me.
In order to know last position:
For your case, you cannot get last position '6' from onPageSelected(int position). If you got position totalNumberofPages - numberOfVisiblePagesAtATime, last page is already visible on screen.
For eg , 7-3 = 4 (If you got position 4, it means last page position 6 is already on screen.)
In order to know first position:
you cannot check in onPageChangeListener(). Because this will not call in first time launch.
You need to INVISIBLE your left arrow after viewPager setup. Then in onPageChangeListener call, VISIBLE again.
private int myImagePosition = -1; // class level variable with default value -1.
set visibility of imageView when user click on next or previous
buttons.
myImageViewViewPagerPrevious.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
if (myImagePosition > 0) {
myImagePosition = myImagePosition - 3;
setNextPreviousArrowVisibility();
}
myViewPagerPlanDetailBroucher.setCurrentItem(myImagePosition);
}
});
myImageViewViewPagerNext.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
if (myImagePosition < myBroucherDataArrayList.size() - 1) {
myImagePosition = myImagePosition + 3;
setNextPreviousArrowVisibility();
}
myViewPagerPlanDetailBroucher.setCurrentItem(myImagePosition);
}
});
private void setNextPreviousArrowVisibility() {
if (myImagePosition > 3) {
myImageViewViewPagerPrevious.setVisibility(View.VISIBLE);
} else {
myImageViewViewPagerPrevious.setVisibility(View.INVISIBLE);
}
if (myImagePosition > -1 && myImagePosition < myArrayList.size() - 1) {
myImageViewViewPagerNext.setVisibility(View.VISIBLE);
} else {
myImageViewViewPagerNext.setVisibility(View.INVISIBLE);
}
}
Im having trouble figuring out how to capture a swipe event on the last page of a view pager.
Basically the requirement is that when the user is on the last page, and they try to swipe to get to the next page, the activity should close.
I've tried doing this onPageScrolled but I cant seem to differentiate a left or right swipe when in there. When on the last page, the user should still maintain the functionality to move to the previous page.
So basically the case is
When on last page
if swipe prev (do normal behaviour)
if swipe next (finish activity)
Can anyone provide any suggestions?
Thanks
Here is working solution!!
Required variables
....
private boolean isLastPageSwiped;
private int counterPageScroll;
....
Inside onPageScrolled of ViewPager
#Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
//Here 6 is last position
if (position == 6 && positionOffset == 0 && !isLastPageSwiped){
if(counterPageScroll != 0){
isLastPageSwiped=true;
//Next Activity here
}
counterPageScroll++;
}else{
counterPageScroll=0;
}
}
You should include an fake blank page to your ViewPager, when user swipes your actual last page, it comes to this fake one. When this fake one is visible finish the Activity.
You should try this way, if it does not work, I can give you an example code.
I am probably really late, but then this might help someone else down the line. I got the snippet from a question on stackoverflow, where in i am just tweaking the code a little bit.
Answer from here.
ViewPager.OnPageChangeListener pagerListener = new ViewPager.OnPageChangeListener() {
boolean lastPageChange = false;
#Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels){
int lastIdx = mPgScreenAdapter.getCount() - 1;
Log.d(TAG, "pos:" + position);
if(lastPageChange && position == lastIdx) {}
}
#Override
public void onPageSelected( int i) {
pgText.setCurrentItem(i/2);
}
#Override
public void onPageScrollStateChanged(int state) {
int lastIdx = mPgScreenAdapter.getCount() - 1;
int curItem = pgScreen.getCurrentItem();
if(curItem==lastIdx && state==1){
lastPageChange = true;
// i put this here since onPageScroll gets called a couple of times.
finish();
}else {
lastPageChange = false;
}
}
};
int lastPosition;
int currentPosition;
//View pager assign
ViewPager viewPager = findViewById(R.id.viewPager);
//PageIndicatorView external https://github.com/romandanylyk
PageIndicatorView pageIndicatorView = findViewById(R.id.pageIndicatorView);
//customise pageIndicatorView design
pageIndicatorView.setAnimationType(AnimationType.THIN_WORM);
pageIndicatorView.setViewPager(viewPager);
//BannerAdapter is an custom adapter for viewpage i made.
//assign data here
BannerAdapter pagerAdapter = new BannerAdapter(this, banner, new OnClickListener() {
#Override
public void onClick() {
}
});
viewPager.setAdapter(pagerAdapter);
//listener for View Pager
//this is where process of making the infinite loop
viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
#Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
//assign position so that can be manipulated
currentPosition = position;
}
#Override
public void onPageSelected(int position) {
//set pager indicator position here
pageIndicatorView.setSelection(position);
}
#Override
public void onPageScrollStateChanged(int state) {
//check state 0 mean no ongoing process
if(state == 0){
//check currentPosition == lastPosition to detect that scroll cant scroll anymore
if(currentPosition == lastPosition){
int lastitem = banner.size() - 1;
//check position if either last item/first item
//and set viewpager current page as follow
if(currentPosition == lastitem){
viewPager.setCurrentItem(0);
}else if(currentPosition == 0){
viewPager.setCurrentItem(lastitem);
}
}else{
lastPosition = currentPosition;
}
Log.d("ScrollState", "currentPosition: " + currentPosition);
}
/*empty*/
}
});
I also tried my luck to find solution. Here is working code.Hope it will help someone else:-
pager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
#Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
#Override
public void onPageSelected(int position) {
}
#Override
public void onPageScrollStateChanged(int state) {
if (state == ViewPager.SCROLL_STATE_IDLE) {
int curr = pager.getCurrentItem();
int lastReal = pager.getAdapter().getCount() - 1;
if (curr < lastReal) {
counter = 0;
} else if (curr == lastReal) {
counter++;
if (counter == 2) {
pager.setOffscreenPageLimit(0);
pager.setCurrentItem(0, false);
counter = 0;
}
}
}
}
});
pagerIndicator.setViewPager(pager);
I used counter as global variable.
Thanks!
I created an abstract listener based on the solutions here. I wanted to do something when swiping right on the last page, or left on the first page.
import android.support.v4.view.ViewPager;
abstract class PageChangeToUnavailablePageListener implements ViewPager.OnPageChangeListener {
private int finalPageIndex;
private boolean finalPageVisible = false;
private boolean firstPageVisible = true;
private int lastScrollState = 0;
abstract void onUnavailablePageChangeOnLastPage();
abstract void onUnavailablePageChangeOnFirstPage();
public PageChangeToUnavailablePageListener(int finalPageIndex) {
this.finalPageIndex = finalPageIndex;
}
#Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
#Override
public void onPageSelected(int i) {
firstPageVisible = (i == 0);
finalPageVisible = (i == finalPageIndex);
}
#Override
public void onPageScrollStateChanged(int state) {
// The normal state sequence for changing pages is
// DRAGGING (user starts drag), SETTLING (next page is chosen), IDLE (animation is complete).
// No next page is chosen when dragging right on last page, or left on first page.
// In this case the SETTLING state is missing and the sequence is (DRAGGING, IDLE).
if (lastScrollState == ViewPager.SCROLL_STATE_DRAGGING && state == ViewPager.SCROLL_STATE_IDLE) {
// The user scrolled to an unavailable page.
if (finalPageVisible) {
onUnavailablePageChangeOnLastPage();
} else if (firstPageVisible) {
onUnavailablePageChangeOnFirstPage();
}
}
lastScrollState = state;
}
}
Hope you got an answer, i came across the same issue and this is what i did:
Incase you are using a viewpager and an activity with a class extending FragmentStatePagerAdapter you can use the getItem(int position) method to check if you reached the last page. This is also if you declared the NUM_PAGES and you can use it to detect the user has reached the last page.
in Activity:
private static int MAX_LAYOUTS = 3;
private boolean isLastPageSwiped;
private int counterPageScroll;
In onCreate:
ViewPager.OnPageChangeListener viewPagerPageChangeListener = new ViewPager.OnPageChangeListener() {
#Override
public void onPageSelected(int position) {
}
#Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
if (position == MAX_LAYOUTS - 1 && positionOffset == 0 && !isLastPageSwiped) {
if (counterPageScroll != 0) {
isLastPageSwiped = true;
Intent intent = new Intent(OldActivity.this, NewActivity.class);
startActivity(intent);
}
counterPageScroll++;
} else {
counterPageScroll = 0;
}
}
#Override
public void onPageScrollStateChanged(int arg0) {
}
};
viewPager.addOnPageChangeListener(viewPagerPageChangeListener);
i have also solution for these(in kotlin),
Example my tabview has 3 pages.
......
private var isLastPageSwiped = false
private var indexPageScroll = 0
......
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
super.onPageScrolled(position, positionOffset, positionOffsetPixels)
if (position == 2 && positionOffset == 0f && !isLastPageSwiped) {
if (indexPageScroll != 0) {
isLastPageSwiped = true
//Next Activity here
//You can perform your action on last page scroll.
}
indexPageScroll++
} else {
indexPageScroll = 0
}
}
This question refers to the SwipeListView component found here: https://github.com/47deg/android-swipelistview
After trying out several implementations and fixes I found on the web I decided to modify the sources a little.
I will post this here since i know it's a known issue and all the versions I found proved to have some issues eventually.
SwipeListViewTouchListener.java has suffered the following changes:
...
/**
* Create reveal animation
*
* #param view affected view
* #param swap If will change state. If "false" returns to the original
* position
* #param swapRight If swap is true, this parameter tells if movement is toward
* right or left
* #param position list position
*/
private void generateRevealAnimate(final View view, final boolean swap, final boolean swapRight, final int position) {
int moveTo = 0;
if (opened.get(position)) {
if (!swap) {
moveTo = openedRight.get(position) ? (int) (viewWidth - rightOffset) : (int) (-viewWidth + leftOffset);
}
} else {
if (swap) {
moveTo = swapRight ? (int) (viewWidth - rightOffset) : (int) (-viewWidth + leftOffset);
}
}
final boolean aux = !opened.get(position);
if(swap) {
opened.set(position, aux);
openedRight.set(position, swapRight);
}
animate(view).translationX(moveTo).setDuration(animationTime).setListener(new AnimatorListenerAdapter() {
#Override
public void onAnimationEnd(Animator animation) {
swipeListView.resetScrolling();
if (swap) {
if (aux) {
swipeListView.onOpened(position, swapRight);
} else {
swipeListView.onClosed(position, openedRight.get(position));
}
}
// if (aux || !swap) {
// resetCell();
// }
}
});
}
...
/**
* Close all opened items
*/
void closeOtherOpenedItems() {
if (opened != null && downPosition != SwipeListView.INVALID_POSITION) {
int start = swipeListView.getFirstVisiblePosition();
int end = swipeListView.getLastVisiblePosition();
for (int i = start; i <= end; i++) {
if (opened.get(i) && i != downPosition) {
closeAnimate(swipeListView.getChildAt(i - start).findViewById(swipeFrontView), i);
}
}
}
}
...
/**
* #see View.OnTouchListener#onTouch(android.view.View,
* android.view.MotionEvent)
*/
#Override
public boolean onTouch(View view, MotionEvent motionEvent) {
...
closeOtherOpenedItems();
view.onTouchEvent(motionEvent);
return true;
}
The rest of the code not mentioned is the same.
Any comments highly appreciated, this changes prevent you from having to implement the SwipeListViewOnTouchListener in the activity which inflates the list.
Cons: doesn't close the row opened by openAnimate()
BaseSwipeListViewListener swipeListViewListener = new BaseSwipeListViewListener() {
int openItem = -1;
#Override
public void onStartOpen(int position, int action, boolean right) {
super.onStartOpen(position, action, right);
if (openItem > -1)
swipeListView.closeAnimate(openItem);
openItem = position;
}
}
Or better way:
#Override
public void onStartOpen(int position, int action, boolean right) {
super.onStartOpen(position, action, right);
swipeListView.closeOpenedItems();
}
And set the listener to the listView:
swipeListView.setSwipeListViewListener(swipeListViewListener);
Your fix worked, but there is a way to do it without affecting the original code:
swipeListView.setSwipeListViewListener(new BaseSwipeListViewListener() {
int openItem = -1;
int lastOpenedItem = -1;
int lastClosedItem = -1;
#Override
public void onOpened(int position, boolean toRight) {
lastOpenedItem = position;
if (openItem > -1 && lastOpenedItem != lastClosedItem) {
swipeListView.closeAnimate(openItem);
}
openItem = position;
}
#Override
public void onStartClose(int position, boolean right) {
Log.d("swipe", String.format("onStartClose %d", position));
lastClosedItem = position;
}
}
You should however, send a pull request to apply your code as that would fix the bug.
Source: https://github.com/47deg/android-swipelistview/issues/46
If you're going to modify the swipelistview library itself I have a simpler solution.
Add the following if block to SwipeListViewTouchListener.java in the onTouch method right at the beginning of case MotionEvent.ACTION_DOWN:
if(lastOpenedPosition != downPosition && opened.get(lastOpenedPosition)) {
closeAnimate(lastOpenedPosition);
return false;
}
Create an int lastOpenedPosition field and initialize it to 0, and in the generateRevealAnimate method inside the if (aux) block add:
lastOpenedPosition = position;
I would also add config variable (in res/values/swipelistview_attrs.xml) to SwipeListView and add it to the onTouch if block, to add the ability to turn this feature off and on.
This basically results in if the list is touched while a row is open, than the row will close. Which, imho, is better functionality than the row closing only after you finished opening another row.
swipeListView.setSwipeListViewListener(new BaseSwipeListViewListener() {
//...
#Override
public void onClickBackView(int position) {
//DELETE ITEM
adapter.notifyDataSetChanged();
swipeListView.closeOpenedItems();
}
//...
});
Yeah, the SwipeListView of the original codes can open many items at the same time. Your code segment here can open one item at one time? Or when open another item, the opened items will be closed?
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 mNavPager;
private ViewPager mMainPager;
private final OnPageChangeListener mNavPagerListener = new OnPageChangeListener() {
private boolean mNavDragging;
private int mScrollPosition;
#Override
public void onPageSelected(int position) {
mScrollPosition = position;
}
#Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
if(mNavDragging)
mMainPager.scrollTo(positionOffsetPixels, 0);
}
#Override
public void onPageScrollStateChanged(int state) {
switch(state) {
case ViewPager.SCROLL_STATE_DRAGGING:
case ViewPager.SCROLL_STATE_SETTLING:
mNavDragging = true;
break;
case ViewPager.SCROLL_STATE_IDLE:
mNavDragging = false;
break;
}
}
};
private OnPageChangeListener mMainPagerListener = new OnPageChangeListener() {
private boolean mMainDragging;
private int mScrollPosition;
#Override
public void onPageSelected(int position) {
mScrollPosition = position;
}
#Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
if(mMainDragging)
mNavPager.scrollTo(positionOffsetPixels, 0);
}
#Override
public void onPageScrollStateChanged(int state) {
switch(state) {
case ViewPager.SCROLL_STATE_DRAGGING:
case ViewPager.SCROLL_STATE_SETTLING:
mMainDragging = true;
break;
case ViewPager.SCROLL_STATE_IDLE:
mMainDragging = false;
break;
}
}
};
If either one is scrolled manually, the other is slaved to it using the scroll state property. It works beautifully till the items reach their final position; at this point, the slaved pager flicks instantly back to the previously selected item, as though the scrolling hadn't taken place.
I have tried calling ViewPager#setCurrentItem(mScrolledPosition) with a variety of different logic constraints but that doesn't work either, though it does occasionally make it worse. I feel as though there must be something that can be done with that but I'm not sure what.
How can I get the slaved pager to remain in the correct position?
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);
}
}
});
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.
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
}
});
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.
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));
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);
}
}
});
I have a listView. When I scroll and stops in a particular place.
How can I get the amount of pixels I scrolled(from top)?
I have tried using get listView.getScrollY(), but it returns 0.
I had the same problem.
I cannot use View.getScrollY() because it always returns 0 and I cannot use OnScrollListener.onScroll(...) because it works with positions not with pixels. I cannot subclass ListView and override onScrollChanged(...) because its parameter values are always 0. Meh.
All I want to know is the amount the children (i.e. content of listview) got scrolled up or down. So I came up with a solution. I track one of the children (or you can say one of the "rows") and follow its vertical position change.
Here is the code:
public class ObservableListView extends ListView {
public static interface ListViewObserver {
public void onScroll(float deltaY);
}
private ListViewObserver mObserver;
private View mTrackedChild;
private int mTrackedChildPrevPosition;
private int mTrackedChildPrevTop;
public ObservableListView(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
if (mTrackedChild == null) {
if (getChildCount() > 0) {
mTrackedChild = getChildInTheMiddle();
mTrackedChildPrevTop = mTrackedChild.getTop();
mTrackedChildPrevPosition = getPositionForView(mTrackedChild);
}
} else {
boolean childIsSafeToTrack = mTrackedChild.getParent() == this && getPositionForView(mTrackedChild) == mTrackedChildPrevPosition;
if (childIsSafeToTrack) {
int top = mTrackedChild.getTop();
if (mObserver != null) {
float deltaY = top - mTrackedChildPrevTop;
mObserver.onScroll(deltaY);
}
mTrackedChildPrevTop = top;
} else {
mTrackedChild = null;
}
}
}
private View getChildInTheMiddle() {
return getChildAt(getChildCount() / 2);
}
public void setObserver(ListViewObserver observer) {
mObserver = observer;
}
}
Couple of notes:
we override onScrollChanged(...) because it gets called when the listview is scrolled (just its parameters are useless)
then we choose a child (row) from the middle (doesn't have to be precisely the child in the middle)
every time scrolling happens we calculate vertical movement based on previous position (getTop()) of tracked child
we stop tracking a child when it is not safe to be tracked (e.g. in cases where it might got reused)
You cant get pixels from top of list (because then you need to layout all views from top of list - there can be a lot of items). But you can get pixels of first visible item: int pixels = listView.getChildAt(0).getTop(); it generally will be zero or negative number - shows difference between top of listView and top of first view in list
edit:
I've improved in this class to avoid some moments that the track was losing due to views being too big and not properly getting a getTop()
This new solution uses 4 tracking points:
first child, bottom
middle child, top
middle child, bottom
last child, top
that makes sure we always have a isSafeToTrack equals to true
import android.view.View;
import android.widget.AbsListView;
/**
* Created by budius on 16.05.14.
* This improves on Zsolt Safrany answer on stack-overflow (see link)
* by making it a detector that can be attached to any AbsListView.
* http://stackoverflow.com/questions/8471075/android-listview-find-the-amount-of-pixels-scrolled
*/
public class PixelScrollDetector implements AbsListView.OnScrollListener {
private final PixelScrollListener listener;
private TrackElement[] trackElements = {
new TrackElement(0), // top view, bottom Y
new TrackElement(1), // mid view, bottom Y
new TrackElement(2), // mid view, top Y
new TrackElement(3)};// bottom view, top Y
public PixelScrollDetector(PixelScrollListener listener) {
this.listener = listener;
}
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
// init the values every time the list is moving
if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL ||
scrollState == AbsListView.OnScrollListener.SCROLL_STATE_FLING) {
for (TrackElement t : trackElements)
t.syncState(view);
}
}
#Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
boolean wasTracked = false;
for (TrackElement t : trackElements) {
if (!wasTracked) {
if (t.isSafeToTrack(view)) {
wasTracked = true;
if (listener != null)
listener.onScroll(view, t.getDeltaY());
t.syncState(view);
} else {
t.reset();
}
} else {
t.syncState(view);
}
}
}
public static interface PixelScrollListener {
public void onScroll(AbsListView view, float deltaY);
}
private static class TrackElement {
private final int position;
private TrackElement(int position) {
this.position = position;
}
void syncState(AbsListView view) {
if (view.getChildCount() > 0) {
trackedChild = getChild(view);
trackedChildPrevTop = getY();
trackedChildPrevPosition = view.getPositionForView(trackedChild);
}
}
void reset() {
trackedChild = null;
}
boolean isSafeToTrack(AbsListView view) {
return (trackedChild != null) &&
(trackedChild.getParent() == view) && (view.getPositionForView(trackedChild) == trackedChildPrevPosition);
}
int getDeltaY() {
return getY() - trackedChildPrevTop;
}
private View getChild(AbsListView view) {
switch (position) {
case 0:
return view.getChildAt(0);
case 1:
case 2:
return view.getChildAt(view.getChildCount() / 2);
case 3:
return view.getChildAt(view.getChildCount() - 1);
default:
return null;
}
}
private int getY() {
if (position <= 1) {
return trackedChild.getBottom();
} else {
return trackedChild.getTop();
}
}
View trackedChild;
int trackedChildPrevPosition;
int trackedChildPrevTop;
}
}
original answer:
First I want to thank #zsolt-safrany for his answer, that was great stuff, total kudos for him.
But then I want to present my improvement on his answer (still is pretty much his answer, just a few improvements)
Improvements:
It's a separate "gesture detector" type of class that can be added to any class that extends AbsListView by calling .setOnScrollListener(), so it's a more flexible approach.
It's using the change in scroll state to pre-allocate the tracked child, so it doesn't "waste" one onScroll pass to allocate its position.
It re-calculate the tracked child on every onScroll pass to avoiding missing random onScroll pass to recalculate child. (this could be make more efficient by caching some heights and only re-calculate after certain amount of scroll).
hope it helps
import android.view.View;
import android.widget.AbsListView;
/**
* Created by budius on 16.05.14.
* This improves on Zsolt Safrany answer on stack-overflow (see link)
* by making it a detector that can be attached to any AbsListView.
* http://stackoverflow.com/questions/8471075/android-listview-find-the-amount-of-pixels-scrolled
*/
public class PixelScrollDetector implements AbsListView.OnScrollListener {
private final PixelScrollListener listener;
private View mTrackedChild;
private int mTrackedChildPrevPosition;
private int mTrackedChildPrevTop;
public PixelScrollDetector(PixelScrollListener listener) {
this.listener = listener;
}
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
// init the values every time the list is moving
if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL ||
scrollState == AbsListView.OnScrollListener.SCROLL_STATE_FLING) {
if (mTrackedChild == null) {
syncState(view);
}
}
}
#Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
if (mTrackedChild == null) {
// case we don't have any reference yet, try again here
syncState(view);
} else {
boolean childIsSafeToTrack = (mTrackedChild.getParent() == view) && (view.getPositionForView(mTrackedChild) == mTrackedChildPrevPosition);
if (childIsSafeToTrack) {
int top = mTrackedChild.getTop();
if (listener != null) {
float deltaY = top - mTrackedChildPrevTop;
listener.onScroll(view, deltaY);
}
// re-syncing the state make the tracked child change as the list scrolls,
// and that gives a much higher true state for `childIsSafeToTrack`
syncState(view);
} else {
mTrackedChild = null;
}
}
}
private void syncState(AbsListView view) {
if (view.getChildCount() > 0) {
mTrackedChild = getChildInTheMiddle(view);
mTrackedChildPrevTop = mTrackedChild.getTop();
mTrackedChildPrevPosition = view.getPositionForView(mTrackedChild);
}
}
private View getChildInTheMiddle(AbsListView view) {
return view.getChildAt(view.getChildCount() / 2);
}
public static interface PixelScrollListener {
public void onScroll(AbsListView view, float deltaY);
}
}
Try to implement OnScrollListener:
list.setOnScrollListener(new OnScrollListener() {
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
int last = view.getLastVisiblePosition();
break;
}
}
#Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
}
});