Android - ViewPager Adapter, set primary item before adapter is instantiated - android

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
}

Related

onBindViewHolder() is never called on view at position even though RecyclerView.findViewHolderForAdapterPosition() returns null at that position

I have a list with 13 items (although items may be added or removed), positions 0-12. When the fragment containing the RecyclerView is first shown, only positions 0 through 7 are visible to the user (position 7 being only half visible). In my adapter I Log every time a view holder is binded/bound (idk if grammar applies here) and record its position.
Adapter
#Override
public void onBindViewHolder(final ViewHolder holder, final int position) {
Log.d(TAG, "onBindViewHolder() position: " + position);
...
}
From my Log I see that positions 0-7 are bound:
I have a selectAll() method that gets each ViewHolder by adapter position. If the returned holder is NOT null I use the returned holder to update the view to show it's selected. If the returned holder IS null I call selectOnBind() a method that flags the view at that position update to show it's selected when it's binded rather than in real time since it's not currently shown:
public void selectAll() {
for (int i = 0; i < numberOfItemsInList; i++) {
MyAdapter.ViewHolder holder = (MyAdapter.ViewHolder)
mRecyclerView.findViewHolderForAdapterPosition(i);
Log.d(TAG, "holder at position " + i + " is " + holder);
if (holder != null) {
select(holder);
} else {
selectOnBind(i);
}
}
}
In this method I Log the holder along with its position:
So up to this point everything seems normal. We have positions 0-7 showing, and according to the Log these are the positions bound. When I hit selectAll() without changing the visible views (scrolling) I see that positions 0-7 are defined and 8-12 are null. So far so good.
Here's where it gets interesting. If after calling selectAll() I scroll further down the list positions 8 and 9 do not show they are selected.
When checking the Log I see that it's because they are never bound even though they were reported to be null:
Even more confusing is that this does not happen every time. If I first launch the app and test this it may work. But it seems to happen without fail afterwards. I'm guessing it has something to do with the views being recycled, but even so wouldn't they have to be bound?
EDIT (6-29-16)
After an AndroidStudio update I cannot seem to reproduce the bug. It works as I expected it to, binding the null views. If this problem should resurface, I will return to this post.
This is happening because:
The views are not added to the recyclerview (getChildAt will not work and will return null for that position)
They are cached also (onBind will not be called)
Calling recyclerView.setItemViewCacheSize(0) will fix this "problem".
Because the default value is 2 (private static final int DEFAULT_CACHE_SIZE = 2; in RecyclerView.Recycler), you'll always get 2 views that will not call onBind but that aren't added to the recycler
In your case views for positions 8 and 9 are not being recycled, they are being detached from the window and will be attached again. And for these detached view onBindViewHolder is not called, only onViewAttachedToWindow is called. If you override these function in your adapter, you can see what I am talking.
#Override
public void onViewRecycled(ViewHolder vh){
Log.wtf(TAG,"onViewRecycled "+vh);
}
#Override
public void onViewDetachedFromWindow(ViewHolder viewHolder){
Log.wtf(TAG,"onViewDetachedFromWindow "+viewHolder);
}
Now in order to solve your problem you need to keep track of the views which were supposed to recycled but get detached and then do your section process on
#Override
public void onViewAttachedToWindow(ViewHolder viewHolder){
Log.wtf(TAG,"onViewAttachedToWindow "+viewHolder);
}
The answers by Pedro Oliveira and Zartha are great for understanding the problem, but I don't see any solutions I'm happy with.
I believe you have 2 good options depending on what you're doing:
Option 1
If you want onBindViewHolder() to get called for an off-screen view regardless if it's cached/detached or not, then you can do:
RecyclerView.ViewHolder view_holder = recycler_view.findViewHolderForAdapterPosition( some_position );
if ( view_holder != null )
{
//manipulate the attached view
}
else //view is either non-existant or detached waiting to be reattached
notifyItemChanged( some_position );
The idea is that if the view is cached/detached, then notifyItemChanged() will tell the adapter that view is invalid, which will result in onBindViewHolder() getting called.
Option 2
If you only want to execute a partial change (and not everything inside onBindViewHolder()), then inside of onBindViewHolder( ViewHolder view_holder, int position ), you need to store the position in the view_holder, and execute the change you want in onViewAttachedToWindow( ViewHolder view_holder ).
I recommend option 1 for simplicity unless your onBindViewHolder() is doing something intensive like messing with Bitmaps.
When you have large number of items in the list you have passed to recyclerview adapter you will not encounter the issue of onBindViewHolder() not executing while scrolling.
But if the list has less items(I have checked on list size 5) you may encounter this issue.
Better solution is to check list size.
Please find sample code below.
private void setupAdapter(){
if (list.size() <= 10){
recycler.setItemViewCacheSize(0);
}
recycler.setAdapter(adapter);
recycler.setLayoutManager(linearLayoutManager);
}
I think playing with view is not a good idea in recyclerview. The approach I always use to follow to just introduce a flag to the model using for RecyclerView. Let assume your model is like -
class MyModel{
String name;
int age;
}
If you are tracking the view is selected or not then introduce one boolean to the model. Now it will look like -
class MyModel{
String name;
int age;
boolean isSelected;
}
Now your check box will be selected/un-selected on the basis of the new flag isSelected (in onBindViewHolder() ). On every selection on view will change the value of corresponding model selected value to true, and on unselected change it to false. In your case just run a loop to change all model's isSelected value to true and then call notifyDataSetChanged().
For Example, let assume your list is
ArrayList<MyModel> recyclerList;
private void selectAll(){
for(MyModel myModel:recyclerList)
myModel.isSelected = true;
notifyDataSetChanged();
}
My suggestion, while using recyclerView or ListView to less try to play with views.
So in your case -
#Override
public void onBindViewHolder(final ViewHolder holder, final int position) {
holder.clickableView.setTag(position);
holder.selectableView.setTag(position);
holder.checkedView.setChecked(recyclerList.get(position).isSelected);
Log.d(TAG, "onBindViewHolder() position: " + position);
...
}
#Override
public void onClick(View view){
int position = (int)view.getTag();
recyclerList.get(position).isSelected = !recyclerList.get(position).isSelected;
}
#Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
int position = (int)buttonView.getTag();
recyclerList.get(position).isSelected = isChecked;
}
Hope it will help you, Please let me know if you need any further explanation :)
So I think you question is answered below by #Pedro Oliveira. The main sense of RecycleView, that he using special algorithms for caching ViewHolder in any time. So next onBindViewHolder(...) may not work, for ex. if view is static, or something else.
And about your question you think to use RecycleView for dynamic changed Views. DON'T DO IT! Because RecycleView invalidates views and has caching system, so you will have a lot of problems.
Use LinkedListView for this task!

