[TOC] 列表控制元件也算是很常見的控制元件了,現在基本都切換到RecycleView了,這邊記錄下列表控制元件的基本的使用以及幾種情況的處理:
RecycleView
使用上基本步驟如下:
- 設定佈局管理器
// 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);
複製程式碼
- 設定介面卡
介面卡需要繼承
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
// 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佈局頂層新增該屬性;