最近在做某项目,其中有个功能是通过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消失那么他的生命周期肯定会发生变化,这里就是我们的着手点。
通过查看日志发现,当我们通过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类对象:
因为
@Override public 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。
当然这里还有问题就是使用时替换了,那么如果划出屏幕区域如何替换呢,这个我们明天继续。