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

[TOC]
列表控件也算是很常見的控件了,現(xiàn)在基本都切換到RecycleView了,這邊記錄下列表控件的基本的使用以及幾種情況的處理:

Demo鏈接

RecycleView

官網(wǎng)介紹

使用上基本步驟如下:

  1. 設(shè)置布局管理器
// LinearLayout布局
LinearLayoutManager mLinearLayoutMgr = new LinearLayoutManager(this);
mLinearLayoutMgr.setOrientation(LinearLayoutManager.HORIZONTAL);
// Grid布局,數(shù)值表示列數(shù)
GridLayoutManager mGridLayoutMgr = new GridLayoutManager(this, 3);
// 瀑布流布局
StaggeredGridLayoutManager mStaggedGridLayoutMgr = new StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.HORIZONTAL);
mRv.setLayoutManager(mLinearLayoutMgr);
  1. 設(shè)置適配器
    適配器需要繼承 RecyclerView.Adapter<? extends RecyclerView.ViewHolder>
    viewHolder 需要繼承 RcycleViewHolder ;
    需要重寫幾個(gè)方法:
  • onCreateViewHolder(ViewGroup parent, int viewType) 根據(jù) viewType 創(chuàng)建具體的行布局
  • onBindViewHolder(PairViewHolder holder, int position) 綁定數(shù)據(jù)到具體布局視圖上,并設(shè)置點(diǎn)擊事件等操作,這個(gè)比較蛋疼,不像 ListView , gridView那樣直接提供了方法
  • getItemCount() 共有多少個(gè) item
  • getItemViewType(int position) 創(chuàng)建 ViewHolder 時(shí)的依據(jù),只有一種布局時(shí),不需關(guān)心

"Talk is cheap. Show me the code"

public class RvAdapter extends RecyclerView.Adapter<RvAdapter.MyViewHolder> {
    private final ArrayList<Integer> data;//數(shù)據(jù)源
    private final LayoutInflater mInflater;//在創(chuàng)建View時(shí)需要用
    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) {
        // 在這里創(chuàng)建ItemView并設(shè)置ViewHolder以便復(fù)用
        MyViewHolder viewHolder = new MyViewHolder(mInflater.inflate(R.layout.item_rv, parent, false));
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(MyViewHolder holder, final int position) {  
        // 設(shè)置數(shù)據(jù)
        holder.iv.setBackgroundResource(data.get(position));
        holder.tv.setText(position + "");

        // 設(shè)置事件
        holder.iv.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.i(TAG, "onClick pos:" + position);
            }
        });
    }

    @Override
    public int getItemCount() {
        // 設(shè)個(gè)沒啥好說的,返回總item個(gè)數(shù)
        return data.size();
    }

    class MyViewHolder extends RecyclerView.ViewHolder {
        // 復(fù)用的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);
        }
    }
}

還有就是設(shè)置分割線和動(dòng)畫,這兩個(gè)我沒基本沒用到,就先跳過了;

添加header

對(duì)于Grid布局管理器,如果想添加一個(gè)占據(jù)一整行的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;
    }
});

跳轉(zhuǎn)動(dòng)效

直接跳轉(zhuǎn)到指定position位置時(shí),recycleView的變化是瞬間的,體驗(yàn)不是很好,我們會(huì)希望是緩慢滑動(dòng)過去,直接想到的方法自然是 smoothScrollTo***,效果類似如下

緩慢跳轉(zhuǎn)到指定位置

看看RecycleView的相應(yīng)方法源碼:

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

最終還是使用smoothScrollToPosition(int position),重寫布局管理器即可:

// 控制滑動(dòng)速度的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);
                    }

                    //返回滑動(dòng)一個(gè)pixel需要多少毫秒
                    @Override
                    protected float calculateSpeedPerPixel
                    (DisplayMetrics displayMetrics) {
                        return MILLISECONDS_PER_INCH / displayMetrics.density;
                    }
                };
        linearSmoothScroller.setTargetPosition(position);
        startSmoothScroll(linearSmoothScroller);
    }

    public void setSpeedSlow() {
        //自己在這里用density去乘,希望不同分辨率設(shè)備上滑動(dòng)速度相同
        //0.3f是自己估摸的一個(gè)值,可以根據(jù)不同需求自己修改
        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部分,下拉刷新其實(shí)可以用系統(tǒng)提供的控件: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);