ViewPager - update view after adapter got empty

I'm using a FragmentStatePagerAdapter with a ViewPager.
Everything is working fine. If I open my activity with an empty Adapter, the ViewPageris empty, if I add items, the ViewPager updates correctly.
BUT, if I open my activity and delete the last item of my ViewPager, the ViewPagerdoes not invalidate correctly and keeps the last Fragment visible.
How can I avoid this?
I'm using my library, it's a wrapper class for ViewPager + ViewPagerIndicator + FragmentPager(State)Adapter:
The class itself is placed here: https://github.com/MichaelFlisar/PagerManager/blob/master/src/com/michaelflisar/pagermanager/MFragmentPagerStateAdapter.java
The implementation is placed here: https://github.com/MichaelFlisar/PagerManager/blob/master/src/com/michaelflisar/pagermanager/MPagerAdapterHelper.java
It implements a simple FragmentStatePagerAdapter with weak references to it's fragments...
My code looks like following:
mPagerManager = new MPagerManager<ExerciseViewFragment, MFragmentPagerStateAdapter<ExerciseViewFragment>>(pager, tpi,
new MFragmentPagerStateAdapter<ExerciseViewFragment>(fragmentManager)
{
#Override
public CharSequence getPageTitle(int pos)
{
return mData.workout.getWExercise().get(pos).getExercise().getName();
}
#Override
public int getCount()
{
return mData.workout.getWExercise().size();
}
#Override
public ExerciseViewFragment createFragment(int pos)
{
return ExerciseViewFragment.newInstance(pos, mData.workout.getWExercise());
}
});
I'm calling mPagerManager.notifyDataSetChanged(); which forwards the call to the FragmentPagerStateAdapter directly...
PS: I know, I can make it invisible, if item count is 0... But I'm wondering if there's a better solution
This is an old question but I thought you might still need to know what to do. It's very common issue. ViewPager does not invalid views which are already created(including these which are ready on the left and right side of your current view).
Solving this is very easy. Just implement the following method in your adapter like this:
#Override
public int getItemPosition(Object object) {
return POSITION_NONE;
}
By default this method returns PagerAdapter.POSITION_UNCHANGED.
For most developers this method usage is misleading, I had this problem myself till I realised that this method is used by ViewPager.dataSetChanged() to establish which items should be recreated. With above code you tell ViewPager to recreate all items whenever data set change.

