I've met the problem that trouble me for days.
There is a ViewPager
in the main activity which holds 3 Fragment
s as tab fragments. In the first fragment there is a ListView
which holds some views, and which is the most important, another ViewPager
. I want to hold some photos in the sub ViewPager
, and use some more fragments here.
Now there is the trouble:
When the first Fragment
is stopped (The third fragment in the parent ViewPager
is seen on screen) and resumed (the user switch to the second fragment), the app crashes and the debugger says:
java.lang.IllegalArgumentException: No view found for id 0x7f05008b (com.example.viewpager:id/sub_viewpager) for fragment ScreenSlidePageFragment
I've already use the getChildFragmentManager()
as this is a situation of nested fragments.
Here is the key code of list adapter corresponding to the first fragment in the parent ViewPager:
@Override public View getView(int position, View convertView, ViewGroup parent) { int type = getItemViewType(position); switch (type) { case TYPE_BANNER: if (convertView == null) { convertView = mBannerView.getBannerView(parent); } mBannerView.update(convertView); break; case TYPE_ITEM: break; } return convertView; }
Here is the code of mBannerView
:
public class BannerView { private static final DisplayImageOptions IMAGE_OPTIONS_SCALE_STRETCHED = new DisplayImageOptions.Builder() .cacheInMemory() .cacheOnDisc() .imageScaleType(ImageScaleType.EXACTLY_STRETCHED) .build(); private FragmentActivity mActivity; private Fragment mFragment; private List<Banner> mBanners; private ScreenSlidePagerAdapter mPagerAdapter; private ViewPager mViewPager; public BannerView(FragmentActivity activity, Fragment fragment) { mActivity = activity; mFragment = fragment; } public void update(View convertView) { mViewPager = (ViewPager) convertView; if (mBanners != null && !mBanners.isEmpty()) { if (mPagerAdapter == null) { mPagerAdapter = new ScreenSlidePagerAdapter(mFragment.getChildFragmentManager()); mViewPager.setAdapter(mPagerAdapter); } } mViewPager.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (mOnBannerClickListener != null) { mOnBannerClickListener.onBannerClick(); } } }); } class ScreenSlidePagerAdapter extends FragmentStatePagerAdapter { public ScreenSlidePagerAdapter(FragmentManager fm) { super(fm); } @Override public Fragment getItem(int position) { return new ScreenSlidePageFragment(mBanners.get(position).getImageUrl()); } @Override public int getCount() { return mBanners == null ? 0 : mBanners.size(); } } class ScreenSlidePageFragment extends Fragment { private String mUrl; ScreenSlidePageFragment(String url) { super(); mUrl = url; } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.item_banner, container, false); if (view != null) { ImageView imageView = (ImageView) view.findViewById(R.id.item_banner_image); imageView.setLayoutParams(new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT)); ImageLoader.getInstance().displayImage(mUrl, imageView, IMAGE_OPTIONS_SCALE_STRETCHED); } return view; } } }
Here is the detailed error list:
11-10 18:12:19.217 1444-1444/? E/MessageQueue-JNI﹕ java.lang.IllegalArgumentException: No view found for id 0x7f05008b (com.example.viewpager:id/sub_viewpager) for fragment ScreenSlidePageFragment{428d8ea0 #0 id=0x7f05008b} at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:919) at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1104) at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1086) at android.support.v4.app.FragmentManagerImpl.dispatchActivityCreated(FragmentManager.java:1884) at android.support.v4.app.Fragment.performActivityCreated(Fragment.java:1514) at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:947) at android.support.v4.app.FragmentManagerImpl.attachFragment(FragmentManager.java:1280) at android.support.v4.app.BackStackRecord.run(BackStackRecord.java:672) at android.support.v4.app.FragmentManagerImpl.execPendingActions(FragmentManager.java:1467) at android.support.v4.app.FragmentManagerImpl.executePendingTransactions(FragmentManager.java:472) at android.support.v4.app.FragmentPagerAdapter.finishUpdate(FragmentPagerAdapter.java:141) at android.support.v4.view.ViewPager.populate(ViewPager.java:1068) at android.support.v4.view.ViewPager.populate(ViewPager.java:914) at android.support.v4.view.ViewPager$3.run(ViewPager.java:244) at android.support.v4.view.ViewPager.completeScroll(ViewPager.java:1761) at android.support.v4.view.ViewPager.onInterceptTouchEvent(ViewPager.java:1896) at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:1854) at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2211) at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:1912) at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2211) at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:1912) at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2211) at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:1912) at com.android.internal.policy.impl.PhoneWindow$DecorView.superDispatchTouchEvent(PhoneWindow.java:2228) at com.android.internal.policy.impl.PhoneWindow.superDispatchTouchEvent(PhoneWindow.java:1471) at android.app.Activity.dispatchTouchEvent(Activity.java:2424) at com.android.internal.policy.impl.PhoneWindow$DecorView.dispatchTouchEvent(PhoneWindow.java:2176) at android.view.View.dispatchPointerEvent(View.java:7571) at android.view.ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl.java:3883) at android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:3778) at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:3379) at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:3429) at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:3398) at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:3483) at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:3406) at android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:3540) at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:3379) at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:3429) at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:3398) at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:3406) at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:3379) at android.view.ViewRootImpl.deliverInputEvent(ViewRootImpl.java:5419) at android.view.ViewRootImpl.doProcessInputEvents(ViewRootImpl.java:5399) at android.view.ViewRootImpl.enqueueInputEvent(ViewRootImpl.java:5370) at android.view.ViewRootImpl$WindowInputEventReceiver.onInputEvent(ViewRootImpl.java:5493) at android.view.InputEventReceiver.dispatchInputEvent(InputEventReceiver.java:182) at android.os.MessageQueue.nativePollOnce(Native Method) at android.os.MessageQueue.next(MessageQueue.java:132) at android.os.Looper.loop(Looper.java:124) at android.app.ActivityThread.main(ActivityThread.java:5289) at java.lang
Update:
I've read the source code of the FragmentManager and finally got the true reason: This exception happens when the fragments want to be attached to the viewpager before the viewpager is attached to its parent. In other words, before the getView() method returns, the fragments are inflated. Then the findViewById() method of the container of the ViewPager is called but the ViewPager is in detached state yet, so null is found and the IllegalArgumentException is thrown.
The solution is to create a custom ViewPager and lazy set the adapter:
public class BannerViewPager extends ViewPager { PagerAdapter mPagerAdapter; @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); if (mPagerAdapter != null) { super.setAdapter(mPagerAdapter); mPageIndicator.setViewPager(this); } } @Override public void setAdapter(PagerAdapter adapter) { } public void storeAdapter(PagerAdapter pagerAdapter) { mPagerAdapter = pagerAdapter; } public BannerViewPager(Context context) { super(context); } public BannerViewPager(Context context, AttributeSet attrs) { super(context, attrs); } }
And in the getView() method, use storeAdapter() instead of setAdapter.
The following statements are not correct. The words above are the actual reason.
Finally I've got the answer. It consists of two parts.
In the parent ViewPager I used a FragmentPagerAdapter
to hold fragments, but now I use a FragmentStatePagerAdapter
instead. The difference between these two can be found here: Difference between FragmentPagerAdapter and FragmentStatePagerAdapter.
Simply speaking, FragmentPagerAdapter will store more information when a fragment is stopped. In this situation, the first fragment in the parent ViewPager is stopped but not destroyed while the views in this fragment are destroyed. After resumed, the fragment try to re-inflate all the views. But before the getView()
method is called and the sub-ViewPager is recreated, the child FragmentManager tries to find the sub-ViewPager to hold those previously stored fragments. Thus the "java.lang.IllegalArgumentException: No view found for id" occurs.
After I replaced the FragmentPagerAdapter with FragmentStatePagerAdapter, another problem appears. the sub-viewpager is missing when the parent fragment (the first fragment in the parent viewpager) was stopped, destroyed and resumed. This happens when the first fragment is choosen, soon afterwards the third fragment is choosen, and at last the first fragment is re-choosen.
I think this is a bug of android sdk. Inspired by here and here, I use some tricky methods to solve the problem. The point is, when a parent fragment is destroyed, the field member --- mChildFragmentManager "ends up with a broken internal state" and is not totally cleaned up. When the parent fragment is recreated, mChildFragmentManager is not null, but the sub fragments is already been destroyed after the parent fragment being destroyed, which was managered by mChildFragmentManager. Thus the sub ViewPager displays an empty view on the screen, which responds to a fake fragment which is actually not existing. The funny thing is, after swiping right on the sub ViewPager for several times, the sub fragments and the views appear again.
Here is the codes:
Parent Adapter:
@Override public View getView(int position, View convertView, ViewGroup parent) { if (convertView == null) { convertView = getBannerView(mParent); } mViewPager = (ViewPager) convertView; if (mBanners != null && !mBanners.isEmpty()) { if (mPagerAdapter == null) { FragmentManager childFM = mFragment.getChildFragmentManager(); removeOldFragment(childFM); mPagerAdapter = new ScreenSlidePagerAdapter(childFM, mBanners); mViewPager.setAdapter(mPagerAdapter); } } return convertView; }
The key method:
private void removeOldFragment(FragmentManager fm) { try { Field added = fm.getClass().getDeclaredField("mAdded"); added.setAccessible(true); added.set(fm, null); } catch (NoSuchFieldException e) { throw new RuntimeException(e); } catch (IllegalAccessException e) { throw new RuntimeException(e); } try { Field active = fm.getClass().getDeclaredField("mActive"); active.setAccessible(true); active.set(fm, null); } catch (NoSuchFieldException e) { throw new RuntimeException(e); } catch (IllegalAccessException e) { throw new RuntimeException(e); } }
Xieyi did a good job of explaining the reasoning behind the exception.
Here's the solution I found:
@Override public void onPause() { super.onPause(); for ( Fragment f : getChildFragmentManager().getFragments() ) { if ( f instanceof MyFragmentType ) { getChildFragmentManager().beginTransaction().remove( f ).commit(); } } }
I was getting the exception whenever I went to a new fragment and came back. Make sure to remove the fragments in your "cleaup" method, then you can load can reload your views when you come back to the fragment.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With