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

接手别人的老项目。新版本测试提出一个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 坑了好多天。

时间: 2024-07-29 18:58:14

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

android开发 Fragment嵌套调用常见错误

在activity中有时需要嵌套调用fragment,但嵌套调用往往带来视图的显示与预期的不一样或是fragment的切换有问题.在使用时要注意几点: 1.fragment中嵌套fragment,子fragment视图无法显示: 如下: 父fragment的.xml文件: <pre name="code" class="html"><LinearLayout xmlns:android="http://schemas.android.co

浅谈Android Fragment嵌套使用存在的一些BUG以及解决方法

自从Android3.0引入了Fragment之后,使用Activity去嵌套一些Fragment的做法也变得更加流行,这确实是Fragment带来的一些优点,比如说:Fragment可以使你能够将activity分离成多个可重用的组件,每个都有它自己的生命周期和UI,更重要的是Fragment解决了Activity间的切换不流畅,实现了一种轻量及的切换,但是在官方提供的android.support.v4包中,Fragment还是或多或少的存在一些BUG,今天就与大家分享一下这些BUG和解决方

MyEclipse导入主题文件epf后xml及jsp等页面中点击标签之后显示灰白

MyEclipse导入主题文件epf后xml及jsp等页面中点击标签之后显示灰白,症状如下: 解决方案如下: MyEclipse导入主题文件epf后xml及jsp等页面中点击标签之后显示灰白,布布扣,bubuko.com

Android Fragment 嵌套使用报错

在新的SDK每次创建activity时,会自动生成 <pre name="code" class="java">public static class PlaceholderFragment extends Fragment fragment模块,在该模块的基础上进行嵌套fragment代码如下: <pre name="code" class="java">public static class Pla

IPhone手机页面中点击文本输入框,弹出键盘,网页会放大,如何解决

在head标签中加入以上meta声明.具体属性可以谷歌/百度. <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> 我查了下viewport,有几个属性:width - viewport的宽度 height - viewport的高度initial-scale - 初始的缩放比例minim

activity 嵌套一级fragment,一级fragment嵌套二级fragment,在一级fragment中刷新二级fragment中的UI

今天遇到挺纠结的问题,由于产品设计的问题,技术上涉及到activity 嵌套一级fragment,一级fragment嵌套二级fragment,在一级fragment中刷新二级fragment中的UI. 其中一级fragment中有顶部搜索栏,搜索栏下面有viewpager+fragment的布局,搜索栏输入内容后要更新子当前页面的fragment的搜索方法,并刷新UI. adapter: private class OrderManagerFragmentPagerAdapter extend

Fragment嵌套ViewPager切换后数据消失ViewPager空白问题

先描述一下现象 如图 解决后如图 之前先在网上找了很多的解决办法,有的网友说要继承FragmentStatePagerAdapter替换掉FragmentPagerAdapter我试了试没管用.后来在解决如下 此方法为初始化ViewPager private void init() { fragmentsList = new ArrayList(); manager = getFragmentManager(); fragmentsList = new ArrayList<Fragment>(

Fragment嵌套

当我们从一个Activity启动了一个Fragment,然后在这个Fragment中又去实例化了一些子Fragment,在子Fragment中去有返回的启动了另外一个Activity,即通过startActivityForResult方式去启动,这时候造成的现象会是,子Fragment接收不到OnActivityResult,如果在子Fragment中是以getActivity.startActivityForResult方式启动,那么只有Activity会接收到OnActivityResult

fragment嵌套,viewpager嵌套 不能正确显示

转帖:http://blog.csdn.net/mybook1122/article/details/24003343 通常为 viewPager.setAdapter(new MyFragmentPagerAdapter(getSupportFragmentManager(), fragmentsList)); 替换为 mPager.setAdapter(new MyFragmentPagerAdapter(getChildFragmentManager(), fragmentsList));