Load only one Fragment in ViewPager

I have used ViewPager with Fragment.But it loads two pages at time.Is there any way to load only one page in viewpager ?
This might be the thing you are looking for:
mPager.setOffscreenPageLimit(n); // where n is the number of offscreen pages you want to load.
**The minimum value of it can be "1" as you said. ** check this link and also read the comments.
setOffScreenPageLimit method has a limit of minimum 1.
If you'll try to set smaller limit than this, it will use its default value which is 1.
You can try to do manual your adapter such as
public int currentIndex;
#Override
public Fragment getItem(int index) {
if(position == currentIndex){
return new EmptyFragment();
}else{
return new YourNormalFragment();
}
}
and becareful to modify
yourViewPager.setCurrentItem(index);
along with
yourAdapter.currentIndex = index;

Endless adapter for ViewPager

I've been using CWAC's EndlessAdapter to achieve infinite scrolling on ListViews.
I'd like to accomplish the equivalent for a ViewPager. Unfortunately, PageAdapter and ListAdapter do not share the same common base class.
What's the best way to go about this? Does a library exist that already handles this?
What's the best way to go about this?
Add "endless" logic to your own implementation of PagerAdapter. Or, if you wish, try creating a decorating PagerAdapter, the way that EndlessAdapter decorates a regular Adapter.
The latter is likely to be tricky, given that PagerAdapter is designed for pages to be views or fragments, and the fragment handling inside of classes like FragmentPagerAdapter is a bit scary.
Does a library exist that already handles this?
None that I am aware of.
Mainly, that is because the use case doesn't seem as compelling. With a ListView, the user can fling the list, scrolling through dozens or hundreds of rows very quickly. Hence, using "we got to the end" as the trigger to load more data seems reasonable. With a ViewPager, though, it typically takes a lot longer to get to the end, particularly if you are not using PagerTabStrip or the equivalent. Hence, waiting until the user gets all the way to the end to begin loading additional data seems like it would be annoying to the user -- you had all this time to go retrieve more data, but didn't use it.
An alternative, therefore, is for you to register a ViewPager.OnPageChangeListener with your ViewPager. When onPageSelected(), and you consider yourself to be close to the end, kick off an AsyncTask (or whatever) to go gather more data. The catch then is that you will need to update the data used by the PagerAdapter and call notifyDataSetChanged() on that adapter once the data has been updated.
#Override
public int getCount() {
return (Integer.MAX_VALUE);
//artificially large value for infinite scrolling
}
public int getRealCount(){
//Do something to return the actual number of objects.
}
#Override
public Object instantiateItem(ViewGroup container, int position) {
int virtualPosition = position % getRealCount();
return instantiateVirtualItem(container, virtualPosition);
}
public Object instantiateVirtualItem(ViewGroup container, final int position) {
//Do the required part here
}
#Override
public void destroyItem(ViewGroup container, int position, Object object) {
int virtualPosition = position % getRealCount();
destroyVirtualItem(container, virtualPosition, object);
}
public void destroyVirtualItem(ViewGroup container, int position, Object object){
container.removeView((View) object);
}
Now, the most important part
pager.setOffscreenPageLimit(10); //your choice
pager.setCurrentItem(Integer.MAX_VALUE/2,false);
//pager is the ViewPager object
PS: I have successfully implemented this. Ask if you still have doubt.
Maybe you can 'fake it out' as follows:
You are likely to show a huuuuge number of pages. Use FragmentStatePagerAdapter class:
https://developer.android.com/reference/android/support/v13/app/FragmentStatePagerAdapter.html
Implement the getCount method by returning Integer.MAX_VALUE.
Implement the getItemPosition method by always returning POSITION_NONE.
Implement the getItem method as you wish, returning the appropriate Fragment.
Then, when the Activity that hosts the ViewPager starts, set the initial position of the ViewPager to a very large number, e.g. viewPager.setCurrentItem(Integer.MAX_VALUE / 2);.
I haven't tried this myself..., YMMV! :)

