Android RecyclerView 使用完全解析 体验艺术般的控件

import android.graphics.drawable.Drawable;

import android.support.v7.widget.LinearLayoutManager;

import android.support.v7.widget.RecyclerView;

import android.support.v7.widget.RecyclerView.State;

import android.util.Log;

import android.view.View;

/**

  • This class is from the v7 samples of the Android SDK. It’s not by me!

  • See the license above for details.

*/

public class DividerItemDecoration extends RecyclerView.ItemDecoration {

private static final int[] ATTRS = new int[]{

android.R.attr.listDivider

};

public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;

public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL;

private Drawable mDivider;

private int mOrientation;

public DividerItemDecoration(Context context, int orientation) {

final TypedArray a = context.obtainStyledAttributes(ATTRS);

mDivider = a.getDrawable(0);

a.recycle();

setOrientation(orientation);

}

public void setOrientation(int orientation) {

if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) {

throw new IllegalArgumentException(“invalid orientation”);

}

mOrientation = orientation;

}

@Override

public void onDraw(Canvas c, RecyclerView parent) {

Log.v(“recyclerview - itemdecoration”, “onDraw()”);

if (mOrientation == VERTICAL_LIST) {

drawVertical(c, parent);

} else {

drawHorizontal(c, parent);

}

}

public void drawVertical(Canvas c, RecyclerView parent) {

final int left = parent.getPaddingLeft();

final int right = parent.getWidth() - parent.getPaddingRight();

final int childCount = parent.getChildCount();

for (int i = 0; i < childCount; i++) {

final View child = parent.getChildAt(i);

android.support.v7.widget.RecyclerView v = new android.support.v7.widget.RecyclerView(parent.getContext());

final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child

.getLayoutParams();

final int top = child.getBottom() + params.bottomMargin;

final int bottom = top + mDivider.getIntrinsicHeight();

mDivider.setBounds(left, top, right, bottom);

mDivider.draw©;

}

}

public void drawHorizontal(Canvas c, RecyclerView parent) {

final int top = parent.getPaddingTop();

final int bottom = parent.getHeight() - parent.getPaddingBottom();

final int childCount = parent.getChildCount();

for (int i = 0; i < childCount; i++) {

final View child = parent.getChildAt(i);

final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child

.getLayoutParams();

final int left = child.getRight() + params.rightMargin;

final int right = left + mDivider.getIntrinsicHeight();

mDivider.setBounds(left, top, right, bottom);

mDivider.draw©;

}

}

@Override

public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {

if (mOrientation == VERTICAL_LIST) {

outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());

} else {

outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);

}

}

}

该实现类可以看到通过读取系统主题中的 android.R.attr.listDivider作为Item间的分割线,并且支持横向和纵向。如果你不清楚它是怎么做到的读取系统的属性用于自身,请参考我的另一篇博文:Android 深入理解Android中的自定义属性

获取到listDivider以后,该属性的值是个Drawable,在getItemOffsets中,outRect去设置了绘制的范围。onDraw中实现了真正的绘制。

我们在原来的代码中添加一句:

mRecyclerView.addItemDecoration(new DividerItemDecoration(this,

DividerItemDecoration.VERTICAL_LIST));

ok,现在再运行,就可以看到分割线的效果了。

该分割线是系统默认的,你可以在theme.xml中找到该属性的使用情况。那么,使用系统的listDivider有什么好处呢?就是方便我们去随意的改变,该属性我们可以直接声明在:

然后自己写个drawable即可,下面我们换一种分隔符:

<?xml version="1.0" encoding="utf-8"?>

<shape xmlns:android=“http://schemas.android.com/apk/res/android”

android:shape=“rectangle” >

<gradient

android:centerColor=“#ff00ff00”

android:endColor=“#ff0000ff”

android:startColor=“#ffff0000”

android:type=“linear” />

现在的样子是:

当然了,你可以根据自己的需求,去随意的绘制,反正是画出来的,随便玩~~

