[Android] 列表控件(RecycleView,GridView)

[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布局顶层添加该属性;

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容