Android 进阶系列———由浅及深的理解和使用RecyclerView+吸顶效果实战

【1】什么是RecyclerView?我们为什么要是用RecyclerView?

  • 我是这样理解的:recyclerView是一个容器,含有多个itemView,其中的ViewHolder进行了对多个View滑动查看时的优化效果,关于这点,可以去看看recyclerView的回收机制。也就是说recyclerView是我们Android开发中在需要滑动组件时候的首选。
  • 效果展示

简单使用:

在这里插入图片描述

自定义ItemDecoration(高级使用):

在这里插入图片描述


【2】RecyclerView如何使用(简单基础版)

①在需要用到滑动组件的地方添加RecyclerView控件(容纳多个View的可滑动组件)

要实现上图,我们的recyclerView要在activity_main.xml中:

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">


    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/mRecycle"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

②在layout文件中添加item.xml文件(这是每一个item的布局文件)

我们创建item.xml文件这是每个View的样式,我们可以创建多个不一样的,但是在这里我们只创建一个一样的就可以了。在这里我们使用RelativeLayout布局,里面放一个TextView就行了,记得给一个id,方便我们等会设置需要显示的文本。

<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@color/colorAccent">

    <TextView
        android:id="@+id/rv_text"
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:textSize="20sp"
        android:gravity="center"/>

</RelativeLayout>

③创建适配器

创建一个类继承RecyclerView.Adapter,这时还需要传入一个RecyclerView.ViewHolder的子类作为泛型放到Adapter,我们通常直接在这个类中,写一个静态内部类继承自RecyclerView.ViewHolder就达到目的了,同时,我们还必须重写3个方法onCreateViewHolderonBindViewHoldergetItemCount。通常我们从外部传入数据进来(我们需要显示的内容等信息)也就是这里的starList,因此我们重写构造方法,将数据传入。
getItemCount: 返回值是这个recyclerView中有多少个子View,通常是传入数据的个数。
onCreateViewHolder: 返回值是创建的ViewHolder对象,通常我们通过LayoutInnflateritem.xml文件转换为View,再将这个View作为参数传给ViewHolder的构造方法。
同时我们在ViewHolder(自己写的继承自RecyclerView.ViewHolder)中通过findViewById给对应的控件赋值,方便我们等下在onBindViewHolder中通过对应控件的id绑定数据。
onBindViewHolder: 绑定数据,显示数据。

public class StarAdapter extends RecyclerView.Adapter<StarAdapter.ViewHolder> {
    private List<Star> starList;
    private Context context;

    public StarAdapter(List<Star> starList, Context context) {
        this.starList = starList;
        this.context = context;
    }

    @NonNull
    @Override
    public StarAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(context).inflate(R.layout.item_text,null);
        return new ViewHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull StarAdapter.ViewHolder holder, int position) {
        holder.textView.setText(starList.get(position).getName());
    }

    @Override
    public int getItemCount() {
        return starList == null ? 0 : starList.size();
    }

    public static class ViewHolder extends RecyclerView.ViewHolder {
        private TextView textView;
        public ViewHolder(@NonNull View itemView) {
            super(itemView);
            textView = itemView.findViewById(R.id.rv_text);
        }
    }

}

④设置recyclerView的各种属性如itemDecoration、LayoutManager、适配器等。

public class MainActivity extends AppCompatActivity {
    private List<Star> starList;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        RecyclerView recyclerView = findViewById(R.id.mRecycle);
        init();
        recyclerView.setLayoutManager(new LinearLayoutManager(this));
        //没有需求就可以不用装饰
        recyclerView.addItemDecoration(new DividerItemDecoration(this, LinearLayout.VERTICAL));
        recyclerView.setAdapter(new StarAdapter(starList,this));
    }

    private void init() {
        starList = new ArrayList<>();
        for (int i = 0; i < 4; i++) {
            for (int j = 0; j < 20; j++) {
                if(i % 2 == 0){
                    starList.add(new Star("清华大学"+j,"C9联盟"+i));
                }else{
                    starList.add(new Star("重庆大学"+j,"985高校"+i));
                }
            }
        }
    }
}

