码迷,mamicode.com
首页 > 其他好文 > 详细

Fragment嵌套带来的坑--页面点击无反应(顺带ViewPager之 FragmentPagerAdapter简单分析)

时间:2016-05-12 19:35:06      阅读:1539      评论:0      收藏:0      [点我收藏+]

标签:

接手别人的老项目。新版本测试提出一个bug:

点击Home最小化的应用—>系统设置界面 改变字体后—>点击进入应用—>3个由viewpager 的fragmentadapter管理的 tab页面点击都没反应。

这是一个比较蛋疼的bug,猜想了很多原因,都不对。
项目的结构是 activity 内有mainfragment,mainfragment又 包含viewpager,viewpager 使用FragmentPagerAdapter 管理3个页面。所以是 activity套2层fragment的结构。

一、大概猜想了一下可能跟这个结构有关,所以先看看FragmentPagerAdapter 的实现。

(注:本文写得比较乱,仅仅是个人笔记,很多地方带?表明本人也不确定,也没弄清楚)
public abstract class FragmentPagerAdapter extends PagerAdapter

    // Do we already have this fragment?
    String name = makeFragmentName(container.getId(), itemId);
    Fragment fragment = mFragmentManager.findFragmentByTag(name);
    if (fragment != null) {
        if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
        mCurTransaction.attach(fragment);
    } else {
        fragment = getItem(position);
        if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment);
        mCurTransaction.add(container.getId(), fragment,
                makeFragmentName(container.getId(), itemId));
    }
    if (fragment != mCurrentPrimaryItem) {
        fragment.setMenuVisibility(false);
        fragment.setUserVisibleHint(false);
    }

    return fragment;

实际上 只是在instantiateItem 中 调用了getItem(position); 这个抽象方法。 其他方法这个adapter 已经基本实现,所以user使用fpadapter的时候 实现getItem方法 就 是给 instantiateItem返回 fragment了。

然而 返回的是一个fragment,并不是view,且PagerAdapter中instantiateItem 要求的只是一个object。 那么ViewPager如何处理这个object?
ItemInfo addNewItem(int position, int index) {
ItemInfo ii = new ItemInfo();
ii.position = position;
ii.object = mAdapter.instantiateItem(this, position);
ii.widthFactor = mAdapter.getPageWidth(position);
if (index < 0 || index >= mItems.size()) {
mItems.add(ii);
} else {
mItems.add(index, ii);
}
return ii;
}

可见会在addNewItem中 将 这个 object封装 成 一个 iteminfo。
private final ArrayList mItems = new ArrayList();
同时将这个info 放在了一个arraylist中、

看看调用栈
04-07 18:43:48.446 14842-14842/? E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.example.testeveryting, PID: 14842
java.lang.NullPointerException
at android.support.v4.app.FragmentPagerAdapter.instantiateItem(FragmentPagerAdapter.java:85)
at android.support.v4.view.ViewPager.addNewItem(ViewPager.java:943)
at android.support.v4.view.ViewPager.populate(ViewPager.java:1091)
at android.support.v4.view.ViewPager.populate(ViewPager.java:1025)
at android.support.v4.view.ViewPager.onMeasure(ViewPager.java:1545)
at android.view.View.measure(View.java:17616)
at android.widget.RelativeLayout.measureChildHorizontal(RelativeLayout.java:719)
at android.widget.RelativeLayout.onMeasure(RelativeLayout.java:455)
at android.view.View.measure(View.java:17616)
at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:5428)
at android.widget.FrameLayout.onMeasure(FrameLayout.java:310)
at android.view.View.measure(View.java:17616)
at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:5428)
at android.widget.LinearLayout.measureChildBeforeLayout(LinearLayout.java:1413)
at android.widget.LinearLayout.measureVertical(LinearLayout.java:696)
at android.widget.LinearLayout.onMeasure(LinearLayout.java:589)
at android.view.View.measure(View.java:17616)
at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:5428)
at android.widget.FrameLayout.onMeasure(FrameLayout.java:310)
at com.android.internal.policy.impl.PhoneWindowDecorView.onMeasure(PhoneWindow.java:2585)atandroid.view.View.measure(View.java:17616)atandroid.view.ViewRootImpl.performMeasure(ViewRootImpl.java:2348)atandroid.view.ViewRootImpl.measureHierarchy(ViewRootImpl.java:1453)atandroid.view.ViewRootImpl.performTraversals(ViewRootImpl.java:1654)atandroid.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1311)atandroid.view.ViewRootImplTraversalRunnable.run(ViewRootImpl.java:6711)
at android.view.ChoreographerCallbackRecord.run(Choreographer.java:813)atandroid.view.Choreographer.doCallbacks(Choreographer.java:613)atandroid.view.Choreographer.doFrame(Choreographer.java:583)atandroid.view.ChoreographerFrameDisplayEventReceiver.run(Choreographer.java:799)
at android.os.Handler.handleCallback(Handler.java:733)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:146)
at android.app.ActivityThread.main(ActivityThread.java:5756)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:515)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1291)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1107)
at dalvik.system.NativeStart.main(Native Method)