// 使用系統(tǒng)控件來監(jiān)聽刷新,記得數(shù)據(jù)更新后要取消刷新動(dòng)畫
mSrl.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
    @Override
    public void onRefresh() {
       // TODO: 更新數(shù)據(jù)
       
       // 取消加載動(dòng)畫
       mSrl.setRefreshing(false);
    }
});

上拉加載更多

update: 現(xiàn)在我一般是用這個(gè)庫 SwipyRefreshLayout ,上拉下拉都是一個(gè)效果
類似分頁加載,由于沒有單獨(dú)提供footer,所以我們考慮通過ViewType來模擬;
在adapter中需有兩種ItemViewType,一種為底部進(jìn)度加載條樣式,我們通過判斷recycleView是否已經(jīng)滑動(dòng)到底部,來動(dòng)態(tài)添加/刪除一行標(biāo)志數(shù)據(jù)用以表示是否需要顯示進(jìn)度條的itemView,另外,數(shù)據(jù)加載完后,需要?jiǎng)h除原先的標(biāo)志數(shù)據(jù),即刪掉加載條,然后更新列表即可:

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的區(qū)別
        if (!isLoading && totalItemCount <= (lastVisibleItemPos + 1)) {
            loadMoreData();
            isLoading = true;
        }
    }
});

