由滑动顶端悬浮引发的性能优化大坑坑坑—ScrollView嵌套ListView以及层层嵌套

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/xiaxiazaizai01/article/details/52781337

请尊重个人劳动成果,转载注明出处,谢谢!
http://blog.csdn.net/xiaxiazaizai01

看题目就知道,今天我们主要讲的主角是关于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;
        }
    }*/
}

在最开始接触代码的时候都只是想着能实现功能就行,根本不会想着这种方式是否优雅,随着不断的积累,总是会出现这样的情况,不知道你们有没有同感,就是,自己写出的代码,虽然达到了功能要求,但是自己总是质疑自己代码的性能是否过关。我觉着有这种忧虑并不是坏事,反而是种积极向上的,说明,,你,,在慢慢的进步。

好了,不扯这么多了,点击源码下载,可以自己调试一下

猜你喜欢

转载自blog.csdn.net/xiaxiazaizai01/article/details/52781337