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

我啥時候說啦jj發表於2018-01-03

[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佈局頂層新增該屬性;

相關文章