// 模擬加載數(shù)據(jù)過程
private void loadMoreData() {
    // 在原數(shù)據(jù)集末尾添加一條標(biāo)志數(shù)據(jù),告訴適配器顯示加載進(jìn)度條
    mData.add(null);//加載什么樣的數(shù)據(jù),只要跟adapter配合能識(shí)別出來即可
    mAdapter.notifyItemInserted(mData.size() - 1);
    
    mHandler.postDelayed(new Runnable() {
        @Override
        public void run() {
            // 加載過程結(jié)束后,記得清除最后一個(gè)標(biāo)志位
            mData.remove(mData.size() - 1);
            mAdapter.notifyItemRemoved(mData.size());
    
            // 獲取新增數(shù)據(jù)
            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) {
    // 標(biāo)志數(shù)據(jù)也可以用其他的,這里我用 null 或者 "" 來表示
    if (TextUtils.isEmpty(mData.get(position))) {
        return TYPE_LOADING;
    } else {
        return TYPE_NORMAL;
    }
}
上拉更多-下拉刷新

默認(rèn)添加刪除動(dòng)畫

Demo
推薦這個(gè)庫
RecyclerView自帶的一個(gè) DefaultItemAnimator 可以實(shí)現(xiàn)添加刪除item時(shí),插入移除動(dòng)畫效果

//kotlin代碼
//設(shè)置recyclerview的動(dòng)畫recyclerView.itemAnimator = DefaultItemAnimator()
//添加或刪除數(shù)據(jù)源后,要調(diào)用如下方法才有動(dòng)畫效果
recyclerView.adapter.notifyItemRangeInserted(addPos,addItemCount)
recyclerView.adapter.notifyItemRemoved(removePos)
添加刪除動(dòng)畫效果
添加刪除動(dòng)畫效果

使用ItemTouchHelper實(shí)現(xiàn)拖拽改變item順序及swipe滑動(dòng)刪除item

Demo

// kotlin
// 添加滑動(dòng)/拖拽功能
// java的匿名內(nèi)部類對(duì)應(yīng)過來就是object對(duì)象表達(dá)式了
ItemTouchHelper(object : ItemTouchHelper.Callback() {
    var vh: RecyclerView.ViewHolder? = null

    /**
     * 設(shè)置itemView可以移動(dòng)的方向
     * */
    override fun getMovementFlags(recyclerView: RecyclerView?, viewHolder: RecyclerView.ViewHolder?): Int {
        // 拖拽的標(biāo)記奏窑,這里允許上下左右四個(gè)方向
        val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN or ItemTouchHelper.LEFT or
                ItemTouchHelper.RIGHT
        // 滑動(dòng)的標(biāo)記锥债,這里允許左右滑動(dòng)
        val swipeFlags = ItemTouchHelper.START or ItemTouchHelper.END
        return makeMovementFlags(dragFlags, swipeFlags)
    }

     /**
     * 當(dāng)一個(gè)Item被另外的Item替代時(shí)回調(diào),也就是數(shù)據(jù)集的內(nèi)容順序改變
     * 返回true, onMoved()才會(huì)進(jìn)行
     * */
    override fun onMove(recyclerView: RecyclerView?, viewHolder: RecyclerView.ViewHolder?, target: RecyclerView.ViewHolder?): Boolean {
        return true
    }

    /**
     *  當(dāng)onMove返回true的時(shí)候回調(diào),刷新列表
     * */
    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)
        // 移動(dòng)完成后修改列表位置并刷新列表
        Collections.swap(data, viewHolder!!.adapterPosition, target!!.adapterPosition)
        rv_main.adapter.notifyItemMoved(viewHolder!!.adapterPosition, target!!.adapterPosition)
    }

    /**
     * 滑動(dòng)完成時(shí)回調(diào),這里設(shè)置為滑動(dòng)刪除,刪除相應(yīng)數(shù)據(jù)后刷新列表
     * */
    override fun onSwiped(viewHolder: RecyclerView.ViewHolder?, direction: Int) {
        data.removeAt(viewHolder!!.adapterPosition)
        rv_main.adapter.notifyItemRemoved(viewHolder!!.adapterPosition)
        toast("刪除成功")
    }

    /**
     * Item是否可以滑動(dòng)
     * */
    override fun isItemViewSwipeEnabled() = true

    /**
     * Item是否可以長按
     * */
    override fun isLongPressDragEnabled() = true

}).attachToRecyclerView(rv_main)
拖拽滑動(dòng)刪除效果
拖拽滑動(dòng)刪除效果

popupWindow中使用RecycleView

recycleView的高度自適應(yīng)

默認(rèn)情況下,即使設(shè)置其高度為wrap_content,其高度也是全屏的,需要重新布局管理器來計(jì)算item總高度

測試時(shí)發(fā)現(xiàn)適用于v7-23.1.1,升級(jí)到23.4.0后就會(huì)數(shù)組下標(biāo)越界,可將 View child = recycler.getViewForPosition(i); 修改為 View child = getChildAt(i);if (child != null) {...} ,但其實(shí)沒有必要,因?yàn)樵趘7-23.4.0的時(shí)候,系統(tǒng)已經(jīng)可以自適應(yīng)高度了,不需要手動(dòng)去計(jì)算

public  class FixGridLayoutManager extends GridLayoutManager {
        public FixGridLayoutManager(Context context, int spanCount) {
            //默認(rèn)方向是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();
            // 奇怪,最近測試發(fā)現(xiàn),上面的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);
    }
}

點(diǎn)擊事件中使用itemNotify時(shí)FC

使用自定義的布局管理器后,點(diǎn)擊事件會(huì)報(bào)錯(cuò):

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

沒去細(xì)究為啥,我在adapter中使用的是 notifyItemChanged(position); 改成普通的全量刷新就可以了

notifyDataSetChanged();

GridView

gridView基本沒再用了,不過之前碰到過幾個(gè)坑,在此也一并記錄下:

// 基本使用方法
GridView mGv = findViewById(R.id.gv_basic);
mGv.setNumColumns(3);//設(shè)置列數(shù),也可在xml中設(shè)定

// 適配器同樣與ListView類似,繼承自BaseAdapter
GvAdapter gvAdapter = new GvAdapter(this, mData, true);
mGv.setAdapter(gvAdapter);

//添加點(diǎn)擊監(jiān)聽
mGv.setOnItemClickListener(new AdapterView.OnItemClickListener() {
    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        Log.i(TAG, "onItemClick 您點(diǎn)擊了第 position: " + position + " 個(gè)item");
    }
});

