实现功能
官方定义:A flexible view for providing a limited window into a large data set。简单来说就是一种可以通过灵活的视图形式展示大量数据的列表组件
设计哲学
我其实也并不知道该组件的设计思路是什么,所以是比较主观的
适配器
该组件需要用户提供相对规整的数据,以及数据对应的自定义的视图,用一个map去存储?不行的,有一个原因是自定义视图中的子view对应的数据也需要用户去决定,所以需要设计一个适配器来处理这样杂乱无章的数据与视图(整流),将通过适配器处理后的数据对象交给组件。
布局管理器
这么多视图,哪个视图放到哪也是一个问题?这就需要布局管理器,其需要整合目前市面是流行的布局且具有可扩展性,所以会设计出一个布局管理器
简单使用
List mDatas = getDatas()
mRecyclerView = (RecyclerView) findViewById(R.id.id_recyclerview);
//设置布局管理器
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
// 设置适配器
mRecyclerView.setAdapter(new HomeAdapter(mDatas))
// adapter
class HomeAdapter extends RecyclerView.Adapter
{
private List mDatas;
HomeAdpater(List datas) {
this.mDatas = datas;
}
@Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
{
MyViewHolder holder = new MyViewHolder(LayoutInflater.from(
HomeActivity.this).inflate(R.layout.item_home, parent,
false));
return holder;
}
@Override
public void onBindViewHolder(MyViewHolder holder, int position)
{
holder.tv.setText(mDatas.get(position));
}
@Override
public int getItemCount()
{
return mDatas.size();
}
// Viewhodler类
class MyViewHolder extends ViewHolder
{
TextView tv;
public MyViewHolder(View view)
{
super(view);
tv = (TextView) view.findViewById(R.id.id_num);
}
}
}
源码分析
主体初始化
构造器
构造器的代码是在findViewById触发,其主要是初始化了三个类分别是
ChildHelper
,AdapterHelper
,LayoutManager
。ChildHelper
是管理recylerview的孩子节点的。AdapterHelper
官方解释是管理adapter的更新的类。最后一个前面提到了是管理布局形式的类
测量(onMeasure)
// 关键源码
protected void onMeasure(int widthSpec, int heightSpec) {
if (mLayout.isAutoMeasureEnabled()) {
...
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
}
mLayout.setMeasureSpecs(widthSpec, heightSpec);
mState.mIsMeasuring = true;
dispatchLayoutStep2();
// now we can get the width and height from the children.
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
...
}
}
由于系统自带的几个布局管理器默认设置了自动测量参数mAutoMeasure=ture,所以测量主要分两步,其中dispatchLayoutStep1,dispatchLayoutStep2。
- dispatchLayoutStep1
主要工作就是记录一些view的状态和初始化动画相关的类其中调用了addToPreLayout去存储当前view的状态
- dispatchLayoutStep2
这一步工作比较关键,用于测量+布局,全权交由布局管理器处理,这里我们以线性布局管理器举例,入口函数为onLayoutChinlder
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
...
// 寻找锚点,即确定布局的起始点
if (!mAnchorInfo.mValid || mPendingScrollPosition != NO_POSITION ||
mPendingSavedState != null) {
updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
}
...
// 确定布局方向,因为这里需要考虑到滚动的情况
if (mAnchorInfo.mLayoutFromEnd) {
firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL :
LayoutState.ITEM_DIRECTION_HEAD;
} else {
firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD :
LayoutState.ITEM_DIRECTION_TAIL;
}
...
// 回调,此处可以继承LinearManger去实现进入RecyclerView时定位在某一项的功能
onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
// 将所有的子itemview都从父布局中剥离(只是简单的操作Viewgroup的数组,没有重绘),然后通过下面代码重新填充。这样画面会很流畅
detachAndScrapAttachedViews(recycler);
...
if (mAnchorInfo.mLayoutFromEnd) {
// end-start的方向
} else {
// start-end的方向
// fill towards end
updateLayoutStateToFillEnd(mAnchorInfo);
// 填充的关键函数
fill(recycler, mLayoutState, state, false);
...
// fill towards start
updateLayoutStateToFillStart(mAnchorInfo);
...
fill(recycler, mLayoutState, state, false);
...
}
...
}
// fill函数
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,RecyclerView.State state, boolean stopOnFocusable) {
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
...
// 回收满足条件的view到mCacheView,比如离开边界的view
recycleByLayoutState(recycler, layoutState);
}
...
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
...
// 关键函数, 开始正式布局
layoutChunk(recycler, state, layoutState, layoutChunkResult);
...
}
...
}
// layoutChunk
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,LayoutState layoutState,LayoutChunkResult result) {
// 此处是从缓存区取view,如果缓存区没有适合条件的view,则调用adapter的onCreateViewHolder和onBindViewHolder
View view = layoutState.next(recycler);
...
// 测量view
measureChildWithMargins(view, 0, 0);
...
// 布局view
layoutDecoratedWithMargins(view, left, top, right, bottom);
...
}
-
缓存机制
布局
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
// 主要关注该方法
dispatchLayout();
TraceCompat.endSection();
mFirstLayoutComplete = true;
}
// dispatchLayout
void dispatchLayout() {
...
mState.mIsMeasuring = false;
// 自动测量模式下一般不执行该步骤
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
}
// 第二个条件即判定在onmeasure中做的布局操作是否成功,因为getWitdh是在layout之后才能得到的值
else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
|| mLayout.getHeight() != getHeight()) {
// First 2 steps are done in onMeasure but looks like we have to run again due to
// changed size.
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else {
// always make sure we sync them (to ensure mode is exact)
mLayout.setExactMeasureSpecsFrom(this);
}
// 一般情况会走该步骤,该步骤与dispatchLayoutStep1对称,所以该步骤调用了addToPostLayout去记录view的完成态,所以我们在dispatchLayoutStep1确定了view的初始态,在dispatchLayoutStep3确定了完成态,便可以执行动画了
dispatchLayoutStep3();
}
绘制
@Override
public void draw(Canvas c) {
// super.draw会回调到onDraw,如下代码所示
super.draw(c);
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDrawOver(c, this, mState);
...
}
@Override
public void onDraw(Canvas c) {
super.onDraw(c);
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDraw(c, this, mState);
}
}
从onDraw的源代码来看,主要是绘制边框样式,可以自定义
ItemDecoration
去实现onDraw以及onDrawOver方法。同时也可以看到onDrawOver会绘制到onDraw图层的上层
滚动
public boolean onTouchEvent(MotionEvent e){
...
switch (action) {
case MotionEvent.ACTION_DOWN:{
...
}
case MotionEvent.ACTION_POINTER_DOWN:{
...
}
case MotionEvent.ACTION_MOVE: {
...
// 与滑动阈值mTouchslop比较,最终得到变化量dx,dy
if (mScrollState != SCROLL_STATE_DRAGGING) {
...
if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
if (dy > 0) {
dy -= mTouchSlop;
} else {
dy += mTouchSlop;
}
startScroll = true;
}
if (startScroll) {
setScrollState(SCROLL_STATE_DRAGGING);
}
}
if (mScrollState == SCROLL_STATE_DRAGGING) {
mLastTouchX = x - mScrollOffset[0];
mLastTouchY = y - mScrollOffset[1];
// 内部执行嵌套滑动接口
// 由于recylerview实现了NestedScrollingChild2,NestedScrollingChild3,所以其父节点需要实现NestedScrollingParent才能实现嵌套滑动
if (scrollByInternal(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
vtev)) {
// 请求不用父组件不要执行拦截事件
getParent().requestDisallowInterceptTouchEvent(true);
}
}
}
...
case MotionEvent.ACTION_UP {
// 惯性滑动
mVelocityTracker.addMovement(vtev);
eventAddedToVelocityTracker = true;
mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
final float xvel = canScrollHorizontally
? -mVelocityTracker.getXVelocity(mScrollPointerId) : 0;
final float yvel = canScrollVertically
? -mVelocityTracker.getYVelocity(mScrollPointerId) : 0;
if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
setScrollState(SCROLL_STATE_IDLE);
}
resetScroll();
}
}
...
}
- 自动滚动部分由fling处理,拖拉滚动部分由scrollByInternal处理
- scrollBy(int x, int y): 基于当前位置的滑动偏移量
- scrollTo(int x, int y): 基于原点位置的滑动偏移量
-
smoothScrollToPositon:
总结
优点和缺点
优点
- 局部刷新
- 多级缓存
- ItemAnimator类提供动画API
- ItemTouchHelper类提供滑动和拖拽的API
- 支持嵌套滚动(CollapsingToolbarLayout)
缺点
- 没有item的监听事件,需要在adapter自己定义
- 没有header和footer的API
- 内存占用较大
重要组件
- LayoutManager:负责Item视图的布局的显示管理
- ItemDecoration:给每一项Item视图添加子View,例如可以进行画分隔线之类
- ItemAnimator: 负责处理数据添加或者删除时候的动画效果
- Adapter:为每一项Item创建视图
- ViewHolder:承载Item视图的子布局
实际应用
- 滚动到顶部
mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()) {
@Override
public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {
super.smoothScrollToPosition(recyclerView, state, position);
LinearSmoothScroller smoothScroller = new LinearSmoothScroller(getContext()){
// 计算滚动速度
@Override
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
return 10f / displayMetrics.densityDpi;
}
// 指定滚动停止的时机,item在底部还是在顶部才停下
@Override
protected int getVerticalSnapPreference() {
return SNAP_TO_START;
}
};
smoothScroller.setTargetPosition(position);
startSmoothScroll(smoothScroller);
}
});
- ItemDecoration实践:stickHeader
public class MyItemDecoration extends RecyclerView.ItemDecoration {
private Drawable divider = new ColorDrawable(Color.parseColor("red"));
private SectionCallback mSectionCallback;
MyItemDecoration(SectionCallback sectionCallback){
mSectionCallback = sectionCallback;
}
// 绘制内容 [RecyclerView]draw -> [View]draw -> [RecyclerView]onDraw -> [ItemDecoration]onDraw
@Override
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.onDraw(c, parent, state);
// 可见范围内的孩子数量
int count = parent.getChildCount();
for (int i = 0; i < count; i++) {
if (i == count - 1) return;
View child = parent.getChildAt(i);
int left = (int) child.getX();
int top = (int) child.getY();
divider.setBounds(left - 10, top, left,
top + child.getHeight());
divider.draw(c);
divider.setBounds(left + child.getWidth(), top, left + child.getWidth() + 10,
top + child.getHeight());
divider.draw(c);
}
}
// 绘制背景 [RecyclerView]draw -> [View]draw -> [RecyclerView]onDraw -> [ItemDecoration]onDraw -> [ItemDecoration]onDrawOver
@Override
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.onDrawOver(c, parent, state);
final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
int count = parent.getChildCount();
View child = parent.getChildAt(0);
int pos = parent.getChildAdapterPosition(child);
int left = (int) child.getX();
int right = left + child.getWidth();
int top = 0;
int bottom = top + child.getHeight();
c.drawRect(left, top, right, bottom, paint);
}
// 在item的OnMeasure的阶段调用
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
// 在onMeasure阶段设置装潢预留值
outRect.set(10, 0, 10, 0);
}
public interface SectionCallback {
boolean isFirstItem(int position);
int getGroupId(int position);
String getTitle(int position);
}
}
扩展
- ListAdapter
// listAdapter
class UserAdapter : ListAdapter(UserDiffCallback()) {
// 不用传递数据集
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return UserAdapter.ViewHolder(inflater.inflate(R.layout.item_user, parent, false))
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (holder is ViewHolder) {
holder.bind(getItem(position))
}
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(user: User) {
}
}
}
// DiffUtil,局部更新的关键接口
class UserDiffCallback : DiffUtil.ItemCallback() {
// 就列表中的对象和新列表中的对象是否是同一个
override fun areItemsTheSame(oldItem: User?, newItem: User?): Boolean {
return oldItem?.userI
id == newItem?.userId
}
// 旧对象和新对象内容是否相同
override fun areContentsTheSame(oldItem: User?, newItem: User?): Boolean {
return oldItem == newItem
}
}
// 提交数据, 数据会存储到listAdapter里面,其中list一定要是一个全新的列表而不能使加载的后的列表
adpater.submitList(list)
优点:更少的代码,更清晰的结构。 缺点:灵活性降低,内存占用较高
- Litho(Facebook)
- 声明式的组件
- 异步布局
- 扁平化的view
- 细粒度复用
可以用来提升RecyclerView复杂列表的滑动性能和降低内存占用