I use a ViewPager with many WebViews, it is for showing an ePub. My Problem is that the WebViews are just rendered/loaded (im not quite sure) wenn their corresponding page becomes visible. After this the page doesn't need to be re-rendered until it is destroyed from the ViewPagerAdapter.
The consequence is that there is always a white page for a little while. How can i pre-render the page that it scrolls smooth to next webView (which was not rendered before).
This is my PagerAdapter:
public class MagazineReaderPagerAdapter extends PagerAdapter {
private MagazineReaderActivity activity;
private EpubDocument epub;
public MagazineReaderPagerAdapter(Context ctx, EpubDocument epub)
{
activity = (MagazineReaderActivity) ctx;
this.epub = epub;
}
#Override
public int getCount() {
return epub.getContentDocuments().size();
}
#Override
public Object instantiateItem(ViewGroup container, int position) {
ReaderWebViewMulti view = new ReaderWebViewMulti(activity);
view.loadContentDocument(epub.getContentDocuments().get(position), epub);
((ViewPager) container).addView(view, 0);
return view;
}
#Override
public boolean isViewFromObject(View view, Object object) {
return view.equals(object);
}
#Override
public void destroyItem(ViewGroup container, int position, Object object) {
((ViewPager) container).removeView((View) object);
}
}
ReaderWebViewMulti extends from WebView ind implements a method loadContentDocument wich loads the content via loadDataWithBaseURL.
edit:
At activity-oncreate the setOffscreenPageLimit is set to 3
viewPager.setOffscreenPageLimit(3);
To point out what the problem is, i made a little video on YouTube
From second 4 you can see that every page is just rendered when it is already visible. When i go back everything is fine.
I had the same problem and fixed it by disabling hardware acceleration for the web view:
webView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
See WebView inside ViewPager or ScrollView - weird rendering bug on Android 3.0+
I put some thoughts into this. Scrolling the screen 1 pixel would render the right page:
class SomeClass implements OnPageChangeListener {
private ViewPager viewPager;
...
viewPager = ...
viewPager.setOnPageChangeListener(this);
...
#Override
public void onPageScrollStateChanged(int state) {
if (state == ViewPager.SCROLL_STATE_IDLE) {
// viewpager finished scrolling to a page
viewPager.scrollBy(1, 0);
}
}
}
You would need to check if you are coming from right or left and according to this scroll by -1 or 1 pixels (the page where you are coming from will stay in memory, so you don't need to reload that one). Only at the the very first scroll (after opening the app) the problem described above will still persist.
Drawback: you can see the page scrolling 1 pixel, if you look real closely.
Someone with more time than me could dive into the android code to see what exactly happends when 1 pixel comes into the screen, and mimic some of this behaviour.
set this property for your application in manifest file
android:hardwareAccelerated="false"
Related
I've been trying to use ViewPagerAndroid to show a carousel with "stops" at each item like so:
It appears react-native-carousel and react-native-swiper don't support these features yet on Android. I have a native ViewPager that does what I need it to do, but I'm having problems incorporating it into the React Native environment.
The native component CarouselContainer uses layout.xml and expects a ViewPager subcomponent.
<com.mycompany.ui.CarouselContainer
android:id="#+id/pager_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp">
<android.support.v4.view.ViewPager
android:id="#+id/viewpager"
android:layout_width="190dp"
android:layout_height="280dp"
android:layout_gravity="center_horizontal" />
</com.mycompany.ui.CarouselContainer >
CarouselContainer is a FrameLayout subclass that gets a ref to the ViewPager in its onFinishInflate method. It calls setPageMargin on the ViewPager and setClipChildren(false) to render multiple ViewPager pages on the screen at the same time.
I took a look at the ViewPagerAndroid implementation, and it looks like it's basically a ViewPager set up by ReactViewPagerManager. The ReactViewPagerManager exposes the ViewPager's child views via the ViewGroupManager interface.
So I copied ReactViewPagerManager in my own CarouselViewPagerManager and ReactViewPager into CarouselViewPager.
The one thing new here is CarouselViewPagerContainer. The manager creates the container and the container creates the view pager instead of the manager creating the view pager directly. Since there's no layout xml the container instantiates the pager directly instead of getting it from onFinishInflate.
For some reason, I'm getting nothing rendered on the screen when I use it. I think it's due to having the CarouselViewPagerContainer view in between but I'm not sure where to even begin. Do I need to mess with LayoutShadowNode? Do I need to add the CarouselViewPager itself to the list of child views returned by the manager?
The Native UI Components documentation only mentions SimpleViewManager. :(
// Copy of ReactViewPagerManager
public class CarouselViewPagerManager extends ViewGroupManager<CarouselViewPagerContainer> {
#Override
protected CarouselViewPagerContainer createViewInstance(ThemedReactContext reactContext) {
return new CarouselViewPagerContainer(reactContext);
}
// ...
#Override
public void addView(CarouselViewPagerContainer parent, View child, int index) {
parent.addViewToAdapter(child, index);
}
#Override
public int getChildCount(CarouselViewPagerContainer parent) {
// Should the CarouselViewPager be also counted?
return parent.getViewCountInAdapter();
}
#Override
public View getChildAt(CarouselViewPagerContainer parent, int index) {
return parent.getViewFromAdapter(index);
}
#Override
public void removeViewAt(CarouselViewPagerContainer parent, int index) {
parent.removeViewFromAdapter(index);
}
}
public class CarouselViewPagerContainer extends FrameLayout implements ViewPager.OnPageChangeListener {
private final CarouselViewPager mPager; // Copy of ReactViewPager
public CarouselViewPagerContainer(ReactContext context) {
mPager = new CarouselViewPager(context);
}
// ...
// Pass through to the CarouselViewPager
public void setCurrentItemFromJs(int item, boolean animated) {
mPager.setCurrentItemFromJs(item, animated);
}
/*package*/ void addViewToAdapter(View child, int index) {
mPager.addViewToAdapter(child, index);
}
/*package*/ void removeViewFromAdapter(int index) {
mPager.removeViewFromAdapter(index);
}
/*package*/ int getViewCountInAdapter() {
return mPager.getViewCountInAdapter();
}
/*package*/ View getViewFromAdapter(int index) {
return mPager.getViewFromAdapter(index);
}
}
If you know of a simpler way to tackle this problem I'm all ears!
I mean the only thing I'm using the FrameLayout container for is to center the first Viewpager page and occupy space so multiple pages show. :/
It's been such a struggle :(
I'm making an application with the ViewPager class and with a FragmentStatePagerAdapter adapter. I've read that the difference between the mentioned adapter and FragmentPagerAdapter is that the later stores all pages in memory at once, whereas FragmentStatePagerAdapter has only 3 loaded in memory at any given time.
So, here is the issue. I have a ViewPager with about 50 pages. There is a fragment on each page with a single ImageView image(and some other elements). After scrolling through around 20 unique pages, I usually get the Out Of Memory Error. So, my question is: How am I supposed to configure FragmentStatePagerAdapter to only have about 3 pages loaded in memory at any given time? This is the code for my adapter:
mViewPager.setAdapter(new FragmentStatePagerAdapter(fm) {
#Override
public Fragment getItem(int position) {
Song song = mSongs.get(position);
return PlayFragment.newInstance(position);
}
#Override
public int getCount() {
return mSongs.size();
}
#Override
public void destroyItem(View collection, int position, Object o) {
View view = (View)o;
((ViewPager) collection).removeView(view);
view = null;
}
#Override
public Object instantiateItem(View context, int position) {
ImageView imageView = new ImageView(getApplicationContext());
imageView.findViewById(R.id.albumimage);
imageView.setImageBitmap(BitmapFactory.decodeResource(getResources(), position));
((ViewPager) context).addView(imageView);
return imageView;
}
});
The destroyItem and instantiateItem methods currently do nothing. I've added them after reading about this from someone else's question. There is no difference as of now if I have these two methods in my code or not.
I've read other questions similar to mine, but I have finally decided to ask a question after having attempted to solve the problem on my own with no good results.
I tried setting the ImageView to null in onDestroy(), but nothing happened.
Bitmap created by BitmapFactory.decodeResource(getResources(), position) has to be released manually by calling Bitmap.recycle()
https://developer.android.com/training/displaying-bitmaps/manage-memory.html
I've started using bitmaps as input for ImageView. The code below works fine.
albumimg = BitmapFactory.decodeFile(mSong.getImg());
mImg.setImageBitmap(albumimg);
mImg.setVisibility(View.VISIBLE);
And this in onDestroy() and onDestroyView():
if(albumimg != null) {`albumimg.recycle(); }`
Thanks for the help. :)
I have a challenging problem and wanted to see if the community has some comments about its implementation.
I have a ViewPager and added 20 pages in it, and each page has a WebView. – This provides vertical page flipping between multiple websites
It should work as a ViewPager by default but when I pinch to zoom, each page will shrink and the ViewPager will scroll like a List View.
I didn’t want to use a different view like a List View as I want a smooth transition and the two modes use the same web contents so want to avoid any construction/deconstruction that might affect a smooth transition.
I was able to implement this scrolling ViewPager with VelocityViewPager.
What I want to do is add a scrollbar to navigate down the pages by holding and dragging a scroller, just like a PC’s behaviour – as opposed to a ListView which only shows the scrollbar but allows no interaction. I have made a custom view that looks like a scrollbar and performs a ViewPager’s fake drag as the scrollbar is moved. The problem is I can only scroll pages ViewPager is holding - so when the ViewPager keeps the whole 20 pages, it works well but If I keep 3 pages as default to make use of page recycling, it doesn’t scroll past 3. It seems as though it doesn’t create the next pages that it usually would when scrolling with your finger.
I’m not sure why ViewPager doesn’t create next pages while fake dragging. Is there anything I have to do other than beginFakeDrag(), endFakeDrag() and fakeDragBy()?
And, Is there any way I can implement this behaviour without keeping all 20 pages in ViewPager?
Have a look into this code, this may be useful to you:
public class MainActivity extends Activity {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ViewPagerAdapter adapter = new ViewPagerAdapter(this, imageArra);
ViewPager myPager = (ViewPager) findViewById(R.id.myfivepanelpager);
myPager.setAdapter(adapter);
myPager.setCurrentItem(0);
}
private int imageArra[] = { R.drawable.gallery_photo_1, R.drawable.gallery_photo_2,
R.drawable.gallery_photo_3, R.drawable.gallery_photo_5,
R.drawable.gallery_photo_6, R.drawable.gallery_photo_7,
R.drawable.gallery_photo_8, R.drawable.ic_launcher };
}
and ViewPager class is:
public class ViewPagerAdapter extends PagerAdapter {
Activity activity;
int imageArray[];
public ViewPagerAdapter(Activity act, int[] imgArra) {
imageArray = imgArra;
activity = act;
}
public int getCount() {
return imageArray.length;
}
public Object instantiateItem(View collection, int position) {
ImageView view = (ImageView) collection.findViewById(R.id.image);
/*
* view.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT,
* LayoutParams.FILL_PARENT)); view.setScaleType(ScaleType.FIT_XY);
*/
view.setBackgroundResource(imageArray[position]);
((ViewPager) collection).addView(view, 0);
return view;
}
#Override
public void destroyItem(View arg0, int arg1, Object arg2) {
((ViewPager) arg0).removeView((View) arg2);
}
#Override
public boolean isViewFromObject(View arg0, Object arg1) {
return arg0 == ((View) arg1);
}
#Override
public Parcelable saveState() {
return null;
}
}
We're suffering from a very strange issue with ViewPager here. We embed lists on each ViewPager page, and trigger notifyDataSetChanged both on the list adapter and the view pager adapter when updating list data.
What we observe is that sometimes, the page does not update its view tree, i.e. remains blank, or sometimes even disappears when paging to it. When paging back and forth a few times, the content will suddenly reappear. It seems as if Android is missing a view update here. I also noticed that when debugging with hierarchy viewer, selecting a view will always make it reappear, apparently because hierarchy viewer forces the selected view to redraw itself.
I could not make this work programmatically though; invalidating the list view, or the entire view pager even, had no effect.
This is with the compatibility-v4_r7 library. I also tried to use the latest revision, since it claims to fix many issues related to view pager, but it made matters even worse (for instance, gestures were broken so that it wouldn't let me page through all pages anymore sometimes.)
Is anyone else running into these issues, too, or do you have an idea of what could be causing this?
If the ViewPager is set inside a Fragment with a FragmentPagerAdapter, use getChildFragmentManager() instead of getSupportFragmentManager() as the parameter to initialize your FragmentPagerAdapter.
mAdapter = new MyFragmentPagerAdapter(getChildFragmentManager());
Instead of
mAdapter = new MyFragmentPagerAdapter(getSupportFragmentManager());
We finally managed to find a solution. Apparently our implementation suffered of two issues:
our adapter did not remove the view in destroyItem().
we were caching views so that we'd have to inflate our layout just once, and, since we were not removing the view in destroyItem(), we were not adding it in instantiateItem() but just returning the cached view corresponding to the current position.
I haven't looked too deeply in the source code of the ViewPager - and it's not exactly explicit that you have to do that - but the docs says :
destroyItem()Remove a page for the given position. The adapter is responsible for removing the view from its container, although it only must ensure this is done by the time it returns from finishUpdate(ViewGroup).
and:
A very simple PagerAdapter may choose to use the page Views themselves as key objects, returning them from instantiateItem(ViewGroup, int) after creation and adding them to the parent ViewGroup. A matching destroyItem(ViewGroup, int, Object) implementation would remove the View from the parent ViewGroup and isViewFromObject(View, Object) could be implemented as return view == object;.
So my conclusion is that ViewPager relies on its underlying adapter to explicitly add/remove its children in instantiateItem()/destroyItem(). That is, if your adapter is a subclass of PagerAdapter, your subclass must implement this logic.
Side note: be aware of this if you use lists inside ViewPager.
I had the exact same problem but I actually destroyed the view in destroyItem (I thought). The problem however was that I destroyed it using viewPager.removeViewAt(index); insted of viewPager.removeView((View) object);
Wrong:
#Override
public void destroyItem(ViewGroup viewPager, int position, Object object) {
viewPager.removeViewAt(position);
}
Right:
#Override
public void destroyItem(ViewGroup viewPager, int position, Object object) {
viewPager.removeView((View) object);
}
ViewPager tries to do clever stuff around re-using items, but it requires you to return new item positions when things have changed. Try adding this to your PagerAdapter:
public int getItemPosition (Object object) { return POSITION_NONE; }
It basically tells ViewPager that everything has changed (and forces it to re-instantiate everything). That's the only thing I can think of off the top of my head.
Tried too many solutions but unexpectedly viewPager.post() worked
mAdapter = new NewsVPAdapter(getContext(), articles);
viewPager.post(new Runnable() {
#Override
public void run() {
viewPager.setAdapter(mAdapter);
}
});
The Android Support Library has a demo Activity that includes a ViewPager with a ListView on every page. You should probably have a look and see what it does.
In Eclipse (with Android Dev Tools r20):
Select New > Android Sample Project
Select your target API level (I suggest the newest available)
Select Support4Demos
Right-click the project and select Android Tools > Add Support Library
Run the app and select Fragment and then Pager
The code for this is in src/com.example.android.supportv4.app/FragmentPagerSupport.java. Good luck!
I ran into this and had very similar issues. I even asked it on stack overflow.
For me, in the parent of the parent of my view someone subclassed LinearLayout and overrode requestLayout() without calling super.requestLayout(). This prevented onMeasure and onLayout from being called on my ViewPager (although hierarchyviewer manually calls these). Without being measured they'll show up as blank in ViewPager.
So check your containing views. Make sure they subclass from View and don't blindly override requestLayout or anything similar.
Had the same issue, which is something to do with ListView (because my empty view shows up fine if the list is empty). I just called requestLayout() on the problematic ListView. Now it draws fine!
I ran into this same problem when using a ViewPager and FragmentStatePagerAdapter. I tried using a handler with a 3 second delay to call invalidate() and requestLayout() but it didn't work. What did work was resetting the viewPager's background color as follows:
MyFragment.java
private Handler mHandler;
private Runnable mBugUpdater;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View rootView = new ViewPager(getActivity());
//...Create your adapter and set it here...
mHandler = new Handler();
mBugUpdater = new Runnable(){
#Override
public void run() {
mVp.setBackgroundColor(mItem.getBackgroundColor());
mHandler = null;
mBugUpdater = null;
}
};
mHandler.postDelayed(mBugUpdater,50);
return rootView;
}
#Override
public void onPause() {
if(mHandler != null){
//Remove the callback if it hasn't triggered yet
mHandler.removeCallbacks(mBugUpdater);
mHandler = null;
mBugUpdater = null;
}
super.onPause();
}
I had a problem with the same symptoms, but a different cause that turned out to be a silly mistake on my part. Thought I'd add it here in case it helps anyone.
I had a ViewPager using FragmentStatePagerAdapter which used to have two fragments, but I later added a third. However, I forgot that the default off screen page limit is 1 -- so, when I'd switch to the new third fragment, the first one would get destroyed, then recreated after switching back. The problem was that my activity was in charge of notifying these fragments to initialize their UI state. This happened to work when the activity and fragment lifecycles were the same, but to fix it I had to change the fragments to initialize their own UI during their startup lifecycle. In the end I also wound up changing setOffscreenPageLimit to 2 so that all three fragments were kept alive at all times (safe in this case since they were not very memory intensive).
I had similar issue. I cache views because I need only 3 views in ViewPager. When I slide forward everything is okay but when I start to slide backward occurs error, it says that "my view already has a parent". The solution is to delete unneeded items manually.
#Override
public Object instantiateItem(ViewGroup container, int position) {
int localPos = position % SIZE;
TouchImageView view;
if (touchImageViews[localPos] != null) {
view = touchImageViews[localPos];
} else {
view = new TouchImageView(container.getContext());
view.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
touchImageViews[localPos] = view;
}
view.setImageDrawable(mDataModel.getPhoto(position));
Log.i(IRViewPagerAdpt.class.toString(), "Add view " + view.toString() + " at pos: " + position + " " + localPos);
if (view.getParent() == null) {
((ViewPager) container).addView(view);
}
return view;
}
#Override
public void destroyItem(ViewGroup container, int position, Object view) {
// ((ViewPager) container).removeView((View) view);
Log.i(IRViewPagerAdpt.class.toString(), "remove view " + view.toString() + " at pos: " + position);
}
..................
private static final int SIZE = 3;
private TouchImageView[] touchImageViews = new TouchImageView[SIZE];
For me the problem was coming back to the activity after the app process was killed. I am using a custom view pager adapter modified from the Android sources.The view pager is embedded directly in the activity.
Calling viewPager.setCurrentItem(position, true);
(with animation) after setting the data and notifyDataSetChanged() seems to work, but if the parameter is set to false it doesn't and the fragment is blank. This is an edge case which may be of help to someone.
For Kotlin users:
In your fragments;
Use childFragmentManager instead of viewPagerAdapter
I need to create ViewPager in Android with 5 slides, each consists of image and text. I have an array with resources for images:
private static final int[] images = {R.drawable.tutorial_step_01, R.drawable.tutorial_step_02, R.drawable.tutorial_step_03, R.drawable.tutorial_step_04, R.drawable.tutorial_step_05, R.drawable.tutorial_step_06};
then I create adapter:
#Override
public Object instantiateItem(ViewGroup container, int position) {
LinearLayout tv = (LinearLayout) inflater.inflate(R.layout.tut_slide, null);
TextView title = (TextView) tv.findViewById(R.id.tut_title);
title.setText(getResources().getText(titles[position]));
TextView content = (TextView) tv.findViewById(R.id.tut_content);
ImageView image = (ImageView) tv.findViewById(R.id.tut_image);
slide_image = BitmapFactory.decodeResource(getResources(), images[position]);
image.setImageBitmap(slide_image);
content.setText(getResources().getText(contents[position]));
((ViewPager) container).addView(tv, 0);
return tv;
}
#Override
public void destroyItem(ViewGroup container, int position, Object object) {
((ViewPager) container).removeView((LinearLayout) object);
//
}
trouble is that fact android don't want to collect image after I choose another page. So, after 10-15 changes it goes out with OutOfMemory exception. Then I added to initializung rows
if (slide_image!= null) {
slide_image.recycle();
System.gc();
}
And it's work good! But except one thing: I have black screen instead of first image, whcih is replaced by real one after few flips. So I don't know what to do with such memory leaking
Well, I solved the problem finally. I faced it with a very similar case and as I've seen so many questions related to the same problem, I chose this question as it's yet not answered.
The PagerAdapter should call the destroyItem method not only when it surpasses the offLimitScreenPageLimit but also when a screen rotation occurs, but it doesn't, so it has to be forced to do so... to achieve it, you just have to set to null the adapter on the onStop or onDestroy method of the activity.
#Override protected void onDestroy(){
pager.setAdapter(null);
}
Cheers!
It's not clear what you are using but I encountered a similar problem.
I'm assuming you are using FragmentPagerAdapter.
When you scroll away using that adapter, it does not destroy the pages out of view and out of cache. If there is an ImageView in a fragment used by FragmentPageAdapter, OOM is inevitable
Just change the extend of the adapter to
FragmentStatePagerAdapter
This will destroy the fragments not in use and leave more memory free for new fragments.
It's still not perfect, I have found that sometimes I can scroll faster than the garbage collector picks up the destroyed bitmaps, but its pretty damn close.
If I was looking to improve it, I would override destroyItem, and then get the bitmap in use from the imageview and .recycle the bitmap.
Recycle ImageView's Bitmap
That behaviour shouldn't be related with a memory leak. It looks like it's related to when and how you recycle bitmaps within your viewpager updating lifecycle. Try calling onPageSelected() or notifyDatasetChanged() manually at some point on your initialization.
This solutions might not solve the problem completely, but give it a try. It's hard to tell with your explanation.
In My case I have 31 page in ViewPager. I use this :
#Override
public void destroyItem(#NonNull ViewGroup container, int position, #NonNull Object object) {
if(dbCon!=null)
dbCon.close();
ViewPager viewPager = (ViewPager)container;
View view = (View) object;
viewPager.removeView(view);
}
and everthing works fine. Alhamdulillah.