1. 固定item高度

之前有個(gè)需求是在一個(gè)頁面顯示9個(gè)item,填滿屏幕:

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    ......
    //固定item高度,這里使用3*3填滿整個(gè)屏幕/gridView
    convertView.setLayoutParams(new AbsListView.LayoutParams(parent.getWidth() / 3, parent.getHeight() / 3));
    // 恢復(fù)默認(rèn)的話設(shè)置高度為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);
}
  • 同時(shí)設(shè)置ListView和GridView的點(diǎn)擊事件,只有GridView的有響應(yīng)
    需要在ListView的item布局頂層屏蔽子元素焦點(diǎn)事件
<LinearLayout 
    ......
    android:descendantFocusability="blocksDescendants">

    <org.lynxz.androiddemos.widget.FixGridView
    ....../>
</LinearLayout>

這樣listView的item點(diǎn)擊事件就能被觸發(fā)了,同時(shí)若是點(diǎn)擊到GridView的item會(huì)觸發(fā)GridView的事件;
同理,若是GridView的item中有搶焦點(diǎn)的控件導(dǎo)致其點(diǎn)擊事件失效,也同樣在其item布局頂層添加該屬性;

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末鄙漏,一起剝皮案震驚了整個(gè)濱河市恃轩,隨后出現(xiàn)的幾起案子脆侮,更是在濱河造成了極大的恐慌瞎领,老刑警劉巖泌辫,帶你破解...
    沈念sama閱讀 206,723評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異九默,居然都是意外死亡震放,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門驼修,熙熙樓的掌柜王于貴愁眉苦臉地迎上來殿遂,“玉大人,你說我怎么就攤上這事乙各∧福” “怎么了?”我有些...
    開封第一講書人閱讀 152,998評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵耳峦,是天一觀的道長恩静。 經(jīng)常有香客問我,道長妇萄,這世上最難降的妖魔是什么蜕企? 我笑而不...
    開封第一講書人閱讀 55,323評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮冠句,結(jié)果婚禮上轻掩,老公的妹妹穿的比我還像新娘。我一直安慰自己懦底,他們只是感情好唇牧,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,355評(píng)論 5 374
  • 文/花漫 我一把揭開白布罕扎。 她就那樣靜靜地躺著,像睡著了一般丐重。 火紅的嫁衣襯著肌膚如雪腔召。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,079評(píng)論 1 285
  • 那天扮惦,我揣著相機(jī)與錄音臀蛛,去河邊找鬼。 笑死崖蜜,一個(gè)胖子當(dāng)著我的面吹牛浊仆,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播豫领,決...
    沈念sama閱讀 38,389評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼抡柿,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼!你這毒婦竟也來了等恐?” 一聲冷哼從身側(cè)響起洲劣,我...
    開封第一講書人閱讀 37,019評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎课蔬,沒想到半個(gè)月后囱稽,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,519評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡购笆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,971評(píng)論 2 325
  • 正文 我和宋清朗相戀三年粗悯,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了虚循。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片同欠。...
    茶點(diǎn)故事閱讀 38,100評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖横缔,靈堂內(nèi)的尸體忽然破棺而出铺遂,到底是詐尸還是另有隱情,我是刑警寧澤茎刚,帶...
    沈念sama閱讀 33,738評(píng)論 4 324
  • 正文 年R本政府宣布襟锐,位于F島的核電站,受9級(jí)特大地震影響膛锭,放射性物質(zhì)發(fā)生泄漏粮坞。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,293評(píng)論 3 307
  • 文/蒙蒙 一初狰、第九天 我趴在偏房一處隱蔽的房頂上張望莫杈。 院中可真熱鬧,春花似錦奢入、人聲如沸筝闹。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽关顷。三九已至糊秆,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間议双,已是汗流浹背痘番。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評(píng)論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留平痰,地道東北人夫偶。 一個(gè)月前我還...
    沈念sama閱讀 45,547評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像觉增,于是被迫代替她去往敵國和親兵拢。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,834評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容