正在使用 MyPagerAdapter extends PagerAdapter 的情况下,instantiateItem方法
@Override
public Object instantiateItem(ViewGroup container, int position) {
View view = View.inflate(MainActivity.this, R.layout.adapter_ad, null);
ImageView imageView = (ImageView) view.findViewById(R.id.image);
imageView.setBackgroundResource(list.get(position%list.size()).getIconResId());

        //将view对象添加到viewpager,交给它管理
        container.addView(view);
        return view;
    }

实际上每次返回的view 都要user手动 添加到viewpager中, destory的时候 要手动remove。 保证 viewpager 缓存pager 数目固定 ,与viewpager内部的items数组 数目应该也保持一致。
猜测fragmentpageradapter应该是在下面某句话中 将fragment的视图addview到了viewpager中? 望指正。—–4.8号更新:错,此时fragment 的oncreateview方法还未被 fragmentmanager调用,因此是不会有view视图出现的。下面单步调试中,给出了真正调用的地方。

二、下面是ViewPager中的与adapter相关的实现

实if (fragment != null) {
if (DEBUG) Log.v(TAG, “Attaching item #” + itemId + “: f=” + fragment);
mCurTransaction.attach(fragment);
} else {
fragment = getItem(position);
if (DEBUG) Log.v(TAG, “Adding item #” + itemId + “: f=” + fragment);
mCurTransaction.add(container.getId(), fragment,
makeFragmentName(container.getId(), itemId));
}
addNewItem 会被populate 调用

populate 这个方法200 行,逻辑比较复杂。 大致是 处理 populate(mCurItem)
负责处理当前位置 int currentItem 的 item的显示,以及 这个位置前后 item的 进出,显示,index位置,在 mItems中是否移除 加入 等。
// Check width measurement of current pages and drawing sort order.
// Update LayoutParams as needed.
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
lp.childIndex = i;
if (!lp.isDecor && lp.widthFactor == 0.f) {
// 0 means requery the adapter for this, it doesn’t have a valid width.
final ItemInfo ii = infoForChild(child);
if (ii != null) {
lp.widthFactor = ii.widthFactor;
lp.position = ii.position;
}
}
}
其中一处代码片段,每次滑动viewpager都会调用,可以看出每次都遍历了子孩子 ,动态改变了他们的layoutparams,达到pager随着手指滑动效果。
populate在整个viewpager中 有8处 地方被调用。

1

private final Runnable mEndScrollRunnable = new Runnable() {
public void run() {
setScrollState(SCROLL_STATE_IDLE);
populate();
}
};
用处不明

2

setAdapter时。line447
else if (!wasFirstLayout) {
populate();
} else {
requestLayout();
}
看出来应该是第一次setAdapter会造成 第一次layout,所以调用了 populate来 additem,获得 view展示。

3.

public void setPageTransformer(boolean reverseDrawingOrder, PageTransformer transformer) line 656
用户调用此方法改变page的 位置时?

4.

public void setOffscreenPageLimit(int limit)
if (limit < DEFAULT_OFFSCREEN_PAGES) {
Log.w(TAG, “Requested offscreen page limit ” + limit + ” too small; defaulting to ” +
DEFAULT_OFFSCREEN_PAGES);
limit = DEFAULT_OFFSCREEN_PAGES;
}
if (limit != mOffscreenPageLimit) {
mOffscreenPageLimit = limit;
populate();
}
这是一个比较常用的方法。 可以看到 当user 设置的 offsreen页面 与当前值不符合的时候, 会调用 populate,来 增加或者减少 当前缓存页面。

5.

void smoothScrollTo(int x, int y, int velocity) line838
平滑移动时

6.

onMeasure()
// Make sure we have created all fragments that we need to have shown.
mInLayout = true;
populate();
mInLayout = false;
可见,populate 完后,就把 layout标记设置为false。 确实 是用来 在滑动,layout情况下页面改变后加载页面的。

7.

public boolean onInterceptTouchEvent(MotionEvent ev)
// Let the user ‘catch’ the pager as it animates.
mScroller.abortAnimation();
mPopulatePending = false;
populate();
mIsBeingDragged = true;
在ACTION_DOWN分支中。

8.

public boolean onTouchEvent(MotionEvent ev) line 2043
依然是在ACTION_DOWN分支中。
mScroller.abortAnimation();
mPopulatePending = false;
populate();

            // Remember where the motion event started


一共8处。

除此之外 populate(mItem)这个作为被populate()直接调用的方法,还在void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity)
中被调用,setCurrentItemInternal方法 即setCurrentItem() 所需调用的方法。