【1】通常我们不需要设置itemDecoration,要使用的话系统为我们提供了DividerItemDecoration类(基于LinearLayout)。
【2】设置布局管理器recyclerView.setLayoutManager(new LinearLayoutManager(this));通常我们设置LinearLayoutManager就行了,有几个构造方法,最简单的就是直接给一个Context的
【3】最后设置adapter就行了recyclerView.setAdapter(new StarAdapter(starList,this));

⑤简单使用效果:

在这里插入图片描述
这里就没做动图了,和开篇一样就是增加了一个分割线recyclerView.addItemDecoration(new DividerItemDecoration(this, LinearLayout.VERTICAL));添加了一个系统的装饰器。

【3】RecyclerView的高阶使用(自定义Decoration)

理解三个方法:

onDrawonDrawOvergetItemOffsets

我们来实现一个如下图功能的RecyclerView(吸顶效果):
在这里插入图片描述

其他的东西我们不需要改变,唯一需要改变的就是自定义ItemDecoration,并设置RecyclerView的装饰为自定义的装饰就可以了。话不多少,直接开始自定义ItemDecoration。

先贴上代码:

public class StarDecoration extends RecyclerView.ItemDecoration {
    private int groupHeadHeight;
    private Context context;
    private Paint mPaint;
    private Paint headPaint;
    public StarDecoration(Context context) {
        this.context = context;
        groupHeadHeight = dp2px(context,100);
        mPaint = new Paint();
        headPaint = new Paint();
        mPaint.setDither(true);
        mPaint.setAntiAlias(true);
        mPaint.setColor(Color.WHITE);
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setTextSize(50);
        headPaint.setColor(Color.RED);
    }


    @Override
    public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.onDraw(c, parent, state);
        if (parent.getAdapter() instanceof StarAdapter){
            StarAdapter starAdapter = (StarAdapter) parent.getAdapter();
            //当前屏幕的item个数
            int count = parent.getChildCount();
            int left = parent.getPaddingLeft();
            int right = parent.getWidth() - parent.getPaddingRight();
            for (int i = 0; i < count; i++) {
                View view = parent.getChildAt(i);
                int position = parent.getChildLayoutPosition(view);
                //是头部
                if(starAdapter.isGroupHead(position) && view.getTop() - parent.getPaddingTop() - groupHeadHeight>=0){
                    c.drawRect(left, view.getTop()-groupHeadHeight,right,view.getTop(),headPaint);
                    c.drawText(starAdapter.getGroupName(position),left+10,view.getTop()-groupHeadHeight/2+10,
                            mPaint);
                }
            }
        }
    }

    @Override
    public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.onDrawOver(c, parent, state);
        if(parent.getAdapter() instanceof StarAdapter){
            StarAdapter starAdapter = (StarAdapter) parent.getAdapter();
            int left = parent.getPaddingLeft();
            int top = parent.getPaddingTop();
            int right = parent.getWidth() - parent.getPaddingRight();
            int position = ((LinearLayoutManager) parent.getLayoutManager()).findFirstVisibleItemPosition();
            //获取对应VIew
            View view = parent.findViewHolderForAdapterPosition(position).itemView;
            boolean isGroupHead = starAdapter.isGroupHead(position+1);
            if (isGroupHead){
                int bottom = Math.min(groupHeadHeight,view.getBottom() - top);
                c.drawRect(left,top,right,top+bottom,headPaint);
                c.drawText(starAdapter.getGroupName(position),left+10,top + bottom - groupHeadHeight/2+10,mPaint);

            }else{
                c.drawRect(left,top,right,top+groupHeadHeight,headPaint);
                c.drawText(starAdapter.getGroupName(position),left+10,top+groupHeadHeight/2+10,mPaint);
            }
        }
    }

    @Override
    public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        if(parent.getAdapter() instanceof StarAdapter){
            StarAdapter starAdapter = (StarAdapter) parent.getAdapter();
            int position = parent.getChildLayoutPosition(view);
            boolean isGroupHead = starAdapter.isGroupHead(position);
            if(isGroupHead){
                outRect.set(0,groupHeadHeight,0,0);
            }else {
                outRect.set(0,2,0,0);
            }
        }

    }
    private int dp2px(Context context,float dpValue){
        float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dpValue *scale * 0.5f);
    }
}

