前言#
在我們的項目里經(jīng)常會用到ViewPager+Fragment實現(xiàn)選項卡滑動切換的效果,ViewPager會預(yù)加載下一個Framgment的內(nèi)容,這樣的機制有優(yōu)點也有缺點:
預(yù)加載讓用戶可以更快的看到接下來的內(nèi)容,瀏覽起來連貫性更好,但是app在展示內(nèi)容的同時還增加了額外的任務(wù),這樣可能影響界面的流暢度,并且可能造成流量的浪費。
目前大部分的app都使用Fragment懶加載機制,例如嗶哩嗶哩,360手機助手等等。
正文#
實現(xiàn)Fragment懶加載也可以有很多種辦法,可能有些朋友會第一時間想到通過監(jiān)聽滾動的位置,通過判斷Fragment的加載狀態(tài)實現(xiàn)懶加載,這種方法的缺點就是把實現(xiàn)暴露在Framgment之外,從封裝的角度來說這不是一個好的方案。
最好的方案在網(wǎng)上已經(jīng)隨處可見了,那就是重寫Fragment的setUserVisibleHint()方法,實現(xiàn)Fragment內(nèi)部的懶加載機制。
首先我們看看這個方法的注釋:
/**
* 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.
*/
英文有點多,簡單的翻譯總結(jié)一下就是,F(xiàn)ramgent沒有直接顯示給用戶(例如滾動出了屏幕)會返回false,值得注意的是這個方法可能在Fragment的生命周期以外調(diào)用。
官方已經(jīng)提示這個方法可能在生命周期之外調(diào)用,先看看這個方法到底是在什么時候會被調(diào)用:
我在Fragment中打印了Fragment生命周期的幾個比較重要的方法,從log上看setUserVisibleHint()的調(diào)用早于onCreateView,所以如果在setUserVisibleHint()要實現(xiàn)懶加載的話,就必須要確保View以及其他變量都已經(jīng)初始化結(jié)束,避免空指針。
然后滑動ViewPager,看一下運行情況:
滑動的時候,兩個Fragment的setUserVisibleHint()方法被調(diào)用,顯示狀態(tài)也發(fā)生了變化。
ok,那就可以直接寫代碼了,先定義我們的MyFragment:
/**
* Created by li.zhipeng on 2017/5/7.
* <p>
* Fragment
*/
public class MyFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener{
private SwipeRefreshLayout swipeRefreshLayout;
private ListView listView;
private Handler handler = new Handler();
/**
* 是否已經(jīng)初始化完成
* */
private boolean isPrepare;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
Log.e("lzp", "onCreateView" + hashCode());
return inflater.inflate(R.layout.fragment_my, container, false);
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
Log.e("lzp", "onViewCreate" + hashCode());
swipeRefreshLayout = (SwipeRefreshLayout) view.findViewById(R.id.swipeRefreshLayout);
listView = (ListView) view.findViewById(R.id.listView);
swipeRefreshLayout.setOnRefreshListener(this);
// 初始化完成
isPrepare = true;
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
Log.e("lzp", "onActivityCreated" + hashCode());
// 創(chuàng)建時要判斷是否已經(jīng)顯示給用戶,加載數(shù)據(jù)
onVisibleToUser();
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
Log.e("lzp", "setUserVisibleHint" + hashCode() + ":" + isVisibleToUser);
// 顯示狀態(tài)發(fā)生變化
onVisibleToUser();
}
/**
* 顯示給用戶的操作
* */
private void onVisibleToUser(){
if (isPrepare && getUserVisibleHint()){
onRefresh();
}
}
/**
* 加載數(shù)據(jù)
* */
private void loadData() {
listView.setAdapter(new ArrayAdapter<>(getActivity(), android.R.layout.simple_list_item_1, new String[]{
"111", "111", "111", "111", "111", "111", "111", "111", "111", "111", "111",
"111", "111", "111", "111", "111", "111", "111", "111", "111", "111", "111"
}));
}
@Override
public void onRefresh() {
// 只加載一次數(shù)據(jù),避免界面切換的時候,加載數(shù)據(jù)多次
if (listView.getAdapter() == null){
swipeRefreshLayout.setRefreshing(true);
new Thread(){
@Override
public void run() {
// 延遲1秒
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
handler.post(new Runnable() {
@Override
public void run() {
loadData();
swipeRefreshLayout.setRefreshing(false);
}
});
}
}.start();
}
}
}
效果圖我就不貼出來了,最后會有源碼鏈接,大家可以下載運行看看效果。
這樣就結(jié)束了嗎?那肯定不行,雖然了解了用法,但是不封裝一下,以后就要寫很多次,簡直不要太low。
封裝之后的BaseFragment的代碼:
/**
* Created by li.zhipeng on 2017/5/8.
* <p>
* Fragment 的基類
*/
public abstract class BaseFragment extends Fragment {
/**
* 布局id
*/
private View contentView;
/**
* 是否啟用懶加載,此屬性僅對BaseLazyLoadFragment有效
* */
private boolean isLazyLoad;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
contentView = inflater.inflate(setLayoutId(), container, false);
return contentView;
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
// 初始化
init();
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// 如果不是懶加載模式,創(chuàng)建就加載數(shù)據(jù)
if (!isLazyLoad){
loadData();
}
}
/**
* 設(shè)置加載的布局Id
*/
protected abstract int setLayoutId();
/**
* 初始化操作
*/
protected abstract void init();
/**
* findViewById
*/
protected View findViewById(int id) {
return contentView.findViewById(id);
}
/**
* 懶加載數(shù)據(jù)
*/
protected abstract void loadData();
/**
* 是否啟用懶加載,此方法僅對BaseLazyLoadFragment有效
* */
public void setLazyLoad(boolean lazyLoad) {
isLazyLoad = lazyLoad;
}
}
接下來是BaseLazyLoadFragment:
/**
* Created by li.zhipeng on 2017/5/8.
*
* 懶加載的Fragment
*/
public abstract class BaseLazyLoadFragment extends BaseFragment {
/**
* 是否已經(jīng)初始化結(jié)束
*/
private boolean isPrepare;
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
setLazyLoad(true);
isPrepare = true;
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// 創(chuàng)建時要判斷是否已經(jīng)顯示給用戶,加載數(shù)據(jù)
onVisibleToUser();
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
// 顯示狀態(tài)發(fā)生變化
onVisibleToUser();
}
/**
* 判斷是否需要加載數(shù)據(jù)
*/
private void onVisibleToUser() {
// 如果已經(jīng)初始化完成,并且顯示給用戶
if (isPrepare && getUserVisibleHint()) {
loadData();
}
}
}
最后看看MyFragment的代碼:
/**
* Created by li.zhipeng on 2017/5/7.
* <p>
* Fragment
*/
public class MyFragment extends BaseLazyLoadFragment implements SwipeRefreshLayout.OnRefreshListener {
private SwipeRefreshLayout swipeRefreshLayout;
private ListView listView;
private Handler handler = new Handler();
@Override
protected int setLayoutId() {
return R.layout.fragment_my;
}
@Override
protected void init() {
swipeRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.swipeRefreshLayout);
listView = (ListView) findViewById(R.id.listView);
swipeRefreshLayout.setOnRefreshListener(this);
}
@Override
protected void loadData() {
onRefresh();
}
@Override
public void onRefresh() {
// 只加載一次數(shù)據(jù),避免界面切換的時候,加載數(shù)據(jù)多次
if (listView.getAdapter() == null) {
swipeRefreshLayout.setRefreshing(true);
new Thread() {
@Override
public void run() {
// 延遲1秒
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
handler.post(new Runnable() {
@Override
public void run() {
listView.setAdapter(new ArrayAdapter<>(getActivity(), android.R.layout.simple_list_item_1, new String[]{
"111", "111", "111", "111", "111", "111", "111", "111", "111", "111", "111",
"111", "111", "111", "111", "111", "111", "111", "111", "111", "111", "111"
}));
swipeRefreshLayout.setRefreshing(false);
}
});
}
}.start();
}
}
}
ok,這樣封裝就結(jié)束了,以后不需要懶加載Framgent可以直接繼承BaseFragment,需要懶加載的直接繼承BaseLazyLoadFragment。
擴展#
如果你也自己敲著代碼去研究懶加載,并且Framgent的數(shù)量大于2個,你會發(fā)現(xiàn)等你翻到第三個,再重新返回第一個的時候,第一個又重新加載了,并且重新走了創(chuàng)建周期,我在這里給不明白的原因的朋友解釋一下:
ViewPager只默認預(yù)加載下一頁的Fragment,其他的Fragment會被移除并銷毀。
下一次再添加的時候就需要重新創(chuàng)建Fragment。
那如果解決這個問題呢?
viewPager.setOffscreenPageLimit(2);
這個方法可以設(shè)置ViewPager預(yù)加載的頁數(shù),我們看一下方法的注釋:
/**
* Set the number of pages that should be retained to either side of the
* current page in the view hierarchy in an idle state. Pages beyond this
* limit will be recreated from the adapter when needed.
*
* <p>This is offered as an optimization. If you know in advance the number
* of pages you will need to support or have lazy-loading mechanisms in place
* on your pages, tweaking this setting can have benefits in perceived smoothness
* of paging animations and interaction. If you have a small number of pages (3-4)
* that you can keep active all at once, less time will be spent in layout for
* newly created view subtrees as the user pages back and forth.</p>
*
* <p>You should keep this limit low, especially if your pages have complex layouts.
* This setting defaults to 1.</p>
*
* @param limit How many pages will be kept offscreen in an idle state.
*/
英文注釋有點長,簡單翻譯總結(jié)一下幾點:
1、設(shè)置當前頁的左右兩邊的預(yù)加載數(shù)量。
2、通過設(shè)置預(yù)加載的數(shù)量,有利于動畫的顯示和交互。(上面的英文提到了懶加載,個人感覺跟懶加載沒什么關(guān)系,預(yù)加載數(shù)量少了,必然流暢度相對會越高)
3、如果頁數(shù)比較少(例如3到4個),可以加載所有的頁數(shù),這樣可以介紹頁數(shù)創(chuàng)建的時間,滑動更流暢
4、保持預(yù)加載個數(shù)偏小,除非你的頁數(shù)有多種復(fù)雜的布局,默認為1。(不可能小于1,方法里判斷)
了解了這個方法的特性,我們只要viewPager.setOffscreenPageLimit(2)就可以了,這樣就解決了剛才的問題。
另外要說的是setUserVisibleHint()只有在ViewPager+Fragment的時候才有效,單用Fragment的時候可以考慮使用onHiddenChanged(boolean hidden)方法。
總結(jié)#
ok,懶加載就到此結(jié)束,這個問題一家國內(nèi)大型的互聯(lián)網(wǎng)上市公司問的問題,我們平時可能覺得懶加載什么的無所謂,產(chǎn)品就是矯情,然后開始抱怨這個那個的,現(xiàn)在看來這就是差別的所在,大公司對于性能的要求真是非常的嚴格,這一點我們都需要耐心的學(xué)習(xí),才能真正的有所提高。