ok,看到这,你可能觉得,这玩意真尼玛麻烦,完全不能比拟的心爱的ListView。那么继续看。

LayoutManager

好了,上面实现了类似ListView样子的Demo,通过使用其默认的LinearLayoutManager。

RecyclerView.LayoutManager吧,这是一个抽象类,好在系统提供了3个实现类:

  1. LinearLayoutManager 现行管理器,支持横向、纵向。

  2. GridLayoutManager 网格布局管理器

  3. StaggeredGridLayoutManager 瀑布就式布局管理器

上面我们已经初步体验了下LinearLayoutManager,接下来看GridLayoutManager。

  • GridLayoutManager

我们尝试去实现类似GridView,秒秒钟的事情:

//mRecyclerView.setLayoutManager(new LinearLayoutManager(this));

mRecyclerView.setLayoutManager(new GridLayoutManager(this,4));

只需要修改LayoutManager即可,还是很nice的。

当然了,改为GridLayoutManager以后,对于分割线,前面的DividerItemDecoration就不适用了,主要是因为它在绘制的时候,比如水平线,针对每个child的取值为:

final int left = parent.getPaddingLeft();

final int right = parent.getWidth() - parent.getPaddingRight();

因为每个Item一行,这样是没问题的。而GridLayoutManager时,一行有多个childItem,这样就多次绘制了,并且GridLayoutManager时,Item如果为最后一列(则右边无间隔线)或者为最后一行(底部无分割线)。

针对上述,我们编写了DividerGridItemDecoration

package com.zhy.sample.demo_recyclerview;

import android.content.Context;

import android.content.res.TypedArray;

import android.graphics.Canvas;

import android.graphics.Rect;

import android.graphics.drawable.Drawable;

import android.support.v7.widget.GridLayoutManager;

import android.support.v7.widget.RecyclerView;

import android.support.v7.widget.RecyclerView.LayoutManager;

import android.support.v7.widget.RecyclerView.State;

import android.support.v7.widget.StaggeredGridLayoutManager;

import android.view.View;

/**

  • @author zhy

*/

public class DividerGridItemDecoration extends RecyclerView.ItemDecoration

