实现ViewPager中的Fragment无感知动态替换

来源:互联网 发布:windows phone 8.1gdr2 编辑:程序博客网 时间:2024/06/06 08:58

        最近在做公司某项目,其中有个功能是通过viewPager 和Fragment左右切换,但是因为要根据数据类型实现某一个Framgent可以在滑动动作停止后播放视频。之前我们是通过根据数据类型实现不同种类的fragment实例化即可。但是大家都知道ViewPager不显示区域可能也会有fragment,如果我们数据连续项都需要播放视频的话,测试中发现遇到会同时播放多个视频的情况,虽然最终通过某中手段屏蔽了这个问题。而且更重要的问题是,我们播放视频使用的VideoView较一般的View是比较耗费系统资源的,所以我们提出能否整个过程中仅实例化一个VideoView,根据当前position去动态替换制定位置的Fragment.

        为了达到以上目标,虽然花费了2天时间研究如何能够平滑无感知替换,但是感觉还是值得的。

        下面记录下供大家解决类似问题,希望对读者有帮助,也希望大家多多吐槽。

第一步:重写了ViewPager类。

         1、添加了如下方法:实现替换制定位置Fragment

              

    public boolean updateNextPrimaryView(Fragment fragment,int pos) {        ItemInfo info = super.infoForPosition(pos);        if (null != info && info.object instanceof Fragment) {            info.object = fragment;            if(super.infoForPosition(pos).object == info.object){                Log.d("LYT","=================================");            }            resetFragmentState(pos-2);            resetFragmentState(pos-1);            resetFragmentState(pos+1);            resetFragmentState(pos+2);//            super.populate(pos);            return true;        }        return false;    }    public void resetFragmentState(int position){        ItemInfo info = super.infoForPosition(position);        if (null != info && info.object instanceof Fragment) {            try {                Field field = ReflectionUtils.getDeclaredField(info.object, "mState");                field.setAccessible(true);                field.set(info.object, 0);            }catch (Exception e){                e.printStackTrace();            }        }    }    public static interface IScrollPageListener {        /**         * @param oldPage old page index         * @param newPage new page index         */        void onScrollToRightPage(int oldPage, int newPage);        /**         * @param oldPage old page index         * @param newPage new page index         */        void onScrollToLeftPage(int oldPage, int newPage);    }    IScrollPageListener splistener;    public void setOnScrollPageListener(IScrollPageListener listener) {        splistener = listener;    }

      其中 方法
void resetFragmentState(int position)
用于改变ViewPager中指定位置的fragment的生命周期,也是比较重要的关键一步,我们带会让再细说。

方法

 void setOnScrollPageListener(IScrollPageListener listener) 
用于设置监听

第二步,我们应该选择在什么实际去进行替换,通过详细测试和观察发现ViewPager中有个重要的监听回调,就是OnPageSelected,大家如果认真看ViewPager源码会发现,这个方法被调用时,其实viewPager还没有真正scroll,

ViewPager相关源码如下:

1、先设置内部当前位置item,即接下来要滚动到的位置,源码如下:

首先调用ViewPager的方法

public boolean onTouchEvent(MotionEvent ev) {
其中,在MotionEvent.ACTION_UP时会调用下面的方法

void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) {    if (mAdapter == null || mAdapter.getCount() <= 0) {        setScrollingCacheEnabled(false);        return;    }    if (!always && mCurItem == item && mItems.size() != 0) {        setScrollingCacheEnabled(false);        return;    }    if (item < 0) {        item = 0;    } else if (item >= mAdapter.getCount()) {        item = mAdapter.getCount() - 1;    }    final int pageLimit = mOffscreenPageLimit;    if (item > (mCurItem + pageLimit) || item < (mCurItem - pageLimit)) {        // We are doing a jump by more than one page.  To avoid        // glitches, we want to keep all current pages in the view        // until the scroll ends.        for (int i=0; i<mItems.size(); i++) {            mItems.get(i).scrolling = true;        }    }    final boolean dispatchSelected = mCurItem != item;    if (mFirstLayout) {        // We don't have any idea how big we are yet and shouldn't have any pages either.        // Just set things up and let the pending layout handle things.        mCurItem = item;        if (dispatchSelected) {            dispatchOnPageSelected(item);        }        requestLayout();    } else {        populate(item);        scrollToItem(item, smoothScroll, velocity, dispatchSelected);    }}
又会调用如下方法:

private void dispatchOnPageSelected(int position) {    if (mOnPageChangeListener != null) {        mOnPageChangeListener.onPageSelected(position);    }    if (mOnPageChangeListeners != null) {        for (int i = 0, z = mOnPageChangeListeners.size(); i < z; i++) {            OnPageChangeListener listener = mOnPageChangeListeners.get(i);            if (listener != null) {                listener.onPageSelected(position);            }        }    }    if (mInternalPageChangeListener != null) {        mInternalPageChangeListener.onPageSelected(position);    }}
经过分析,这时候只是设置了下一页要显示的位置item而且这时候它是不可见的,所以我们选择在此时进行替换fragment.

第三步,如何替换制定位置的Fragment呢,我们仍然需要从源码中寻找答案。

我们再看ViewPager源码的时候会发现如下的私有方法定义

ItemInfo infoForPosition(int position) {    for (int i = 0; i < mItems.size(); i++) {        ItemInfo ii = mItems.get(i);        if (ii.position == position) {            return ii;        }    }    return null;}
那么我们会发现原来ViewPager中显示的Fragment都存放在mItems中,而他的元素类型却是ItemItem,其定义如下:

static class ItemInfo {    Object object;    int position;    boolean scrolling;    float widthFactor;    float offset;}

我们能得出的结论就是 object对象就是暂存fragment的地方,position就是当前位置索引。

于是就有了第一步中我们通过

ItemInfo info = super.infoForPosition(pos);
来获取指定pos位置的info,只有我们进行替换即可。

世界上什么事情都不是一蹴而就的,接下来新问题来了,我们运行后发现,目标位置的Fragment正常按照预想成功完成替换,但是左右滑动时却惊奇发现主屏幕左右两侧Fragment位置竟然消失了(我司项目表现为黑屏),这是什么原因呢,瞬间头又大了。怎么办呢?用我们强大的log工具呀,Fragment消失那么他的生命周期肯定会发生变化,这里就是我们的着手点。


D/LYT: destroyItem,time=1173D/LYT: setPrimaryItem,time=1174D/LYT: finishUpdate,executePendingTransactions,time=1174D/LYT: LockWallpaperStatePreviewFragment,onCreate,id = I01026408D/LYT: LockWallpaperStatePreviewFragment,onPause,id = I01026409D/LYT: LockWallpaperStatePreviewFragment,onStop,id = I01026409D/LYT: LockWallpaperStatePreviewFragment,onDestroy,id = I01026409D/LYT: LockWallpaperStatePreviewFragment,onDetach,id = I01026409D/LYT: LockWallpaperStatePreviewFragment,onPause,id = I01026973D/LYT: LockWallpaperStatePreviewFragment,onStop,id = I01026973D/LYT: LockWallpaperStatePreviewFragment,onDestroy,id = I01026973D/LYT: LockWallpaperStatePreviewFragment,onDetach,id = I01026973D/LYT: LockWallpaperStatePreviewFragment,onPause,id = I01026974D/LYT: LockWallpaperStatePreviewFragment,onStop,id = I01026974D/LYT: LockWallpaperStatePreviewFragment,onDestroy,id = I01026974D/LYT: LockWallpaperStatePreviewFragment,onDetach,id = I01026974D/LYT: LockWallpaperStatePreviewFragment,onPause,id = I01026972D/LYT: LockWallpaperStatePreviewFragment,onStop,id = I01026972D/LYT: LockWallpaperStatePreviewFragment,onDestroy,id = I01026972D/LYT: LockWallpaperStatePreviewFragment,onDetach,id = I01026972D/LYT: LockWallpaperStatePreviewFragment,onPause,id = I00003793D/LYT: LockWallpaperStatePreviewFragment,onStop,id = I00003793D/LYT: LockWallpaperStatePreviewFragment,onDestroy,id = I00003793D/LYT: LockWallpaperStatePreviewFragment,onDetach,id = I00003793D/LYT: initView,id=I01026973,time=1307D/LYT: ,time=1307D/LYT: onHiddenChanged,hidden=false,id=I01026973D/LYT: startVideo,id=I01026973 D/LYT: onResume,id=I01026973


通过查看日志发现,当我们通过Replace的时候,竟然都onDetch()掉了(我们替换,id = I00003793,但其他也都被移除),什么原因,只能再从源码找答案了。

找了半天原来是我们自己主动触发的,即下方代码执行方法FragmentManager.executePendingTransactions()引起的

public void replaceNextItem(Fragment fragment, int newPos) {    if (mCurTransaction == null) {        mCurTransaction = mFragmentManager.beginTransaction();    }    if(null != mFragments && null != mCurrentPrimaryItem){        repFragment = mFragments.get(newPos);        if (repFragment != null) {            mCurTransaction.replace(vg.getId(), fragment);            if (fragment != null) {                fragment.setMenuVisibility(true);                fragment.setUserVisibleHint(true);            }            try {                mFragmentManager.executePendingTransactions();            } catch (Exception e) {            }        }    }}

其中executePendingTransactions方法定义如下:

/** * Only call from main thread! */public boolean execPendingActions() {    if (mExecutingActions) {        throw new IllegalStateException("Recursive entry to executePendingTransactions");    }        if (Looper.myLooper() != mHost.getHandler().getLooper()) {        throw new IllegalStateException("Must be called from main thread of process");    }    boolean didSomething = false;    while (true) {        int numActions;                synchronized (this) {            if (mPendingActions == null || mPendingActions.size() == 0) {                break;            }                        numActions = mPendingActions.size();            if (mTmpActions == null || mTmpActions.length < numActions) {                mTmpActions = new Runnable[numActions];            }            mPendingActions.toArray(mTmpActions);            mPendingActions.clear();            mHost.getHandler().removeCallbacks(mExecCommit);        }                mExecutingActions = true;        for (int i=0; i<numActions; i++) {            mTmpActions[i].run();            mTmpActions[i] = null;        }        mExecutingActions = false;        didSomething = true;    }    doPendingDeferredStart();    return didSomething;}
其中执行了mTmpActions[i].run(),那么这个对象是谁呢,再仔细查找你会发现是BackStackRecord类对象:

因为

@Overridepublic FragmentTransaction beginTransaction() {    return new BackStackRecord(this);}
继续看,我们会发现在BackStackRecord的run()中,

case OP_REPLACE: {    Fragment f = op.fragment;    int containerId = f.mContainerId;    if (mManager.mAdded != null) {        for (int i = mManager.mAdded.size() - 1; i >= 0; i--) {            Fragment old = mManager.mAdded.get(i);            if (FragmentManagerImpl.DEBUG) {                Log.v(TAG,                        "OP_REPLACE: adding=" + f + " old=" + old);            }            if (old.mContainerId == containerId) {                if (old == f) {                    op.fragment = f = null;                } else {                    if (op.removed == null) {                        op.removed = new ArrayList<Fragment>();                    }                    op.removed.add(old);                    old.mNextAnim = op.exitAnim;                    if (mAddToBackStack) {                        old.mBackStackNesting += 1;                        if (FragmentManagerImpl.DEBUG) {                            Log.v(TAG, "Bump nesting of "                                    + old + " to " + old.mBackStackNesting);                        }                    }                    mManager.removeFragment(old, mTransition, mTransitionStyle);                }            }        }    }    if (f != null) {        f.mNextAnim = op.enterAnim;        mManager.addFragment(f, false);    }}
由于我们使用的是Replace去替换Fragment,所以又会调用FragmentManager.removeFragment方法,其定义

public void removeFragment(Fragment fragment, int transition, int transitionStyle) {    if (DEBUG) Log.v(TAG, "remove: " + fragment + " nesting=" + fragment.mBackStackNesting);    final boolean inactive = !fragment.isInBackStack();    if (!fragment.mDetached || inactive) {        if (false) {            // Would be nice to catch a bad remove here, but we need            // time to test this to make sure we aren't crashes cases            // where it is not a problem.            if (!mAdded.contains(fragment)) {                throw new IllegalStateException("Fragment not added: " + fragment);            }        }        if (mAdded != null) {            mAdded.remove(fragment);        }        if (fragment.mHasMenu && fragment.mMenuVisible) {            mNeedMenuInvalidate = true;        }        fragment.mAdded = false;        fragment.mRemoving = true;        moveToState(fragment, inactive ? Fragment.INITIALIZING : Fragment.CREATED,                transition, transitionStyle, false);    }}
接着继续调用moveToState,这是一个比较重要的方法,折腾了好长时间才明白,会根据Fragment的mState去以及新状态newState去控制Fragment的生命周期,定义:

void moveToState(Fragment f, int newState, int transit, int transitionStyle,        boolean keepActive) {    if (DEBUG && false) Log.v(TAG, "moveToState: " + f        + " oldState=" + f.mState + " newState=" + newState        + " mRemoving=" + f.mRemoving + " Callers=" + Debug.getCallers(5));    // Fragments that are not currently added will sit in the onCreate() state.    if ((!f.mAdded || f.mDetached) && newState > Fragment.CREATED) {        newState = Fragment.CREATED;    }    if (f.mRemoving && newState > f.mState) {        // While removing a fragment, we can't change it to a higher state.        newState = f.mState;    }    // Defer start if requested; don't allow it to move to STARTED or higher    // if it's not already started.    if (f.mDeferStart && f.mState < Fragment.STARTED && newState > Fragment.STOPPED) {        newState = Fragment.STOPPED;    }    if (f.mState < newState) {        // For fragments that are created from a layout, when restoring from        // state we don't want to allow them to be created until they are        // being reloaded from the layout.        if (f.mFromLayout && !f.mInLayout) {            return;        }        if (f.mAnimatingAway != null) {            // The fragment is currently being animated...  but!  Now we            // want to move our state back up.  Give up on waiting for the            // animation, move to whatever the final state should be once            // the animation is done, and then we can proceed from there.            f.mAnimatingAway = null;            moveToState(f, f.mStateAfterAnimating, 0, 0, true);        }        switch (f.mState) {            case Fragment.INITIALIZING:                if (DEBUG) Log.v(TAG, "moveto CREATED: " + f);                if (f.mSavedFragmentState != null) {                    f.mSavedViewState = f.mSavedFragmentState.getSparseParcelableArray(                            FragmentManagerImpl.VIEW_STATE_TAG);                    f.mTarget = getFragment(f.mSavedFragmentState,                            FragmentManagerImpl.TARGET_STATE_TAG);                    if (f.mTarget != null) {                        f.mTargetRequestCode = f.mSavedFragmentState.getInt(                                FragmentManagerImpl.TARGET_REQUEST_CODE_STATE_TAG, 0);                    }                    f.mUserVisibleHint = f.mSavedFragmentState.getBoolean(                            FragmentManagerImpl.USER_VISIBLE_HINT_TAG, true);                    if (!f.mUserVisibleHint) {                        f.mDeferStart = true;                        if (newState > Fragment.STOPPED) {                            newState = Fragment.STOPPED;                        }                    }                }                f.mHost = mHost;                f.mParentFragment = mParent;                f.mFragmentManager = mParent != null                        ? mParent.mChildFragmentManager : mHost.getFragmentManagerImpl();                f.mCalled = false;                f.onAttach(mHost.getContext());                if (!f.mCalled) {                    throw new SuperNotCalledException("Fragment " + f                            + " did not call through to super.onAttach()");                }                if (f.mParentFragment == null) {                    mHost.onAttachFragment(f);                } else {                    f.mParentFragment.onAttachFragment(f);                }                if (!f.mRetaining) {                    f.performCreate(f.mSavedFragmentState);                } else {                    f.restoreChildFragmentState(f.mSavedFragmentState, true);                    f.mState = Fragment.CREATED;                }                f.mRetaining = false;                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);                    }                }            case Fragment.CREATED:                if (newState > Fragment.CREATED) {                    if (DEBUG) Log.v(TAG, "moveto ACTIVITY_CREATED: " + f);                    if (!f.mFromLayout) {                        ViewGroup container = null;                        if (f.mContainerId != 0) {                            if (f.mContainerId == View.NO_ID) {                                throwException(new IllegalArgumentException(                                        "Cannot create fragment "                                                + f                                                + " for a container view with no id"));                            }                            container = (ViewGroup) mContainer.onFindViewById(f.mContainerId);                            if (container == null && !f.mRestored) {                                String resName;                                try {                                    resName = f.getResources().getResourceName(f.mContainerId);                                } catch (NotFoundException e) {                                    resName = "unknown";                                }                                throwException(new IllegalArgumentException(                                        "No view found for id 0x"                                        + Integer.toHexString(f.mContainerId) + " ("                                        + resName                                        + ") for fragment " + f));                            }                        }                        f.mContainer = container;                        f.mView = f.performCreateView(f.getLayoutInflater(                                f.mSavedFragmentState), container, f.mSavedFragmentState);                        if (f.mView != null) {                            f.mView.setSaveFromParentEnabled(false);                            if (container != null) {                                Animator anim = loadAnimator(f, transit, true,                                        transitionStyle);                                if (anim != null) {                                    anim.setTarget(f.mView);                                    setHWLayerAnimListenerIfAlpha(f.mView, anim);                                    anim.start();                                }                                container.addView(f.mView);                            }                            if (f.mHidden) f.mView.setVisibility(View.GONE);                            f.onViewCreated(f.mView, f.mSavedFragmentState);                        }                    }                    f.performActivityCreated(f.mSavedFragmentState);                    if (f.mView != null) {                        f.restoreViewState(f.mSavedFragmentState);                    }                    f.mSavedFragmentState = null;                }            case Fragment.ACTIVITY_CREATED:                if (newState > Fragment.ACTIVITY_CREATED) {                    f.mState = Fragment.STOPPED;                }            case Fragment.STOPPED:                if (newState > Fragment.STOPPED) {                    if (DEBUG) Log.v(TAG, "moveto STARTED: " + f);                    f.performStart();                }            case Fragment.STARTED:                if (newState > Fragment.STARTED) {                    if (DEBUG) Log.v(TAG, "moveto RESUMED: " + f);                    f.performResume();                    // Get rid of this in case we saved it and never needed it.                    f.mSavedFragmentState = null;                    f.mSavedViewState = null;                }        }    } else if (f.mState > newState) {        switch (f.mState) {            case Fragment.RESUMED:                if (newState < Fragment.RESUMED) {                    if (DEBUG) Log.v(TAG, "movefrom RESUMED: " + f);                    f.performPause();                }            case Fragment.STARTED:                if (newState < Fragment.STARTED) {                    if (DEBUG) Log.v(TAG, "movefrom STARTED: " + f);                    f.performStop();                }            case Fragment.STOPPED:            case Fragment.ACTIVITY_CREATED:                if (newState < Fragment.ACTIVITY_CREATED) {                    if (DEBUG) Log.v(TAG, "movefrom ACTIVITY_CREATED: " + f);                    if (f.mView != null) {                        // Need to save the current view state if not                        // done already.                        if (mHost.onShouldSaveFragmentState(f) && f.mSavedViewState == null) {                            saveFragmentViewState(f);                        }                    }                    f.performDestroyView();                    if (f.mView != null && f.mContainer != null) {                        Animator anim = null;                        if (mCurState > Fragment.INITIALIZING && !mDestroyed) {                            anim = loadAnimator(f, transit, false,                                    transitionStyle);                        }                        if (anim != null) {                            final ViewGroup container = f.mContainer;                            final View view = f.mView;                            final Fragment fragment = f;                            container.startViewTransition(view);                            f.mAnimatingAway = anim;                            f.mStateAfterAnimating = newState;                            anim.addListener(new AnimatorListenerAdapter() {                                @Override                                public void onAnimationEnd(Animator anim) {                                    container.endViewTransition(view);                                    if (fragment.mAnimatingAway != null) {                                        fragment.mAnimatingAway = null;                                        moveToState(fragment, fragment.mStateAfterAnimating,                                                0, 0, false);                                    }                                }                            });                            anim.setTarget(f.mView);                            setHWLayerAnimListenerIfAlpha(f.mView, anim);                            anim.start();                        }                        f.mContainer.removeView(f.mView);                    }                    f.mContainer = null;                    f.mView = null;                }            case Fragment.CREATED:                if (newState < Fragment.CREATED) {                    if (mDestroyed) {                        if (f.mAnimatingAway != null) {                            // The fragment's containing activity is                            // being destroyed, but this fragment is                            // currently animating away.  Stop the                            // animation right now -- it is not needed,                            // and we can't wait any more on destroying                            // the fragment.                            Animator anim = f.mAnimatingAway;                            f.mAnimatingAway = null;                            anim.cancel();                        }                    }                    if (f.mAnimatingAway != null) {                        // We are waiting for the fragment's view to finish                        // animating away.  Just make a note of the state                        // the fragment now should move to once the animation                        // is done.                        f.mStateAfterAnimating = newState;                        newState = Fragment.CREATED;                    } else {                        if (DEBUG) Log.v(TAG, "movefrom CREATED: " + f);                        if (!f.mRetaining) {                            f.performDestroy();                        } else {                            f.mState = Fragment.INITIALIZING;                        }                        f.performDetach();                        if (!keepActive) {                            if (!f.mRetaining) {                                makeInactive(f);                            } else {                                f.mHost = null;                                f.mParentFragment = null;                                f.mFragmentManager = null;                            }                        }                    }                }        }    }        if (f.mState != newState) {        Log.w(TAG, "moveToState: Fragment state for " + f + " not updated inline; "                + "expected state " + newState + " found " + f.mState);        f.mState = newState;    }}
这个代码比较多,大家可以直接看源码,经过分析,原来是因为替换工作进行后,触发executePendingTransactions()由于mState状态等于5

static final int INVALID_STATE = -1;   // Invalid state used as a null value.static final int INITIALIZING = 0;     // Not yet created.static final int CREATED = 1;          // Created.static final int ACTIVITY_CREATED = 2; // The activity has finished its creation.static final int STOPPED = 3;          // Fully created, not started.static final int STARTED = 4;          // Created and started, not resumed.static final int RESUMED = 5;          // Created started and resumed.
也就是RESUMED状态,而Replace的时候只会是INITIZLIZING和CREATED状态,这就导致我们这时候只能走到分支if(f.mState > newState),也就是只能会导致Fragment的OnPause、OnStop、OnDestoryView、OnDestach的调用,分析到这里我们就找到原因了 ,只要我们替换后改变Fragment成员变量状态mState即可,于是有了我们第一步中提到的,通过反射改变Fragment状态

public void resetFragmentState(int position){        ItemInfo info = super.infoForPosition(position);        if (null != info && info.object instanceof Fragment) {            try {                Field field = ReflectionUtils.getDeclaredField(info.object, "mState");                field.setAccessible(true);                field.set(info.object, 0);            }catch (Exception e){                e.printStackTrace();            }        }    }

另外还要修改其余未显示的Fragment

resetFragmentState(pos-2);resetFragmentState(pos-1);resetFragmentState(pos+1);resetFragmentState(pos+2);

到目前位置我们的目标就实现了,实现了无感知提换Fragment。

当然这里还有问题就是使用时替换了,那么如果划出屏幕区域如何替换呢,这个我们明天继续。

0 0
原创粉丝点击