从源码的角度理解Fragment setUserVisibleH
Fragment一直是android开发者经常使用的控件,但是本人一直对这两个方法回调的时机弄得不是很清楚,今天就通过源码来看看,这两个api究竟怎么用。
setUserVisibleHint
/**
* Set a hint to the system about whether this fragment's UI is currently visible
* to the user. This hint defaults to true and is persistent across fragment instance
* state save and restore.
*
* <p>An app may set this to false to indicate that the fragment's UI is
* scrolled out of visibility or is otherwise not directly visible to the user.
* This may be used by the system to prioritize operations such as fragment lifecycle updates
* or loader ordering behavior.</p>
*
* <p><strong>Note:</strong> This method may be called outside of the fragment lifecycle.
* and thus has no ordering guarantees with regard to fragment lifecycle method calls.</p>
*
* @param isVisibleToUser true if this fragment's UI is currently visible to the user (default),
* false if it is not.
*/
public void setUserVisibleHint(boolean isVisibleToUser) {
if (!mUserVisibleHint && isVisibleToUser && mState < STARTED
&& mFragmentManager != null && isAdded()) {
mFragmentManager.performPendingDeferredStart(this);
}
mUserVisibleHint = isVisibleToUser;
mDeferStart = mState < STARTED && !isVisibleToUser;
}
* <p><strong>Note:</strong> This method may be called outside of the fragment lifecycle.
* and thus has no ordering guarantees with regard to fragment lifecycle method calls.</p>
给系统设置一个提示当前fragment是否对用户可见,默认是可见的并且在fragment发生重启之后也会保存这个状态。注意看note,这个方法可能被调用在Fragment生命周期之外,并且不能保证这个方法与Fragment生命周期的排序。
查看源码,setUserVisibleHint被引用的状况,
image.png
发现在FragmentPageAdapter和FragmentStatePageAdapter中被调用了,看到这两个类是不是很熟悉,不就是我们常用的ViewPager+Fragment框架使用的适配器吗,下面我通过一个ViewPager+Fragment来查看
setUserVisibleHint的调用情况:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.example.ziv.myapplication.MainActivity">
<android.support.v4.view.ViewPager
android:id="@+id/layout_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/btn_next"
android:orientation="horizontal" />
</RelativeLayout>
package com.example.ziv.myapplication;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.view.ViewPager;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.EditText;
import java.util.List;
public class MainActivity extends AppCompatActivity {
private final static String TAG = "BaseFragment";
private ViewPager mViewPager;
private EditText etPageNum;
private Fragment[] mFragments;
private FragmentManager mFragmentManager;
private Frgament1 mFrgament1;
private Frgament2 mFrgament2;
private Frgament3 mFrgament3;
private Frgament4 mFrgament4;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.e(TAG, "onCreate");
setContentView(R.layout.activity_main);
initViews();
findViewById(R.id.btn_inflate_content).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int num = Integer.valueOf(etPageNum.getText().toString());
Fragment fragment = mFragmentManager.findFragmentByTag(String.format("%s%d", "Frgament", num));
FragmentTransaction fragmentTransaction = mFragmentManager.beginTransaction();
List<Fragment> fragmentList = mFragmentManager.getFragments();
if (fragment == null) {
fragmentTransaction.add(R.id.layout_content, getFragmentByNum(num), String.format("%s%d", "Frgament", num));
} else {
if (fragment.isAdded()) {
fragmentTransaction.show(fragment);
}
}
for (Fragment f : fragmentList) {
if (!f.equals(fragment)) {
fragmentTransaction.hide(f);
}
}
fragmentTransaction.addToBackStack(null);
fragmentTransaction.commit();
}
});
}
private BaseFragment getFragmentByNum(int num) {
BaseFragment fragment = null;
switch (num) {
case 1:
fragment = mFrgament1;
break;
case 2:
fragment = mFrgament2;
break;
case 3:
fragment = mFrgament3;
break;
case 4:
fragment = mFrgament4;
break;
default:
fragment = mFrgament1;
break;
}
return fragment;
}
private void initViews() {
mViewPager = findViewById(R.id.layout_content);
// etPageNum = findViewById(R.id.et_page_num);
mFrgament1 = new Frgament1();
mFrgament2 = new Frgament2();
mFrgament3 = new Frgament3();
mFrgament4 = new Frgament4();
mFragments = new Fragment[]{mFrgament1, mFrgament2, mFrgament3, mFrgament4};
mFragmentManager = getSupportFragmentManager();
mViewPager.setOffscreenPageLimit(1);
mViewPager.setAdapter(new MyAdapter(mFragmentManager));
}
@Override
protected void onStart() {
super.onStart();
Log.e(TAG, "onStart");
}
@Override
protected void onResume() {
super.onResume();
Log.e(TAG, "onResume");
}
@Override
protected void onStop() {
super.onStop();
Log.e(TAG, "onStop");
}
@Override
protected void onPause() {
super.onPause();
Log.e(TAG, "onPause");
}
@Override
protected void onRestart() {
super.onRestart();
Log.e(TAG, "onRestart");
}
@Override
protected void onDestroy() {
super.onDestroy();
Log.e(TAG, "onDestroy");
}
private class MyAdapter extends FragmentPagerAdapter {
public MyAdapter(FragmentManager fm) {
super(fm);
}
@Override
public Fragment getItem(int position) {
return mFragments[position];
}
@Override
public int getCount() {
return mFragments.length;
}
}
}
package com.example.ziv.myapplication;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
/**
* Created by ziv on 2017/12/20.
*/
public class Frgament1 extends BaseFragment {
private final static String TAG = "BaseFragment";
private View rootView;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
Log.e("", "onCreateView rootView is null ?"+(rootView==null));
if (rootView == null) {
rootView = inflater.inflate(R.layout.fragment_1, container, false);
}
return rootView;
}
}
其他fragment类似,再来看看BaseFragment
package com.example.ziv.myapplication;
import android.content.Context;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
/**
* Created by ziv on 2017/12/20.
*/
public class BaseFragment extends Fragment {
private final static String TAG = BaseFragment.class.getSimpleName();
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.e(TAG, this.getClass().getSimpleName() + "onCreate");
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
return super.onCreateView(inflater, container, savedInstanceState);
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
Log.e(TAG, this.getClass().getSimpleName() + "onAttach");
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
Log.e(TAG, this.getClass().getSimpleName() + "onActivityCreated");
}
@Override
public void onStart() {
super.onStart();
Log.e(TAG, this.getClass().getSimpleName() + "onStart");
}
@Override
public void onResume() {
super.onResume();
Log.e(TAG, this.getClass().getSimpleName() + "onResume");
}
@Override
public void onStop() {
super.onStop();
Log.e(TAG, this.getClass().getSimpleName() + "onStop");
}
@Override
public void onPause() {
super.onPause();
Log.e(TAG, this.getClass().getSimpleName() + "onPause");
}
@Override
public void onDestroy() {
super.onDestroy();
Log.e(TAG, this.getClass().getSimpleName() + "onDestroy");
}
@Override
public void onDestroyView() {
super.onDestroyView();
Log.e(TAG, this.getClass().getSimpleName() + "onDestroyView");
}
@Override
public void onDetach() {
super.onDetach();
Log.e(TAG, this.getClass().getSimpleName() + "onDetach");
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
Log.e(TAG, this.getClass().getSimpleName()+"setUserVisibleHint isVisibleToUser is: " + isVisibleToUser);
}
@Override
public void onHiddenChanged(boolean hidden) {
super.onHiddenChanged(hidden);
Log.e(TAG, this.getClass().getSimpleName()+"onHiddenChanged hidden is: " + hidden);
}
}
先来看看mViewPager.setOffscreenPageLimit(1);设置ViewPager预加载页数。看看看log:
首次加载时:
image.png
可以看到setUserVisibleHint是在onAttach之前调用的,因为设置了预加载,所以Fragment2的setUserVisibleHint也会被调用,然后就是正常的Fragment生命周期了。
滑动到Fragment2时:
image.png
由于预加载了一页,也就是当前Fragment的前后各自一页,所以这个时后Fragment3也会被加载,Fragment1这是后被缓存了。
滑动到Fragment3时:
image.png
由于只能预加载一页所以Fragment1被销毁了,setUserVisibleHint是在被加载或缓存的Fragment调用(Frgamen2/Fragment4),Fragment1已经被销毁了,如果是初次加载则在onAttach之前调用。
从Fragment3滑动到Fragment2:
image.png
综上所诉,setUserVisibleHint的调用与ViewPager预加载有关系,如果当前Fragment是属于预加载的页,setUserVisibleHint才会被调用。并且在onAttach之前调用,所以如果想要在setUserVisibleHint做懒加载,一定要判断当前Fragment的View是否已经初始化了。因为不去额定setUserVisibleHint的调用时机,所以做懒加载时最好在onResume和setUserVisibleHint都进行判断是否显示和是否Resumed的,示例代码如下:
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
mVisibleToUser = isVisibleToUser;
Log.debug(TAG, "setUserVisibleHint isVisibleToUser=" + isVisibleToUser + ", [" + getClass().getSimpleName() + "]");
if(isVisibleToUser && mResumed) {
Log.debug(TAG, "onPageStart !! [" + getClass().getSimpleName() + "]");
onPageStart();
}
}
@Override
public void onResume() {
super.onResume();
Log.debug(TAG, "onResume !! mVisibleToUser =" + mVisibleToUser + ", [" + getClass().getSimpleName() + "]");
if(mVisibleToUser) {
Log.debug(TAG, "onPageStart !! [" + getClass().getSimpleName() + "]");
onPageStart();
}
}
onHiddenChanged
/**
* Called when the hidden state (as returned by {@link #isHidden()} of
* the fragment has changed. Fragments start out not hidden; this will
* be called whenever the fragment changes state from that.
* @param hidden True if the fragment is now hidden, false otherwise.
*/
public void onHiddenChanged(boolean hidden) {
}
大致意思就是Fragment的隐藏状态改变。隐藏状态,首先Fragment得隐藏。加载Fragment有两种方式,一种是replace,一种是add+show+hide;
这里先说结论:第一次add时是不会调用onHiddenChanged的,只有当一个Fragment之前的状态是hidden,然后show,这时候才会调用onHiddenChanged,或者之前的状态是show,然后置为hidden时,也会回调onHiddenChanged。现在从源码的角度来看,onHiddenChanged的调用时机:
int num = Integer.valueOf(etPageNum.getText().toString());
Fragment fragment = mFragmentManager.findFragmentByTag(String.format("%s%d", "Frgament", num));
FragmentTransaction fragmentTransaction = mFragmentManager.beginTransaction();
List<Fragment> fragmentList = mFragmentManager.getFragments();
if (fragment == null) {
fragmentTransaction.add(R.id.layout_content, getFragmentByNum(num), String.format("%s%d", "Frgament", num));
} else {
if (fragment.isAdded()) {
fragmentTransaction.show(fragment);
}
}
for (Fragment f : fragmentList) {
if (!f.equals(fragment)) {
fragmentTransaction.hide(f);
}
}
fragmentTransaction.addToBackStack(null);
fragmentTransaction.commit();
来看fragmentTransaction.show
@Override
public FragmentTransaction show(Fragment fragment) {
addOp(new Op(OP_SHOW, fragment));
return this;
}
void addOp(Op op) {
mOps.add(op);
op.enterAnim = mEnterAnim;
op.exitAnim = mExitAnim;
op.popEnterAnim = mPopEnterAnim;
op.popExitAnim = mPopExitAnim;
}
static final class Op {
int cmd;
Fragment fragment;
int enterAnim;
int exitAnim;
int popEnterAnim;
int popExitAnim;
Op() {
}
Op(int cmd, Fragment fragment) {
this.cmd = cmd;
this.fragment = fragment;
}
}
// cmd指的是当前Fragment的要进行的动作
static final int OP_NULL = 0;
static final int OP_ADD = 1;
static final int OP_REPLACE = 2;
static final int OP_REMOVE = 3;
static final int OP_HIDE = 4;
static final int OP_SHOW = 5;
static final int OP_DETACH = 6;
static final int OP_ATTACH = 7;
static final int OP_SET_PRIMARY_NAV = 8;
static final int OP_UNSET_PRIMARY_NAV = 9;
把当前操作存在一个List里,再来看看commit方法:
@Override
public int commit() {
return commitInternal(false);
}
int commitInternal(boolean allowStateLoss) {
if (mCommitted) throw new IllegalStateException("commit already called");
if (FragmentManagerImpl.DEBUG) {
Log.v(TAG, "Commit: " + this);
LogWriter logw = new LogWriter(TAG);
PrintWriter pw = new PrintWriter(logw);
dump(" ", null, pw, null);
pw.close();
}
mCommitted = true;
if (mAddToBackStack) {
mIndex = mManager.allocBackStackIndex(this);
} else {
mIndex = -1;
}
mManager.enqueueAction(this, allowStateLoss);
return mIndex;
}
接着来看enqueueAction,
public void enqueueAction(OpGenerator action, boolean allowStateLoss) {
if (!allowStateLoss) {
checkStateLoss();
}
synchronized (this) {
if (mDestroyed || mHost == null) {
if (allowStateLoss) {
// This FragmentManager isn't attached, so drop the entire transaction.
return;
}
throw new IllegalStateException("Activity has been destroyed");
}
if (mPendingActions == null) {
mPendingActions = new ArrayList<>();
}
mPendingActions.add(action);
scheduleCommit();
}
}
再来看 scheduleCommit(),
/**
* Schedules the execution when one hasn't been scheduled already. This should happen
* the first time {@link #enqueueAction(OpGenerator, boolean)} is called or when
* a postponed transaction has been started with
* {@link Fragment#startPostponedEnterTransition()}
*/
private void scheduleCommit() {
synchronized (this) {
boolean postponeReady =
mPostponedTransactions != null && !mPostponedTransactions.isEmpty();
boolean pendingReady = mPendingActions != null && mPendingActions.size() == 1;
if (postponeReady || pendingReady) {
mHost.getHandler().removeCallbacks(mExecCommit);
mHost.getHandler().post(mExecCommit);
}
}
}
这里是执行一个排队的动作
Runnable mExecCommit = new Runnable() {
@Override
public void run() {
execPendingActions();
}
};
/**
* Only call from main thread!
*/
public boolean execPendingActions() {
ensureExecReady(true);
boolean didSomething = false;
while (generateOpsForPendingActions(mTmpRecords, mTmpIsPop)) {
mExecutingActions = true;
try {
removeRedundantOperationsAndExecute(mTmpRecords, mTmpIsPop);
} finally {
cleanupExec();
}
didSomething = true;
}
doPendingDeferredStart();
burpActive();
return didSomething;
}
接着就是removeRedundantOperationsAndExecute中执行executeOpsTogether,executeOpsTogether中执行executeOps
最后就来看下executeOps,已经找到真相了
/**
* Executes the operations contained within this transaction. The Fragment states will only
* be modified if optimizations are not allowed.
*/
void executeOps() {
final int numOps = mOps.size();
for (int opNum = 0; opNum < numOps; opNum++) {
final Op op = mOps.get(opNum);
final Fragment f = op.fragment;
if (f != null) {
f.setNextTransition(mTransition, mTransitionStyle);
}
switch (op.cmd) {
case OP_ADD:
f.setNextAnim(op.enterAnim);
mManager.addFragment(f, false);
break;
case OP_REMOVE:
f.setNextAnim(op.exitAnim);
mManager.removeFragment(f);
break;
case OP_HIDE:
f.setNextAnim(op.exitAnim);
mManager.hideFragment(f);
break;
case OP_SHOW:
f.setNextAnim(op.enterAnim);
mManager.showFragment(f);
break;
case OP_DETACH:
f.setNextAnim(op.exitAnim);
mManager.detachFragment(f);
break;
case OP_ATTACH:
f.setNextAnim(op.enterAnim);
mManager.attachFragment(f);
break;
case OP_SET_PRIMARY_NAV:
mManager.setPrimaryNavigationFragment(f);
break;
case OP_UNSET_PRIMARY_NAV:
mManager.setPrimaryNavigationFragment(null);
break;
default:
throw new IllegalArgumentException("Unknown cmd: " + op.cmd);
}
if (!mReorderingAllowed && op.cmd != OP_ADD && f != null) {
mManager.moveFragmentToExpectedState(f);
}
}
if (!mReorderingAllowed) {
// Added fragments are added at the end to comply with prior behavior.
mManager.moveToState(mManager.mCurState, true);
}
}
再来看看showFragment和hideFragment
/**
* Marks a fragment as hidden to be later animated in with
* {@link #completeShowHideFragment(Fragment)}.
*
* @param fragment The fragment to be shown.
*/
public void hideFragment(Fragment fragment) {
if (DEBUG) Log.v(TAG, "hide: " + fragment);
if (!fragment.mHidden) {
fragment.mHidden = true;
// Toggle hidden changed so that if a fragment goes through show/hide/show
// it doesn't go through the animation.
fragment.mHiddenChanged = !fragment.mHiddenChanged;
}
}
/**
* Marks a fragment as shown to be later animated in with
* {@link #completeShowHideFragment(Fragment)}.
*
* @param fragment The fragment to be shown.
*/
public void showFragment(Fragment fragment) {
if (DEBUG) Log.v(TAG, "show: " + fragment);
if (fragment.mHidden) {
fragment.mHidden = false;
// Toggle hidden changed so that if a fragment goes through show/hide/show
// it doesn't go through the animation.
fragment.mHiddenChanged = !fragment.mHiddenChanged;
}
}
这两个方法都对mHiddenChanged进行重新赋值,就导致了onHiddenChanged的回调。