由滑動頂端懸浮引發的效能優化大坑坑坑—ScrollView巢狀ListView以及層層巢狀
看題目就知道,今天我們主要講的主角是關於scrollview巢狀listview以及再層層巢狀導致的效能優化問題。現在市面上好多app都有這樣一種功能,在頁面中間某一位置有一個佈局,在頁面整體向上滑動時,當此佈局到達螢幕頂端或者某一位置時要求此佈局懸浮停靠,本文實現的思路是在需要懸浮停靠的位置設定一個一模一樣的佈局,滑動到該位置時就讓原先隱藏的佈局顯示,同理,當頁面向下滑動時,在將其隱藏。到這裡你該吐槽了,這種思路在網上不是一搜一大堆嗎?的確,但是這只是一個導火索,因為這是我沒事的時候隨便寫著玩的,因為裡面涉及到scrollview巢狀listview的情況,這時你又該說了,這種情況如果不重寫listview的話,則listview的item就不能全部展開,於是你會說了網上一搜又是一大堆,其中有一種是重寫listview的onMeasure()方法,程式碼量極少,使用也很簡單。之前我也是一直這麼用的,但是在寫這個demo的時候在除錯的時候發現,adapter的getView()會被重複呼叫多次,也就是說,假如我的item只有10個,則在getView()中列印position正常情況下應該是0-9, 也就是說getView()執行10次,但是,如果scrollview中巢狀的listview是通過重寫onMeasure()方法的listview的話,getView()就會被重複執行很多次,所以,如果巢狀的越深,那麼getView()重複執行的次數就會成倍的增加,對效能的影響就可想而知了。嚇尿了。。。於是我搜到了
接下來再看看列印的listview的adapter中的getView()方法的執行結果,你會大吃一鯨,,,沒錯,是鯨。。
說明一下,這裡我還只是嵌套了一層(巢狀多層的話,結果會讓你更加大吃一鯨),總共設定了20條資料,但是根據上面這個截圖(這只是部分截圖)可以看出來,當scrollview巢狀listview時,為了讓listview的item能夠完整的展開,如果你採用的是如下所示通過重寫listview的onMeasure()方法的話,你會發現,並不是你手指滑到哪裡就列印哪裡,而是發現adapter的getView()會一次性的將所有的資料全部加載出來並且反覆迴圈好多次,因為巢狀的 ListView 裡的 View 在一開始就全部被例項化了,所以listview也就不再具有複用的機制了。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2,
MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, expandSpec);
}
既然通過這種方式得到的listview不再具有複用機制,那麼再使用listview控制元件的話意義不大並且還會影響效能。這裡我們可以使用LinearLayout模擬ListView的方式,自己inflate addView findViewById等操作。在此基礎上,利用ViewHolder 思想,儘量避免每次重新整理都走findViewById這些耗效能的方法。
為啥要使用ViewHolder,為什麼要封裝這些快取?
這種頁面往往需要重新整理,最無腦的辦法就是removeAllViews(),簡單粗暴,啥都不考慮,使用者體驗將會變成,重新整理時閃一下,很差,因為View全部要inflate,addView,findViewById一遍。
所以我們在封裝的NestFullListView裡盡力避免重新整理時View的inflate、addView, 在ViewHolder 盡力避免重新整理時 findViewById();畢竟findViewById()操作也是很耗時的。zxt0601大牛已經封裝好了,我們先來大致看一下
/**
* 完全伸展開的ListView(LinearLayout)
*
*/
public class NestFullListView extends LinearLayout {
private LayoutInflater mInflater;
private List<NestFullViewHolder> mVHCahces;//快取ViewHolder,按照add的順序快取,
public NestFullListView(Context context) {
this(context, null);
}
public NestFullListView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public NestFullListView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
mInflater = LayoutInflater.from(context);
mVHCahces = new ArrayList<NestFullViewHolder>();
//annotate by zhangxutong 2016 09 23 for 讓本控制元件能支援水平佈局,專案的意外收穫= =
//setOrientation(VERTICAL);
}
private NestFullListViewAdapter mAdapter;
/**
* 外部呼叫 同時重新整理檢視
*
* @param mAdapter
*/
public void setAdapter(NestFullListViewAdapter mAdapter) {
this.mAdapter = mAdapter;
updateUI();
}
public void updateUI() {
if (null != mAdapter) {
if (null != mAdapter.getDatas() && !mAdapter.getDatas().isEmpty()) {
//資料來源有資料
if (mAdapter.getDatas().size() > getChildCount()) {//資料來源大於現有子View不清空
} else if (mAdapter.getDatas().size() < getChildCount()) {//資料來源小於現有子View,刪除後面多的
removeViews(mAdapter.getDatas().size(), getChildCount() - mAdapter.getDatas().size());
//刪除View也清快取
while (mVHCahces.size() > mAdapter.getDatas().size()) {
mVHCahces.remove(mVHCahces.size() - 1);
}
}
for (int i = 0; i < mAdapter.getDatas().size(); i++) {
NestFullViewHolder holder;
if (mVHCahces.size() - 1 >= i) {//說明有快取,不用inflate,否則inflate
holder = mVHCahces.get(i);
} else {
holder = new NestFullViewHolder(getContext(), mInflater.inflate(mAdapter.getItemLayoutId(), this, false));
mVHCahces.add(holder);//inflate 出來後 add進來快取
}
mAdapter.onBind(i, holder);
//如果View沒有父控制元件 新增
if (null == holder.getConvertView().getParent()) {
this.addView(holder.getConvertView());
}
}
} else {
removeAllViews();//資料來源沒資料 清空檢視
}
} else {
removeAllViews();//介面卡為空 清空檢視
}
}
}
每次updateUI()時,如果是異常情況:介面卡為空 清空檢視,資料來源沒資料 清空檢視
那麼資料來源有資料的情況下,比較資料來源的size 和現在子View(ItemView)的size,
如果資料來源大於現有子View,說明螢幕上的View不夠用,當然不remove子View,也不用清快取。
如果資料來源小於現有子View,刪除尾部多的子View,清理多餘快取的ItemView
遍歷資料來源,比較i(postion)和mVHCahces的size,
如果快取不夠就inflate一個新View,
如果快取有,就取出快取的View。
回撥Adapter的onBind方法,
判斷這個View有沒有父控制元件,
如果View沒有父控制元件 才addView()。
在上面的程式碼中已經儘可能的避免了View的inflate,addView()操作。可是我們都知道,findViewById()的操作也是很費時的,所以在上面的程式碼中使用了ViewHolder,下面就來看看這個ViewHolder,程式碼有點長,作者考慮的比較細緻,封裝一些常用的方法,例如setText、setImageResource等,供外部呼叫使用,同時還包括一些監聽事件。
public class NestFullViewHolder {
private SparseArray<View> mViews;
private View mConvertView;
private Context mContext;
public NestFullViewHolder(Context context, View view) {
mContext = context;
this.mViews = new SparseArray<View>();
mConvertView = view;
}
/**
* 通過viewId獲取控制元件
*
* @param viewId
* @return
*/
public <T extends View> T getView(int viewId) {
View view = mViews.get(viewId);
if (view == null) {
view = mConvertView.findViewById(viewId);
mViews.put(viewId, view);
}
return (T) view;
}
public View getConvertView() {
return mConvertView;
}
public NestFullViewHolder setSelected(int viewId, boolean flag) {
View v = getView(viewId);
v.setSelected(flag);
return this;
}
/**
* 設定TextView的值
*
* @param viewId
* @param text
* @return
*/
public NestFullViewHolder setText(int viewId, String text) {
TextView tv = getView(viewId);
tv.setText(text);
return this;
}
public NestFullViewHolder setImageResource(int viewId, int resId) {
ImageView view = getView(viewId);
view.setImageResource(resId);
return this;
}
public NestFullViewHolder setImageBitmap(int viewId, Bitmap bitmap) {
ImageView view = getView(viewId);
view.setImageBitmap(bitmap);
return this;
}
public NestFullViewHolder setImageDrawable(int viewId, Drawable drawable) {
ImageView view = getView(viewId);
view.setImageDrawable(drawable);
return this;
}
public NestFullViewHolder setBackgroundColor(int viewId, int color) {
View view = getView(viewId);
view.setBackgroundColor(color);
return this;
}
public NestFullViewHolder setBackgroundRes(int viewId, int backgroundRes) {
View view = getView(viewId);
view.setBackgroundResource(backgroundRes);
return this;
}
public NestFullViewHolder setTextColor(int viewId, int textColor) {
TextView view = getView(viewId);
view.setTextColor(textColor);
return this;
}
public NestFullViewHolder setTextColorRes(int viewId, int textColorRes) {
TextView view = getView(viewId);
view.setTextColor(mContext.getResources().getColor(textColorRes));
return this;
}
@SuppressLint("NewApi")
public NestFullViewHolder setAlpha(int viewId, float value) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
getView(viewId).setAlpha(value);
} else {
// Pre-honeycomb hack to set Alpha value
AlphaAnimation alpha = new AlphaAnimation(value, value);
alpha.setDuration(0);
alpha.setFillAfter(true);
getView(viewId).startAnimation(alpha);
}
return this;
}
public NestFullViewHolder setVisible(int viewId, boolean visible) {
View view = getView(viewId);
view.setVisibility(visible ? View.VISIBLE : View.GONE);
return this;
}
public NestFullViewHolder linkify(int viewId) {
TextView view = getView(viewId);
Linkify.addLinks(view, Linkify.ALL);
return this;
}
public NestFullViewHolder setTypeface(Typeface typeface, int... viewIds) {
for (int viewId : viewIds) {
TextView view = getView(viewId);
view.setTypeface(typeface);
view.setPaintFlags(view.getPaintFlags() | Paint.SUBPIXEL_TEXT_FLAG);
}
return this;
}
public NestFullViewHolder setProgress(int viewId, int progress) {
ProgressBar view = getView(viewId);
view.setProgress(progress);
return this;
}
public NestFullViewHolder setProgress(int viewId, int progress, int max) {
ProgressBar view = getView(viewId);
view.setMax(max);
view.setProgress(progress);
return this;
}
public NestFullViewHolder setMax(int viewId, int max) {
ProgressBar view = getView(viewId);
view.setMax(max);
return this;
}
public NestFullViewHolder setRating(int viewId, float rating) {
RatingBar view = getView(viewId);
view.setRating(rating);
return this;
}
public NestFullViewHolder setRating(int viewId, float rating, int max) {
RatingBar view = getView(viewId);
view.setMax(max);
view.setRating(rating);
return this;
}
public NestFullViewHolder setTag(int viewId, Object tag) {
View view = getView(viewId);
view.setTag(tag);
return this;
}
public NestFullViewHolder setTag(int viewId, int key, Object tag) {
View view = getView(viewId);
view.setTag(key, tag);
return this;
}
public NestFullViewHolder setChecked(int viewId, boolean checked) {
Checkable view = (Checkable) getView(viewId);
view.setChecked(checked);
return this;
}
/**
* 關於事件的
*/
public NestFullViewHolder setOnClickListener(int viewId,
View.OnClickListener listener) {
View view = getView(viewId);
view.setOnClickListener(listener);
return this;
}
public NestFullViewHolder setOnTouchListener(int viewId,
View.OnTouchListener listener) {
View view = getView(viewId);
view.setOnTouchListener(listener);
return this;
}
public NestFullViewHolder setOnLongClickListener(int viewId,
View.OnLongClickListener listener) {
View view = getView(viewId);
view.setOnLongClickListener(listener);
return this;
}
}
利用private SparseArray mViews,以viewId為key,儲存ItemView裡的各種View。
通過public T getView(int viewId)方法,以viewId為key,獲取ItemView裡的各種View,
該方法是先從mViews的快取裡尋找View,如果找到了直接返回,
如果沒找到就view = mConvertView.findViewById(viewId);執行findViewById,得到這個View,並放入mViews的快取裡,這樣下次就不用執行findViewById方法。詳解請看鴻洋的相關文章
然後再看看adapter
/**
* 介紹:完全伸展開的ListView的介面卡
* 作者:zhangxutong
* 郵箱:[email protected]
* CSDN:http://blog.csdn.net/zxt0601
* 時間: 16/09/09.
*/
public abstract class NestFullListViewAdapter<T> {
private int mItemLayoutId;//item佈局檔案id
private List<T> mDatas;//資料來源
public NestFullListViewAdapter(int mItemLayoutId, List<T> mDatas) {
this.mItemLayoutId = mItemLayoutId;
this.mDatas = mDatas;
}
/**
* 被FullListView呼叫
*
* @param i
* @param holder
*/
public void onBind(int i, NestFullViewHolder holder) {
//回撥bind方法,多傳一個data過去
onBind(i, mDatas.get(i), holder);
}
/**
* 資料繫結方法
*
* @param pos 位置
* @param t 資料
* @param holder ItemView的ViewHolder
*/
public abstract void onBind(int pos, T t, NestFullViewHolder holder);
public int getItemLayoutId() {
return mItemLayoutId;
}
public void setItemLayoutId(int mItemLayoutId) {
this.mItemLayoutId = mItemLayoutId;
}
public List<T> getDatas() {
return mDatas;
}
public void setDatas(List<T> mDatas) {
this.mDatas = mDatas;
}
}
最後再來看MainActivity,呼叫也相當簡單
mListView.setAdapter(new NestFullListViewAdapter<TestBean>(R.layout.item_listview,list) {
@Override
public void onBind(int pos, TestBean testBean, NestFullViewHolder holder) {
Log.i("巢狀一層時的getView()執行情況:",String.valueOf(pos));
holder.setText(R.id.tvName,testBean.getName());
}
});
那就再貼下通過此方法實現的列印資訊,看看是不是像預期的那樣
可以看到通過LinearLayout模擬ListView的方式,onBind()方法類似getView(),資料來源為20,那麼此方法只執行20次,不多不少,不會出現onBind()重複執行的現象,達到了我們的要求。
最後貼下MainActivity的完整程式碼,裡面有重寫了listview的onMeasure()的adapter實現,也有改進的優雅方案NestFullListView。方便大家除錯對比
public class MainActivity extends AppCompatActivity implements MyScrollView.ScrollViewListener{
private ImageView ivTop;//頂部區域
private ListView listView;
private NestFullListView mListView;
private RelativeLayout rl;
// private MyAdapter adapter;
private MyScrollView myScrollView;
private List<TestBean> list;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
getSupportActionBar().hide();
ivTop = (ImageView) findViewById(R.id.ivTop);
rl = (RelativeLayout) findViewById(R.id.rl);//處於頂部隱藏的搜尋佈局
// listView = (ListView) findViewById(R.id.listview);
mListView = (NestFullListView) findViewById(R.id.listview);
myScrollView = (MyScrollView) findViewById(R.id.myScrollView);
//模擬資料
list = new ArrayList<>();
for(int i=0;i<20;i++){
TestBean bean = new TestBean();
bean.setName("我是:"+i);
list.add(bean);
}
/**
* 通過LinearLayout模擬ListView的方式,onBind()方法類似getView(),資料來源為20,那麼此方法只執行20次,不多不少
* 不會出現onBind()重複執行的現象
*/
mListView.setAdapter(new NestFullListViewAdapter<TestBean>(R.layout.item_listview,list) {
@Override
public void onBind(int pos, TestBean testBean, NestFullViewHolder holder) {
Log.i("巢狀一層時的getView()執行情況:",String.valueOf(pos));
holder.setText(R.id.tvName,testBean.getName());
}
});
/* adapter = new MyAdapter(list);
listView.setAdapter(adapter);*/
myScrollView.setScrollViewListener(this);
//ScrollView巢狀ListView後,進入頁面不從頂部開始顯示的問題,這是由於又重繪了ListView的高度導致的,解決辦法就是讓listview失去焦點
mListView.setFocusable(false);
}
//當Scrollview向上滑動的距離大於等於頂部區域的高度時,
// 也就是浮動區域A的頂邊貼到螢幕頂部的時候,這是將浮動區域B的可見性設定為VISIBLE即可,否則設定為GONE即可。
@Override
public void onScrollChanged(int x, int y, int oldx, int oldy) {
if (y >= ivTop.getHeight()){
rl.setVisibility(View.VISIBLE);
}else {
rl.setVisibility(View.GONE);
}
}
/**
* 此adapter主要是測試巢狀重寫listview的onMeasure()方法,
* getView()會多次重複執行
*/
/* class MyAdapter extends BaseAdapter{
private List<TestBean> lists;
public MyAdapter(List<TestBean> lists){
this.lists = lists;
}
@Override
public int getCount() {
return lists != null ? lists.size() : 0;
}
@Override
public Object getItem(int position) {
return lists != null ? lists.get(position) : 0;
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
Log.i("巢狀一層時的getView()執行情況:",String.valueOf(position));
ViewHolder holder = null;
if(convertView == null){
convertView = LayoutInflater.from(MainActivity.this).inflate(R.layout.item_listview,null);
holder = new ViewHolder();
holder.name = (TextView) convertView.findViewById(R.id.tvName);
convertView.setTag(holder);
}else{
holder = (ViewHolder) convertView.getTag();
}
TestBean bean = lists.get(position);
holder.name.setText(bean.getName());
return convertView;
}
class ViewHolder{
TextView name;
}
}*/
}