RecyclerView
的强大无人不知,它封装了ViewHolder
,便于我们回收复用;配合LayoutManager
、ItemDecoration
、ItemAnimator
便于你制定各种列表效果。当然可能还有一些“遗珠”你不太了解,今天就说说它们。
1.SortedList
顾名思义就是排序列表,它适用于列表有序且不重复的场景。并且SortedList
会帮助你比较数据的差异,定向刷新数据。而不是简单粗暴的notifyDataSetChanged()
。
我想到了一个场景,在选择城市页面,我们都需要根据拼音首字母来排序。我们来使用SortedList
来实现一下。
City对象:
public class City {
private int id;
private String cityName;
private String firstLetter;
public City(int id, String cityName, String firstLetter) {
this.id = id;
this.cityName = cityName;
this.firstLetter = firstLetter;
}
}
创建SortedListAdapterCallback
的实现类 SortedListCallback
,SortedListCallback
定义了如何排序和如何判断重复项。
public class SortedListCallback extends SortedListAdapterCallback<City> {
public SortedListCallback(RecyclerView.Adapter adapter) {
super(adapter);
}
/**
* 排序条件
*/
@Override
public int compare(City o1, City o2) {
return o1.getFirstLetter().compareTo(o2.getFirstLetter());
}
/**
* 用来判断两个对象是否是相同的Item。
*/
@Override
public boolean areItemsTheSame(City item1, City item2) {
return item1.getId() == item2.getId();
}
/**
* 用来判断两个对象是否是内容的Item。
*/
@Override
public boolean areContentsTheSame(City oldItem, City newItem) {
if (oldItem.getId() != newItem.getId()) {
return false;
}
return oldItem.getCityName().equals(newItem.getCityName());
}
}
Adapter
部分
public class SortedAdapter extends RecyclerView.Adapter<SortedAdapter.ViewHolder> {
// 数据源使用SortedList
private SortedList<City> mSortedList;
private LayoutInflater mInflater;
public SortedAdapter(Context mContext) {
mInflater = LayoutInflater.from(mContext);
}
public void setSortedList(SortedList<City> mSortedList) {
this.mSortedList = mSortedList;
}
/**
* 批量更新操作,例如:
* <pre>
* mSortedList.beginBatchedUpdates();
* try {
* mSortedList.add(item1)
* mSortedList.add(item2)
* mSortedList.remove(item3)
* ...
* } finally {
* mSortedList.endBatchedUpdates();
* }
* </pre>
* */
public void setData(List<City> mData){
mSortedList.beginBatchedUpdates();
mSortedList.addAll(mData);
mSortedList.endBatchedUpdates();
}
public void removeData(int index){
mSortedList.removeItemAt(index);
}
public void clear(){
mSortedList.clear();
}
@Override
@NonNull
public SortedAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new ViewHolder(mInflater.inflate(R.layout.item_test, parent, false));
}
@Override
public void onBindViewHolder(@NonNull SortedAdapter.ViewHolder holder, final int position) {
...
}
@Override
public int getItemCount() {
return mSortedList.size();
}
...
}
使用部分:
public class SortedListActivity extends AppCompatActivity {
private SortedAdapter mSortedAdapter;
private int count = 10;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_sorted_list);
RecyclerView mRecyclerView = findViewById(R.id.rv);
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
mSortedAdapter = new SortedAdapter(this);
// SortedList初始化
SortedListCallback mSortedListCallback = new SortedListCallback(mSortedAdapter);
SortedList mSortedList = new SortedList<>(City.class, mSortedListCallback);
mSortedAdapter.setSortedList(mSortedList);
mRecyclerView.setAdapter(mSortedAdapter);
updateData();
}
private void addData() {
mSortedAdapter.setData(new City(count, "城市 " + count, "c"));
count ++;
}
private List<City> mList = new ArrayList();
private void updateData() {
mList.clear();
mList.add(new City(0, "北京", "b"));
mList.add(new City(1, "上海", "s"));
mList.add(new City(2, "广州", "g"));
mList.add(new City(3, "深圳", "s"));
mList.add(new City(4, "杭州", "h"));
mList.add(new City(5, "西安", "x"));
mList.add(new City(6, "成都", "c"));
mList.add(new City(7, "武汉", "w"));
mList.add(new City(8, "南京", "n"));
mList.add(new City(9, "重庆", "c"));
mSortedAdapter.setData(mList);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu, menu);
return true;
}
private Random mRandom = new Random();
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int i = item.getItemId();
if (i == R.id.menu_add) {
addData();
} else if (i == R.id.menu_update) {
// 修改,自动去重
updateData();
} else if (i == R.id.menu_delete) {
// 随意删除一个
if (mSortedAdapter.getItemCount() > 0){
mSortedAdapter.removeData(mRandom.nextInt(mSortedAdapter.getItemCount()));
}
}else if (i == R.id.menu_clear){
mSortedAdapter.clear();
}
return true;
}
}
使用起来还是很简单的,来看一下效果图:
可以看到,我每次添加一条c字母的数据,它会自动帮我排序好,同时刷新列表。修改数据时,自动去重。比起暴力刷新,优雅多了。
2. AsyncListUtil
AsyncListUtil
在 support-v7:23就存在了。它是异步加载数据的工具,它一般用于加载数据库数据,我们无需在UI线程上查询游标,同时它可以保持UI和缓存同步,并且始终只在内存中保留有限数量的数据。使用它可以获得更好的用户体验。
注意,这个类使用单个线程来加载数据,因此它适合从磁盘、数据库加载数据,不适用于从网络加载数据。
用法如下,首先实现AsyncListUtil
:
public class MyAsyncListUtil extends AsyncListUtil<TestBean> {
/**
* 一次加载数据的个数,分页数量
*/
private static final int TILE_SIZE = 20;
public MyAsyncListUtil(RecyclerView mRecyclerView) {
super(TestBean.class, TILE_SIZE, new MyDataCallback(), new MyViewCallback(mRecyclerView));
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
// 更新当前可见范围数据
onRangeChanged();
}
});
}
/**
* 获取数据回调
*/
public static class MyDataCallback extends DataCallback<TestBean>{
/**
* 总数据个数
*/
@Override
public int refreshData() {
return 200;
}
/**
* 填充数据(后台线程),一般为读取数据库数据
*/
@Override
public void fillData(@NonNull TestBean[] data, int startPosition, int itemCount) {
for (int i = 0; i < itemCount; i++) {
TestBean item = data[i];
if (item == null) {
item = new TestBean(startPosition, "Item:" + (startPosition + i));
data[i] = item;
}
}
try {
// 模拟加载数据中
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 用于获取可见项范围和更新通知的回调
*/
public static class MyViewCallback extends ViewCallback {
private RecyclerView mRecyclerView;
public MyViewCallback(RecyclerView mRecyclerView) {
this.mRecyclerView = mRecyclerView;
}
/**
* 展示数据的范围
*/
@Override
public void getItemRangeInto(@NonNull int[] outRange) {
RecyclerView.LayoutManager manager = mRecyclerView.getLayoutManager();
LinearLayoutManager mgr = (LinearLayoutManager) manager;
outRange[0] = mgr.findFirstVisibleItemPosition();
outRange[1] = mgr.findLastVisibleItemPosition();
}
/**
* 刷新数据
*/
@Override
public void onDataRefresh() {
mRecyclerView.getAdapter().notifyDataSetChanged();
}
/**
* Item更新
*/
@Override
public void onItemLoaded(int position) {
mRecyclerView.getAdapter().notifyItemChanged(position);
}
}
}
Adapter:
public class AsyncListUtilAdapter extends RecyclerView.Adapter<AsyncListUtilAdapter.ViewHolder> {
private MyAsyncListUtil mMyAsyncListUtil;
public AsyncListUtilAdapter(Context mContext, MyAsyncListUtil mMyAsyncListUtil) {
this.mMyAsyncListUtil = mMyAsyncListUtil;
}
@Override
public int getItemCount() {
return mMyAsyncListUtil.getItemCount();
}
@Override
public void onBindViewHolder(@NonNull AsyncListUtilAdapter.ViewHolder holder, final int position) {
TestBean bean = mMyAsyncListUtil.getItem(position);
// 有可能获取为空,这是可以显示加载中,等待同步数据。
if (bean == null){
holder.mTvName.setText("加载中...");
}else {
holder.mTvName.setText(bean.getName());
}
}
......
}
注释还是很清除的,直接上效果图:
3.AsyncListDiffer
虽然SortedList
、AsyncListUtil
很方便了,但是大多数的列表都无需我们排序和加载本地数据,大多是获取网络数据展示。这个时候就可以使用DiffUtil
了。DiffUtil
是support-v7:24.2.0中的新工具类,它用来比较新旧两个数据集,寻找最小变化量,定向刷新列表。关于DiffUtil
的介绍很早之前在张旭童的 【Android】RecyclerView的好伴侣:详解DiffUtil 博客中就有详细介绍,我这里就不赘述了。
不过DiffUtil
的问题在于计算数据差异DiffUtil.calculateDiff(mDiffCallback)
时是一个耗时操作,需要我们放到子线程去处理,最后在主线程刷新。为了方便这一操作,在support-v7:27.1.0又新增了一个DiffUtil
的封装类,那就是AsyncListDiffer
。
首先先上效果图,一个简单的列表展示,同时增、删、改操作。
我用AsyncListDiffer
来实现这一效果。首先实现DiffUtil.ItemCallback
,类似SortedList
,制定规则,如何区分数据。这里和DiffUtil
用法几乎一样,只是它是实现DiffUtil.Callback
。
public class MyDiffUtilItemCallback extends DiffUtil.ItemCallback<TestBean> {
/**
* 是否是同一个对象
*/
@Override
public boolean areItemsTheSame(@NonNull TestBean oldItem, @NonNull TestBean newItem) {
return oldItem.getId() == newItem.getId();
}
/**
* 是否是相同内容
*/
@Override
public boolean areContentsTheSame(@NonNull TestBean oldItem, @NonNull TestBean newItem) {
return oldItem.getName().equals(newItem.getName());
}
/**
* areItemsTheSame()返回true而areContentsTheSame()返回false时调用,也就是说两个对象代表的数据是一条,但是内容更新了。此方法为定向刷新使用,可选。
*/
@Nullable
@Override
public Object getChangePayload(@NonNull TestBean oldItem, @NonNull TestBean newItem) {
Bundle payload = new Bundle();
if (!oldItem.getName().equals(newItem.getName())) {
payload.putString("KEY_NAME", newItem.getName());
}
if (payload.size() == 0){
//如果没有变化 就传空
return null;
}
return payload;
}
}
Adapter部分有两种实现方法,一种是实现RecyclerView.Adapter
,
public class AsyncListDifferAdapter extends RecyclerView.Adapter<AsyncListDifferAdapter.ViewHolder> {
private LayoutInflater mInflater;
// 数据的操作由AsyncListDiffer实现
private AsyncListDiffer<TestBean> mDiffer;
public AsyncListDifferAdapter(Context mContext) {
// 初始化AsyncListDiffe
mDiffer = new AsyncListDiffer<>(this, new MyDiffUtilItemCallback());
mInflater = LayoutInflater.from(mContext);
}
public void setData(TestBean mData){
List<TestBean> mList = new ArrayList<>();
mList.addAll(mDiffer.getCurrentList());
mList.add(mData);
mDiffer.submitList(mList);
}
public void setData(List<TestBean> mData){
// 由于DiffUtil是对比新旧数据,所以需要创建新的集合来存放新数据。
// 实际情况下,每次都是重新获取的新数据,所以无需这步。
List<TestBean> mList = new ArrayList<>();
mList.addAll(mData);
mDiffer.submitList(mList);
}
public void removeData(int index){
List<TestBean> mList = new ArrayList<>();
mList.addAll(mDiffer.getCurrentList());
mList.remove(index);
mDiffer.submitList(mList);
}
public void clear(){
mDiffer.submitList(null);
}
@Override
@NonNull
public AsyncListDifferAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new ViewHolder(mInflater.inflate(R.layout.item_test, parent, false));
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position, @NonNull List<Object> payloads) {
if (payloads.isEmpty()) {
onBindViewHolder(holder, position);
} else {
Bundle bundle = (Bundle) payloads.get(0);
holder.mTvName.setText(bundle.getString("KEY_NAME"));
}
}
@Override
public void onBindViewHolder(@NonNull AsyncListDifferAdapter.ViewHolder holder, final int position) {
TestBean bean = mDiffer.getCurrentList().get(position);
holder.mTvName.setText(bean.getName());
}
@Override
public int getItemCount() {
return mDiffer.getCurrentList().size();
}
static class ViewHolder extends RecyclerView.ViewHolder {
......
}
}
另一种Adapter写法可以实现ListAdapter
,它的内部帮我们实现了getItemCount()
、getItem()
和AsyncListDiffer
的初始化。
源码如下,很简单:
public abstract class ListAdapter<T, VH extends ViewHolder> extends Adapter<VH> {
private final AsyncListDiffer<T> mHelper;
protected ListAdapter(@NonNull ItemCallback<T> diffCallback) {
this.mHelper = new AsyncListDiffer(new AdapterListUpdateCallback(this), (new Builder(diffCallback)).build());
}
protected ListAdapter(@NonNull AsyncDifferConfig<T> config) {
this.mHelper = new AsyncListDiffer(new AdapterListUpdateCallback(this), config);
}
public void submitList(@Nullable List<T> list) {
this.mHelper.submitList(list);
}
protected T getItem(int position) {
return this.mHelper.getCurrentList().get(position);
}
public int getItemCount() {
return this.mHelper.getCurrentList().size();
}
}
不过有个缺点,没有提供直接获取当前集合的getCurrentList()
方法。所以需要自己维护一个集合。希望以后可以添加上吧。所以现阶段我还是不推荐这种写法。。。不过我们可以去做这个封装。
public class MyListAdapter extends ListAdapter<TestBean, MyListAdapter.ViewHolder> {
private LayoutInflater mInflater;
// 自己维护的集合
private List<TestBean> mData = new ArrayList<>();
public MyListAdapter(Context mContext) {
super(new MyDiffUtilItemCallback());
mInflater = LayoutInflater.from(mContext);
}
public void setData(TestBean testBean){
mData.add(testBean);
List<TestBean> mList = new ArrayList<>();
mList.addAll(mData);
// 提交新的数据集
submitList(mList);
}
public void setData(List<TestBean> list){
mData.clear();
mData.addAll(list);
List<TestBean> mList = new ArrayList<>();
mList.addAll(mData);
submitList(mList);
}
public void removeData(int index){
mData.remove(index);
List<TestBean> mList = new ArrayList<>();
mList.addAll(mData);
submitList(mList);
}
public void clear(){
mData.clear();
submitList(null);
}
@Override
@NonNull
public MyListAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new ViewHolder(mInflater.inflate(R.layout.item_test, parent, false));
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position, @NonNull List<Object> payloads) {
if (payloads.isEmpty()) {
onBindViewHolder(holder, position);
} else {
Bundle bundle = (Bundle) payloads.get(0);
holder.mTvName.setText(bundle.getString("KEY_NAME"));
}
}
@Override
public void onBindViewHolder(@NonNull MyListAdapter.ViewHolder holder, final int position) {
TestBean bean = getItem(position);
holder.mTvName.setText(bean.getName());
}
static class ViewHolder extends RecyclerView.ViewHolder {
......
}
}
最后就是Activity了:
public class AsyncListDifferActivity extends AppCompatActivity {
private AsyncListDifferAdapter mAsyncListDifferAdapter;
private int count = 10;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_sorted_list);
RecyclerView mRecyclerView = findViewById(R.id.rv);
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
mAsyncListDifferAdapter = new AsyncListDifferAdapter(this);
mRecyclerView.setAdapter(mAsyncListDifferAdapter);
initData();
}
private void addData() {
mAsyncListDifferAdapter.setData(new TestBean(count, "Item " + count));
count ++;
}
private List<TestBean> mList = new ArrayList();
private void initData() {
mList.clear();
for (int i = 0; i < 10; i++){
mList.add(new TestBean(i, "Item " + i));
}
mAsyncListDifferAdapter.setData(mList);
}
private void updateData() {
mList.clear();
for (int i = 9; i >= 0; i--){
mList.add(new TestBean(i, "Item " + i));
}
mAsyncListDifferAdapter.setData(mList);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu, menu);
return true;
}
private Random mRandom = new Random();
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int i = item.getItemId();
if (i == R.id.menu_add) {
addData();
} else if (i == R.id.menu_update) {
updateData();
} else if (i == R.id.menu_delete) {
if (mAsyncListDifferAdapter.getItemCount() > 0){
mAsyncListDifferAdapter.removeData(mRandom.nextInt(mAsyncListDifferAdapter.getItemCount()));
}
}else if (i == R.id.menu_clear){
mAsyncListDifferAdapter.clear();
}
return true;
}
}
我们简单的看一下AsyncListDiffer
的 submitList
源码:
public void submitList(@Nullable final List<T> newList) {
final int runGeneration = ++this.mMaxScheduledGeneration;
if (newList != this.mList) {
if (newList == null) {
// 新数据为null时清空列表
int countRemoved = this.mList.size();
this.mList = null;
this.mReadOnlyList = Collections.emptyList();
this.mUpdateCallback.onRemoved(0, countRemoved);
} else if (this.mList == null) {
// 旧数据为null时添加数据
this.mList = newList;
this.mReadOnlyList = Collections.unmodifiableList(newList);
this.mUpdateCallback.onInserted(0, newList.size());
} else {
final List<T> oldList = this.mList;
// 计算数据差异放在子线程
this.mConfig.getBackgroundThreadExecutor().execute(new Runnable() {
public void run() {
final DiffResult result = DiffUtil.calculateDiff(new Callback() {
...
});
// 主线程刷新列表
AsyncListDiffer.this.mMainThreadExecutor.execute(new Runnable() {
public void run() {
if (AsyncListDiffer.this.mMaxScheduledGeneration == runGeneration) {
AsyncListDiffer.this.latchList(newList, result);
}
}
});
}
});
}
}
}
void latchList(@NonNull List<T> newList, @NonNull DiffResult diffResult) {
this.mList = newList;
this.mReadOnlyList = Collections.unmodifiableList(newList);
// 熟悉的dispatchUpdatesTo方法
diffResult.dispatchUpdatesTo(this.mUpdateCallback);
}
AsyncListDiffer
就是在这里帮我们做了线程的处理。方便我们正确规范的使用。
4.SnapHelper
SnapHelper
是support-v7:24.2.0新增的,用于控制RecyclerView
滑动停止后Item的对齐方式。默认提供了两种对齐方式PagerSnapHelper
与 LinearSnapHelper
。PagerSnapHelper
和ViewPage效果一样,一次滑动一页。LinearSnapHelper
这是Item居中对齐。使用方式非常简单:
PagerSnapHelper mPagerSnapHelper = new PagerSnapHelper();
mPagerSnapHelper.attachToRecyclerView(mRecyclerView);
效果如下:
当然我们可以自定义SnapHelper,来实现我们想要的对齐方式,下面我们来实现一下左对齐。
public class MySnapHelper extends LinearSnapHelper{
/**
* 水平、垂直方向的度量
*/
@Nullable
private OrientationHelper mVerticalHelper;
@Nullable
private OrientationHelper mHorizontalHelper;
@NonNull
private OrientationHelper getVerticalHelper(@NonNull RecyclerView.LayoutManager layoutManager) {
if (mVerticalHelper == null) {
mVerticalHelper = OrientationHelper.createVerticalHelper(layoutManager);
}
return mVerticalHelper;
}
@NonNull
private OrientationHelper getHorizontalHelper(@NonNull RecyclerView.LayoutManager layoutManager) {
if (mHorizontalHelper == null) {
mHorizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager);
}
return mHorizontalHelper;
}
/**
* 计算出View对齐到指定位置,所需的x、y轴上的偏移量。
*/
@Nullable
@Override
public int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) {
int[] out = new int[2];
// 水平方向滑动时计算x方向,否则偏移为0
if (layoutManager.canScrollHorizontally()) {
out[0] = distanceToStart(layoutManager, targetView, getHorizontalHelper(layoutManager));
} else {
out[0] = 0;
}
// 垂直方向滑动时计算y方向,否则偏移为0
if (layoutManager.canScrollVertically()) {
out[1] = distanceToStart(layoutManager, targetView, getVerticalHelper(layoutManager));
} else {
out[1] = 0;
}
return out;
}
private int distanceToStart(RecyclerView.LayoutManager layoutManager, View targetView, OrientationHelper helper) {
// RecyclerView的边界x值,也就是左侧Padding值
final int start;
if (layoutManager.getClipToPadding()) {
start = helper.getStartAfterPadding();
} else {
start = 0;
}
return helper.getDecoratedStart(targetView) - start;
}
/**
* 查找需要对齐的View
*/
@Nullable
@Override
public View findSnapView(RecyclerView.LayoutManager layoutManager) {
if (layoutManager.canScrollVertically()) {
return findStartView(layoutManager, getVerticalHelper(layoutManager));
} else {
return findStartView(layoutManager, getHorizontalHelper(layoutManager));
}
}
private View findStartView(RecyclerView.LayoutManager layoutManager, OrientationHelper helper) {
int childCount = layoutManager.getChildCount();
if (childCount == 0) {
return null;
}
View closestChild = null;
final int start;
if (layoutManager.getClipToPadding()) {
start = helper.getStartAfterPadding();
} else {
start = 0;
}
int absClosest = Integer.MAX_VALUE;
for (int i = 0; i < childCount; i++) {
final View child = layoutManager.getChildAt(i);
// ItemView 的左侧坐标
int childStart = helper.getDecoratedStart(child);
// 计算此ItemView 与 RecyclerView左侧的距离
int absDistance = Math.abs(childStart - start);
// 找到那个最靠近左侧的ItemView然后返回
if (absDistance < absClosest) {
absClosest = absDistance;
closestChild = child;
}
}
return closestChild;
}
/**
* 找到需要对齐的View的position,主要用于fling 操作
*/
@Override
public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX, int velocityY) {
// 左对齐和居中对齐一样,无需自定义处理
return super.findTargetSnapPosition(layoutManager, velocityX, velocityY);
}
}
上面的注释已将关键地方注明,效果我就不展示了。大家可以下载代码去体验。本篇所有代码已上传至Github。希望点赞支持!!
5.参考
今天的文章RecyclerView库中的遗珠分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/20421.html