使用FragmentPagerAdapter的fragment第二次打开不能显示内容的问题分析

在使用viewpager的时候有两个adapter:FragmentPagerAdapter和FragmentStatePagerAdapter,区别在于他们的instantiateItem方法是不一样的:

FragmentPagerAdapter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
    public Object instantiateItem(ViewGroup container, int position) {
        if (mCurTransaction == null) {

            mCurTransaction = mFragmentManager.beginTransaction();
        }

        final long itemId = getItemId(position);

        // 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;
    }

FragmentStatePagerAdapter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
    public Object instantiateItem(ViewGroup container, int position) {
        // If we already have this item instantiated, there is nothing
        // to do.  This can happen when we are restoring the entire pager
        // from its saved state, where the fragment manager has already
        // taken care of restoring the fragments we previously had instantiated.
        if (mFragments.size() > position) {
            Fragment f = mFragments.get(position);
            if (f != null) {
                return f;
            }
        }

        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }

        Fragment fragment = getItem(position);
        if (DEBUG) Log.v(TAG, "Adding item #" + position + ": f=" + fragment);
        if (mSavedState.size() > position) {
            Fragment.SavedState fss = mSavedState.get(position);
            if (fss != null) {
                fragment.setInitialSavedState(fss);
            }
        }
        while (mFragments.size() <= position) {
            mFragments.add(null);
        }
        fragment.setMenuVisibility(false);
        fragment.setUserVisibleHint(false);
        mFragments.set(position, fragment);
        mCurTransaction.add(container.getId(), fragment);

        return fragment;
    }

FragmentPagerAdapter会将adapter的每个fragment加入fragmentManager管理,如果初始化adapter传的是getSupportFragmentManager(),则每个fragment都会放到activity的fragmentManager里面去。这样就会造成当viewpager所在的fragment因为popFromBackStack被移除后,再次add一次viewpager所在的fragment,viewpager里面不会显示出来fragment内容。原因是这样的:

第一次FragmentPagerAdapter的instantiateItem方法会把每个fragment加入activity的fragmentManager当中,popFromBackStack这个viewpager所在的fragment时,并不会将viewpager里面的每个fragment调用detach。第二次就会发现fragment已经存在,则直接调用mCurTransaction.attach方法,而attachFragment方法缺发现fragment未被detach则直接跳过了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    public void attachFragment(Fragment fragment, int transition, int transitionStyle) {
        if (DEBUG) Log.v(TAG, "attach: " + fragment);
        if (fragment.mDetached) {
            fragment.mDetached = false;
            if (!fragment.mAdded) {
                if (mAdded == null) {
                    mAdded = new ArrayList<Fragment>();
                }
                if (mAdded.contains(fragment)) {
                    throw new IllegalStateException("Fragment already added: " + fragment);
                }
                if (DEBUG) Log.v(TAG, "add from attach: " + fragment);
                mAdded.add(fragment);
                fragment.mAdded = true;
                if (fragment.mHasMenu && fragment.mMenuVisible) {
                    mNeedMenuInvalidate = true;
                }
                moveToState(fragment, mCurState, transition, transitionStyle, false);
            }
        }
    }

如果使用FragmentStatePagerAdapter则不会出现这个问题,它自己内部使用mFragments数组维持fragment缓存,因为我们重新打开viewpager所在的fragment时,重新初始化了一个新的viewpager adapter,所以其实始终没用到这个缓存,每次都会重新使用getItem来new出来新fragment,然后调用mCurTransaction.add加入。

要避免这个问题有几种方法:

1.使用FragmentStatePagerAdapter 2.初始化viewpager的adapter的时候不要使用getSupportFragmentManager(),而使用getChildFragmentManager()。这样就会找不到缓存的fragment。 3.或者使用FragmentPagerAdapter,在viewpager所在的fragment被detach的时候显示detach viewpager里面的每一个fragment

FragmentPagerAdapter和FragmentStatePagerAdapter区别在于:前者会保存fragment对象,后者仅会保存fragment的状态能进行恢复,占用内存少一些。

官方也有篇讨论帖:看这里

Android子view的pressed状态受父view的clickable影响分析

android从sdk16开始,修改了onTouchEvent的ACTION_DOWN代码(Android: Child elements sharing pressed state with their parent even when duplicateParentState specified),如下:

sdk16以前:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
case MotionEvent.ACTION_DOWN:
            mHasPerformedLongPress = false;

            if (performButtonActionOnTouchDown(event)) {
                break;
            }

            // Walk up the hierarchy to determine if we're inside a scrolling container.
            boolean isInScrollingContainer = isInScrollingContainer();

            // For views inside a scrolling container, delay the pressed feedback for
            // a short period in case this is a scroll.
            if (isInScrollingContainer) {
                mPrivateFlags |= PREPRESSED;
                if (mPendingCheckForTap == null) {
                    mPendingCheckForTap = new CheckForTap();
                }
                postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
            } else {
                // Not inside a scrolling container, so show the feedback right away
                mPrivateFlags |= PRESSED; //comment by bran
                refreshDrawableState();
                checkForLongClick(0);
            }
            break;

sdk16及以后

java case MotionEvent.ACTION_DOWN: mHasPerformedLongPress = false;

            if (performButtonActionOnTouchDown(event)) {
                break;
            }

            // Walk up the hierarchy to determine if we're inside a scrolling container.
            boolean isInScrollingContainer = isInScrollingContainer();

            // For views inside a scrolling container, delay the pressed feedback for
            // a short period in case this is a scroll.
            if (isInScrollingContainer) {
                mPrivateFlags |= PFLAG_PREPRESSED;
                if (mPendingCheckForTap == null) {
                    mPendingCheckForTap = new CheckForTap();
                }
                postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
            } else {
                // Not inside a scrolling container, so show the feedback right away
                setPressed(true);
                checkForLongClick(0);
            }
            break;
1
后者会调用setPressed方法代码如下

java

public void setPressed(boolean pressed) {
    final boolean needsRefresh = pressed != ((mPrivateFlags & PFLAG_PRESSED) == PFLAG_PRESSED);

    if (pressed) {
        mPrivateFlags |= PFLAG_PRESSED;
    } else {
        mPrivateFlags &= ~PFLAG_PRESSED;
    }

    if (needsRefresh) {
        refreshDrawableState();
    }
    dispatchSetPressed(pressed);
}

//viewgroup的实现
protected void dispatchSetPressed(boolean pressed) {
    final View[] children = mChildren;
    final int count = mChildrenCount;
    for (int i = 0; i < count; i++) {
        final View child = children[i];
        // Children that are clickable on their own should not
        // show a pressed state when their parent view does.
        // Clearing a pressed state always propagates.
        if (!pressed || (!child.isClickable() && !child.isLongClickable())) {
            child.setPressed(pressed);
        }
    }
}

“`

可以看出sdk16以后如果子view是!isClickable,父view是isClickable(父view如果是!isClickable都不会处理ACTION_DOWN事件),则即使在子view区域外的点击也会触发子view的pressed行为。这样改的原因不得而知,但个人觉得这不是正常期望的,UI表现上来看,父view点击任何区域子view都会触发pressed。

这种情景是存在的,比如子view是不可点,但又要设置父view可点以防点击父view会触发父父view事件。如果要处理这种情况,父view可以覆盖dispatchSetPressed方法进行重写,或者直接父view设置setEnable(false)。