如何应对Android面试官->我用RecyclerView实现了吸顶效果

前言

RecyclerView 计划用两个章节来讲解,今天主要是以 itemDecoration 和 实现吸顶效果为主;

ItemDecoration

ItemDecoration 允许应用给具体的 View 添加具体的图画或者 Layout 的偏移,对于绘制 View 之间的分割线,视觉分组边界等等是非常有用;

当我们调用 RecyclerView 的 addItemDecoration 添加 decoration 的时候,RecyclerView 就会调用该类的 onDraw 方法去绘制分割线,也就是说分割线是绘制出来的;

RecyclerView.ItemDecoration 类是抽象类;

public abstract static class ItemDecoration {    public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull State state) {        onDraw(c, parent);    }    @Deprecated    public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent) {    }    public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent,            @NonNull State state) {        onDrawOver(c, parent);    }    @Deprecated    public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent) {    }    @Deprecated    public void getItemOffsets(@NonNull Rect outRect, int itemPosition,            @NonNull RecyclerView parent) {        outRect.set(0, 0, 0, 0);    }    public void getItemOffsets(@NonNull Rect outRect, @NonNull View view,            @NonNull RecyclerView parent, @NonNull State state) {        getItemOffsets(outRect, ((LayoutParams) view.getLayoutParams()).getViewLayoutPosition(),                parent);    }
}

Android 官方只提供了一个实现类 DividerItemDecoration;我们先来简单看下它是如何实现的;

根据注释我们可以知道,DividerItemDecoration 需要结合 LinearLayoutManager 一起使用,以及它是如何创建并和 RecycerlView 如何绑定的;

我们进入构造方法看下:

可以看到,分割线其实就是一个 Drawable,我们也可以通过 setDrawable 方法自定义一个 Drawable 来定义我们的分割线;

public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {    if (parent.getLayoutManager() == null || mDivider == null) {        return;    }//  绘制的时候,根据方向,绘制不同的分割线    if (mOrientation == VERTICAL) {        drawVertical(c, parent);    } else {        drawHorizontal(c, parent);    }
}

我们进入这两个方法,分别看下:

private void drawVertical(Canvas canvas, RecyclerView parent) {    canvas.save();    final int left;    final int right;    //noinspection AndroidLintNewApi - NewApi lint fails to handle overrides.    if (parent.getClipToPadding()) {        left = parent.getPaddingLeft();        right = parent.getWidth() - parent.getPaddingRight();        canvas.clipRect(left, parent.getPaddingTop(), right,                parent.getHeight() - parent.getPaddingBottom());    } else {        left = 0;        right = parent.getWidth();    }    final int childCount = parent.getChildCount();    for (int i = 0; i < childCount; i++) {        final View child = parent.getChildAt(i);        parent.getDecoratedBoundsWithMargins(child, mBounds);        final int bottom = mBounds.bottom + Math.round(child.getTranslationY());        final int top = bottom - mDivider.getIntrinsicHeight();        mDivider.setBounds(left, top, right, bottom);        mDivider.draw(canvas);    }    canvas.restore();
}

可以看到,如果 parent.getClipToPadding() 为 true 的话,RecyclerView 的 padding 区域是可以绘制分割线的,否则就可以绘制;用来获取 left 和 right;

然后获取 bottom 和 top,最后调用 Drawable 的 darw 方法,进行绘制;

drawHorizontal 方法,大家可以自行看下;我们来看下 getItemOffsets 方法

public void getItemOffsets(Rect outRect, View view, RecyclerView parent,        RecyclerView.State state) {    if (mDivider == null) {        outRect.set(0, 0, 0, 0);        return;    }    if (mOrientation == VERTICAL) {        outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());    } else {        outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);    }
}

这个 outRect 就是在 item 的四周留出指定的间隙;可以看下面这张图来加深下理解:

ItemDecoration 提供了 onDraw 和 onDrawOver 方法,这两个方法有什么区别呢?

本质上来说,onDraw 方法的绘制区域,可能会被 item 遮挡,onDrawOver 的绘制区域不会被 item 遮挡;

onDraw 的绘制区域:

onDrawOver 的绘制区域,它会在 itemview 绘制之后才进行绘制:

接下来,我们来一步一步撸码实现吸顶效果,我们先来搭一个简单的架子:

public class NBAStarDecoration extends RecyclerView.ItemDecoration {        NBAStarDecoration(Context context) {            }    @Override    public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {                super.onDraw(c, parent, state);    }    @Override    public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {        super.onDrawOver(c, parent, state);    }    @Override    public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {        super.getItemOffsets(outRect, view, parent, state);            }
}

我们如果想实现吸顶效果,那么就需要判断是不是头部,如果是头部,预留出吸顶的 View 空间,那么如何判断是不是头部呢?我们可以根据每组数据的组名来判断;

public class NBAStar {    private String name;    private String groupName;    public NBAStar(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;    }
}

然后我们在 adapter 中根据 position 来判断当前位置是不是 groupName;

public class NBAStarAdapter extends RecyclerView.Adapter<NBAStarAdapter.NBAStarHolder> {    private Context context;    private List<NBAStar> starList;    public NBAStarAdapter(Context context, List<NBAStar> starList) {        this.context = context;        this.starList = starList;    }// 根据 position 来判断 当前 groupName 与前一个是不是相等;   public boolean isGroupHeader(int position) {        if (position == 0) {            return true;        } else {                String currentGroupName = getGroupName(position);            String preGroupName = getGroupName(position - 1);            if (TextUtils.equals(currentGroupName, preGroupName)) {                return false;            } else {                return true;            }        }        }    public String getGroupName(int position) {        return starList.get(position).getGroupName();    }// ...// 省略部分代码
}

NBAStarDecoration 中根据这个判断来预留对应的空间

public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {    super.getItemOffsets(outRect, view, parent, state);    RecyclerView.Adapter adapter = parent.getAdapter();    if (adapter instanceof NBAStarAdapter) {        NBAStarAdapter nbaStarAdapter = (NBAStarAdapter) adapter;        int position = parent.getChildLayoutPosition(view);        boolean groupHeader = nbaStarAdapter.isGroupHeader(position);        if (groupHeader) {            outRect.set(0, dp2px(100),0, 0);        } else {            outRect.set(0, 1, 0 , 0);        }    }
}

我们运行看下效果:

我们给头部预留出来了指定的位置;

接下里我们来绘制头部区域,绘制背景色和头部区域中的文字;

绘制背景色:

public void onDraw(@NonNull Canvas canvas, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {    super.onDraw(canvas, parent, state);    RecyclerView.Adapter adapter = parent.getAdapter();    if (adapter instanceof NBAStarAdapter) {        NBAStarAdapter nbaStarAdapter = (NBAStarAdapter) adapter;        int childCount = parent.getChildCount();        int left = parent.getPaddingLeft();        int right = parent.getWidth() - parent.getPaddingRight();        for (int i = 0; i < childCount; i++) {            View view = parent.getChildAt(i);            int position = parent.getChildLayoutPosition(view);            boolean groupHeader = nbaStarAdapter.isGroupHeader(position);            if (groupHeader) {                canvas.drawRect(new Rect(left, view.getTop() - dp2px(50), right, view.getTop()), paint);            }        }    }
}

我们运行看下效果:

可以看到,我们把头部颜色绘制了出来,接下来我们绘制头部的文字,也就是组名

String groupName = nbaStarAdapter.getGroupName(position);
textPaint.getTextBounds(groupName, 0 , groupName.length(), textRect);
// 文字中心绘制,上一层有讲解
Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();
float centerHeight = view.getTop() - dp2px(50) / 2 + ((fontMetrics.descent - fontMetrics.ascent)/2) - fontMetrics.descent;
canvas.drawText(groupName, left + 20, centerHeight, textPaint);

文字中心绘制的原理可以查看上一章,如何应对Android面试官->文字中心绘制和颜色渐变,实战头条炫酷ViewPager指示器

可以看到,文字绘制了出来,但是还没达到我们想要的吸顶效果;

接下里我们来实现吸顶效果:

我们想要实现吸顶效果,那么我们需要在 onDrawOver 中实现,因为它的绘制是在 ItemView 之后,并且是固定的位置;我们需要拿到可见区域的第一个 item 的位置,并判断它是不是头部,以及当我们滑动的时候,当第二个头部的 top 滑动到第一个头部的 bottom 的位置的时候,第一个头部的 bottom 要缩小(移除屏幕),那么 bottom 什么时机开始变小呢?就是在我第一个头部的最后一个 itemView 的 getBottom 小于第一个头部的 bottom 的时候开始变小,具体实现如下:

@Override
public void onDrawOver(@NonNull Canvas canvas, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {    super.onDrawOver(canvas, parent, state);    RecyclerView.Adapter adapter = parent.getAdapter();    if (adapter instanceof NBAStarAdapter) {        NBAStarAdapter nbaStarAdapter = (NBAStarAdapter) adapter;        // 获取屏幕可见的第一个 View 的位置        int position = ((LinearLayoutManager) parent.getLayoutManager()).findFirstVisibleItemPosition();        // 获取这个位置的 itemView        View view = parent.findViewHolderForLayoutPosition(position).itemView;        int left = parent.getPaddingLeft();        int right = parent.getWidth() - parent.getPaddingRight();        int top = parent.getPaddingTop();        boolean groupHeader = nbaStarAdapter.isGroupHeader(position + 1);        if (groupHeader) {            // 判断是不是 itemView 的高度比 头部的高度小            int bottom = Math.min(dp2px(50), view.getBottom());            // 区域绘制            canvas.drawRect(left, top, right, bottom + top, paint);            // 文字绘制            String groupName = nbaStarAdapter.getGroupName(position);            textPaint.getTextBounds(groupName, 0 , groupName.length(), textRect);// textRect.height()/2 可以替换成根据 fontMetrics 计算的高度,这里简约下            canvas.drawText(groupName, left + 20, top - dp2px(50) / 2 + textRect.height() / 2, textPaint);        } else {            // 区域绘制            canvas.drawRect(left, top, right, top + dp2px(50), paint);            // 文字绘制            String groupName = nbaStarAdapter.getGroupName(position);            textPaint.getTextBounds(groupName, 0 , groupName.length(), textRect);// textRect.height()/2 可以替换成根据 fontMetrics 计算的高度,这里简约下            canvas.drawText(groupName, left + 20, top + dp2px(50) / 2 + textRect.height() / 2 , textPaint);        }    }
}

我们运行看下效果:

我们实现了吸顶效果,但是在滑动到顶部的时候,文字的滑出是有问题的,我们接着来看下;

说明我们的高度计算的不太对,应该是 top + bottom - 头部高度/2 + textRect.height()/2;

我们替换看下效果:

canvas.drawText(groupName, left + 20, top + bottom - dp2px(50) / 2 + textRect.height() / 2, textPaint);

可以看到,没有问题了;

通常我们在使用 RecyclerView 的时候,可能会直接在 RecyclerView 上设置各种 margin 或者 padding,如果接下来,如果我们给 RecyclerView 设置一个 padding 的话,可能会有什么效果?

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="match_parent">    <androidx.recyclerview.widget.RecyclerView        android:id="@+id/rv_list"        android:layout_width="match_parent"        android:layout_height="match_parent"        android:background="@color/colorPrimary"        android:paddingTop="150dp"/>
</RelativeLayout>

我们运行看下效果:

我们发现,顶部的 padding 区域不符合我们的预期,那么我们应该如何处理呢?

可以看到,这个 padding 内的内容,应该是 onDraw 绘制的头部区域,它应该消失掉,但是并没有,为什么不是 onDrawOver,因为 onDrawOver 绘制的区域是固定不动的;

也就是说我们需要在 onDraw 中处理,那么怎么处理呢?我们需要获取这段 padding 的距离并和 view 的 getTop 以及 头部的高度 之差做对比

也就是:

view.getTop() - dip2px(50) - parent.getPaddingTop() >= 0

所以,onDraw 中的判断规则改为:

if (groupHeader && view.getTop() - dp2px(50) - parent.getPaddingTop() >= 0) {}

我们运行看下效果:

达到了我们期望的效果,但是还有一个问题,就是 现役球星0 文字没有被推上去,我们需要在 onDrawOver 中处理一下,文字没有推上去,是 【现役球星0】的 bottom 没有改变导致的,也就是我们需要改变 bottom 的高度才行;

int bottom = Math.min(dp2px(50), view.getBottom() - parent.getPaddingTop());

我们需要减去这个 paddingTop 的值,我们运行看下效果:

可以看到,文字被推了上去,但是,文字推的优点过多了,进入了 padding 区域,我们需要 drawText 的时候限制绘制的区域才行,也就是我们需要减去这个  paddingTop 的距离 修改如下:

if (top + bottom - dp2px(50) / 2 - parent.getPaddingTop() >= 0) {    canvas.drawText(groupName, left + 20, top + bottom - dp2px(50) / 2 + textRect.height() / 2, textPaint);
}

运行看下效果:

文字可以被推上去了,并且也没有绘制到 paddingTop 区域,至此,我们的吸顶效果实现到这吧~~

简历润色

深度理解 ItemDecoration 实现原理,可自定义 ItemDecoration 的实现;

下一章预告

RecyclerView 的缓存复用原理和 LayoutManager 的实现原理解析

欢迎三连

来都来了,点个关注点个赞吧,你的支持是我最大的动力~~~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/624063.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

用bat脚本执行py文件以及批量执行py文件(全网超详细)

1.前言 对于python代码&#xff0c;每次执行一个文件就要运行一个命令&#xff0c;太过麻烦 在Windows电脑上&#xff0c;想一次性执行多个python文件的代码&#xff0c;就需要用到bat脚本 2.python代码 先写几个python代码的文件 如下图 3.py文件为中文&#xff0c;用bat执…

如何使用CFImagehost结合内网穿透搭建私人图床并无公网ip远程访问

[TOC] 推荐一个人工智能学习网站点击跳转 1.前言 图片服务器也称作图床&#xff0c;可以说是互联网存储中最重要的应用之一&#xff0c;不仅网站需要图床提供的外链调取图片&#xff0c;个人或企业也用图床存储各种图片&#xff0c;方便随时访问查看。不过由于图床很不挣钱&a…

CentOS中如何让新建用户拥有root权限

adduser newuser 新建用户newuser passwd newuser 设置密码 New UNIX password: Retype new UNIX password: 成功创建用户密码 passwd: all authentication tokens updated successfully. 2、赋予root权限 方法一&#xff1a; 修改 /etc/sudoers 文件&#xff0c;找…

WordPiece和SentencePiece区别

BERT&#xff08;Bidirectional Encoder Representations from Transformers&#xff09;模型的分词器通常使用子词级别的分词方法&#xff0c;其中最常用的分词器包括 WordPiece 和 SentencePiece。这些分词器用于将文本分成子词&#xff08;subwords&#xff09;或标记&#…

腾讯云价格计算器怎么用?太简单了一键报价

腾讯云服务器价格计算器可以一键计算出云服务器的精准报价&#xff0c;包括CVM实例规格价格、CPU内存费用、公网带宽收费、存储系统盘和数据盘详细费用&#xff0c;腾讯云百科txybk.com分享腾讯云价格计算器链接入口、使用方法说明&#xff1a; 腾讯云服务器价格计算器 打开腾…

❤ React报错问题分析

❤ React报错问题分析 ❤️ You passed a second argument to root.render(…) but it only accepts one argument. You passed a second argument to root.render(…) but it only accepts one argument. react-dom.development.js:86 Warning: You passed a second argumen…

css 居中方式

居中分为水平居中和垂直居中

医院患者满意度调查指标设计

医院患者满意度调查指标的设计是确保调查能够准确反映患者体验和医院服务质量的关键步骤。以下是一些常见的医院患者满意度调查指标&#xff0c;可以根据特定需求和目标进行定制&#xff1a; 整体满意度&#xff1a;通过一个综合评分或问卷问题来评估患者对整体医院体验的满意…

js对象和数组的区别

在JavaScript中&#xff0c;对象&#xff08;Object&#xff09;和数组&#xff08;Array&#xff09;是两种不同的数据结构&#xff0c;它们有着不同的用途和特性。 对象&#xff08;Object&#xff09;&#xff1a; 定义形式&#xff1a;对象是由键值对组成的&#xff0c;每个…

C++ 数组分页,经常有用到分页,索性做一个简单封装 已解决

在项目设计中&#xff0c; 有鼠标滑动需求&#xff0c;但是只能说能力有限&#xff0c;索性使用 php版本的数组分页&#xff0c;解决问题。 经常有用到分页&#xff0c;索性做一个简单封装、 测试用例 QTime curtime QTime::currentTime();nHour curtime.hour();nMin curtim…

各种排序算法学习笔记

Docshttps://r0dhfl3ujy9.feishu.cn/docx/XFlEdnqv9oCEoVx7ok8cpc4knnf?fromfrom_copylink如果你认为有错误&#xff0c;欢迎指出&#xff01;

Three.js 镜面反射Reflector 为MeshStandardMaterial增加Reflector能力

效果效果官方案例 区别&#xff1a;官方的案例更像一个镜子 没有纹理等属性 也没有透明度修改 根据源码进行修改为 MeshStandardMaterial实现反射 使用案例 createReflector() {const plane this.helper.create.plane(2, 2);this.helper.add(plane.mesh);plane.mesh.rotat…

juniper EX系列交换机POE配置

PoE&#xff08;Power over Ethernet&#xff0c;以太网供电&#xff0c;又称远程供电&#xff09;是指设备通过以太网接口&#xff0c;利用双绞线对外接PD&#xff08;Powered Device&#xff0c;受电设备&#xff09;设备&#xff08;如IP 电话、无线AP、网络摄像头等&#x…

什么是视频短信,能用在什么地方?

视频短信是指通过106短信将带有视频的短信内容发送到对应的手机中&#xff0c;也称之为点对点的信息传递方式&#xff0c;视频短信可以支持2兆以内的多媒体信息发送&#xff0c;是直接、直观的宣传、沟通方式。 一、怎么就偏偏要找视频短信 根据目前的行情状况&#xff0c;尽管…

为什么模方崩过后重新新建工程打开会提示“OSG读取Node失败”?

为什么模方崩过后重新新建工程打开会提示“OSG读取Node失败”&#xff1f; 答&#xff1a;瓦块数据中可能有空文件或者不符合osgb组织结构的文件&#xff0c;可以检查移除。 模方是一款针对实景三维模型的冗余碎片、水面残缺、道路不平、标牌破损、纹理拉伸模糊等共性问题研发的…

3d模型素材亮度和对比度如何调整呢?

1、修改材质参数&#xff1a;打开3ds Max后&#xff0c;选择要调整亮度和对比度的3D模型素材。然后&#xff0c;进入材质编辑器&#xff0c;选择相应的材质球。在材质编辑器中&#xff0c;你可以调整材质的漫反射、反射和高光等参数&#xff0c;这些参数将影响模型的亮度和对比…

C语言实现学生成绩管理系统(单链表)

本次我就用学到的相关链表知识总结回顾一下学生成绩管理系统的实现。 首先还是先创建一个项目&#xff0c;分别创建头文件和源文件&#xff0c;头文件用来声明函数&#xff0c;源文件用来定义函数以及实现学生成绩管理系统。 创建完成后如上图。 先创建一个结构体用来存放学生…

第9章-第3节-Java中的自定义注解

注解&#xff1a;有Override,我们把它称为重写的注解。那么注解到底什么呢&#xff1f;我们能不能自定义注解。 1、概念&#xff1a; 注解是一种能被添加到java代码中的元数据&#xff0c;类、方法、变量、参数和包都可以用注解来修饰。注解对于它所修饰的代码并没有直接的影…

低端单片机彩色屏幕的内存占用疑惑

前言 问题&#xff1a; 假设320*240的rgb565屏幕&#xff0c;320*240*2153600&#xff0c;内存已经150K了&#xff0c;而很多低端单片机接口速度虽然勉强能用&#xff0c;但内存只有20K/8K&#xff0c;整屏的显存是绝对放不下的&#xff0c;只刷一部分都占很多内存&#xff0…

重建大师程序卡死过一次以后,再打开就显示“工程被本地机器打开”,怎么解决?

答&#xff1a;可以检查下本机是不是开了多个重建大师软件&#xff0c;将多开的软件关闭。或者可以修改下电脑名称&#xff0c;重启电脑&#xff0c;然后再打开工程。