美文网首页Android开发实战总结
[Android] 列表控件(RecycleView,GridV

[Android] 列表控件(RecycleView,GridV

作者: 冰川孤辰js | 来源:发表于2016-05-30 11:04 被阅读3585次

    [TOC]
    列表控件也算是很常见的控件了,现在基本都切换到RecycleView了,这边记录下列表控件的基本的使用以及几种情况的处理:

    Demo链接

    RecycleView

    官网介绍

    使用上基本步骤如下:

    1. 设置布局管理器
    // LinearLayout布局
    LinearLayoutManager mLinearLayoutMgr = new LinearLayoutManager(this);
    mLinearLayoutMgr.setOrientation(LinearLayoutManager.HORIZONTAL);
    // Grid布局,数值表示列数
    GridLayoutManager mGridLayoutMgr = new GridLayoutManager(this, 3);
    // 瀑布流布局
    StaggeredGridLayoutManager mStaggedGridLayoutMgr = new StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.HORIZONTAL);
    mRv.setLayoutManager(mLinearLayoutMgr);
    
    1. 设置适配器
      适配器需要继承 RecyclerView.Adapter<? extends RecyclerView.ViewHolder>
      viewHolder 需要继承 RcycleViewHolder ;
      需要重写几个方法:
    • onCreateViewHolder(ViewGroup parent, int viewType) 根据 viewType 创建具体的行布局
    • onBindViewHolder(PairViewHolder holder, int position) 绑定数据到具体布局视图上,并设置点击事件等操作,这个比较蛋疼,不像 ListView , gridView那样直接提供了方法
    • getItemCount() 共有多少个 item
    • getItemViewType(int position) 创建 ViewHolder 时的依据,只有一种布局时,不需关心

    "Talk is cheap. Show me the code"

    public class RvAdapter extends RecyclerView.Adapter<RvAdapter.MyViewHolder> {
        private final ArrayList<Integer> data;//数据源
        private final LayoutInflater mInflater;//在创建View时需要用
        private static final String TAG = "RvAdapter";
    
        public RvAdapter(Context cxt, ArrayList<Integer> picList) {
            this.data = picList;
            mInflater = LayoutInflater.from(cxt);
        }
    
        @Override
        public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            // 在这里创建ItemView并设置ViewHolder以便复用
            MyViewHolder viewHolder = new MyViewHolder(mInflater.inflate(R.layout.item_rv, parent, false));
            return viewHolder;
        }
    
        @Override
        public void onBindViewHolder(MyViewHolder holder, final int position) {  
            // 设置数据
            holder.iv.setBackgroundResource(data.get(position));
            holder.tv.setText(position + "");
    
            // 设置事件
            holder.iv.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    Log.i(TAG, "onClick pos:" + position);
                }
            });
        }
    
        @Override
        public int getItemCount() {
            // 设个没啥好说的,返回总item个数
            return data.size();
        }
    
        class MyViewHolder extends RecyclerView.ViewHolder {
            // 复用的ViewHolder 需要继承RecycleView
            
            ImageView iv;
            TextView tv;
    
            public MyViewHolder(View itemView) {
                super(itemView);
                iv = (ImageView) itemView.findViewById(R.id.iv_item);
                tv = (TextView) itemView.findViewById(R.id.tv_index);
            }
        }
    }
    

    还有就是设置分割线和动画,这两个我没基本没用到,就先跳过了;

    添加header

    对于Grid布局管理器,如果想添加一个占据一整行的header,需要重写指定位置的item所占的宽度:

    mLayoutMgr = new GridLayoutManager(this, 3);
    mRv.setLayoutManager(mLayoutMgr);
    
    mLayoutMgr.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
        @Override
        public int getSpanSize(int position) {
            return position == 0 ? mLayoutMgr.getSpanCount() : 1;
        }
    });
    

    跳转动效

    直接跳转到指定position位置时,recycleView的变化是瞬间的,体验不是很好,我们会希望是缓慢滑动过去,直接想到的方法自然是 smoothScrollTo***,效果类似如下

    缓慢跳转到指定位置

    看看RecycleView的相应方法源码:

    public void smoothScrollToPosition(RecyclerView recyclerView, State state,
                    int position) {
                Log.e(TAG, "You must override smoothScrollToPosition to support smooth scrolling");
    }
    

    最终还是使用smoothScrollToPosition(int position),重写布局管理器即可:

    // 控制滑动速度的LinearLayoutManager
    public class ScrollSpeedLinearLayoutManger extends LinearLayoutManager {
        private float MILLISECONDS_PER_INCH = 0.3f;
        private Context context;
    
        public ScrollSpeedLinearLayoutManger(Context context) {
            super(context);
            this.context = context;
        }
    
        @Override
        public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {
            LinearSmoothScroller linearSmoothScroller =
                    new LinearSmoothScroller(recyclerView.getContext()) {
                        @Override
                        public PointF computeScrollVectorForPosition(int targetPosition) {
                            return ScrollSpeedLinearLayoutManger.this
                                    .computeScrollVectorForPosition(targetPosition);
                        }
    
                        //返回滑动一个pixel需要多少毫秒
                        @Override
                        protected float calculateSpeedPerPixel
                        (DisplayMetrics displayMetrics) {
                            return MILLISECONDS_PER_INCH / displayMetrics.density;
                        }
                    };
            linearSmoothScroller.setTargetPosition(position);
            startSmoothScroll(linearSmoothScroller);
        }
    
        public void setSpeedSlow() {
            //自己在这里用density去乘,希望不同分辨率设备上滑动速度相同
            //0.3f是自己估摸的一个值,可以根据不同需求自己修改
            MILLISECONDS_PER_INCH = context.getResources().getDisplayMetrics().density * 0.3f;
        }
    
        public void setSpeedFast() {
            MILLISECONDS_PER_INCH = context.getResources().getDisplayMetrics().density * 0.03f;
        }
    }
    

    下拉刷新

    RecycleView也没有了类似ListView那样的header和footer部分,下拉刷新其实可以用系统提供的控件:SwipeRefreshLayout

    <android.support.v4.widget.SwipeRefreshLayout
        android:id="@+id/srl_refresh"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <android.support.v7.widget.RecyclerView
            android:id="@+id/rv_load_more"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
    </android.support.v4.widget.SwipeRefreshLayout>
    
    SwipeRefreshLayout mSrl = findView(R.id.srl_refresh);
    // 使用系统控件来监听刷新,记得数据更新后要取消刷新动画
    mSrl.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
        @Override
        public void onRefresh() {
           // TODO: 更新数据
           
           // 取消加载动画
           mSrl.setRefreshing(false);
        }
    });
    

    上拉加载更多

    update: 现在我一般是用这个库 SwipyRefreshLayout ,上拉下拉都是一个效果
    类似分页加载,由于没有单独提供footer,所以我们考虑通过ViewType来模拟;
    在adapter中需有两种ItemViewType,一种为底部进度加载条样式,我们通过判断recycleView是否已经滑动到底部,来动态添加/删除一行标志数据用以表示是否需要显示进度条的itemView,另外,数据加载完后,需要删除原先的标志数据,即删掉加载条,然后更新列表即可:

    mRv.addOnScrollListener(new RecyclerView.OnScrollListener() {
        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            super.onScrolled(recyclerView, dx, dy);
    
            totalItemCount = mLayoutMgr.getItemCount();
            lastVisibleItemPos = mLayoutMgr.findLastVisibleItemPosition();
    
            // 加1是position和size的区别
            if (!isLoading && totalItemCount <= (lastVisibleItemPos + 1)) {
                loadMoreData();
                isLoading = true;
            }
        }
    });
    
    // 模拟加载数据过程
    private void loadMoreData() {
        // 在原数据集末尾添加一条标志数据,告诉适配器显示加载进度条
        mData.add(null);//加载什么样的数据,只要跟adapter配合能识别出来即可
        mAdapter.notifyItemInserted(mData.size() - 1);
        
        mHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                // 加载过程结束后,记得清除最后一个标志位
                mData.remove(mData.size() - 1);
                mAdapter.notifyItemRemoved(mData.size());
        
                // 获取新增数据
                int start = mData.size();
                int end = start + 10;
                for (int i = start; i < end; i++) {
                    mData.add("added pos: " + i);
                }
        
                // 更新列表
                mAdapter.notifyDataSetChanged();
                isLoading = false;
            }
        }, 2000);
    }
    
    // 在adapter中重写判断itemViewType的方法
    @Override
    public int getItemViewType(int position) {
        // 标志数据也可以用其他的,这里我用 null 或者 "" 来表示
        if (TextUtils.isEmpty(mData.get(position))) {
            return TYPE_LOADING;
        } else {
            return TYPE_NORMAL;
        }
    }
    
    上拉更多-下拉刷新

    默认添加删除动画

    Demo
    推荐这个库
    RecyclerView自带的一个 DefaultItemAnimator 可以实现添加删除item时,插入移除动画效果

    //kotlin代码
    //设置recyclerview的动画recyclerView.itemAnimator = DefaultItemAnimator()
    //添加或删除数据源后,要调用如下方法才有动画效果
    recyclerView.adapter.notifyItemRangeInserted(addPos,addItemCount)
    recyclerView.adapter.notifyItemRemoved(removePos)
    
    添加删除动画效果添加删除动画效果

    使用ItemTouchHelper实现拖拽改变item顺序及swipe滑动删除item

    Demo

    // kotlin
    // 添加滑动/拖拽功能
    // java的匿名内部类对应过来就是object对象表达式了
    ItemTouchHelper(object : ItemTouchHelper.Callback() {
        var vh: RecyclerView.ViewHolder? = null
    
        /**
         * 设置itemView可以移动的方向
         * */
        override fun getMovementFlags(recyclerView: RecyclerView?, viewHolder: RecyclerView.ViewHolder?): Int {
            // 拖拽的标记,这里允许上下左右四个方向
            val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN or ItemTouchHelper.LEFT or
                    ItemTouchHelper.RIGHT
            // 滑动的标记,这里允许左右滑动
            val swipeFlags = ItemTouchHelper.START or ItemTouchHelper.END
            return makeMovementFlags(dragFlags, swipeFlags)
        }
    
         /**
         * 当一个Item被另外的Item替代时回调,也就是数据集的内容顺序改变
         * 返回true, onMoved()才会进行
         * */
        override fun onMove(recyclerView: RecyclerView?, viewHolder: RecyclerView.ViewHolder?, target: RecyclerView.ViewHolder?): Boolean {
            return true
        }
    
        /**
         *  当onMove返回true的时候回调,刷新列表
         * */
        override fun onMoved(recyclerView: RecyclerView?, viewHolder: RecyclerView.ViewHolder?, fromPos: Int, target: RecyclerView.ViewHolder?, toPos: Int, x: Int, y: Int) {
            super.onMoved(recyclerView, viewHolder, fromPos, target, toPos, x, y)
            // 移动完成后修改列表位置并刷新列表
            Collections.swap(data, viewHolder!!.adapterPosition, target!!.adapterPosition)
            rv_main.adapter.notifyItemMoved(viewHolder!!.adapterPosition, target!!.adapterPosition)
        }
    
        /**
         * 滑动完成时回调,这里设置为滑动删除,删除相应数据后刷新列表
         * */
        override fun onSwiped(viewHolder: RecyclerView.ViewHolder?, direction: Int) {
            data.removeAt(viewHolder!!.adapterPosition)
            rv_main.adapter.notifyItemRemoved(viewHolder!!.adapterPosition)
            toast("删除成功")
        }
    
        /**
         * Item是否可以滑动
         * */
        override fun isItemViewSwipeEnabled() = true
    
        /**
         * Item是否可以长按
         * */
        override fun isLongPressDragEnabled() = true
    
    }).attachToRecyclerView(rv_main)
    
    拖拽滑动删除效果拖拽滑动删除效果

    popupWindow中使用RecycleView

    recycleView的高度自适应

    默认情况下,即使设置其高度为wrap_content,其高度也是全屏的,需要重新布局管理器来计算item总高度

    测试时发现适用于v7-23.1.1,升级到23.4.0后就会数组下标越界,可将 View child = recycler.getViewForPosition(i); 修改为 View child = getChildAt(i);if (child != null) {...} ,但其实没有必要,因为在v7-23.4.0的时候,系统已经可以自适应高度了,不需要手动去计算

    public  class FixGridLayoutManager extends GridLayoutManager {
            public FixGridLayoutManager(Context context, int spanCount) {
                //默认方向是VERTICAL
                super(context, spanCount);
            }
    
            public FixGridLayoutManager(Context context, int spanCount, int orientation, boolean reverseLayout) {
                super(context, spanCount, orientation, reverseLayout);
            }
    
        @Override
        public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state, int widthSpec, int heightSpec) {
            int height = 0;
            int childCount = getItemCount();
            for (int i = 0; i < childCount; i++) {
                View child = recycler.getViewForPosition(i);
                // measureChild(child, widthSpec, heightSpec);
                ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) child.getLayoutParams();
                // 奇怪,最近测试发现,上面的measureChild方法好像不太管用,换成下面
                int childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec, getPaddingTop() + getPaddingBottom(), lp.height);
                child.measure(widthSpec, childHeightSpec);
    
                if (i % getSpanCount() == 0) {
                    int measuredHeight = child.getMeasuredHeight() + getDecoratedBottom(child) + lp.topMargin + lp.bottomMargin;
                    height += measuredHeight;
                }
            }
            setMeasuredDimension(View.MeasureSpec.getSize(widthSpec), height);
        }
    }
    

    点击事件中使用itemNotify时FC

    使用自定义的布局管理器后,点击事件会报错:

    java.lang.IllegalArgumentException: Tmp detached view should be removed from RecyclerView before it can be recycled: ViewHolder

    没去细究为啥,我在adapter中使用的是 notifyItemChanged(position); 改成普通的全量刷新就可以了

    notifyDataSetChanged();
    

    GridView

    gridView基本没再用了,不过之前碰到过几个坑,在此也一并记录下:

    // 基本使用方法
    GridView mGv = findViewById(R.id.gv_basic);
    mGv.setNumColumns(3);//设置列数,也可在xml中设定
    
    // 适配器同样与ListView类似,继承自BaseAdapter
    GvAdapter gvAdapter = new GvAdapter(this, mData, true);
    mGv.setAdapter(gvAdapter);
    
    //添加点击监听
    mGv.setOnItemClickListener(new AdapterView.OnItemClickListener() {
        @Override
        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
            Log.i(TAG, "onItemClick 您点击了第 position: " + position + " 个item");
        }
    });
    

    1. 固定item高度

    之前有个需求是在一个页面显示9个item,填满屏幕:

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ......
        //固定item高度,这里使用3*3填满整个屏幕/gridView
        convertView.setLayoutParams(new AbsListView.LayoutParams(parent.getWidth() / 3, parent.getHeight() / 3));
        // 恢复默认的话设置高度为wrap_content就可以了
        // convertView.setLayoutParams(new AbsListView.LayoutParams(parent.getWidth() / 3,ViewGroup.LayoutParams.WRAP_CONTENT));
        ......
        return convertView;
    }
    

    2. ListView中嵌套GridView

    • gridView只显示一行的问题
    //重写GridView的onMeasure()方法
    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2,
                MeasureSpec.AT_MOST);
        super.onMeasure(widthMeasureSpec, expandSpec);
    }
    
    • 同时设置ListView和GridView的点击事件,只有GridView的有响应
      需要在ListView的item布局顶层屏蔽子元素焦点事件
    <LinearLayout 
        ......
        android:descendantFocusability="blocksDescendants">
    
        <org.lynxz.androiddemos.widget.FixGridView
        ....../>
    </LinearLayout>
    

    这样listView的item点击事件就能被触发了,同时若是点击到GridView的item会触发GridView的事件;
    同理,若是GridView的item中有抢焦点的控件导致其点击事件失效,也同样在其item布局顶层添加该属性;

    相关文章

      网友评论

        本文标题:[Android] 列表控件(RecycleView,GridV

        本文链接:https://www.haomeiwen.com/subject/jbkfdttx.html