根据我的步骤来,保证你能懂:

  • 我们重写时,只需要注意其中的三个方法就行了:
    getItemOffsets
    onDraw
    onDrawOver
①我们先开看看getItemOffsets,其实理解了里面的outRect也就理解了他。
  • 什么是outRect?
    在这里插入图片描述
    set的4个参数:左、上、右、下。不是说设置这个会让itemView变小,不是这样的,设置这个其实是一个预留的空间,比如设置左上右下全是50,那么每个itemView都如图所示:
    在这里插入图片描述

现在应该懂了什么是outRect了吧。

②我们再来看看onDraw和onDrawOver有什么不同

在这里插入图片描述
在这里插入图片描述
我们可以这样理解:onDraw -> itemView -> onDrawOver。先绘制onDraw,绘制完成后再绘制每一个itemView,itemView绘制完后再绘制onDrawOver。onDrawOver在最上层,会覆盖掉前面所绘制的内容,这也是吸顶的精髓所在。

绘制思路:
  • 【1】首先我们判断,这一个VIew是不是一个组的头部,如果是,我们在这个View的上面留一个大的空间,我们在这个大的空间中绘制组头。

  • 【2】要想实现吸顶的效果那么顶部就一直有一个组头,直到下一个组头把他推上去,要想一直存在,那就应该是在最上层绘制,因为这样可以覆盖掉itemView和onDraw的绘制内容。一直显示在bar下面(在onDrawOver中绘制组头)

  • 【3】难点来了,我们怎样实现一个组头把onDrawOver中的组头推上去?,推是一个动画,那么什么东西是改变的呢?bottom!对就是吸附在bar下面的组头的bottom是慢慢变小的,也就看起来像推了上去。
    大概思路就是如此,剩下的就是明确的算法了。算法不太好讲,所以直接看上面的自定义ItemDecoration的代码。

整个项目工程的代码:
在这里插入图片描述
mainActivity:

public class MainActivity extends AppCompatActivity {
    private List<Star> starList;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        RecyclerView recyclerView = findViewById(R.id.mRecycle);
        init();
        recyclerView.setLayoutManager(new LinearLayoutManager(this));
        recyclerView.addItemDecoration(new StarDecoration(this));
        recyclerView.setAdapter(new StarAdapter(starList,this));
    }

    private void init() {
        starList = new ArrayList<>();
        for (int i = 0; i < 4; i++) {
            for (int j = 0; j < 20; j++) {
                if(i % 2 == 0){
                    starList.add(new Star("清华大学"+j,"C9联盟"+i));
                }else{
                    starList.add(new Star("重庆大学"+j,"985高校"+i));
                }
            }
        }
    }
}

Star:

public class Star {
    private String name;
    private String groupName;

    public Star(String name, String groupName) {
        this.name = name;
        this.groupName = groupName;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getGroupName() {
        return groupName;
    }

    public void setGroupName(String groupName) {
        this.groupName = groupName;
    }
}

StarAdapter:

public class StarAdapter extends RecyclerView.Adapter<StarAdapter.ViewHolder> {
    private List<Star> starList;
    private Context context;

    public StarAdapter(List<Star> starList, Context context) {
        this.starList = starList;
        this.context = context;
    }

    @NonNull
    @Override
    public StarAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(context).inflate(R.layout.item_text,null);
        return new ViewHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull StarAdapter.ViewHolder holder, int position) {
        holder.textView.setText(starList.get(position).getName());
    }

    @Override
    public int getItemCount() {
        return starList == null ? 0 : starList.size();
    }

    public static class ViewHolder extends RecyclerView.ViewHolder {
        private TextView textView;
        public ViewHolder(@NonNull View itemView) {
            super(itemView);
            textView = itemView.findViewById(R.id.rv_text);
        }
    }

