RecyclerView ,自从推出到现在已经经历很长一段时间了,公司的项目代码大部分还使用的ListView 来实现页面的刷新,总结下用法,后面将ListView 全部替换成RecyclerView
一、RecyclerView基本使用
1、RecyclerView都有哪些常用功能
- 上拉加载、下拉刷新
- 给列表添加头和尾
- 动画
- 点击事件(item点击及item的子控件点击)
- 分割线
- 拖拽排序和侧划删除
- 滑动及顶部标题透明度变化
- 复杂布局
- 卡片式Gallery效果
- 增加和删除条目
- 上下滑动、左右滑动
- GridView 上下滑动、左右滑动
- 瀑布流显示效果
- ...
2、简单实现
- 添加依赖库,Eclipse 导入jar包
implementation 'com.android.support:appcompat-v7:26.1.0'
关于compile、api 和implementation的区别,如果还没有升级3.0的小伙伴们赶紧升级试下~
compile 在3.0版本已经过时,替代他的是api 和implementation ,
api 和compile 功能一样,没有区别
implementation 引入的库,只可在本module下使用,本module不会通过自身的接口向外部暴露其依赖module的内容。
- 添加布局文件或者在动态代码设置
布局文件:
- 编写Adapter,具体代码就不贴了
- 设置布局管理器,添加分割线
rvRace.setLayoutManager(new LinearLayoutManager(mActivity));
rvRace.addItemDecoration(new SimpleDividerDecoration(mActivity));
- 设置Adapter,绑定数据
mAdapter = new RaceDetailsAdapter(mActivity);
rvRace.setAdapter(mAdapter);
mAdapter.setDatas(mLists);
3、复杂用法
RecyclerBaseAdapter封装,此类主要封装的功能有:点击事件(条目点击事件及其子控件点击事件)、动画、头部和尾部的添加逻辑、复杂布局的处理、数据初使化及刷新、拖拽排序等功能
具体代码就不贴了,有点长,可参考此连接RecyclerBaseViewHolder封装,采用之前listview baseAdapter的封装逻辑,将子控件存放于SparseArray数组中,每次通过id获取,如果有直接取,没有通过findViewById获取,并存放于SparseArray中;
该类中同时将常用控件设值封装此类中,子类中直接调用即可
public class RecyclerBaseViewHolder extends RecyclerView.ViewHolder {
private final SparseArray mViews;
private Context mContext;
public View convertView;
public BaseViewHolder(View itemView, Context context) {
super(itemView);
mViews = new SparseArray<>();
mContext = context;
convertView = itemView;
}
public View getConvertView() {
return convertView;
}
public T getView(int id) {
View view = mViews.get(id);
if (view == null) {
view = itemView.findViewById(id);
mViews.put(id, view);
}
return (T) view;
}
/**
* Will set the text of a TextView.
*
* @param viewId The view id.
* @param value The text to put in the text view.
* @return The BaseViewHolder for chaining.
*/
public BaseViewHolder setText(@IdRes int viewId, CharSequence value) {
TextView view = getView(viewId);
view.setText(value);
return this;
}
/**
* Will set the text of a TextView.
*
* @param viewId The view id.
* @param strId The text to put in the text view.
* @return The BaseViewHolder for chaining.
*/
public BaseViewHolder setText(@IdRes int viewId, @StringRes int strId) {
TextView view = getView(viewId);
view.setText(strId);
return this;
}
/**
* Will set text color of a TextView.
*
* @param viewId The view id.
* @param textColor The text color (not a resource id).
* @return The BaseViewHolder for chaining.
*/
public BaseViewHolder setTextColor(@IdRes int viewId, @ColorInt int textColor) {
TextView view = getView(viewId);
view.setTextColor(textColor);
return this;
}
/**
* Will set the image of an ImageView from a resource id.
*
* @param viewId The view id.
* @param imageResId The image resource id.
* @return The BaseViewHolder for chaining.
*/
public BaseViewHolder setImageResource(@IdRes int viewId, @DrawableRes int imageResId) {
ImageView view = getView(viewId);
view.setImageResource(imageResId);
return this;
}
/**
* Will set the image of an ImageView from a drawable.
*
* @param viewId The view id.
* @param drawable The image drawable.
* @return The BaseViewHolder for chaining.
*/
public BaseViewHolder setImageDrawable(@IdRes int viewId, Drawable drawable) {
ImageView view = getView(viewId);
view.setImageDrawable(drawable);
return this;
}
/**
* Add an action to set the image of an image view. Can be called multiple times.
*/
public BaseViewHolder setImageBitmap(@IdRes int viewId, Bitmap bitmap) {
ImageView view = getView(viewId);
view.setImageBitmap(bitmap);
return this;
}
}
- 添加头布局、尾布局和下拉刷新、上拉加载更多遇到一块如何处理?
RecyclerView是一种插拔式的设计思路,就是我们想要什么插入什么就可以了, 从而很好的实现了解耦
这里采取的是装饰者设计模式的思路 ,将常用的添加头部和尾部的功能放到BaseAdapter 中,可通过addHeaderView、addFooterView进行添加;将下拉刷新和上拉加载更多的功能放到装饰类里实现,装饰类也是继承BaseAdapter,就是在原有的BaseAdapter的基础上,设计一个类,增强BaseAdapter的功能,而不用总去修改BaseAdapter,使其支持下拉刷新和上拉加载更多的功能,所以我们在setAdapter时会通过装饰类装饰后,再调用RecyclerView 的setAdapter进行数据加载。代码如下:
@Override
public void setAdapter(Adapter adapter) {
if (adapter instanceof BaseAdapter) {
mWrapAdapter = new WrapAdapter(adapter);
super.setAdapter(mWrapAdapter);
} else {
super.setAdapter(adapter);
}
adapter.registerAdapterDataObserver(mDataObserver);
mDataObserver.onChanged();
}
- 加入动画
RecyclerView提供了默认的动画类DefaultItemAnimator,通过setItemAnimator就可以设置一些简单动画,如果想自己实现动画,可以拷贝源码类DefaultItemAnimator 修改源码实现,也可以在方法onBindViewHolder中对每个条目设置动画,主要这三种实现方式。
- 分割线
RecyclerView是通过addItemDecoration来实现的,目前最新版本的RecyclerView 提供了DividerItemDecoration 默认实现,如果无法满足我们的需求,我们可以自己实现RecyclerView.ItemDecoration 丰富,使用方法请参考 源码中的DividerItemDecoration类,也希望google 多提供几种实现类。
- 卡片式Gallery 效果
有时候我们需要实现Gallery效果,如何做呢?
问题:
a、每次滑动都让其中一张图片显示在正中间
b、最左边的和最右边的卡片距离边距要和其它卡片保持一致
c、中间图片放大,两边图片缩小(由大变小、由小变大)
思路:
a、google 在24系统版本提供了一个工具类 SnapHelper,它有两个实现类LinearSnapHelper 和PagerSnapHelper,LinearSnapHelper可以一次滑动多个卡片,而PagerSnapHelper只能滑动一个卡片,就有点类似于ViewPager一次只能滑动一个,这里我们用LinearSnapHelper类
b、其中一种方式是onBindViewHolder 判断是不是第0个或者 size-1个item 位置,如果是,修改leftMargin或rightMargin,然后用setLayoutParams 给item 设置位置;另外一种方式是通过添加分割线的方式来实现,需要实现抽象类 RecyclerView.ItemDecoration,并复写 getItemOffsets 方法来实现;第一种方式,需要修改Adapter ,耦合性比较高,建议采取第二种方式。
c、关于动画,获取到中间和两边的item ,分别对item添加缩放动画即可
源码如下:
public class RecyclerScaleUtils {
public boolean mStateIdle = false;
private RecyclerView mRecycler;
private int mItemCount;
private static int mDefaultMargin = 40;
private int mMargin = 0;
private int mItemwidth;
private int mCurrentPosition = 0;
private int mDistances = 0;
public void attachToRecyclerView(RecyclerView recyclerView, int margin){
if(recyclerView == null){
return;
}
mRecycler = recyclerView;
if(margin <= 0){
mMargin = mDefaultMargin;
}else {
mMargin = margin;
}
initView();
final RecyclerLinearSnapHelper helper = new RecyclerLinearSnapHelper();
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if(newState == RecyclerView.SCROLL_STATE_IDLE){
if(mDistances == 0 || mDistances == (mItemCount*mItemwidth)){
mStateIdle = true;
}else{
mStateIdle = false;
}
}
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if(recyclerView.getLayoutManager().getLayoutDirection() == LinearLayoutManager.HORIZONTAL){
mDistances += dx;
getCurrentPosition();
// setItemScale();
}
}
});
helper.attachToRecyclerView(recyclerView);
}
protected void initView( ){
mRecycler.post(new Runnable() {
@Override
public void run() {
mItemCount = mRecycler.getAdapter().getItemCount();
mItemwidth = mRecycler.getWidth() - 2 * mMargin;
mRecycler.smoothScrollToPosition(mCurrentPosition);
// setItemScale();
}
});
}
public void setItemScale(){
View leftView = null;
View rightView = null;
if(mCurrentPosition > 0){
leftView = mRecycler.getLayoutManager().findViewByPosition(mCurrentPosition - 1);
}
View currentView = mRecycler.getLayoutManager().findViewByPosition(mCurrentPosition);
if(mCurrentPosition < (mItemCount - 1)){
rightView = mRecycler.getLayoutManager().findViewByPosition(mCurrentPosition +1);
}
//滑动百分比,左右的都是放大,中间缩小
float percent = Math.abs((mDistances - mCurrentPosition * mItemwidth*1.0f)/mItemwidth);
Log.d("tests","======percent=========>"+percent);
if(leftView != null){
//这里是缩小原来大小的0.8-1.0 左右0.8,中间1.0 0.8+(percent*0.2)
leftView.setScaleY(0.8f+(percent*0.2f));
}
if(currentView != null){
currentView.setScaleY(1-0.2f*percent);
}
if(rightView != null){
rightView.setScaleY(0.8f+(percent*0.2f));
}
}
protected void getCurrentPosition(){
if(mItemwidth <= 0) return;
boolean change = false;
if (Math.abs(mDistances - mCurrentPosition * mItemwidth) >= mItemwidth) {
change = true;
}
if (change) { //以为这里是从0开始的
mCurrentPosition = mDistances / mItemwidth;
}
}
public static void onCreateViewHolder(ViewGroup parent, View itemView,int margin) {
RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) itemView.getLayoutParams();
if(margin <= 0){
margin = mDefaultMargin;
}
lp.width = parent.getWidth() - 2*margin;
itemView.setLayoutParams(lp);
}
//这里是处理最左边和最右边的宽度
public static void onBindViewHolder(View itemView, final int position, int itemCount,int margin) {
int leftMarin = 0;
int rightMarin = 0;
int topMarin = 0;
int bottomMarin = 0;
if(position == 0){
leftMarin = margin;
rightMarin = 0;
}else if(position == (itemCount-1)){
leftMarin = 0;
rightMarin = margin;
}
ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) itemView.getLayoutParams();
if (lp.leftMargin != leftMarin || lp.topMargin != topMarin || lp.rightMargin != rightMarin || lp.bottomMargin != bottomMarin) {
lp.setMargins(leftMarin, topMarin, rightMarin, bottomMarin);
itemView.setLayoutParams(lp);
}
}
// setViewMargin(itemView, leftMarin, 0, rightMarin, 0);
private static void setViewMargin(View view, int left, int top, int right, int bottom) {
ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
if (lp.leftMargin != left || lp.topMargin != top || lp.rightMargin != right || lp.bottomMargin != bottom) {
lp.setMargins(left, top, right, bottom);
view.setLayoutParams(lp);
}
}
public void setItemPosition(int position){
mCurrentPosition = position ;
}
private class RecyclerLinearSnapHelper extends LinearSnapHelper {
/**
// * int[] snapDistance = calculateDistanceToFinalSnap(layoutManager, snapView);
// if (snapDistance[0] != 0 || snapDistance[1] != 0) {
// mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]);
// }
// 这个方法返回的数组值,是让recyclerview移动并居中显示的,如果是第一个或者最后一个位置,
// 因无法居中而调用recyclerview的OnScrollListener监听
// */
@Override
public int[] calculateScrollDistance(int velocityX, int velocityY) {
if(mStateIdle){
return new int[2];
}else{
int[] ints = super.calculateScrollDistance(velocityX, velocityY);
for (int i:ints){
Log.e("tests","==i=="+i);
}
return super.calculateScrollDistance(velocityX, velocityY);
}
}
}
}
通过如下代码就可让gallery生效,如果想让gellery有缩放动画,将上述源码中 setItemScale()方法取消注释即可
RecyclerScaleUtils utils = new RecyclerScaleUtils();
utils.attachToRecyclerView(rvRaceView, DensityUtil.dip2px(mActivity, 44.0f));
在onCreateViewHolder时加入如入代码,计算每个卡片的大小
view = mInflater.inflate(R.layout.item_recycler_workbook, parent, false);
//这里设置view的宽度
RecyclerScaleUtils.onCreateViewHolder(parent, view, DensityUtil.dip2px(parent.getContext(), 44.0));
onBindViewHolder中针对第一个和最后一个位置设置左边距和右边距
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
//设置第一个与最后item对称显示
RecyclerScaleUtils.onBindViewHolder(holder.itemView, position, getItemCount(), DensityUtil.dip2px(holder.itemView.getContext(), 30f));
}
- 点击事件
分item 点击事件和item 子控件的点击事件,两种点击事件需要分别实现,代码如下:
/**
* Listener
*/
private OnItemClickListener mOnItemClickListener;
private OnItemLongClickListener mOnItemLongClickListener;
private OnRecyclerViewItemChildClickListener mChildClickListener;
private OnRecyclerViewItemChildLongClickListener mChildLongClickListener;
/**
* Listener api
*/
public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
mOnItemClickListener = onItemClickListener;
}
public void setOnItemLongClickListener(OnItemLongClickListener onItemLongClickListener) {
mOnItemLongClickListener = onItemLongClickListener;
}
/**
* Register a callback to be invoked when childView in this AdapterView has
* been clicked and held {@link OnRecyclerViewItemChildClickListener}
*
* @param childClickListener The callback that will run
*/
public void setOnItemChildClickListener(
OnRecyclerViewItemChildClickListener childClickListener) {
this.mChildClickListener = childClickListener;
}
public class OnItemChildClickListener implements View.OnClickListener {
public RecyclerView.ViewHolder mViewHolder;
@Override
public void onClick(View v) {
if (mChildClickListener != null)
mChildClickListener.onItemChildClick(SuperBaseAdapter.this, v,
mViewHolder.getLayoutPosition() - getHeaderViewCount());
}
}
/**
* Register a callback to be invoked when childView in this AdapterView has
* been longClicked and held
* {@link OnRecyclerViewItemChildLongClickListener}
*
* @param childLongClickListener The callback that will run
*/
public void setOnItemChildLongClickListener(
OnRecyclerViewItemChildLongClickListener childLongClickListener) {
this.mChildLongClickListener = childLongClickListener;
}
public class OnItemChildLongClickListener implements View.OnLongClickListener {
public RecyclerView.ViewHolder mViewHolder;
@Override
public boolean onLongClick(View v) {
if (mChildLongClickListener != null) {
return mChildLongClickListener.onItemChildLongClick(SuperBaseAdapter.this, v,
mViewHolder.getLayoutPosition() - getHeaderViewCount());
}
return false;
}
}
item点击事件会在BaseAdapter 的onCreateViewHolder中加入,只要页面设置了点击事件就会触发,而item的子控件监听需要在BaseAdapter的实现类中对子控件设置监听才会生效
- 多布局
这个和单布局维一的区别就是得通过getItemViewType判断类型,然后根据类型去创建不同的布局,然后去绑定数据,封装后子类的实现代码如下:
public class MultiItemAdapter extends BaseAdapter {
public MultiItemAdapter(Context context, List data) {
super(context, data);
}
@Override
protected void convert(BaseViewHolder holder, MultipleItemBean item, int position) {
if(item.getType() == 0){
holder.setText(R.id.name_tv,item.getName());
} else if(item.getType() == 1){
holder.setText(R.id.name_tv,item.getName())
.setText(R.id.info_tv,item.getInfo());
}
}
@Override
protected int getItemViewLayoutId(int position, MultipleItemBean item) {
if(item.getType() == 0){
return R.layout.adapter_multi_item1_layout;
} else if(item.getType() == 1){
return R.layout.adapter_multi_item2_layout;
} else{
return R.layout.adapter_multi_item3_layout;
}
}
}
复杂布局
这个得根据产品的具体功能去分析,没有RecyclerView 前,要实现多布局、多层嵌套,挺麻烦的,我们经常需要重写一些方法来解决冲突问题,如ListView 嵌套ListView 时,相信大家都不会陌生;有了RecyclerView后,这方面的问题就不用考虑了。滑动及顶部标题透明度变化
通过RecyclerView 提供的addOnSrollListener 监听滑动,根据滑动的距离做透明度的变化和背景颜色的调整,代码如下:
raceRecycler.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
isScrollIdle = (newState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE);
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (isScrollIdle && adViewTopSpace < 0)
return;
adViewTopSpace = DensityUtil.px2dip(GradualChangeActivity.this,
mAdapter.getHeaderLayout().getTop());
adViewHeight = DensityUtil.px2dip(GradualChangeActivity.this,
mAdapter.getHeaderLayout().getHeight());
if (-adViewTopSpace <= 50) {
tv_title.setVisibility(View.GONE);
} else {
tv_title.setVisibility(View.VISIBLE);
}
handleTitleBarColorEvaluate();
}
});
}
// 处理标题栏颜色渐变
private void handleTitleBarColorEvaluate() {
float fraction;
rlBar.setAlpha(1f);
if (adViewTopSpace > 5) {
fraction = 1f - adViewTopSpace * 1f / 60;
if (fraction < 0f)
fraction = 0f;
rlBar.setAlpha(fraction);
return;
}
float space = Math.abs(adViewTopSpace) * 1f;
fraction = space / (adViewHeight - titleViewHeight);
if (fraction < 0f)
fraction = 0f;
if (fraction > 1f)
fraction = 1f;
rlBar.setAlpha(1f);
if (fraction >= 1f) {
viewTitleBg.setAlpha(0f);
viewActionMoreBg.setAlpha(0f);
rlBar.setBackgroundColor(this.getResources().getColor(R.color.orange));
} else {
viewTitleBg.setAlpha(1f - fraction);
viewActionMoreBg.setAlpha(1f - fraction);
rlBar.setBackgroundColor(evaluate(this, fraction,
this.getResources().getColor(R.color.transparent), this.getResources().getColor(R.color.orange)));
}
}
/**
* 成新的颜色值
* @param fraction 颜色取值的级别 (0.0f ~ 1.0f)
* @param startValue 开始显示的颜色
* @param endValue 结束显示的颜色
* @return 返回生成新的颜色值
*/
public static int evaluate(float fraction, int startValue, int endValue) {
int startA = (startValue >> 24) & 0xff;
int startR = (startValue >> 16) & 0xff;
int startG = (startValue >> 8) & 0xff;
int startB = startValue & 0xff;
int endA = (endValue >> 24) & 0xff;
int endR = (endValue >> 16) & 0xff;
int endG = (endValue >> 8) & 0xff;
int endB = endValue & 0xff;
return ((startA + (int) (fraction * (endA - startA))) << 24) |
((startR + (int) (fraction * (endR - startR))) << 16) |
((startG + (int) (fraction * (endG - startG))) << 8) |
((startB + (int) (fraction * (endB - startB))));
}
- 拖拽排序和侧划删除
目前项目中没有用到,后面再整理
二、源码分析:
后续补充