三、总结一下:

1.viewpager 并没有像listview一样,将user(使用这个控件的人) 返回的view 内部自己addview 到 viewgroip的成员mChildren[] 数组中。需要user手动在instantiateItem方法中手动调用一次addview。
同时viewpager也没有像listview一样 有从mChildren[]中移除子view的操作,需要用户在destory方法中手动remove。
为何viewpager要这么设计?
2.viewpager内部自己定义了一个ArrayList的 mItems成员 来保存 user返回的object(view),猜想 这个mItems的数量应该和mChildren[]数量应该是一致的?
3.viewpager如果有上百个页面,开始只加载3个,那么在快速滑动的时候 肯定是不断的在创建新的view销毁旧的view,并没有复用机制?viewpager没有复用机制 是因为考虑到每个page条目可能差别较大无法复用?不像listview一样 都属于同一类型?

四、简单分析完了之后 继续回到之前的bug。

猜测 改变字体后 应用中所有activity会销毁 再重新创建。
而在此次创建中,init处的 3个fragment 对象是生成了。如图viewpager的 mItems成员 有3个item对象包含了3个fragment。

但是viewpager的mChildren成员却为空

正常的可以点击显示时应该是这样。

所以猜测viewpager的 setCurrentItem 中的populate应该是操作显示mChildren中的子view来显示ui,而不是控制mItems。
mChildren为空,自然显示不出来,(但是却显示出了应用最小化之前的fragment页面,且这个页面点击无任何反应,猜测是残留了一个视图?怎么做到的?)

因此viewpager的 addView方法一定没有调用。

五、下面看看addview是什么时候被调用的。

1.fmimpl中的方法:

void moveToState(Fragment f, int newState, int transit, int transitionStyle,
boolean keepActive)

在case Fragment.INITIALIZING:分支中
if (f.mFromLayout) {
// For fragments that are part of the content view
// layout, we need to instantiate the view immediately
// and the inflater will take care of adding it.
f.mView = f.performCreateView(f.getLayoutInflater(
f.mSavedFragmentState), null, f.mSavedFragmentState);
if (f.mView != null) {
f.mView.setSaveFromParentEnabled(false);
if (f.mHidden) f.mView.setVisibility(View.GONE);
f.onViewCreated(f.mView, f.mSavedFragmentState);
}
}

可见fmimpl中的moveToState方法中调用了performCreateView ,并将返回的view 赋值给f.mView,这一过程 其实是activity的performStart 调用的。

2.视图赋值到 f.mView中之后,什么时候被viewpager.addview add进来呢?

可见doFrame –由wms发消息 viewrootimpl.w收到消息,发给 哪个handler? ActivityThread .H还是viewrootimpl中的 handler?
—都不对

view包下,Choreographer中的FrameHandler处理
然后handler处理,调用doFrame。 处理这一帧视图?
有run()方法,是Post到主线程 调用vrimpl的 performtraversal。经过层层递归的measure和onMeasure的调用 (16次),终于调用到viewpager的onMeasure—populate

