1. 程式人生 > >由滑動頂端懸浮引發的效能優化大坑坑坑—ScrollView巢狀ListView以及層層巢狀

由滑動頂端懸浮引發的效能優化大坑坑坑—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()重複執行的次數就會成倍的增加,對效能的影響就可想而知了。嚇尿了。。。於是我搜到了

zxt0601 大牛也遇到了這樣的問題以及相對文藝的解決方案,在這裡把牛人的方案拿過來做個記錄,時刻提醒自己。喜歡原文的請移駕,說了這麼一大堆的意思就是說在保證產量的同時更要注重效能的優化。好了,我們來看幾張效果圖吧。
這裡寫圖片描述
接下來再看看列印的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;
        }
    }*/
}

在最開始接觸程式碼的時候都只是想著能實現功能就行,根本不會想著這種方式是否優雅,隨著不斷的積累,總是會出現這樣的情況,不知道你們有沒有同感,就是,自己寫出的程式碼,雖然達到了功能要求,但是自己總是質疑自己程式碼的效能是否過關。我覺著有這種憂慮並不是壞事,反而是種積極向上的,說明,,你,,在慢慢的進步。

好了,不扯這麼多了,點選原始碼下載,可以自己除錯一下