How can I make ViewPager.arrowScroll() work the first time?
Asked Answered
B

1

1

I have a ViewPager (using a FragmentStatePagerAdapter to page fragments). The user can use left/right swiping to page from one fragment to the next; this works fine. I've also added < and > buttons for paging. The onClick listeners for these are implemented using:

mViewPager.arrowScroll(View.FOCUS_LEFT);

and

mViewPager.arrowScroll(View.FOCUS_RIGHT);

This works most of the time: when the user taps < or >, the view scrolls left or right as expected.

But on the first tap of the > button, nothing happens. I do hear a "click" confirming that the tap occurred. And I have log statements showing that the onClick listener was called, and mViewPager.arrowScroll(View.FOCUS_RIGHT) was called. I even check the result return by arrowScroll(); it's true! Yet no paging happens.

After that, all tapping on the < and > buttons works fine.

Why would the first call to arrowScroll(View.FOCUS_RIGHT) have no effect, and how can I fix it?

I guess I can try calling it twice the first time, but since I don't know why the documented behavior isn't happening, I don't know whether that approach will cause a double paging on some phones or Android versions.

Update: some logging

// Before any button taps.
QuestionAdapter: getItem(0)
QuestionAdapter: instantiateItem: position 0
QuestionAdapter: getItem(1)
QuestionAdapter: instantiateItem: position 1
QuestionAdapter: setPrimaryItem: position 0
QuestionAdapter: setPrimaryItem: position 0
// The screen is displaying page 0.
// Now I tap the > button:
QuestionAdapter: setPrimaryItem: position 0
// The screen is still displaying page 0.
// Now I tap the > button again:
QuestionAdapter: getItem(2)
QuestionAdapter: instantiateItem: position 2
QuestionAdapter: setPrimaryItem: position 1
QuestionAdapter: setPrimaryItem: position 1
QuestionAdapter: setPrimaryItem: position 1
// Now the screen displays page 1.
Bough answered 27/8, 2019 at 14:20 Comment(7)
Add some debug/breakpoints in the adapter, where it is fetching data for current view, if it goes for second item and it gets somehow not displayed, or something happens already before that and the adapter is asked for first item again. (i.e. you posted only debug about the UI side and events, which seems to be there, so now verify your underlying data, if they correspond to the UI events)Formulate
(and if the underlying data are correct, the next thing I would check is some sort of cache/view recycling) (I would go with "calling paging twice first time" solution only when I would be 100% sure it's API bug and everything else is correct, but from the limited post you did provide there are many possible places where your code may have gone wrong and you didn't provide them for scrutiny, especially things recycling view instances are a bit tricky to write correctly)Formulate
@Formulate Good suggestions. I'll try that. I'll certainly check whether adapter.getItem(position) is getting called, with what positions.Bough
adapter.getItem(position) is called for positions 0 and 1 immediately, before any buttons are tapped. This fetch-ahead behavior is consistent with what I've seen before. When I tap > the first time, adapter.getItem(position) is not called. When I tap > again, adapter.getItem(2) is called as the view scrolls from page 0 to 1, again consistent with the usual fetch-ahead behavior. What can I conclude from this? To me it just says that arrowScroll(View.FOCUS_RIGHT) isn't doing anything the first time it's called.Bough
I also traced instantiateItem() and setPrimaryItem(). instantiateItem follows getItem: when and only when getItem(i) is called, instantiateItem(i) is called with the same position. setPrimaryItem on the other hand tracks the visible page. When I tap > the first time, neither getItem nor instantiateItem is called, but setPrimaryItem(0) is called (again... it was also called before any buttons were tapped). When I tap > again, we see getItem and instantiateItem doing their fetch-ahead, and setPrimaryItem(1) is called as page 1 is displayed.Bough
@Formulate I've added some logging output to the question. Any other suggestions on how to investigate the underlying data, or problems with view instance recycling?Bough
@Formulate I think I've found the problem and the solution. Thanks again for your suggestions, including debugging.Bough
B
2

OK, I think I'm starting to figure out the problem. When I trace ViewPager.arrowScroll(FOCUS_RIGHT) (source code here) it looks like what it's doing is trying first to move the focus to the right (hmm, maybe that explains why the argument says FOCUS_RIGHT!).

So depending on what currently has focus, and whether there's a nextFocus view that's to the right of it, it will just move focus there, consider its job done, and return true to signal success.

Only if there's not a nextFocus view to the right will it actually pageRight() as I want it to do.

In my case, when I first press >, currentFocus = this.findFocus() returns null. Then it calls nextFocused = FocusFinder.findNextFocus() and comes up with a ListView that's on the currently displayed page. Since currentFocus is null and nextFocus is not (among other conditions), arrowScroll() is satisfied with setting focus to the ListView.

The second time I tap >, currentFocus = this.findFocus() returns the ListView, and nextFocused = FocusFinder.findNextFocus() yields null. Because nextFocused is null, it doesn't try to nextFocused.requestFocus() but instead calls pageRight(), which is what I wanted in the first place.

That being the case, what is the solution that fits the design of the ViewPager? It sounds like arrowScroll() is not intended for just paging left/right, like a left/right button would be expected to do. Instead it's meant to do what a keyboard arrow key should do; hence the name.

So then what method should be used to just page left/right, without regard for what currently has or can get focus?

I could try to work around arrowScroll's behavior by setting focus to the right view before calling arrowScroll, but that seems like a kludge. There are the pageRight and pageLeft methods, which look like they do exactly what I need, but they're not public, nor documented!

Well, pageRight and pageLeft call setCurrentItem(mCurItem + or - 1) to do their work, a method that is public and documented. So I guess I can just copy the code for pageRight and pageLeft into my own code.

This API design seems strange to me, given how common it is to have left/right buttons on a pager screen that are expected to page left/right regardless of focus. It's also frustrating that the documentation for arrowScroll(), and for paging the ViewPager left/right, is so vague. But in any case I think I've found a decent solution.

Bough answered 27/8, 2019 at 16:55 Comment(1)
read quickly through your answer, and seems all good, good catch with the focus. Setting current position +-1 upon button action sounds like reasonable solution to me, especially if it does handle the out-of-bound values well and you don't need to resolve those explicitly in your button handler code. (but even with boundary handling I think it's still viable solution, don't see anything wrong about it)Formulate

© 2022 - 2024 — McMap. All rights reserved.