I'm creating a slideshow with ViewPager2. For example, the slideshow has 3 items and I want to show the second item when the activity opens. I use setCurrentItem(int item, boolean smoothScroll) method but it doesn't work and nothing happens. How can I achieve it?
viewPager.adapter = adapter
viewPager.setCurrentItem(1, true)
I think an easier more reliable fix is to defer to next run cycle instead of unsecure delay e.g
viewPager.post {
viewPager.setCurrentItem(1, true)
}
setCurrentItem(int item, boolean smoothScroll) works correctly in ViewPager but in ViewPager2 it does not work as expected. Finally, I faced this problem by adding setCurrentItem(int item, boolean smoothScroll) method into a delay like this:
Handler().postDelayed({
view.viewPager.setCurrentItem(startPosition, false)
}, 100)
Do not use timers, you will run into a lot of probable states in which the user has a slow phone and it actually takes a lot longer than 100 ms to run, also, you wouldn't want too slow of a timer making it ridiculously un-reliable.
Below we do the following, we set a listener to our ViewTreeObserver and wait until a set number of children have been laid out in our ViewPager2's RecyclerView (it's inner working). Once we are sure x number of items have been laid out, we start our no-animation scroll to start at the position.
val recyclerView = (Your ViewPager2).getChildAt(0)
recyclerView.apply {
val itemCount = adapter?.itemCount ?: 0
if(itemCount >= #(Position you want to scroll to)) {
viewTreeObserver.addOnGlobalLayoutListener(object: ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
viewTreeObserver.removeOnGlobalLayoutListener(this)
// False for without animation scroll
(Your ViewPager2).scrollToPosition(#PositionToStartAt, false)
}
}
}
First off, I think that the accepted answer shouldn't be #hosseinAmini 's, since it's suggesting to use a delay to work around the problem. You should first be looking for what the assumed bug is caused by, rather than trusting unreasonable solutions like that.
#Rune's proposal is correct, instead; so I'm quoting their code in my answer:
viewPager.post {
viewPager.setCurrentItem(1, true)
}
The only thing I'd argue about is the aforementioned one's belief that their solution is just deferring the execution of that lambda in the next run cycle. This wouldn't make anything buggy work properly. Rather, what it is actually being done is deferring the execution of that lambda to once the view has been attached to a window, which implies it's also been added to a parent view. Indeed, there looks to be an issue as to changing the current ViewPager2 item before being attached to a window. Some evidence to support this claim follows:
Using whichever Handler won't work nearly as effectively.
Handler(Looper.getMainLooper()).post {
viewPager.setCurrentItem(1, true) // Not working properly
}
From a theoretical standpoint, it might incidentally work due to the ViewPager2 being attached to a window acquiring priority in the message queue of the main looper, but this shouldn't ever be relied upon as there's just no guarantee that it'll work (it's even more likely it won't) and if it even turned out to be working, further investigation running multiple tests should make my point clear.
View.handler gets null, which means the view hasn't been attached to any window yet.
View.handler // = null
Despite Android UI being tied to the main looper, which will always uniquely correspond to the main thread –hence also called the UI thread,– a weird design choice stands in the handler not being associated to each view until they get attached to a window. A reason why this may lay on the consequent inability of views to schedule any work on the main thread while they're not part of the hierarchy, which may turn useful when implementing a view controller that schedules view updates while unaware of their lifecycle (in which case it would employ the View's handler, if any — or just skip scheduling whatever it was going to if none).
EDIT:
Also, #josias has pointed out in a comment that it'd be clearer to use:
viewPager.doOnAttach {
viewPager.setCurrentItem(1, true)
}
Thanks for that suggestion! It expresses better the actual intent, rather than relying on the behavior of the View.post method.
Do not use timers and all that stuff with 'post', it's not the reliable solution and just a piece of code that smells.
Instead, try use viewPager.setCurrentItem(1, false). That 'false' is about smoothScroll, you can't smooth scroll your viewPager2 when your activity is just opened. Tested it on a fragment in onViewCreated() method, it also didn't work with "true", but works with "false"
As it was mentioned above you have to use setCurrentItem(position, smoothScroll) method on ViewPager2 in order to show selected item. To make it work you have to define a callback, here is an example:
ViewPager2.OnPageChangeCallback callback = new ViewPager2.OnPageChangeCallback() {
#Override
public void onPageSelected(int position) {
super.onPageSelected(position);
}
};
And then you have to register it as follow:
viewPager.registerOnPageChangeCallback(callback);
Also do not forget to unregister it:
viewPager.unregisterOnPageChangeCallback(callback);
When you call setCurrentItem(position) method it will call onPageSelected(int position) method from your callback passing your argument, and then method createFragment(int position) from FragmentStateAdapter class will be called to show your fragment.
I tried changing viewpager2 page in Handler().dely() and viewPager2.post{} and even 'viewPager2.get(0).post all didn't work for me, I'm using ViewPager with FragmentStateAdapter with Tablayout.
What worked for me is changing the position of the RecylerView in ViewPager2 after binding FragmentStateAdapter to yourViewPager2View.adapter manually:
(yourViewPager2View[0] as RecyclerView).scrollToPosition(moveToTabNumber)
Why
My problem is onCreateFragment(position:Int):Fragmeet function in FragmentStateAdapter always starting fragment at 0 position no matter what pageNumber I set the page
viewPager.setCurrentItem = pageNumber
I checked where it's called in FragmentStateAdapter it's called in FragmentStateAdapter:
onBindViewHolder(final #NonNull FragmentViewHolder holder, int position)`
so all I needed is to force onBindViewHolder to call onCreateFragment(position:Int) with the page number I wanted.
mViewPager.setCurrentItem(1, true); ---> this is sufficient as you written above
That should work,
in doubt, just check your position:
#Override
public void onPageSelected(int i) {
if (LOG_DEBUG) Log.v(TAG, " ++++++++ onPageSelected: " + i);
mViewPager.setCurrentItem(i);
//TODO You can use this position: to write other dependent logic
}
and also check
getItem(int position) in PagerAdapter
or else paste your code.
I noticed that it works fine when the view is initially created if you opt to not animate it.
viewPager2.setCurrentItem(index, false)
This is usually fine depending on your use case - this initial/default item probably doesn't need to be animated in.
I met the same problem. In my case, I make the viewPager2 Gone by default until network requests succeed, I fix it by setting the CurrentItem after I make the viewPager2 visible.
My answer may not be helpful now but i see no harm to post my expreince, i just came to this problem using ViewPager and ViewPager2 and unexpectedly solved it by just changing some line codes order.
Here is (java) solution for ViewPager:
reviewWordViewPager.addOnPageChangeListener(changeListener);
reviewWordViewPager.setCurrentItem(viewPosition, true/false);
reviewWordTabIndicator.setupWithViewPager(reviewWordViewPager, true);
(Java) solution for ViewPager2:
wordViewPager.registerOnPageChangeCallback(viewPager2OnPageChangeCallback);
wordViewPager.setCurrentItem(vpPosition, true/false);
new TabLayoutMediator(tabIndicator, wordViewPager,
((tab, position) -> tab.setText(viewPagerTitle[position]))).attach();
I did not look up for ViewPager2 whether it needs the following old code used in ViewPager
#Override
public int getItemPosition(#NonNull Object object) {
// refresh all fragments when data set changed
return POSITION_NONE;
}
But surprisingly no need for it in ViewPager2 to solve the problem i've been having, hope it helps others
In case you use context.startActivity to start new activities no need to use wordViewPager.setCurrentItem(item, smoothScroll) in your onResume function to get back to the last selected tab before you started new activity you just save ViewPager/ViewPager2 position like vpPisition = wordViewPager.getCurrentItem(); in onStop function.
vpPisition is a global variable.
as #Daniel Kim but a java version
RecyclerView rvOfViewPager2 = (RecyclerView) viewPager2.getChildAt(0);
rvOfViewPager2.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener()
{
#Override
public void onGlobalLayout()
{
rvOfViewPager2.getViewTreeObserver().removeOnGlobalLayoutListener(this);
viewPager2.setCurrentItem(currentTabId, false);
}
});
First You need to Initilaze the Main activity under any listener or button You want then After that You need to put this Line..
here MainActvity is the Viewpager Main Class You are using and and 2 is the position where you want to move
MainActivity main = (MainActivity ) mContext;
main.selectTab(2, true);
I'm trying to implement an autoplay feature for video items in a RecyclerView (linear vertical layout). I can't figure out how to know when a certain item is currently on/off the screen so I can autoplay/pause the video. If I put the code in onBindViewHolder method all videos start playing simultaniously. Couldn't find a solution by googling it either. Help, please!
For Recyclerview you should rely on your layout manager to give you this information.
https://developer.android.com/reference/android/support/v7/widget/GridLayoutManager
or
https://developer.android.com/reference/android/support/v7/widget/LinearLayoutManager
Assign a layout to the RecyclerView when you assign the adapter. Then use it to see what is visible.
Let me help a bit more with some Psedo Kotlin code for you to help with the player aspect.
Let's pretend you have some object that is bound into each row that can trigger the playing and has a unique ID. Let's call that an ActiveRowPlayer for this example.
NOTE*
If you are using databinding, it is simple to bind your video player playing content to a property in your model that is populating the row, but that's different story for a different post.
You can make an interface like:
interface IActivePlayerUpdater{
fun onUpdateCurrentPlayer
}
You can make a helper method like:
in your activity, you can implement an interface like :IActivePlayerUpdater and override the methods for it.
override fun onUpdateCurrentPlayer(){
var activeRowPlayer = recyclerView.layoutManager.findFirstCompletelyVisibleItemPosition()
if(activeRowplayer.someID != currentRowPlayer.someID){
currentRowPlayer.stopPlaying
currentRowPlayer = activeRowPlayer
activeRowPlayer.startPlaying
}
}
Then pass it into your adapter and just monitor your onBind method and anytime a new onBind is called that means the content has moved enough to trigger a new row item.
MyAdapterConstructor(IActivePlayerUpdater myCallback)
fun recyclerView.onBindMethod(stuffThatComesHere){
//do normal stuff
myCallback.onUpdateCurrentPlayer()
}
Keep in mind, this is just pseudo to help you on your journey. not intended to be direct copy and paste.
----NOTE* REQUESTED FROM COMMENT TO SUPPLY HOW TO TOUCH VIEWMODEL FROM OUTSIDE OF ADAPTER---
#Goran, this example I had setup a long time ago to avoid
notifyDataSetChanged on selection changed to toggle a checkbox for
each item. I eventually moved to a better option, but you asked how do
you get the viewmodel, here is a sample. rvCameraRoll was my
recyclerView, I was using it to display camera media, but that is not
relevant, just focus on getting the viewModel piece.
The only part you should care about is getting the ViewHolder, I just left the rest there in case it helps you with anything else.
int count = rvCameraRoll.getChildCount();
for (int i = 0; i < count; i++) {
MediaModelGridAdapter.ViewHolder childRow = (MediaModelGridAdapter.ViewHolder)rvCameraRoll.getChildViewHolder(rvCameraRoll.getChildAt(i));
if(isVisible) {
childRow.imgCellSelected.setVisibility(View.VISIBLE);
if(getMediaModelList().get(i).getIsSelected()){
childRow.imgCellSelected.setBackgroundResource(R.drawable.ic_ap_selected_on);
}
}else{
//check that exists, because after fresh delete list may be short while updating cells
if(getMediaModelList().size() > i) {
getMediaModelList().get(i).setIsSelected(false);
}
childRow.imgCellSelected.setBackgroundResource(R.drawable.ic_ap_selected_off);
childRow.imgCellSelected.setVisibility(View.GONE);
}
}
OLD
leaving this for anyone using a ListView
There is a OnScrollListener for ListView
You can override the
onScrollStateChanged(AbsListView view, int scrollState)
and
the
onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)
this will give you the ListView item that is visible. So by using the onScroll you can detect which items are visible and determine which one to play and stop playing.
if you use with recyclerview
https://developer.android.com/reference/android/support/v7/widget/RecyclerView.OnScrollListener
The RecyclerView is only holding the items and the LayoutManager is responsible for displaying the items, so in order to get the ones that are visible to the user, assuming that you use LinearLayoutManager, you should call :
((LinearLayoutManager)recyclerView.getLayoutManager()).findFirstVisibleItemPosition();
or
((LinearLayoutManager)recyclerView.getLayoutManager()).findLastVisibleItemPosition();
I've seen many questions (and answers) regarding ViewTreeObserver but none have completely answer the question/problem i'm facing. So here's what i have:
I have some views being added programmatically inside a scrollview. After all the layout has been set I want to scroll to a specific view that is inside the scrollview. For this I'm using View Tree Observer and the code is as follows (Some pseudo of what I'm doing)
viewInsideScrollView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public synchronized void onGlobalLayout() {
firstTime++; // Counts how many times I enter the listener
if (firstTime == 1) {
// Here the fully measure of the scroll view has not happened
} else if (firstTime == 2) {
// I still don't have the full measure of the scroll view (Let's say that, for example, paddings are missing/weren't calculated)
} else if (firstTime == 3) {
// Here is when I can safely get the locationOfTheView variable and the scroll down will happen perfectly!
viewInsideScrollView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
scrollView.scrollTo(0, locationOfTheView);
}
}
});
Note: The reason I add firstTime == 3 is because, in this specific case, I noticed that it only calls the global layout 3 times.
So my question is as follows: How can I detect, cleanly, when the Global Layout has finished all the necessary measurements?
Yes I have some work arounds that works like adding a small delay which will give time for the Global Layout to have it's measures done but I was looking for another (cleaner) way to do that.
Any ideas?
Thanks!
I have a ListView that potentially can have hundreds of entries. When a selection is made I've been using a smoothScrollToPosition, thusly:
if (lv != null) { //Are we created yet?
lv.post(new Runnable() {
public void run() {
lv.smoothScrollToPosition(k);
}
});
}
but my users have told me they don't like the scrolling animation and would prefer to just instantly go there. So I replaced my smooth scroll with
lv.setSelection(k);
... and now it does nothing at all. FWIW this is all happening right after a notifyDatasetChanged
In searching for a solution I came across this discussion on http://code.google.com/p/android/issues/detail?id=6741 which implies this is a known problem. Is there a workaround or am I just doing this wrong?
Thanks in advance.
The documentation of setSelection says that it only scrolls to the selected position when the ListView is in touch mode. Maybe ListView is no more in touch mode once the data set has changed or maybe setSelection is simply forgotten for the next UI update cycle.
I guess you could try a workaround by calling setSelection with a delay. You could use the postDelayed method with a delay of 100 milliseconds for example. Or you could extend ListView and override layoutChildren or something related that probably gets called when the data set changes in order to re-calculate the list view item measurements. At that point it should be safe to call setSelection and you don't need to rely on guesstimating a delay.
I'd like to have my application do some on-screen changes (outside the ListView) based on what the first visible item in my list is.
What I've done is override the onScroll handler. When this handler is invoked, I set the position of my Cursor to the first visible position so I can look at the data for that row:
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
mCursor.moveToPosition(getListView().getFirstVisiblePosition());
...
I then reference the data in the Cursor to make my display changes.
Here's the thing: This working is spotty. Most of the time it works perfectly. I can't make it screw up.
However, sometimes getListView().getFirstVisiblePosition() does not return 1 until the 3rd item (index 2) is really the first visible item. It displays 0 when the 0 index is showing. It displays 0 when the 1 index is showing. It displays 1 when the 2 index is showing and so on. Additionally, it only does this when the user is scrolling down the list. If the user is scrolling backwards (up), getFirstVisiblePosition() returns the correct value.
I'm wondering if this isn't related to some efficiency problem. This handler is going to execute over and over and over as the user scrolls.
Does anyone have any ideas on what could be the problem? Does anyone have any better solutions?
Update:
Do I remember hearing that getFirstVisiblePosition() is really only a guess; and that Android essentially supposes what index might be available by rows' probable height? If this is so, then I have a problem. Some rows in my list are bigger than others. If this is the case, is there any way around it?
I would actually perform your call in onScrollStateChanged and only execute when scrolling is idle. You will drastically cut down on number of executed requests. Something like this:
public void onScrollStateChanged(final AbsListView view, final int scrollState) {
if (scrollState == OnScrollListener.SCROLL_STATE_IDLE) {
// do yer stuff
}
}