    /**
     * 是否是组的头部
     * @param position
     * @return
     */
    public boolean isGroupHead(int position){
        if(position == 0){
            return true;
        }else{
            String currentGroupName = getGroupName(position);
            String preGroupName = getGroupName(position - 1);
            if (currentGroupName.equals(preGroupName)){
                return false;
            }else
                return true;
        }
    }

    /**
     *
     * @param position
     * @return
     */
    public String getGroupName(int position){
        return starList.get(position).getGroupName();
    }
}

StarDecoration:

public class StarDecoration extends RecyclerView.ItemDecoration {
    private int groupHeadHeight;
    private Context context;
    private Paint mPaint;
    private Paint headPaint;
    public StarDecoration(Context context) {
        this.context = context;
        groupHeadHeight = dp2px(context,100);
        mPaint = new Paint();
        headPaint = new Paint();
        mPaint.setDither(true);
        mPaint.setAntiAlias(true);
        mPaint.setColor(Color.WHITE);
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setTextSize(50);
        headPaint.setColor(Color.RED);
    }


    @Override
    public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.onDraw(c, parent, state);
        if (parent.getAdapter() instanceof StarAdapter){
            StarAdapter starAdapter = (StarAdapter) parent.getAdapter();
            //当前屏幕的item个数
            int count = parent.getChildCount();
            int left = parent.getPaddingLeft();
            int right = parent.getWidth() - parent.getPaddingRight();
            for (int i = 0; i < count; i++) {
                View view = parent.getChildAt(i);
                int position = parent.getChildLayoutPosition(view);
                //是头部
                if(starAdapter.isGroupHead(position) && view.getTop() - parent.getPaddingTop() - groupHeadHeight>=0){
                    c.drawRect(left, view.getTop()-groupHeadHeight,right,view.getTop(),headPaint);
                    c.drawText(starAdapter.getGroupName(position),left+10,view.getTop()-groupHeadHeight/2+10,
                            mPaint);
                }
            }
        }
    }

    @Override
    public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.onDrawOver(c, parent, state);
        if(parent.getAdapter() instanceof StarAdapter){
            StarAdapter starAdapter = (StarAdapter) parent.getAdapter();
            int left = parent.getPaddingLeft();
            int top = parent.getPaddingTop();
            int right = parent.getWidth() - parent.getPaddingRight();
            int position = ((LinearLayoutManager) parent.getLayoutManager()).findFirstVisibleItemPosition();
            //获取对应VIew
            View view = parent.findViewHolderForAdapterPosition(position).itemView;
            boolean isGroupHead = starAdapter.isGroupHead(position+1);

            if (isGroupHead){
                //如果屏幕上显示的第一个View的下一个View是组头的话
                //即将推上去
                int bottom = Math.min(groupHeadHeight,view.getBottom() - top);
                c.drawRect(left,top,right,top+bottom,headPaint);
                c.drawText(starAdapter.getGroupName(position),left+10,top + bottom - groupHeadHeight/2+10,mPaint);
            }else{
                //如果屏幕上显示的第一个View的下一个View不是组头的话,就画一个head一直显示在bar下面
                c.drawRect(left,top,right,top+groupHeadHeight,headPaint);
                c.drawText(starAdapter.getGroupName(position),left+10,top+groupHeadHeight/2+10,mPaint);
            }
        }
    }

    @Override
    public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        if(parent.getAdapter() instanceof StarAdapter){
            StarAdapter starAdapter = (StarAdapter) parent.getAdapter();
            int position = parent.getChildLayoutPosition(view);
            boolean isGroupHead = starAdapter.isGroupHead(position);
            if(isGroupHead){
                outRect.set(0,groupHeadHeight,0,0);
            }else {
                outRect.set(0,2,0,0);
            }
        }

    }
    private int dp2px(Context context,float dpValue){
        float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dpValue *scale * 0.5f);
    }
}

activity_main.xml布局文件:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">


    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/mRecycle"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

item_text.xml布局文件:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@color/colorAccent">

    <TextView
        android:id="@+id/rv_text"
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:textSize="20sp"
        android:gravity="center"/>

</RelativeLayout>

【4】效果:

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值