{

private static final int[] ATTRS = new int[] { android.R.attr.listDivider };

private Drawable mDivider;

public DividerGridItemDecoration(Context context)

{

final TypedArray a = context.obtainStyledAttributes(ATTRS);

mDivider = a.getDrawable(0);

a.recycle();

}

@Override

public void onDraw(Canvas c, RecyclerView parent, State state)

{

drawHorizontal(c, parent);

drawVertical(c, parent);

}

private int getSpanCount(RecyclerView parent)

{

// 列数

int spanCount = -1;

LayoutManager layoutManager = parent.getLayoutManager();

if (layoutManager instanceof GridLayoutManager)

{

spanCount = ((GridLayoutManager) layoutManager).getSpanCount();

} else if (layoutManager instanceof StaggeredGridLayoutManager)

{

spanCount = ((StaggeredGridLayoutManager) layoutManager)

.getSpanCount();

}

return spanCount;

}

public void drawHorizontal(Canvas c, RecyclerView parent)

{

int childCount = parent.getChildCount();

for (int i = 0; i < childCount; i++)

{

final View child = parent.getChildAt(i);

final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child

.getLayoutParams();

final int left = child.getLeft() - params.leftMargin;

final int right = child.getRight() + params.rightMargin

  • mDivider.getIntrinsicWidth();

final int top = child.getBottom() + params.bottomMargin;

final int bottom = top + mDivider.getIntrinsicHeight();

mDivider.setBounds(left, top, right, bottom);

mDivider.draw©;

}

}

public void drawVertical(Canvas c, RecyclerView parent)

{

final int childCount = parent.getChildCount();

for (int i = 0; i < childCount; i++)

{

final View child = parent.getChildAt(i);

final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child

.getLayoutParams();

final int top = child.getTop() - params.topMargin;

final int bottom = child.getBottom() + params.bottomMargin;

final int left = child.getRight() + params.rightMargin;

final int right = left + mDivider.getIntrinsicWidth();

mDivider.setBounds(left, top, right, bottom);

mDivider.draw©;

}

}

private boolean isLastColum(RecyclerView parent, int pos, int spanCount,

int childCount)

{

LayoutManager layoutManager = parent.getLayoutManager();

if (layoutManager instanceof GridLayoutManager)

{

if ((pos + 1) % spanCount == 0)// 如果是最后一列,则不需要绘制右边

{

return true;

}

} else if (layoutManager instanceof StaggeredGridLayoutManager)

{

int orientation = ((StaggeredGridLayoutManager) layoutManager)

.getOrientation();

if (orientation == StaggeredGridLayoutManager.VERTICAL)

{

if ((pos + 1) % spanCount == 0)// 如果是最后一列,则不需要绘制右边

{

return true;

}

} else

{

childCount = childCount - childCount % spanCount;

if (pos >= childCount)// 如果是最后一列,则不需要绘制右边

return true;

}

}

return false;

}

private boolean isLastRaw(RecyclerView parent, int pos, int spanCount,

int childCount)

{

LayoutManager layoutManager = parent.getLayoutManager();

if (layoutManager instanceof GridLayoutManager)

{

childCount = childCount - childCount % spanCount;

if (pos >= childCount)// 如果是最后一行,则不需要绘制底部

return true;

} else if (layoutManager instanceof StaggeredGridLayoutManager)

{

int orientation = ((StaggeredGridLayoutManager) layoutManager)

.getOrientation();

// StaggeredGridLayoutManager 且纵向滚动

if (orientation == StaggeredGridLayoutManager.VERTICAL)

{

childCount = childCount - childCount % spanCount;

// 如果是最后一行,则不需要绘制底部

if (pos >= childCount)

return true;

} else

// StaggeredGridLayoutManager 且横向滚动

{

// 如果是最后一行,则不需要绘制底部

if ((pos + 1) % spanCount == 0)

{

return true;

}

}

}

return false;

}

@Override

public void getItemOffsets(Rect outRect, int itemPosition,

RecyclerView parent)

{

int spanCount = getSpanCount(parent);

int childCount = parent.getAdapter().getItemCount();

if (isLastRaw(parent, itemPosition, spanCount, childCount))// 如果是最后一行,则不需要绘制底部

{

outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);

} else if (isLastColum(parent, itemPosition, spanCount, childCount))// 如果是最后一列,则不需要绘制右边

{

outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());

} else

{

outRect.set(0, 0, mDivider.getIntrinsicWidth(),

mDivider.getIntrinsicHeight());

}

}

}

主要在getItemOffsets方法中,去判断如果是最后一行,则不需要绘制底部;如果是最后一列,则不需要绘制右边,整个判断也考虑到了StaggeredGridLayoutManager的横向和纵向,所以稍稍有些复杂。最重要还是去理解,如何绘制什么的不重要。一般如果仅仅是希望有空隙,还是去设置item的margin方便。

最后的效果是:

ok,看到这,你可能还觉得RecyclerView不够强大?

但是如果我们有这么个需求,纵屏的时候显示为ListView,横屏的时候显示两列的GridView,我们RecyclerView可以轻松搞定,而如果使用ListView去实现还是需要点功夫的~~~

当然了,这只是皮毛,下面让你心服口服。

  • StaggeredGridLayoutManager

瀑布流式的布局,其实他可以实现GridLayoutManager一样的功能,仅仅按照下列代码:

// mRecyclerView.setLayoutManager(new GridLayoutManager(this,4));

mRecyclerView.setLayoutManager(new StaggeredGridLayoutManager(4, StaggeredGridLayoutManager.VERTICAL));