Can a ListView contain Fragments

As in, can the ELEMENTS of a ListView be Fragments. I know that you can assign a TextView XML to a ListView to change the way it looks, but can you add Fragments into a ListView.
For instance: I have a Fragment. The XML for said Fragment contains an ImageView, a couple of large-style TextViews, and a small-style TextView. The Fragment class code receives a Bundle, then based on the contents populates the TextViews and ImageView accordingly. Both the Fragment XML and the Fragment code work without issue
(I can display an individual Fragment just fine). I have a FragmentActivity in which I want to display the aforementioned list of Fragments. Here is the code I'm using to try to populate the ListView inside of the FragmentActivity's View:
ArrayList<Fragment> fragList = new ArrayList<Fragment>();
Fragment fragment = Fragment.instantiate(this, TileItem.class.getName());
Bundle bundle = new Bundle();
bundle.putInt("key", 0);
fragment.setArguments(bundle);
fragList.add(fragment);
ArrayAdapter<Fragment> adapter = new ArrayAdapter<Fragment>(this, R.layout.tile_item, fragList);
listItems.setAdapter(adapter);
Here's my mode of thinking on this. I make an ArrayList of Fragments to hold all of my instantiated Views. I then create a Fragment, create a Bundle, add data to the Bundle (so that the Fragment can marshal data into it's Views correctly), add the Bundle to the Fragment, then finally add the Fragment to the ArrayList. After that, I make an ArrayAdapter, add the element layout I want to use, and the list of Fragments I've made; then set the ListView to read from my adapter.
Anyone running this code will likely get the NPE # instantiating the ArrayAdapter. What gives? Is this even possible? Before I keep racking my brain on this can someone tell me if I'm just wasting my time? Is there a better way? I've been thinking of using a ScrollView, but so much of the functionality of a ListView would need to re-implemented and I hate-hate-hate reinventing the wheel when it's not necessary.
Thanks to anyone reading, and especially thank you for your thoughts if you decide to leave them. I've tried searching around for an established answer to this but all I seem to find are questions/web pages concerning using a ListView INSIDE of a Fragment; not using Fragments AS THE ELEMENTS of a ListView
Edit: I took the suggestions below and started investigating more. From the way things appear I should be able to use a custom adapter that inflates fragments instead of just flat out building from XML (for lack of a better way to describe the process) However, my current implementation is throwing an NPE when trying to set the adapter.
Here is my custom adapter code (shortened for brevity):
public class AdapterItem extends ArrayAdapter<Fragment> {
Context c;
List<Fragment> f;
public AdapterItem(Context c, List<Fragment> f) {
super(c, R.layout.tile_item, f);
this.c = c;
this.f = f;
}
#Override
public View getView(int pos, View v, ViewGroup vg) {
LayoutInflater i = (LayoutInflater) c.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
return i.inflate(R.layout.tile_item, vg, false);
}
}
and here is how I'm implementing it:
ArrayList<Fragment> fragList = new ArrayList<Fragment>();
Fragment fragment = Fragment.instantiate(this, TileItem.class.getName());
Bundle bundle = new Bundle();
bundle.putInt("key", 0);
fragment.setArguments(bundle);
fragList.add(fragment);
AdapterItem adapter = new AdapterItem(this, fragList);
adapter.add(fragment);
listItems.setAdapter(adapter);
So it's been a few days and I'm pretty sure this thread has been buried. However, I thought I would add one last update just in case someone wants to try this and a google search brings them here. So in my implementation I'm getting an NPE when the ListView is given the adapter. It doesn't take a rocket surgeon to figure out that it's certainly the adapter and not the ListView throwing the error. For the life of me I can't figure out why though...
At any rate, I think I have some idea though. First, a little back story: A while back I was trying to make FragmentTransactions inside of a FragmentDialog. Everytime I attempted to do so, I would get an NPE. Eventually, through much research, I discovered that the reason pertained to the way that Fragments are instanced. When a Fragment is called it needs the context from it's parent. Since a Dialog's parent is the Activity that started it, the Dialog itself didn't meet the criteria necessary. I believe, that when attempting to add fragments to a ListView, this is also the case. Since the ListView doesn't meet the agreement with instancing a Fragment it throws the NPE and thus, leaves me hanging and going back to conventions. D#mn...I had really hoped I would be able to do this. Using Fragments instead of simple XML would have made it so much easier to organize/search through the list. Oh well... guess it can't be done in case anyone is wondering.
I'd say this is not possible to do as putting a fragment in a ListView would mean the fragment can be multiplied across multiple containers. When you use the FragmentManager to create a fragment, it is tagged with an identifier, making it simple to reload and rearrange on orientation and other configuration changes. It also encourages uses across multiple device configs.
A Fragment is really a subset of an Activity. Would you ever have an Activity as part of a list? Definitely not (should be the answer!)!!!
Moreover, it is not very useful to attach() and detach() a fragment continuously as they move in and out of view (cells get recycled). These are all expensive operations that a ListView shouldn't deal with. Lists should scroll quickly.
From the conversation on the comments, I can see you want to achieve nice code with a good separation of view setup code and adapter in the Activity. Do so with either:
Override the View class and do your custom drawing and setup there.
Create a new class, in which you supply a context and data set required for it to get you back the view a list needs to show - this is what I usually do.
Have a Utils class to build your video elsewhere (silly).
Just don't use Fragments in Lists. Not the use case they are aiming for. HTH.
It turns out that you can create a ListView where each item in the listView is a Fragment. The trick is wrapping the Fragment in a FrameLayout.
UPDATE 9/16/2014
Even though it is possible to create a ListView that contain Fragments, it doesn't look like it's a good idea. This seems to definitely be a corner case in the Android world and there be dragons. For a simple fragment like the one in the example below everything works beautifully, but if you have a complex project with a lot going on in it then this is probably not the way to go. My new approach is to pull all of the GUI related code into a View that extends FrameLayout, and insert that into a the ListView -- this works MUCH BETTER and is more in line with how Android expects to be used. If you need the functionality of a Fragment in other parts of your code, you can simply use this new View there too.
Back to the original answer...
I've added a new ManyFragments example to my AnDevCon 14 Fragments example app if you want to try it out. Essentially it comes down the the BaseAdapter, which in my example looks like this:
BaseAdapter adapter = new BaseAdapter() {
#Override public int getCount() { return 10000; }
#Override public Object getItem(int i) { return new Integer(i); }
#Override public long getItemId(int i) { return i; }
#Override
public View getView(int i, View view, ViewGroup viewGroup) {
if (view!=null){
ManyListItemFragment fragment = (ManyListItemFragment) view.getTag();
fragment.setCount(i);
} else {
FrameLayout layout = new FrameLayout(getActivity());
layout.setLayoutParams(frameLayoutParams);
int id = generateViewId();
layout.setId(id);
ManyListItemFragment fragment = new ManyListItemFragment();
fragment.setCount(i);
getChildFragmentManager()
.beginTransaction()
.replace(id,fragment)
.commit();
view = layout;
view.setTag(fragment);
}
return view;
}
};
In case you're curious here's generateViewId():
#TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
public static int generateViewId() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
for (;;) {
final int result = sNextGeneratedId.get();
// aapt-generated IDs have the high byte nonzero; clamp to the range under that.
int newValue = result + 1;
if (newValue > 0x00FFFFFF) newValue = 1; // Roll over to 1, not 0.
if (sNextGeneratedId.compareAndSet(result, newValue)) {
return result;
}
}
} else {
return View.generateViewId();
}
}
private static final AtomicInteger sNextGeneratedId = new AtomicInteger(1);
You don't need to use Fragments.
Write a custom ViewAdapter and have it inflate a more complex layout (or maybe several more complex layouts if you need to get really fancy) then populate the fields of the layout as necessary.
[Aside: to the people who answered in comments -- please use answers rather than comments if you are actually answering the question! If only because you get more reputation points that way!]

Categories

Resources