前言#
在我們的項(xiàng)目里經(jīng)常會(huì)用到ViewPager+Fragment實(shí)現(xiàn)選項(xiàng)卡滑動(dòng)切換的效果,ViewPager會(huì)預(yù)加載下一個(gè)Framgment的內(nèi)容弹灭,這樣的機(jī)制有優(yōu)點(diǎn)也有缺點(diǎn):
預(yù)加載讓用戶可以更快的看到接下來的內(nèi)容督暂,瀏覽起來連貫性更好,但是app在展示內(nèi)容的同時(shí)還增加了額外的任務(wù)穷吮,這樣可能影響界面的流暢度逻翁,并且可能造成流量的浪費(fèi)。
目前大部分的app都使用Fragment懶加載機(jī)制捡鱼,例如嗶哩嗶哩八回,360手機(jī)助手等等。
正文#
實(shí)現(xiàn)Fragment懶加載也可以有很多種辦法,可能有些朋友會(huì)第一時(shí)間想到通過監(jiān)聽滾動(dòng)的位置缠诅,通過判斷Fragment的加載狀態(tài)實(shí)現(xiàn)懶加載溶浴,這種方法的缺點(diǎn)就是把實(shí)現(xiàn)暴露在Framgment之外,從封裝的角度來說這不是一個(gè)好的方案管引。
最好的方案在網(wǎng)上已經(jīng)隨處可見了士败,那就是重寫Fragment的setUserVisibleHint()方法,實(shí)現(xiàn)Fragment內(nèi)部的懶加載機(jī)制褥伴。
首先我們看看這個(gè)方法的注釋:
/**
* 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.
*/
英文有點(diǎn)多谅将,簡單的翻譯總結(jié)一下就是,F(xiàn)ramgent沒有直接顯示給用戶(例如滾動(dòng)出了屏幕)會(huì)返回false重慢,值得注意的是這個(gè)方法可能在Fragment的生命周期以外調(diào)用戏自。
官方已經(jīng)提示這個(gè)方法可能在生命周期之外調(diào)用,先看看這個(gè)方法到底是在什么時(shí)候會(huì)被調(diào)用:
我在Fragment中打印了Fragment生命周期的幾個(gè)比較重要的方法伤锚,從log上看setUserVisibleHint()的調(diào)用早于onCreateView,所以如果在setUserVisibleHint()要實(shí)現(xiàn)懶加載的話志衣,就必須要確保View以及其他變量都已經(jīng)初始化結(jié)束屯援,避免空指針。
然后滑動(dòng)ViewPager念脯,看一下運(yùn)行情況:
滑動(dòng)的時(shí)候狞洋,兩個(gè)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)建時(shí)要判斷是否已經(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í)候,加載數(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();
}
}
}
效果圖我就不貼出來了转培,最后會(huì)有源碼鏈接恶导,大家可以下載運(yùn)行看看效果。
這樣就結(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)建時(shí)要判斷是否已經(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í)候,加載數(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恕汇。
擴(kuò)展#
如果你也自己敲著代碼去研究懶加載腕唧,并且Framgent的數(shù)量大于2個(gè),你會(huì)發(fā)現(xiàn)等你翻到第三個(gè)瘾英,再重新返回第一個(gè)的時(shí)候枣接,第一個(gè)又重新加載了,并且重新走了創(chuàng)建周期缺谴,我在這里給不明白的原因的朋友解釋一下:
ViewPager只默認(rèn)預(yù)加載下一頁的Fragment但惶,其他的Fragment會(huì)被移除并銷毀。
下一次再添加的時(shí)候就需要重新創(chuàng)建Fragment湿蛔。
那如果解決這個(gè)問題呢膀曾?
viewPager.setOffscreenPageLimit(2);
這個(gè)方法可以設(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.
*/
英文注釋有點(diǎn)長阳啥,簡單翻譯總結(jié)一下幾點(diǎn):
1添谊、設(shè)置當(dāng)前頁的左右兩邊的預(yù)加載數(shù)量。
2察迟、通過設(shè)置預(yù)加載的數(shù)量斩狱,有利于動(dòng)畫的顯示和交互。(上面的英文提到了懶加載扎瓶,個(gè)人感覺跟懶加載沒什么關(guān)系所踊,預(yù)加載數(shù)量少了,必然流暢度相對會(huì)越高)
3栗弟、如果頁數(shù)比較少(例如3到4個(gè))污筷,可以加載所有的頁數(shù),這樣可以介紹頁數(shù)創(chuàng)建的時(shí)間乍赫,滑動(dòng)更流暢
4瓣蛀、保持預(yù)加載個(gè)數(shù)偏小,除非你的頁數(shù)有多種復(fù)雜的布局雷厂,默認(rèn)為1惋增。(不可能小于1,方法里判斷)
了解了這個(gè)方法的特性改鲫,我們只要viewPager.setOffscreenPageLimit(2)就可以了诈皿,這樣就解決了剛才的問題林束。
另外要說的是setUserVisibleHint()只有在ViewPager+Fragment的時(shí)候才有效,單用Fragment的時(shí)候可以考慮使用onHiddenChanged(boolean hidden)方法稽亏。
總結(jié)#
ok壶冒,懶加載就到此結(jié)束,這個(gè)問題一家國內(nèi)大型的互聯(lián)網(wǎng)上市公司問的問題截歉,我們平時(shí)可能覺得懶加載什么的無所謂胖腾,產(chǎn)品就是矯情,然后開始抱怨這個(gè)那個(gè)的瘪松,現(xiàn)在看來這就是差別的所在咸作,大公司對于性能的要求真是非常的嚴(yán)格,這一點(diǎn)我們都需要耐心的學(xué)習(xí)宵睦,才能真正的有所提高记罚。