这两种写法显示的效果是一致的,但是注意StaggeredGridLayoutManager构造的第二个参数传一个orientation,如果传入的是StaggeredGridLayoutManager.VERTICAL代表有多少列;那么传入的如果是StaggeredGridLayoutManager.HORIZONTAL就代表有多少行,比如本例如果改为:

mRecyclerView.setLayoutManager(new StaggeredGridLayoutManager(4,

StaggeredGridLayoutManager.HORIZONTAL));

那么效果为:

可以看到,固定为4行,变成了左右滑动。有一点需要注意,如果是横向的时候,item的宽度需要注意去设置,毕竟横向的宽度没有约束了,应为控件可以横向滚动了。

如果你需要一样横向滚动的GridView,那么恭喜你。

ok,接下来准备看大招,如果让你去实现个瀑布流,最起码不是那么随意就可以实现的吧?但是,如果使用RecyclerView,分分钟的事。

那么如何实现?其实你什么都不用做,只要使用StaggeredGridLayoutManager我们就已经实现了,只是上面的item布局我们使用了固定的高度,下面我们仅仅在适配器的onBindViewHolder方法中为我们的item设置个随机的高度(代码就不贴了,最后会给出源码下载地址),看看效果图:

是不是棒棒哒,通过RecyclerView去实现ListView、GridView、瀑布流的效果基本上没有什么区别,而且可以仅仅通过设置不同的LayoutManager即可实现。

还有更nice的地方,就在于item增加、删除的动画也是可配置的。接下来看一下ItemAnimator。

ItemAnimator

ItemAnimator也是一个抽象类,好在系统为我们提供了一种默认的实现类,期待系统多

添加些默认的实现。

借助默认的实现,当Item添加和移除的时候,添加动画效果很简单:

// 设置item动画

mRecyclerView.setItemAnimator(new DefaultItemAnimator());

系统为我们提供了一个默认的实现,我们为我们的瀑布流添加以上一行代码,效果为:

如果是GridLayoutManager呢?动画效果为:

注意,这里更新数据集不是用adapter.notifyDataSetChanged()而是

notifyItemInserted(position)notifyItemRemoved(position)

否则没有动画效果。

上述为adapter中添加了两个方法:

public void addData(int position) {

mDatas.add(position, “Insert One”);

notifyItemInserted(position);

}

public void removeData(int position) {

mDatas.remove(position);

notifyItemRemoved(position);

}

Activity中点击MenuItem触发:

@Override

public boolean onCreateOptionsMenu(Menu menu)

{

getMenuInflater().inflate(R.menu.main, menu);

return super.onCreateOptionsMenu(menu);

}

@Override

public boolean onOptionsItemSelected(MenuItem item)

{

switch (item.getItemId())

{

case R.id.id_action_add:

mAdapter.addData(1);

break;

case R.id.id_action_delete:

mAdapter.removeData(1);

break;

}

return true;

}

好了,到这我对这个控件已经不是一般的喜欢了~~~

当然了只提供了一种动画,那么我们肯定可以去自定义各种nice的动画效果。

高兴的是,github上已经有很多类似的项目了,这里我们直接引用下:RecyclerViewItemAnimators,大家自己下载查看。

提供了SlideInOutLeftItemAnimator,SlideInOutRightItemAnimator,

SlideInOutTopItemAnimator,SlideInOutBottomItemAnimator等动画效果。

Click and LongClick

不过一个挺郁闷的地方就是,系统没有提供ClickListener和LongClickListener。

不过我们也可以自己去添加,只是会多了些代码而已。

实现的方式比较多,你可以通过mRecyclerView.addOnItemTouchListener去监听然后去判断手势,

当然你也可以通过adapter中自己去提供回调,这里我们选择后者,前者的方式,大家有兴趣自己去实现。

那么代码也比较简单:

class HomeAdapter extends RecyclerView.Adapter<HomeAdapter.MyViewHolder>