populate中,可以看到调用了viewpager与fragmentpageradaper交互的一个重要方法fragmentpageradaper.finishUpdate()

开启了事物,因此会调用到fmimpl中的moveToState
该方法 对Fragment 的mState进行switch,
case Fragment.INITIALIZING:
case Fragment.CREATED:
case Fragment.ACTIVITY_CREATED:
case Fragment.STOPPED:
case Fragment.STARTED:
等等。
addview则是在CREATE分支中—意思大概是 我已经是出于create状态中了,fragment对象已经生成了,下面该处理显示,处理将我的视图添加到window当中的逻辑了

仔细看上面的大图 可以看到,此时container就是 viewpager。
通过container = (ViewGroup)mContainer.onFindViewById(f.mContainerId);找到。
mContainer是FragmentActivity的callback 对象。

还可以看到此时的mItems数量是2,说明1.fragment对象全部通过inistiateItem放进viewpager的mItems之后(viewpager默认只加载2个pager) 2.才 finishupdate 来addview
上述第一步 应该是activitystart 的时候调用 由ams管理,后面的这一步 通过 wms控制,viewrootimpl 在measure过程中调用。

Debug看看各方法执行顺序
instantiateItem —measure—performTraversal position0
instantiateItem —measure—performTraversal position1

Fragment.onCreate—–case Fragment.INITIALIZING: f.performCreate(f.mSavedFragmentState);——-measure—performTraversal position0

Fragment.onCreate—–case Fragment.INITIALIZING: f.performCreate(f.mSavedFragmentState);——-measure—performTraversal position1

Fragment.onCreateView—–case Fragment.CREATED: f.mView = f.performCreateView(f.getLayoutInflater(f.mSavedFragmentState), container, f.mSavedFragmentState); position0
Fragment.onCreateView—–case Fragment.CREATED: f.mView = f.performCreateView(f.getLayoutInflater(f.mSavedFragmentState), container, f.mSavedFragmentState); position1
猜测addView 在执行这一次CREATED分支中,onCreateView之后执行。作用就是把 fragment视图添加到container中,如果是transaction.replace就是把fragment视图 添加到 replace第一次参数id的视图中,fragmentviewpager 就是add到viewpager中(这里把viewpager的id引用赋值给了其mID)

六、5.9更新—为什么addView没有调用。

很久没看了,分析到第五步之后陷入僵局...因为虽然知道了addview没有被调用导致了这个bug,但是并不知道如何修改代码使addview得以调用,或者如何修改能让fragment执行到case CREATE分支后能按正常或者fragment的状态,从而按正常步骤执行下去,生成新fragment对象,装入viewpager中。

通过第五步的分析(请忽略这混乱的截图和语言组织),这个bug产生的原因其实很明显了。addview没有被调用是因为切换语言—回收activity—activity在ondestroy前通知回收fragment—但关键就在这一步
activity会通知fragment执行detached ,发现Fragment在detached之后都会被reset掉,但是它并没有对ChildFragmentManager做reset,所以会造成ChildFragmentManager的状态错误。
因此在再次创建的时候,因为错误的状态 而没有生成新的fragment。
but,我按照别人的方法操作:
我们需要在Fragment被detached的时候去重置ChildFragmentManager,即:

@Override
public void onDetach() {
super.onDetach();
try {
Field childFragmentManager = Fragment.class
.getDeclaredField(“mChildFragmentManager”);
childFragmentManager.setAccessible(true);
childFragmentManager.set(this, null);

} catch (NoSuchFieldException e) {
  throw new RuntimeException(e);
} catch (IllegalAccessException e) {
  throw new RuntimeException(e);
}

}
还是没有解决问题。

七、再次总结

后来没有办法只好花时间重构了一下代码,将中间的那层mainfragment去掉了。 activity直接通过viewpager来管理3个fragment。bug 解决。
看来在fragment 嵌套的使用过程中还是要格外小心…一个小bug 坑了好多天。

Fragment嵌套带来的坑--页面点击无反应(顺带ViewPager之 FragmentPagerAdapter简单分析)

标签:

原文地址:http://blog.csdn.net/tmac2000/article/details/51353830

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!