本文会不按期更新,推荐watch下项目javascript
若是喜欢请star,若是以为有纰漏请提交issue,若是你有更好的点子能够提交pull request。php
本文的示例代码主要是基于CommonAdapter这个库编写的,若你有其余的技巧和方法能够参与进来一块儿完善这篇文章。 html
本文固定链接:github.com/tianzhijiex…java
为了下降项目代码的复杂度,让你们能专一于业务而不是考虑性能,咱们必需要对adapter进行一个封装。android
基础:ios
性能:git
findviewById()
应有自动的优化策略,相似于ViewHolder 扩展:程序员
设计:github
其余: 编程
include
标签notifyDataSetChanged()
来更新界面本篇会大量利用CommonAdapter这个库和其他的工具类进行实现,下文会直接使用CommonAdapter的api。
item要能独立的处理自身的逻辑和事件,让自身成为一个独立的ui模块。假设你的item就是一个textView:
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/text" android:layout_width="match_parent" android:layout_height="wrap_content" />复制代码
如今只须要这么写:
public class TextItem implements AdapterItem<JsonModel> {
private TextView text;
public int getLayoutResId() {
return R.layout.demo_item_text;
}
public void bindViews(View root) {
text = (TextView) root.findViewById(R.id.textView);
}
public void setViews() {}
public void handleData(JsonModel model, int position) {
text.setText(model.content);
}
}复制代码
如今,你能够将它放入不一样的界面,只须要给他一样的数据模型便可。在一个item被多个页面用的情形中还能够作更多的优化,好比设置全局的缓存池等等。
分离后的item能够更加易于维护,而且咱们能够针对listview和item两者进行独立的性能优化,好比作一个通用的list页面组件,item经过插拔的方式接入,item自身的数据进行diff优化等等。
我强烈建议不要用ItemOnListener作点击的判断,而是在每一个item中作判断。在item中能够经过root.getContext()
来获得当前页面的activity,这样就能够处理各类页面的跳转了。
private Activity mActivity;
@Override
public void bindViews(View root) {
mActivity = (Activity) root.getContext();
}
public Activity getActivity() {
return mActivity;
}复制代码
好处:
item自身能知道本身的全部操做,而ListView仅仅作个容器。如今RecyclerView的设计思路也是如此的,让item独立性增长。并且若是要带数据到别的页面,也能够直接拿到数据。
坏处:
外部对内部彻底不知情,对于统一的事件没办法作到很好的统一处理。
为了说明,我创建了一个数据模型:
public class DemoModel {
public String content;
public String type;
}复制代码
它就是一个POJO,没有任何特别之处,它彻底不知道其余对象的存在。
adapter作的事情是将数据和ui进行绑定,不一样页面的adapter基本是不可复用的状态,并且如今主要的事情在item中处理了,因此adapter就一般是以一个内部类的形式出现,如:
listView.setAdapter(new CommonAdapter<DemoModel>(data) {
@Override
public AdapterItem<DemoModel> createItem(Object itemType) {
return new Item();
}
});复制代码
listView.setAdapter(new CommonAdapter<DemoModel>(data) {
@Override
public Object getItemType(DemoModel demoModel) {
// 返回item的类型,强烈建议是string,int,float之类的基础类型,也容许class类型
return demoModel.type;
}
@Override
public AdapterItem<DemoModel> createItem(Object type) {
switch ((String) type) {
case "text":
return new TextItem();
case "button":
return new ButtonItem();
case "image":
return new ImageItem();
}
}
});复制代码
如今若是加了新的需求,要多支持一个item类型,你只须要在switch-case语句块中新增一个case就行,简单且安全。
咱们以前对adapter的优化常常是须要在getView中判断convertView是否为null,若是不为空就不new出新的view,这样来实现item复用。先来看看上面已经出现屡次的AdapterItem
是个什么。
public interface AdapterItem<T> {
/** * @return item布局文件的layoutId */
@LayoutRes
int getLayoutResId();
/** * 初始化views */
void bindViews(final View root);
/** * 设置view */
void setViews();
/** * 根据数据来设置item的内部views * * @param model 数据list内部的model * @param position 当前adapter调用item的位置 */
void handleData(T model, int position);
}复制代码
方法 | 描述 | 作的工做 |
---|---|---|
getLayoutResId | 你这个item的布局文件是什么 | 返回一个R.layout.xxx |
bindViews | 在这里作findviewById的工做 | btn = findViewById(R.id.xx) |
setViews | 在这里初始化view各个参数 | setcolor ,setOnClickListener... |
handleData | 数据更新时会调用(相似getView) | button.setText(model.text) |
其实这里就是view的几个过程,首先初始化布局文件,而后绑定布局文件中的各个view,接着进行各个view的初始化操做,最后在数据更新时进行更新的工做。
分析完毕后,我去源码里面翻了一下,发现了这个库对item复用的优化:
LayoutInflater mInflater;
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// 不重复建立inflater对象,不管你有多少item,我都仅仅建立一次
if (mInflater == null) {
mInflater = LayoutInflater.from(parent.getContext());
}
AdapterItem<T> item;
if (convertView == null) {
// 当convertView为null,说明没有复用的item,那么就new出来
item = getItemView(mType);
convertView = mInflater.inflate(item.getLayoutResId(), parent, false);
convertView.setTag(R.id.tag_item, item);
// 调用bindView进行view的findview,仅仅是新new出来的view才会调用一次
item.onBindViews(convertView);
// findview后开始setView。将绑定和设置分离,方便整理代码结构
item.onSetViews();
} else {
// 若是这个item是能够复用的,那么直接返回
item = (AdapterItem<T>) convertView.getTag(R.id.tag_item);
}
// 不管你是否是复用的item,都会在getView时触发updateViews方法,更新数据
item.onUpdateViews(mDataList.get(position), position);
return convertView;
}复制代码
关键代码就是这一段,因此只须要明白这一段代码作的事情,不管在使用这个库时遇到了什么问题,你均可以没必要惊慌,由于你掌握了它的原理。
明白了第三方库的原理,才能够放心大胆的使用
经过上述对源码的分析,如今只须要在bindViews中写findview的代码便可让这个库自动实现优化。若是你用了databinding,一行代码解决问题:
private DemoItemImageBinding b;
@Override
public void bindViews(View root) {
b = DataBindingUtil.bind(root);
}复制代码
传统作法:
TextView textView;
@Override
public void bindViews(View root) {
textView = (TextView) root.findViewById(R.id.textView);
}复制代码
咱们以前会图省事在listview的getView中随便写监听器,以致于出现了new不少多余listener的现象。
public View getView(int positon, View convertView, ViewGroup parent){
if(null == convertView){
convertView = LayoutInflater.from(context).inflate(R.layout.item, null);
}
Button button = ABViewUtil.obtainView(convertView, R.id.item_btn);
button.setOnClickListener(new View.OnClickListener(){ // 每次getView都会new一个listener
@Override
public void onClick(View v){
Toast.makeText(context, "position: " + position, Toast.LENGTH_SHORT).show();
}
});
}复制代码
如今,咱们在setViews中写上监听器就行。
public class ButtonItem implements AdapterItem<DemoModel> {
/** * 由于这个方法仅仅在item创建时才调用,因此不会重复创建监听器。 */
@Override
public void setViews() {
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// ...
}
});
}
@Override
public void handleData(DemoModel model, int position) {
// 这里避免作耗时的操做
}
}复制代码
这样setViews()
保证了item只会new一次监听器,在handleData()
中若是要加载图片,请在线程中加载,加载好了后切回主线程显示(通常图片库都作了这样的处理)。
若是咱们要在每次点击的时候获得当前item中的data和positon就比较麻烦了,因此只能写几个getXxxx()。
private T data;
private int pos;
@Override
public void handleData(T t, int position) {
data = t;
pos = position;
}
public T getData() {
return data;
}
public int getPos() {
return pos;
}复制代码
建议:这块的代码建议抽到baseItem中去写。
这个功能在recyclerView中就已经提供了,我就不废话了。网上流传比较多的是用下面的代码作listview的单条刷新:
private void updateSingleRow(ListView listView, long id) {
if (listView != null) {
int start = listView.getFirstVisiblePosition();
for (int i = start, j = listView.getLastVisiblePosition(); i <= j; i++)
if (id == ((Messages) listView.getItemAtPosition(i)).getId()) {
View view = listView.getChildAt(i - start);
getView(i, view, listView);
break;
}
}
}复制代码
其实就是手动调用了对应position的item的getView方法,我的以为不是很好,如今直接使用recyclerView的notifyItemChanged(index)
就行。
/** * Notify any registered observers that the item at <code>position</code> has changed. * Equivalent to calling <code>notifyItemChanged(position, null);</code>. * * <p>This is an item change event, not a structural change event. It indicates that any * reflection of the data at <code>position</code> is out of date and should be updated. * The item at <code>position</code> retains the same identity.</p> * * @param position Position of the item that has changed * * @see #notifyItemRangeChanged(int, int) */
public final void notifyItemChanged(int position) {
mObservable.notifyItemRangeChanged(position, 1);
}复制代码
上面提到的是对局部的某个item进行刷新,可是若是咱们须要对某个item中的某个view进行刷新呢?
/** * Notify any registered observers that the item at <code>position</code> has changed with an * optional payload object. * * <p>This is an item change event, not a structural change event. It indicates that any * reflection of the data at <code>position</code> is out of date and should be updated. * The item at <code>position</code> retains the same identity. * </p> * * <p> * Client can optionally pass a payload for partial change. These payloads will be merged * and may be passed to adapter's {@link #onBindViewHolder(ViewHolder, int, List)} if the * item is already represented by a ViewHolder and it will be rebound to the same * ViewHolder. A notifyItemRangeChanged() with null payload will clear all existing * payloads on that item and prevent future payload until * {@link #onBindViewHolder(ViewHolder, int, List)} is called. Adapter should not assume * that the payload will always be passed to onBindViewHolder(), e.g. when the view is not * attached, the payload will be simply dropped. * * @param position Position of the item that has changed * @param payload Optional parameter, use null to identify a "full" update * * @see #notifyItemRangeChanged(int, int) */
public final void notifyItemChanged(int position, Object payload) {
mObservable.notifyItemRangeChanged(position, 1, payload);
}复制代码
notifyItemChanged(index, obj)
这个方法的第一个参数用来肯定刷新的item位置,第二个参数一般用来传递一个标志,来告诉item须要刷新的东西。
在《RecyclerView animations done right》一文中经过点赞动画举出了一个很不错的例子。
整个的item是巨大且复杂的,但咱们点赞后只须要对一个view进行动画的操做,处理方式就须要从新考虑了。
notifyItemChanged(index, obj)
来通知item当前是否要作动画一般状况下咱们都会选择方案一,可是若是要用第二种方式呢?
notifyItemChanged(adapterPosition, ACTION_LIKE_BUTTON_CLICKED);复制代码
@Override
public void onBindViewHolder(ViewHolder holder, int position, List<Object> payloads) {
if (payloads.isEmpty()) {
// payloads为空,说明是更新整个viewHolder
onBindViewHolder(holder, position);
} else {
// payloads 不为空,这只更新须要更新的view便可
if(payloas.get(0).equals(ACTION_LIKE_BUTTON_CLICKED)) {
// ...
}
}
}复制代码
这里的关键点在于payloads这个参数,往大里说你能够通知某个item产生了某个事件,至于接收到事件后作什么就看你了。
这个的关键思路是外部不该该知道内部的数据,而是产生一个事件,好比“点赞了”,而item内部是根据这个事件来进行本身的操做的,是面向对象的思路。
关于这点是facebook提出的android优化技巧,后来我了解到ios自己也能够这么作。
如图所示,这个item很复杂,并且很大。当你的item占据三分之二屏幕的时候就能够考虑这样的优化方案了。右图说明了将一个总体的item变成多个小item的效果。在这种拆分后,你会发现原来拆分后的小的item可能在别的界面或别的type中也用到了,这就出现了item模块化的思想,总之是一个挺有意思的优化思路。
详细的文章(中文)请参考《facebook新闻页ListView的优化方案》,十分感谢做者的分享和翻译!
若是你是作论坛的项目,会有各类楼层或者回复嵌套的状况,你能够考虑用这种方式,但确定会遇到不少坑。下面是《Android ListView中复杂数据流的高效渲染》中提到的一些坑。
这个在QQ空间和微信朋友圈详情页中很常见,目前的小视频列表也是大图加文字的形式。滚动时自动中止的功能我但愿交给图片框架作,而不是手动处理,若是你要手动处理,那么你还得考虑不一样页面的不一样状况,感受性价比过低。
若是你的图片库没有作这样的处理,能够参考Android-Universal-Image-Loader中的实现方法。
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
switch (scrollState) {
case OnScrollListener.SCROLL_STATE_IDLE:
imageLoader.resume();
break;
case OnScrollListener.SCROLL_STATE_TOUCH_SCROLL:
if (pauseOnScroll) {
imageLoader.pause();
}
break;
case OnScrollListener.SCROLL_STATE_FLING:
if (pauseOnFling) {
imageLoader.pause();
}
break;
}
if (externalListener != null) {
externalListener.onScrollStateChanged(view, scrollState);
}
}复制代码
若是你是作bbs或者作新闻的,你会发现item中会有大量的文字,而文字过多或者有着大量的表情和特殊符号的时候,列表确定会卡顿。textview实际上是一个很基本但不简单的view,里面作了大量的判断和处理,因此并不是十分高效。
Instagram(现已在facebook旗下)分享了他们是如何优化他们的TextView渲染的效率的,在国内有做者也专门写了一篇文章来讲明其原理的。
当你有心想要优化textview的时候,你会发如今咱们知道这个item中textview的宽度和文字大小的状况下能够把初始化的配置作个缓存,每一个textview只须要用这个配置好的东西进行文字的渲染便可。下面是经过优化获得的结果:
这里测试的机器是MX3,左侧是直接使用StaticLayout的方案,右侧是系统的默认方案,Y轴是FPS,能够看出来,使用优化以后的方案,帧率提高了许多。
我只推荐在measure成为瓶颈的时候才去使用这样的优化策略,不要过分优化。
原理
textview支持上下左右的drawable,并且支持超链和emoji表情,每次绘制的时候都会进行检查,效率天然不会十分出众。在Android中,文本的渲染是很慢的。即便在一个像Nexus 5这样的新设备上,一段有十几行复杂文本的图片说明的初始绘制时间可能会达到50ms,而其文本的measure阶段就须要30ms。这些都发生在UI线程,在滚动时会致使app跳帧。
textview的绘制本质是layout的绘制,setText()被调用后,就会选择合适的layout进行绘制工做。textview的onDraw()
中能够看到以下方法:
void onDraw() {
// ...
if (mLayout == null) {
assumeLayout();
}
Layout layout = mLayout;
// ....
}复制代码
/** * Make a new Layout based on the already-measured size of the view, * on the assumption that it was measured correctly at some point. */
private void assumeLayout() {
int width = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
if (width < 1) {
width = 0;
}
int physicalWidth = width;
if (mHorizontallyScrolling) {
width = VERY_WIDE;
}
makeNewLayout(width, physicalWidth, UNKNOWN_BORING, UNKNOWN_BORING,
physicalWidth, false);
}复制代码
makeNewLayout(...)
是个很长的方法,就补贴出来了。总之咱们能够经过本身定义一个layoutView来进行文本的绘制,再配合Android在ICS中引入了TextLayoutCache实现text的预渲染。使人欣喜的是,目前facebook开源了一个至关不错的layout的build,有了它就能够帮助咱们快速创建一个高性能的textview了,感兴趣的同窗能够用起来了。
扩展阅读:
fb的的人发现目前项目中有不少稳定的item的绘制效率不高,因此就开始研究measure的耗时。
用linearlayout的时候:
> LinearLayout [horizontal] [w: 1080 exactly, h: 1557 exactly ]
> ProfilePhoto [w: 120 exactly, h: 120 exactly ]
> LinearLayout [vertical] [w: 0 unspecified, h: 0 unspecified]
> Title [w: 0 unspecified, h: 0 unspecified]
> Subtitle [w: 0 unspecified, h: 0 unspecified]
> Title [w: 222 exactly, h: 57 exactly ]
> Subtitle [w: 222 exactly, h: 57 exactly ]
> Menu [w: 60 exactly, h: 60 exactly ]
> LinearLayout [vertical] [w: 900 exactly, h: 1557 at_most ]
> Title [w: 900 exactly, h: 1557 at_most ]
> Subtitle [w: 900 exactly, h: 1500 at_most ]复制代码
用RelativeLayout的时候:
> RelativeLayout [w: 1080 exactly, h: 1557 exactly]
> Menu [w: 60 exactly, h: 1557 at_most]
> ProfilePhoto [w: 120 exactly, h: 1557 at_most]
> Title [w: 900 exactly, h: 1557 at_most]
> Subtitle [w: 900 exactly, h: 1557 at_most]
> Title [w: 900 exactly, h: 1557 at_most]
> Subtitle [w: 900 exactly, h: 1500 at_most]
> Menu [w: 60 exactly, h: 60 exactly]
> ProfilePhoto [w: 120 exactly, h: 120 exactly]复制代码
咱们都发现了对于menu,title,subtitle的重复测量。fb的工程师最终用自定义的viewgroup手动控制了布局和测量参数,最终实现了每一个view仅仅测量一次的优秀结果。优化事后,facebook的工程师讲解了他们对上面这个布局的优化策略,内容翔实,是个很好的分享。
扩展阅读:
原文:《Custom ViewGroups》
中文:《听FackBook工程师讲Custom ViewGroups》。
Recycled view pools allow multiple RecyclerViews to share a common pool of scrap views. This can be useful if you have multiple RecyclerViews with adapters that use the same view types, for example if you have several data sets with the same kinds of item views displayed by a ViewPager.
RecyclerView automatically creates a pool for itself if you don’t provide one.
正如上文所说RecycledViewPool
的主要做用是多个页面的item共享,好比是能够滑动的tab页面,每一个页面的vh都是同样的,在这种状况下用它就很合适了。
斗鱼的个tab的页面里面的item都是彻底同样的,对于首页这个多fragment的结构来讲,采用viewpool会大大提性能。
Tips:
RecycledViewPool pool = new RecycledViewPool();
// ...
recyclerView.setRecycledViewPool(pool);
adapter.setTypePool(pool.getTypePool());复制代码
RecycledViewPool是依据ItemViewType来索引ViewHolder的,因此不一样页面的相同的item的type必须是同样的值才能被准确的复用。
RecycledViewPool也能够经过mPool.setMaxRecycledViews(itemViewType, number)
来设置缓存数目。
RecyclerView能够经过recyclerView.setItemViewCacheSize(number)
设置本身所须要的ViewHolder数量,只有超过这个数量的detached ViewHolder才会丢进ViewPool中与别的RecyclerView共享。也就说每一个页面能够设置本身不想和别的页面共用的viewholder数目。
在合适的时机,RecycledViewPool会自我清除掉所持有的ViewHolder对象引用,固然你也能够在你认为合适的时机手动调用clear()。
若是是加载图片,我仍是但愿你去看看你用的图片框架有没有作这样的优化,若是有就请放心,若是没有那就本身处理吧。若是你的item中文字不少,常常有几百个文字。那么也能够先判断要显示的文字和textview中已经有的文字是否一致,若是不一致再调用setText方法。
@Override
public void handleData(DemoModel model, int position) {
if (b.imageView.getTag() != null) {
mOldImageUrl = (int) b.imageView.getTag();
}
int imageUrl = Integer.parseInt(model.content);
if (mOldImageUrl == 0 && mOldImageUrl != imageUrl) {
b.imageView.setTag(imageUrl); // set tag
b.imageView.setImageResource(imageUrl); // load local image
}
}复制代码
在滚动和滑动的时候,RecyclerView须要显示进入屏幕的新item,这些item须要被绑定数据(若是缓存中没有相似的item极可能还须要建立),而后把它们放入布局并绘制。当全部这些工做慢吞吞进行的时候,UI线程会慢慢停下来等待其完成,而后渲染才能进行,滚动才能继续。google的工程师看到在须要一个新的item时,咱们花了太多时间去准备这个item,但同时UI线程却早早的完成了前一帧的任务,休眠了大量时间,因而修改了创建vh和绘制的工做流程。
详细内容请参考:RecyclerView的新机制:预取(Prefetch) - 泡在网上的日子
现在recyclerView大有接替listview的趋势,要知道listview的适配器和recyclerView的适配器的写法是不一样的。
listview的写法以下:
listView.setAdapter(new CommonAdapter<DemoModel>(data,1) {
@Override
public AdapterItem<DemoModel> getItemView(Object type) {
return new TextItem();
}
});复制代码
换成recyclerView的适配器应该须要不少步吧?不,改一行足矣。
recyclerView.setAdapter(new CommonRcvAdapter<DemoModel>(data) {
public AdapterItem<DemoModel> getItemView(Object type) {
return new TextItem();
}
});复制代码
这里换了一个适配器的类名和容器名,其他的都没变。
咱们的adapter是有一个泛型的,item也是有泛型,通常状况下adapter的泛型对象就是item的对象。
return new CommonAdapter<DemoModel>(data, 1) { // DemoModel
public AdapterItem createItem(Object type) {
// 若是就一种,那么直接return一种类型的item便可。
return new TextItem();
}
};复制代码
public class TextItem implements AdapterItem<DemoModel> { // DemoModel
// ...
}复制代码
但这并不是是必须的,因此你能够经过adapter的getConvertedData(...)
进行数据的转换,让adapter接收的数据和item的数据不一样。
/** * 作数据的转换,这里算是数据的精细拆分 */
public Object getConvertedData(DemoModel data, Object type) {
// 这样能够容许item自身的数据和list数据不一样
return data.content; // model -> string
}复制代码
CommonAdapter能够结合dataBinding中的ObservableList
进行数据的自动绑定操做。源码以下:
protected CommonRcvAdapter(@NonNull ObservableList<T> data) {
this((List<T>) data);
data.addOnListChangedCallback(new ObservableList.OnListChangedCallback<ObservableList<T>>() {
@Override
public void onChanged(ObservableList<T> sender) {
notifyDataSetChanged();
}
@Override
public void onItemRangeChanged(ObservableList<T> sender, int positionStart, int itemCount) {
notifyItemRangeChanged(positionStart, itemCount);
}
@Override
public void onItemRangeInserted(ObservableList<T> sender, int positionStart, int itemCount) {
notifyItemRangeInserted(positionStart, itemCount);
notifyItemRangeChanged(positionStart, itemCount);
}
@Override
public void onItemRangeRemoved(ObservableList<T> sender, int positionStart, int itemCount) {
notifyItemRangeRemoved(positionStart, itemCount);
notifyItemRangeChanged(positionStart, itemCount);
}
@Override
public void onItemRangeMoved(ObservableList<T> sender, int fromPosition, int toPosition, int itemCount) {
// Note:不支持一次性移动"多个"item的状况!!!!
notifyItemMoved(fromPosition, toPosition);
notifyDataSetChanged();
}
});
}复制代码
如今只要咱们对list对象进行操做,adapter就会自动去更新界面,不再用去手动notify了。
咱们可能还记得support中的一个新的工具类——diffUtil,它能够配合recycleview进行自动的notify操做,若是咱们要用它就须要作一些处理了。
public static abstract class DiffRcvAdapter<T> extends CommonRcvAdapter<T> {
DiffRcvAdapter(@Nullable List<T> data) {
super(data);
}
@Override
public void setData(@NonNull final List<T> data) {
DiffUtil.calculateDiff(new DiffUtil.Callback() {
@Override
public int getOldListSize() {
return getItemCount();
}
@Override
public int getNewListSize() {
return data.size();
}
/** * 检测是不是相同的item,这里暂时经过位置判断 */
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
boolean result = oldItemPosition == newItemPosition;
Log.d(TAG, "areItemsTheSame: " + result);
return result;
}
/** * 检测是不是相同的数据 * 这个方法仅仅在areItemsTheSame()返回true时,才调用。 */
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
boolean result = isContentSame(getData().get(oldItemPosition), data.get(newItemPosition));
Log.d(TAG, "areContentsTheSame: " + result);
return result;
}
}).dispatchUpdatesTo(this); // 传递给adapter
super.setData(data);
}
protected abstract boolean isContentSame(T oldItemData, T newItemData);
}复制代码
final DiffRcvAdapter<DemoModel> adapter = new DiffRcvAdapter<DemoModel>(DataManager.loadData(this, 3)) {
@NonNull
@Override
public AdapterItem createItem(Object type) {
return new TextItem();
}
@Override
protected boolean isContentSame(DemoModel oldItemData, DemoModel newItemData) {
return oldItemData.content.equals(newItemData.content);
}
};复制代码
这里须要多作的是手动判断item的数据是否要更新,因此不如用ObservableArrayList
比较简单,并且是直接更新,不占cpu。
须要注意的是,若是用diffutil,你的item必须是viewholder,由于它最终调用的是adapter.notifyItemRangeChanged(position, count, payload)
,因此就会调用adapter中的onBindViewHolder(VH holder, int position, List<Object> payloads)
。
[diffUtil]
public void dispatchUpdatesTo(final RecyclerView.Adapter adapter) {
dispatchUpdatesTo(new ListUpdateCallback() {
@Override
public void onInserted(int position, int count) {
adapter.notifyItemRangeInserted(position, count);
}
@Override
public void onRemoved(int position, int count) {
adapter.notifyItemRangeRemoved(position, count);
}
@Override
public void onMoved(int fromPosition, int toPosition) {
adapter.notifyItemMoved(fromPosition, toPosition);
}
@Override
public void onChanged(int position, int count, Object payload) {
adapter.notifyItemRangeChanged(position, count, payload);
}
});
}复制代码
当咱们让adapter变成一个内部类的时候,剩下的问题就是adapter应该处于view层仍是presenter或model层了。在实际的运用当中,我最终定义adapter是处于presenter层(mvp)或者model层(mvvm)。
是否放在p或vm层有一个简单的原则就是不可复用,p或vm的代码复用性是极其低的,因此当你认为有代码是不可复用的时候,那么你就能够放在里面。何况ui层面有可能会出现复用的状况,并且adapter中还会出现和数据相关的一些操做,因此应该让其与ui层隔离。
当你和ui隔离了,你彻底能够实现一个list页面统一的ui,进行空状态等细节的处理,方便复用统一的ui,十分有用。
item的接口化提供了更大的灵活性,可是就实际项目而言,我强烈推荐去作一个baseItem,这样能够快速获得activity,position,context等等对象。
public abstract class BaseAdapterItem<Bind extends ViewDataBinding, Model> implements AdapterItem<Model> {
private View root;
private int pos;
protected Bind b;
private Activity activity;
public BaseAdapterItem(Activity activity) {
this.activity = activity;
}
public BaseAdapterItem() {
}
@CallSuper
@Override
public void bindViews(View view) {
root = view;
b = DBinding.bind(view);
beforeSetViews();
}
protected void beforeSetViews() {
}
@CallSuper
@Override
public void handleData(Model t, int position) {
pos = position;
}
public View getRoot() {
return root;
}
public int getCurrentPosition() {
return pos;
}
protected static void setVizOrInViz(View view, CharSequence str) {
if (TextUtils.isEmpty(str)) {
view.setVisibility(View.INVISIBLE);
} else {
view.setVisibility(View.VISIBLE);
}
}
protected static void setVizOrGone(View view, CharSequence str) {
if (TextUtils.isEmpty(str)) {
view.setVisibility(View.GONE);
} else {
view.setVisibility(View.VISIBLE);
}
}
protected int getColor(@ColorRes int colorResId) {
return root.getResources().getColor(colorResId);
}
protected Context getContext() {
return root.getContext();
}
}复制代码
我经过上面的base和databinding结合后,快速的实现了findview的操做,十分简洁。
若是list页面中有多个type,你确定会发现不一样type的item的有相同的逻辑,最多见的是点击跳转的逻辑。对于这样的状况我建议再抽取一个base来作,之后修改的时候你会发现十分方便。对于ui层面的类似,我也但愿能够适当的使用include
标签进行复用。
我以前偷懒常常不抽取公共部分,由于以为作基类复杂,公共部分的代码也很少,可是后面维护的时候处处都要改,因此就给出了这条实践经验。
OnRcvScrollListener
是我经常使用的一个监听类,能够监听滚动方向、滚动距离、是否混动到底。
/** * @author Jack Tony * recyle view 滚动监听器 * @date 2015/4/6 */
public class OnRcvScrollListener extends RecyclerView.OnScrollListener {
private static final int TYPE_LINEAR = 0;
private static final int TYPE_GRID = 1;
private static final int TYPE_STAGGERED_GRID = 2;
/** * 最后一个的位置 */
private int[] mLastPositions;
/** * 最后一个可见的item的位置 */
private int mLastVisibleItemPosition;
/** * 触发在上下滑动监听器的容差距离 */
private static final int HIDE_THRESHOLD = 20;
/** * 滑动的距离 */
private int mDistance = 0;
/** * 是否须要监听控制 */
private boolean mIsScrollDown = true;
/** * Y轴移动的实际距离(最顶部为0) */
private int mScrolledYDistance = 0;
/** * X轴移动的实际距离(最左侧为0) */
private int mScrolledXDistance = 0;
private int mOffset = 0;
/** * @param offset 设置:倒数几个才断定为到底,默认是0 */
public OnRcvScrollListener(int offset) {
mOffset = offset;
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
int firstVisibleItemPosition = 0;
RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
// 判断layout manager的类型
int type = judgeLayoutManager(layoutManager);
// 根据类型来计算出第一个可见的item的位置,由此判断是否触发到底部的监听器
firstVisibleItemPosition = calculateFirstVisibleItemPos(type, layoutManager, firstVisibleItemPosition);
// 计算并判断当前是向上滑动仍是向下滑动
calculateScrollUpOrDown(firstVisibleItemPosition, dy);
// 移动距离超过必定的范围,咱们监听就没有啥实际的意义了
mScrolledXDistance += dx;
mScrolledYDistance += dy;
mScrolledXDistance = (mScrolledXDistance < 0) ? 0 : mScrolledXDistance;
mScrolledYDistance = (mScrolledYDistance < 0) ? 0 : mScrolledYDistance;
onScrolled(mScrolledXDistance, mScrolledYDistance);
}
/** * 判断layoutManager的类型 */
private int judgeLayoutManager(RecyclerView.LayoutManager layoutManager) {
if (layoutManager instanceof GridLayoutManager) {
return TYPE_GRID;
} else if (layoutManager instanceof LinearLayoutManager) {
return TYPE_LINEAR;
} else if (layoutManager instanceof StaggeredGridLayoutManager) {
return TYPE_STAGGERED_GRID;
} else {
throw new RuntimeException("Unsupported LayoutManager used. Valid ones are "
+ "LinearLayoutManager, GridLayoutManager and StaggeredGridLayoutManager");
}
}
/** * 计算第一个元素的位置 */
private int calculateFirstVisibleItemPos(int type, RecyclerView.LayoutManager layoutManager, int firstVisibleItemPosition) {
switch (type) {
case TYPE_LINEAR:
mLastVisibleItemPosition = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition();
firstVisibleItemPosition = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition();
break;
case TYPE_GRID:
mLastVisibleItemPosition = ((GridLayoutManager) layoutManager).findLastVisibleItemPosition();
firstVisibleItemPosition = ((GridLayoutManager) layoutManager).findFirstVisibleItemPosition();
break;
case TYPE_STAGGERED_GRID:
StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager) layoutManager;
if (mLastPositions == null) {
mLastPositions = new int[staggeredGridLayoutManager.getSpanCount()];
}
mLastPositions = staggeredGridLayoutManager.findLastVisibleItemPositions(mLastPositions);
mLastVisibleItemPosition = findMax(mLastPositions);
staggeredGridLayoutManager.findFirstCompletelyVisibleItemPositions(mLastPositions);
firstVisibleItemPosition = findMax(mLastPositions);
break;
}
return firstVisibleItemPosition;
}
/** * 计算当前是向上滑动仍是向下滑动 */
private void calculateScrollUpOrDown(int firstVisibleItemPosition, int dy) {
if (firstVisibleItemPosition == 0) {
if (!mIsScrollDown) {
onScrollDown();
mIsScrollDown = true;
}
} else {
if (mDistance > HIDE_THRESHOLD && mIsScrollDown) {
onScrollUp();
mIsScrollDown = false;
mDistance = 0;
} else if (mDistance < -HIDE_THRESHOLD && !mIsScrollDown) {
onScrollDown();
mIsScrollDown = true;
mDistance = 0;
}
}
if ((mIsScrollDown && dy > 0) || (!mIsScrollDown && dy < 0)) {
mDistance += dy;
}
}
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
int visibleItemCount = layoutManager.getChildCount();
int totalItemCount = layoutManager.getItemCount();
int bottomCount = totalItemCount - 1 - mOffset;
if (bottomCount < 0) {
bottomCount = totalItemCount - 1;
}
if (visibleItemCount > 0 && newState == RecyclerView.SCROLL_STATE_IDLE
&& mLastVisibleItemPosition >= bottomCount && !mIsScrollDown) {
onBottom();
}
}
protected void onScrollUp() {
}
protected void onScrollDown() {
}
protected void onBottom() {
}
protected void onScrolled(int distanceX, int distanceY) {
}
private int findMax(int[] lastPositions) {
int max = lastPositions[0];
for (int value : lastPositions) {
max = Math.max(max, value);
}
return max;
}
}复制代码
CommonAdapter中提供了RcvAdapterWrapper
来作头部、底部、空状态的处理,方法也就是setXxx()。值得一提的是,当有头部的时候,空状态的view会自动占用屏幕-头部的空间,不会阻碍到头部的显示。
用不用一个第三方库我有下面的几点建议:
探索无止境,优化没底线,我仍是但愿能有库在库中作好不少的优化操做,下降对程序员的要求,最终但愿谁均可以写代码。简单编程,快乐生活。本文的完成离不开朋友们的支持和帮助,感谢:MingleArch、豪哥的批评和建议。
参考文章: