I know with the Gallery widget I was able to use getSelectedItemPosition(); to retrieve the current position, however it doesnt seem ViewPager has that.
I know I can setup a listener and retrieve the position when the page is switched. But I want the current view position.
You can use:
mViewPager.getCurrentItem()
Create a listener and set it on your viewpager:
/**
* Get the current view position from the ViewPager by
* extending SimpleOnPageChangeListener class and adding your method
*/
public class DetailOnPageChangeListener extends ViewPager.SimpleOnPageChangeListener {
private int currentPage;
#Override
public void onPageSelected(int position) {
currentPage = position;
}
public final int getCurrentPage() {
return currentPage;
}
}
Update 2019
Now you can set addOnPageChangeListener on View Pager to Observe change in Page position.
Since you wanted to setup a listener and retrieve the position when the page is switched
mViewPager.addOnPageChangeListener(object : OnPageChangeListener {
override fun onPageScrollStateChanged(state: Int) {}
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {}
override fun onPageSelected(position: Int) {
pagePosition.setText("" + position + "/" + galleryAdapter!!.count)
}
})
My solution would only work if you have a TabLayout linked to your ViewPager.
This is how they're linked:
tabLayout.setupWithViewPager(viewPager);
And then to get current position, you can make use of:
tabLayout.getSelectedTabPosition()
I'm telling you now its a hack, so there is no reason to downvote for that reason. Meaning, it either will be helpful to you specifically or not. Either way, the description below will provide some insight and be helpful to the community. Also, this solution is good for the older APIs that don't have ViewPager.getCurrentItem().
First, a little information. If you iterate thru all children of a ViewPager with ViewPager.getChildAt(x); and print out with toString() (or getLeft()) each child View (a page) and then do this everytime you change pages, you will notice that the children will not be in the logical order that they are displayed in when you start going back pages (paging back to the beginning). Apparently, it will remove the unneeded child from the array then append the newest child to the array. So, for example, lets say you are looking at page 2 then changed to page 3, your list of children will be in this order page 2, page 3, page 4 meaning that ViewPager.getChildAt(1); will return the current page. But, if you then change back to page 2 (from page 3) your list of children will be in this order page 2, page 3, page 1 which means that ViewPager.getChildAt(1); does not return the current page. I have not yet been able to find simple logic to weed out the current page using this information. Because the order of the pages in the array behind getChildAt is in an arbitrary order based off of how the user has been paging around.
That being said, I developed a hack work-around. I have no clue if this function will work in all in environments, but it works for my current project. I would suspect if doesn't for you, then its an issue of different API level. But I dont actually suspect any issues for other environments.
Now, onto the meat. What I noticed was that the result of ViewPager.getChildAt(x).getLeft() will have some type of horizontal pixel coordinate relative to the parent. So, I used this information to weed out which view is the current one.
private int getCurrentPageIndex(ViewPager vp){
int first,second,id1,id2,left;
id1 = first = second = 99999999;
View v;
for ( int i = 0, k = vp.getChildCount() ; i < k ; ++i ) {
left = vp.getChildAt(i).getLeft();
if ( left < second ) {
if ( left < first ) {
second = first;
id2 = id1;
first = left;
id1 = i;
} else {
second = left;
id2 = i;
}
}
}
return id2;
}
This function is probably a questionable hack because it relies on the value of getLeft() to figure it all out. But, I grab the left coordinate of each child. I then compare this to the other values and store the first and second pages, returning the second page (current page) out of the function. It seems to work beautifully.
Why (might you ask) didn't I just use onClickListenter or whatever solution? Well, I was darned determined that there was a straight forward way to do this without having to include listeners, other classes, inconclusive focus, and other bloat. Unfortunately, this solution is not exactly straight forward. But, it does do away with the bloat, other classes and listeners. If I can figure out a more straight forward way, I will be rewriting this function. Or maybe, this will provide insight for someone else to have an epiphany.
Related
Background
I work on an app that has a RecyclerView which you can scroll up and down however you wish.
The data items are loaded from the server, so if you are about to reach the bottom or the top, the app gets new data to show there.
To avoid weird scrolling behavior, and staying on the current item, I use 'DiffUtil.Callback' , overriding 'getOldListSize', 'getNewListSize', 'areItemsTheSame', 'areContentsTheSame'.
I've asked about this here, since all I get from the server is a whole new list of items, and not the difference with the previous list.
The problem
The RecyclerView doesn't have only data to show. There are some special items in it too:
Since Internet connection might be slow, there is a header item and a footer item in this RecyclerView, which just have a special Progress view, to show you've reached the edge and that it will get loaded soon.
The header and footer always exist in the list, and they are not received from the server. It's purely a part of the UI, just to show things are about to be loaded.
Thing is, just like the other items, it needs to be handled by DiffUtil.Callback, so for both areItemsTheSame and areContentsTheSame, I just return true if the old header is the new header, and the old footer is the new footer:
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldItems[oldItemPosition]
val newItem = newItems[newItemPosition]
when {
oldItem.itemType != newItem.itemType -> return false
oldItem.itemType == ItemType.TYPE_FOOTER || oldItem.itemType == AgendaItem.TYPE_HEADER -> return true
...
}
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldItems[oldItemPosition]
val newItem = newItems[newItemPosition]
return when {
oldItem.itemType == ItemType.TYPE_FOOTER || oldItem.itemType == ItemType.TYPE_HEADER -> true
...
}
}
}
Seems right? Well it's wrong. If the user is at the top of the list, showing the header, and the list gets updated with new items, the header will stay at the top, meaning the previous items you've seen will get pushed away by the new ones.
Example:
Before: header, 0, 1, 2, 3, footer
After: header, -3, -2, -1, 0, 1, 2, 3, footer
So if you stayed on the header, and the server sent you the new list, you still see the header, and below the new items, without seeing the old ones. It scrolls for you instead of staying on the same position .
Here's a sketch showing the issue. The black rectangle shows the visible part of the list.
As you can see, before loading, the visible part has the header and some items, and after loading it still has the header and some items, but those are new items that pushed away the old ones.
I need the header to be gone on this case, because the real content is below it. Instead of the area of the header, it might show other items (or a part of them) above it, but the visible position of the current items should stay where they are.
This issue only occurs when the header is shown, at the top of the list. In all other cases it works fine, because only normal items are shown at the top of the visible area.
What I've tried
I tried to find how to set DiffUtil.Callback to ignore some items, but I don't think such a thing exists.
I was thinking of some workarounds, but each has its own disadvantages:
A NestedScrollView (or RecyclerView) which will hold the header&footer and the RecyclerView in the middle, but this might cause some scrolling issues, especially due to the fact I already have a complex layout that depends on the RecyclerView (collapsing of views etc...).
Maybe in the layout of the normal items, I could put the layout of the header and footer too (or just the header, because this one is the problematic one). But this is a bad thing for performance as it inflates extra views for nothing. Plus it requires me to toggle hiding and viewing of the new views within.
I could set a new ID for the header each time there is an update from the server, making it as if the previous header is gone, and there is a totally new header at the top of the new list. However, this might be risky in the case of no real updates of the list at the top, because the header will be shown as if it's removed and then re-added.
The questions
Is there a way to solve this without such workarounds?
Is there a way to tell DiffUtil.Callback : "these items (header&footer) are not real items to scroll to, and these items (the real data items) should be" ?
I will try to explain what I see as a solution to your problem:
Step 1: Remove all the code for FOOTER and HEADER views.
Step 2: Add these methods that add and remove dummy model items in adapter based on the user scroll direction:
/**
* Adds loader item in the adapter based on the given boolean.
*/
public void addLoader(boolean isHeader) {
if (!isLoading()) {
ArrayList<Model> dataList = new ArrayList<>(this.oldDataList);
if(isHeader) {
questions.add(0, getProgressModel());
else {
questions.add(getProgressModel());
setData(dataList);
}
}
/**
* Removes loader item from the UI.
*/
public void removeLoader() {
if (isLoading() && !dataList.isEmpty()) {
ArrayList<Model> dataList = new ArrayList<>(this.oldDataList);
dataList.remove(getDummyModel());
setData(questions);
}
}
public MessageDetail getChatItem() {
return new Model(0, 0, 0, "", "", "")); // Here the first value is id which is set as zero.
}
And here is your rest of the adapter logic that you need to decide if the item is a loader item or an actual data item:
#Override
public int getItemViewType(int position) {
return dataList.get(position).getId() == 0 ? StaticConstants.ItemViewTypes.PROGRESS : StaticConstants.ItemViewTypes.CONTENT;
}
According to the view type, you can add a progress bar view holder in your adapter.
Step 3: use these methods in data loading logic:
While making the API call in onScrolled() method of recyclerView, you need to add a loader item before the api call and then remove it after the api call. Use the given adapter methods above. The coded in onScrolled should look a little like this:
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (dy < 0) { //This is top scroll, so add a loader as the header.
recyclerViewAdapter.addLoader(true);
LinearLayoutManager linearLayoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
if (!recyclerViewAdapter.isLoading(true)) {
if (linearLayoutManager.findFirstCompletelyVisibleItemPosition() <= 2) {
callFetchDataApi();
}
}
}
} else {
if (!recyclerViewAdapter.isLoading(false)) {
if (linearLayoutManager.findLastCompletelyVisibleItemPosition() >= linearLayoutManager.getItemCount() - 2) {
callFetchDataApi();
}
}
});
Now after the api call gives you the data you need. Simply remove the added loader from the list like this:
private void onGeneralApiSuccess(ResponseModel responseModel) {
myStreamsDashboardAdapter.removeLoader();
if (responseModel.getStatus().equals(SUCCESS)) {
// Manage your pagination and other data loading logic here.
dataList.addAll(responseModel.getDataList());
recyclerViewAdapter.setData(dataList);
}
}
And lastly, you need to avoid any scroll during data loading operation is add a logic method for that is isLoading() method. which is used in the code of method onScrolled():
public boolean isLoading(boolean isFromHeader) {
if (isFromHeader) {
return dataList.isEmpty() || dataList.get(0).getId() == 0;
} else {
return dataList.isEmpty() || dataList.get(dataList.size() -1).getId() == 0;
}
}
Let me know if you don't understand any of this.
I think for now, the solution I took will suffice. It's a bit weird, but I think it should work:
The header item gets a new id each time the list is different in its first real item. The footer always have the same id, because it's ok for it to move in the current way it works. I don't even need to check that its id is the same. The check of areItemsTheSame is as such for them:
oldItem.agendaItemType == AgendaItem.TYPE_HEADER -> return oldItem.id == newItem.id
oldItem.agendaItemType == AgendaItem.TYPE_FOOTER -> return true
This way, if the header belongs to a new list data, old one will be removed, and new one will be at the top.
It's not the perfect solution, as it doesn't really push the original header to be at the top, and theoretically it makes us "kinda" have 2 headers at the same time (one being removed and one being added) but I think it's good enough.
Also, for some reason, I can't use notifyItemChanged on the header and footer in case only they get updated (internet connection changes its state, so need to change the header&footer alone). Only notifyDataSetChanged works for some reason.
Still, if there is a more official way, could be nice to know.
I am building a component based on RecyclerView, allowing user to reorder items by drag and drop.
Once I am on the DragListener side, I need the position it has in the adapter in order to perform correct move, but I only have access to the view.
So here is what I am doing in the adapter view binding :
#Override
public void onBindViewHolder(ViewHolder viewHolder, int position) {
Track track = mArray.get(position);
viewHolder.itemView.setTag(R.string.TAG_ITEM_POSITION, position);
}
Does it seem correct to you ?
Because if I move an item like this :
public void move(int from, int to){
Track track = mArray.remove(from);
mArray.add(to, track);
notifyItemMoved(from, to);
}
then position tag is not correct anymore, and if I notifyDataSetChanged(), I lose the fancy animation.
Any suggestion ?
There is a way to preserve fancy animations with just notifyDataSetChanged()
You need to make your own GridLayoutManager with overriden supportsPredictiveItemAnimations() method returning true;
You need to mAdapter.setHasStableIds(true)
The part I find tricky is you need to override you adapter's getItemId() method. It should return value that is truly unique and not a direct function of position. Something like mItems.get(position).hashCode()
Worked perfectly fine in my case - beautiful animations for adding, removing and moving items only using notifyDataSetChanged()
No, it is wrong. First of all, you cannot reference to the position passed to the onBindViewHolder after that method returns. RecyclerView will not rebind a view when its position changes (due to items moving etc).
Instead, you can use ViewHolder#getPosition() which will return you the updated position.
If you fix that, your move code should work & provide nice animations.
Calling notifyDataSetChanged will prevent predictive animations so avoid it as long as you can. See documentation for details.
Edit (from comment): to get position from the outside, get child view holder from recyclerview and then get position from the vh. See RecyclerView api for details
1) You'll use notifyItemInserted(position); or notifyItemRemoved(position); instead of notifyDataSetChanged() for animation.
2) You can just manually fix your problem - using
public void move(int from, int to){
Track track = mArray.remove(from);
mArray.add(to, track);
notifyItemMoved(from, to);
ViewHolder fromHolder = (ViewHolder) mRecyclerView.findViewHolderForPosition(from);
ViewHolder toHolder = (ViewHolder) mRecyclerView.findViewHolderForPosition(to);
Tag fromTag = fromHolder.itemView.getTag();
fromHolder.itemView.setTag(toHolder.itemView.getTag());
toHolder.itemView.setTag(fromTag);
}
You should move your method to OnCreateViewHolder, then notifyItemRemoved(index) works properly.
I'm able to maintain the touch animations by adding this to my list item's outer element
<View
android:foreground="?android:attr/selectableItemBackground"
...>
I fixed it with using 'notifyItemChanged(int position);' instead of 'notifyDataSetChanged();'
My adapter shows fancy animations perfectly and without any lags
Edit: I got position from onBindViewHolder's position.
as stated by others above, you can have animation while using notifyDataSetChanged on your adapter, although you need to specifically use stable ids. if your items IDs are strings, you can generate a long id for each string id you have and keep them in a map. for example:
class StringToLongIdMap {
private var stringToLongMap = HashMap<String, Long>()
private var longId: Long = 0
fun getLongId(stringId: String): Long {
if (!stringToLongMap.containsKey(stringId)) {
stringToLongMap[stringId] = longId++
}
return stringToLongMap[stringId] ?: -1
}
}
and then in your adapter:
private var stringToLongIdMap = StringToLongIdMap()
override fun getItemId(position: Int): Long {
val item = items[position]
return stringToLongIdMap.getLongId(item.id)
}
another useful thing to consider, if you are using kotlin data class as items in your adapter, and you don't have an id, you can use the hashCode of the data class itself as stable id (if you are sure that the item properties combination are unique in your data set):
override fun getItemId(position: Int): Long = items[position].hashCode().toLong()
I know i can do
viewPager.setCurrentItem(position)
to set my view pager at the desired position. My question is if and how i can do this before the adapter is instantiated.
Meaning that if I do
pagerAdapter = new ViewPagerAdapter(arg1,arg2....);
viewPager.setAdapter(pagerAdapter);
viewPager.setCurrentItem(position);
item 0 is first build and after that the item at the desired position is also build. Which takes double the time...In my adapter each item needs quite a lot of work to be build, so it would be best to avoid the 0 position item to be build if possible. Maybe by passing the desired position as an argument at the adapter's instantiation...?
Any way to do that?
If the time and work is what you're worry about, I'd try to avoid building the page at position 0 until the desired one has been built. You could use a variable lets say "desiredPageHasBeenBuilt" in your adapter, when requesting the item at position 0 you could return an "empty page" if the variable desiredPageHasBeenBuilt is false, when your desired page has been build set the variable to true and the page 0 can be built.
For a solution that works in Android M as well as older versions, use reflection as follows:
int currentItem = 5;
// Set initial position first...
Field field = ViewPager.class.getDeclaredField("mRestoredCurItem");
field.setAccessible(true);
field.set(mPager, currentItem);
// ...and then set adapter
mPager.setAdapter(adapter);
Using reflection is safe, because you control the ViewPager implementation (it's included with your app).
If you use Proguard, then you need to include the following in its config:
-keepclassmembers class android.support.v4.view.ViewPager {
private int mRestoredCurItem;
}
or the field mRestoredCurItem will be renamed by Proguard.
You can just set blank adapter and after that set your real adapter this way you will "trick" the viewpager and you want load any data you dont want to.
this.viewPager.setAdapter(new PagerAdapter() {
#Override
public int getCount() {
return 0;
}
#Override
public boolean isViewFromObject(View view, Object object) {
return false;
}
});
this.viewPager.setCurrentItem(imagePosition, false);
this.viewPager.setAdapter(adapter);
this.viewPager.setCurrentItem(imagePosition, false);
You can fool the viewpager to start at a given position before the adapter is set, by calling onRestoreInstanceState, like this:
int currentItem = 5;
Parcel parcel = Parcel.obtain();
writeParcelable(BaseSavedState.EMPTY_STATE, 0);
writeInt(currentItem);
writeParcelable(null, 0);
setDataPosition(0);
SavedState savedState = ViewPager.SavedState.CREATOR.createFromParcel(parcel);
mPager.onRestoreInstanceState(savedState);
mPager.setAdapter(mAdapter);
Maybe this is not the answer you're looking for, but have you tried to it as it was designed? Do not start heavy work on page fragment before it is attached to the screen. That way you'll get that behavior you want without hacking android implementation.
you can do this trick:
refactor all of your heavy work into a function, because creating a fragment is not takes much time, and only execute the function when user is going to see it by calling that function inside OnPageChangeListener listener and at the
#Override
public void onPageSelected(int position){
// call your function that do heavy working
}
I am using a ViewPager from Android support v13 and I'd like to scroll to a specific item using setCurrentItem(int), but when I try to scroll more than 2 pages the application freezes, and after a few seconds the system shows an ANR window.
I tried to increase the offscreen screen limit using setOffscreenPageLimit(2), that way it did not freeze when i tried to scroll 2 pages, but did the same for 3 pages.
My problem is that my fragments are pretty memory consuming so I would not like to have too much in memory. I used the same code with support library v4, but I had to update it to v13 to use NotificationCompat.Builder.addAction(int, CharSequence, PendingIntent).
Does any of you know what could be the problem, and what could be the solution?
Is your adapter handling very large amounts of items? (very large > ~220 items)
ViewPager.populate(int) will loop from the current item position to N, which is the number of items in the adapter - so if you've set it to something large (such as Integer.MAX_VALUE) this will takes some time, but will eventually finish.
If this is your case, search for "endless pager" related questions, such as Endless ViewPager android, or limit the number of items to something that's reasonable for the main thread.
I had the same issue. I used PagerAdapter for infinite scrolling.
public static final int INITIAL_OFFSET = 1000;
#Override
public int getCount() {
return Integer.MAX_VALUE / 2;
}
And started the ViewPager in this way:
viewPager.setCurrentItem(INITIAL_OFFSET);
So to get rid of the freezing I wrote small function:
private void movePagerToPosition(int position) {
int current = viewPager.getCurrentItem();
int sign = current > position ? -1: 1;
while (viewPager.getCurrentItem() != position){
viewPager.setCurrentItem(viewPager.getCurrentItem() + sign, true);
}
}
i have this issue.
add below method to custom viewPager and use this.
private void setupCurrentItem(int cur)
{
try
{
Class<?> viewpager = ViewPager.class;
Field mCurItemField = viewpager.getDeclaredField("mCurItem");
mCurItemField.setAccessible(true);
mCurItemField.set(this, cur);
}
catch (Exception ignored) {}
}
setupCurrentItem(100);
setCurrentItem(100, false);
I'm building a magazine e-reader app, using a viewpager as the primary navigational element. Article objects are deserialized (from data received from a REST web service), and used to inflate "cover pages" that are inflated by the pager adapter, which the user "flips through".
This all works perfectly using a PagerAdapter with an instantiateItem method like this (with an ArrayList of articles passed to the adapter's constructor):
#Override
public Object instantiateItem(View arg0, int position) {
final Article s = toShow.get(position);
View v = getLayoutInflater().inflate(R.layout.previewpage, null);
TextView hl = (TextView)v.findViewById(R.id.coverHl);
ImageView img = (ImageView)v.findViewById(R.id.coverImg);
....
hl.setText(s.title);
img.setImageBitmap(s.image);
My issue is that, at every 6th position, I'd like to inflate a different view to show an ad. At the moment, I have an if statement added to the top of this method like :
if ((position != 0) && (position%6 == 0)){
after which I inflate the other view and place the appropriate ad on it. However, this leads to numerous complications: On the next view, I can no longer say toShow.get(position) because doing so skips one of the articles in the array (the one at the position at which the ad was shown). So, I created an int adsShown, which gets incremented every time an ad is shown, and call final Article s = toShow.get(position - adsShown). But when I do that, if the user flips back through the viewpager, an ArrayIndexOutOfBounds error is shown because it position-adsshown is -1.
I know I can figure out workarounds for these issues, but I'm questioning whether there is a simpler way to handle this task in the first place.
Any ideas?
You can get the correct position using something like this:
// check if position > 0 to make sure we don't start with an ad
if(position > 0 && position%6 == 0) {
// get ad view here
else {
int index = position - (int)Math.floor(position/6);
final Article s = toShow.get(index);
...
}
Also remember that your getCount will have to return the number of articles plus the number of ads or else it will not show all the articles you have in your array.