{

//…

public interface OnItemClickLitener

{

void onItemClick(View view, int position);

void onItemLongClick(View view , int position);

}

private OnItemClickLitener mOnItemClickLitener;

public void setOnItemClickLitener(OnItemClickLitener mOnItemClickLitener)

{

this.mOnItemClickLitener = mOnItemClickLitener;

}

@Override

public void onBindViewHolder(final MyViewHolder holder, final int position)

{

holder.tv.setText(mDatas.get(position));

// 如果设置了回调,则设置点击事件

if (mOnItemClickLitener != null)

{

holder.itemView.setOnClickListener(new OnClickListener()

{

@Override

public void onClick(View v)

{

int pos = holder.getLayoutPosition();

mOnItemClickLitener.onItemClick(holder.itemView, pos);

}

});

holder.itemView.setOnLongClickListener(new OnLongClickListener()

{

@Override

public boolean onLongClick(View v)

{

int pos = holder.getLayoutPosition();

mOnItemClickLitener.onItemLongClick(holder.itemView, pos);

return false;

}

});

}

}

//…

}

adapter中自己定义了个接口,然后在onBindViewHolder中去为holder.itemView去设置相应

的监听最后回调我们设置的监听。

最后别忘了给item添加一个drawable:

<?xml version="1.0" encoding="utf-8"?>

Activity中去设置监听:

mAdapter.setOnItemClickLitener(new OnItemClickLitener()

{

@Override

public void onItemClick(View view, int position)

{

Toast.makeText(HomeActivity.this, position + " click",

Toast.LENGTH_SHORT).show();

}

@Override

public void onItemLongClick(View view, int position)

{

Toast.makeText(HomeActivity.this, position + " long click",

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

写在最后

由于本文罗列的知识点是根据我自身总结出来的,并且由于本人水平有限,无法全部提及,欢迎大神们能补充~

将来我会对上面的知识点一个一个深入学习,也希望有童鞋跟我一起学习,一起进阶。

提升架构认知不是一蹴而就的,它离不开刻意学习和思考。

**这里,笔者分享一份从架构哲学的层面来剖析的视频及资料分享给大家,**梳理了多年的架构经验,筹备近1个月最新录制的,相信这份视频能给你带来不一样的启发、收获。

最近还在整理并复习一些Android基础知识点,有问题希望大家够指出,谢谢。

希望读到这的您能转发分享和关注一下我,以后还会更新技术干货,谢谢您的支持!

转发+点赞+关注,第一时间获取最新知识点

Android架构师之路很漫长,一起共勉吧!

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!**

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-a0ZcRrr7-1713723493491)]

[外链图片转存中…(img-vJLHiajp-1713723493492)]

[外链图片转存中…(img-HhapCoWU-1713723493494)]

[外链图片转存中…(img-ZWl6c8cF-1713723493495)]

[外链图片转存中…(img-NRbON42v-1713723493496)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

[外链图片转存中…(img-qUIl6f7K-1713723493497)]

写在最后

由于本文罗列的知识点是根据我自身总结出来的,并且由于本人水平有限,无法全部提及,欢迎大神们能补充~

将来我会对上面的知识点一个一个深入学习,也希望有童鞋跟我一起学习,一起进阶。

提升架构认知不是一蹴而就的,它离不开刻意学习和思考。

**这里,笔者分享一份从架构哲学的层面来剖析的视频及资料分享给大家,**梳理了多年的架构经验,筹备近1个月最新录制的,相信这份视频能给你带来不一样的启发、收获。

[外链图片转存中…(img-ufRiHhbK-1713723493498)]

[外链图片转存中…(img-4aWeziTJ-1713723493499)]

最近还在整理并复习一些Android基础知识点,有问题希望大家够指出,谢谢。

希望读到这的您能转发分享和关注一下我,以后还会更新技术干货,谢谢您的支持!

转发+点赞+关注,第一时间获取最新知识点

Android架构师之路很漫长,一起共勉吧!

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 24
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值