如何应对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…

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

腾讯云服务器价格计算器可以一键计算出云服务器的精准报价&#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 居中方式

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

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;尽管…

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

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

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

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

DaisyDisk for mac 中文激活版 可视化磁盘清理工具

DaisyDisk 是一款专为 Mac 设计的磁盘空间分析工具。它以直观、图形化的方式展示硬盘使用情况&#xff0c;帮助用户迅速找到占用空间大的文件和文件夹。通过扫描磁盘&#xff0c;DaisyDisk 生成彩色的扇形图表&#xff0c;每个扇区代表一个文件或文件夹&#xff0c;大小直观反映…

Java多线程并发篇----第十五篇

系列文章目录 文章目录 系列文章目录前言一、偏向锁二、分段锁三、锁优化四、线程基本方法前言 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站,这篇文章男女通用,看懂了就去分享给你的码吧。 一、偏向锁 Hotspot 的…

肯尼斯·里科《C和指针》第6章 指针(4)实例

肯尼斯里科《C和指针》第6章 指针&#xff08;1&#xff09;-CSDN博客 肯尼斯里科《C和指针》第6章 指针&#xff08;2&#xff09;-CSDN博客 肯尼斯里科《C和指针》第6章 指针&#xff08;3&#xff09;-CSDN博客 6.12 实例 /* ** 计算一个字符串的长度。 */ #include <…

leetcode 349 两个数组的集合

题目 给定两个数组 nums1 和 nums2 &#xff0c;返回 它们的交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序 。 示例 1&#xff1a; 输入&#xff1a;nums1 [1,2,2,1], nums2 [2,2] 输出&#xff1a;[2] 示例 2&#xff1a; 输入&#xff1a…

【playwright】新一代自动化测试神器playwright+python系列课程00——playwright安装

playwright安装 本文主要分享由微软开发的实现Web UI自动化测试工具Playwright库&#xff0c;相比于之前学习过selenium库&#xff0c;playwright对于编写自动化代码绝对是更轻松了&#xff0c;因为它支持脚本录制&#xff0c;如果只求简单点可以不用写一行代码就能够实现自动…

网络安全|GitHub 已成为恶意软件传播的严重污染源

Recorded Future 凸显了全球合法平台威胁的上升。 根据 Recorded Future最近 的一份报告&#xff0c;开发者平台GitHub最近已成为黑客用来托管和传播恶意软件的流行工具。 该平台为攻击者提供了将其行为伪装成合法网络流量的能力&#xff0c;这使得跟踪和确定攻击者的身份变得…

ubuntu20.04 deepstream 6.3安装

1.基础环境gstreamer sudo apt install \ libssl-dev \ libgstreamer1.0-0 \ gstreamer1.0-tools \ gstreamer1.0-plugins-good \ gstreamer1.0-plugins-bad \ gstreamer1.0-plugins-ugly \ gstreamer1.0-libav \ libgstreamer-plugins-base1.0-dev \ libgstrtspserver-1.0-0 …

C练习——模拟投掷6000次骰子

题目&#xff1a; 模拟骰子投6000次&#xff0c;并计算每一面出现的概率 解析&#xff1a; 6000次&#xff0c;首先想到用数组记录六个面各出现次数 其次&#xff0c;使用随机数&#xff08;1~6的数&#xff09;模拟骰子 然后统计1~6每个数出现的几次&#xff0c;最后除以6…