当前位置: 首页 > news >正文

Android实现RecyclerView粘性头部效果,模拟微信账单列表的月份标题平移

效果链接

https://live.csdn.net/v/494980

1、在res/values/colors.xml中添加:

<?xml version="1.0" encoding="utf-8"?>
<resources><color name="green">#07C160</color><color name="red">#FF3B30</color>
</resources>

2、月份标题布局 (item_header.xml)

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="48dp"android:background="#F5F5F5"android:orientation="vertical"android:paddingLeft="16dp"><TextViewandroid:id="@+id/tv_month"android:layout_width="match_parent"android:layout_height="match_parent"android:gravity="center_vertical"android:textColor="#000000"android:textSize="16sp" />
</LinearLayout>

3、主布局 (activity_main.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="match_parent"><androidx.recyclerview.widget.RecyclerViewandroid:id="@+id/recycler_view"android:layout_width="match_parent"android:layout_height="match_parent"android:clipToPadding="false" /></RelativeLayout>

4、账单项布局 (item_bill.xml)

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="wrap_content"android:background="#FFFFFF"android:orientation="vertical"android:padding="16dp"><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="horizontal"><TextViewandroid:id="@+id/tv_date"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:textColor="#888888"android:textSize="14sp" /><TextViewandroid:id="@+id/tv_amount"android:layout_width="wrap_content"android:layout_height="wrap_content"android:textSize="16sp" /></LinearLayout><TextViewandroid:id="@+id/tv_description"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_marginTop="8dp"android:textColor="#000000"android:textSize="16sp" />
</LinearLayout>

5、自定义StickyHeaderDecoration 类


/*** @author: 魏* @date: 2025/9/29* 自定义StickyHeaderDecoration类*/
public class StickyHeaderDecoration extends RecyclerView.ItemDecoration {private StickyHeaderListener listener;private View currentStickyView;private int currentStickyPosition = RecyclerView.NO_POSITION;private int lastTranslateY = 0;public StickyHeaderDecoration(StickyHeaderListener listener) {this.listener = listener;}@Overridepublic void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent,@NonNull RecyclerView.State state) {super.onDrawOver(c, parent, state);// 查找当前需要置顶的月份标题位置int topChildPosition = findTopVisibleItemPosition(parent);if (topChildPosition == RecyclerView.NO_POSITION) {return;}// 获取当前月份标题位置int headerPosition = listener.getHeaderPositionForItem(topChildPosition);if (headerPosition == RecyclerView.NO_POSITION) {return;}// 获取或创建月份标题视图if (currentStickyPosition != headerPosition || currentStickyView == null) {currentStickyView = listener.getHeaderView(parent, headerPosition);measureHeaderView(currentStickyView, parent);currentStickyPosition = headerPosition;}// 计算标题绘制位置int nextHeaderPosition = findNextHeaderPosition(parent, topChildPosition);int translateY = 0;if (nextHeaderPosition != RecyclerView.NO_POSITION) {View nextHeader = parent.getChildAt(nextHeaderPosition - topChildPosition);if (nextHeader != null) {int bottom = currentStickyView.getBottom();int nextHeaderTop = nextHeader.getTop();// 当两个标题相遇时的动画效果if (nextHeaderTop < bottom) {translateY = nextHeaderTop - bottom;}}}// 平滑过渡动画if (Math.abs(translateY - lastTranslateY) > 1) {ValueAnimator animator = ValueAnimator.ofInt(lastTranslateY, translateY);animator.setDuration(200);animator.setInterpolator(new DecelerateInterpolator());animator.addUpdateListener(animation -> {parent.invalidate();});animator.start();lastTranslateY = translateY;}// 绘制月份标题c.save();c.translate(0, translateY);currentStickyView.draw(c);c.restore();}private int findTopVisibleItemPosition(RecyclerView parent) {LinearLayoutManager layoutManager = (LinearLayoutManager) parent.getLayoutManager();return layoutManager.findFirstVisibleItemPosition();}private int findNextHeaderPosition(RecyclerView parent, int position) {int itemCount = parent.getAdapter().getItemCount();for (int i = position + 1; i < itemCount; i++) {if (listener.isHeader(i)) {return i;}}return RecyclerView.NO_POSITION;}private void measureHeaderView(View headerView, ViewGroup parent) {int widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.EXACTLY);int heightSpec = View.MeasureSpec.makeMeasureSpec(parent.getHeight(), View.MeasureSpec.UNSPECIFIED);int childWidth = ViewGroup.getChildMeasureSpec(widthSpec,parent.getPaddingLeft() + parent.getPaddingRight(), headerView.getLayoutParams().width);int childHeight = ViewGroup.getChildMeasureSpec(heightSpec,parent.getPaddingTop() + parent.getPaddingBottom(), headerView.getLayoutParams().height);headerView.measure(childWidth, childHeight);headerView.layout(0, 0, headerView.getMeasuredWidth(), headerView.getMeasuredHeight());}
}

6、定义粘性头部监听接口

/*** @author: 魏* @date: 2025/9/29* 定义粘性头部监听接口*/
public interface StickyHeaderListener {// 判断是否是头部项boolean isHeader(int position);// 获取指定位置对应的头部位置int getHeaderPositionForItem(int itemPosition);// 获取头部视图View getHeaderView(RecyclerView parent, int headerPosition);
}

7、账单数据模型

/*** @author: 魏* @date: 2025/9/29* 数据模型与适配器* 账单数据模型*/
public class BillItem {public static final int TYPE_HEADER = 0;public static final int TYPE_ITEM = 1;private int type;private String month; // 用于标题private String date;  // 用于账单项private String description;private double amount;private boolean isIncome; // 收入或支出public BillItem(int type, String month, String date, String description, double amount, boolean isIncome) {this.type = type;this.month = month;this.date = date;this.description = description;this.amount = amount;this.isIncome = isIncome;}// Getter方法public int getType() { return type; }public String getMonth() { return month; }public String getDate() { return date; }public String getDescription() { return description; }public double getAmount() { return amount; }public boolean isIncome() { return isIncome; }
}

8、账单适配器实现

/*** @author: 魏* @date: 2025/9/29* 账单适配器实现*/
public class BillAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>implements StickyHeaderListener {private List<BillItem> billItems;private Context context;public BillAdapter(Context context, List<BillItem> billItems) {this.context = context;this.billItems = billItems;}@Overridepublic int getItemViewType(int position) {return billItems.get(position).getType();}@NonNull@Overridepublic RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {if (viewType == BillItem.TYPE_HEADER) {View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_header, parent, false);return new HeaderViewHolder(view);} else {View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_bill, parent, false);return new BillViewHolder(view);}}@Overridepublic void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {BillItem item = billItems.get(position);if (holder instanceof HeaderViewHolder) {((HeaderViewHolder) holder).tvMonth.setText(item.getMonth());} else if (holder instanceof BillViewHolder) {BillViewHolder billHolder = (BillViewHolder) holder;billHolder.tvDate.setText(item.getDate());billHolder.tvDescription.setText(item.getDescription());// 设置金额颜色(收入绿色,支出红色)if (item.isIncome()) {billHolder.tvAmount.setTextColor(ContextCompat.getColor(context, R.color.green));billHolder.tvAmount.setText(String.format("+¥%.2f", item.getAmount()));} else {billHolder.tvAmount.setTextColor(ContextCompat.getColor(context, R.color.red));billHolder.tvAmount.setText(String.format("-¥%.2f", item.getAmount()));}}}@Overridepublic int getItemCount() {return billItems.size();}// StickyHeaderListener接口实现@Overridepublic boolean isHeader(int position) {return billItems.get(position).getType() == BillItem.TYPE_HEADER;}@Overridepublic int getHeaderPositionForItem(int itemPosition) {for (int i = itemPosition; i >= 0; i--) {if (isHeader(i)) {return i;}}return RecyclerView.NO_POSITION;}@Overridepublic View getHeaderView(RecyclerView parent, int headerPosition) {HeaderViewHolder holder = new HeaderViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_header, parent, false));holder.tvMonth.setText(billItems.get(headerPosition).getMonth());return holder.itemView;}public static class HeaderViewHolder extends RecyclerView.ViewHolder {TextView tvMonth;public HeaderViewHolder(@NonNull View itemView) {super(itemView);tvMonth = itemView.findViewById(R.id.tv_month);}}public static class BillViewHolder extends RecyclerView.ViewHolder {TextView tvDate;TextView tvDescription;TextView tvAmount;public BillViewHolder(@NonNull View itemView) {super(itemView);tvDate = itemView.findViewById(R.id.tv_date);tvDescription = itemView.findViewById(R.id.tv_description);tvAmount = itemView.findViewById(R.id.tv_amount);}}
}

9、Activity集成

/*** @param* @return* @author 魏* @time 2025/9/29 14:40*/
public class MainActivity extends AppCompatActivity {private RecyclerView recyclerView;private BillAdapter adapter;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);recyclerView = findViewById(R.id.recycler_view);recyclerView.setLayoutManager(new LinearLayoutManager(this));// 生成测试数据List<BillItem> billItems = generateTestData();adapter = new BillAdapter(this, billItems);recyclerView.setAdapter(adapter);// 添加粘性头部装饰器recyclerView.addItemDecoration(new StickyHeaderDecoration(adapter));}private List<BillItem> generateTestData() {List<BillItem> items = new ArrayList<>();// 添加测试数据items.add(new BillItem(BillItem.TYPE_HEADER, "2025年9月", null, null, 0, false));items.add(new BillItem(BillItem.TYPE_ITEM, null, "9月29日", "微信支付-超市购物", 128.50, false));items.add(new BillItem(BillItem.TYPE_ITEM, null, "9月28日", "微信支付-工资", 20000.00, true));items.add(new BillItem(BillItem.TYPE_ITEM, null, "9月27日", "微信支付-餐饮", 56.80, false));items.add(new BillItem(BillItem.TYPE_ITEM, null, "9月26日", "微信支付-超市购物", 128.50, false));items.add(new BillItem(BillItem.TYPE_ITEM, null, "9月25日", "微信支付-工资", 20000.00, true));items.add(new BillItem(BillItem.TYPE_ITEM, null, "9月24日", "微信支付-餐饮", 56.80, false));items.add(new BillItem(BillItem.TYPE_ITEM, null, "9月23日", "微信支付-超市购物", 128.50, false));items.add(new BillItem(BillItem.TYPE_ITEM, null, "9月22日", "微信支付-工资", 20000.00, true));items.add(new BillItem(BillItem.TYPE_ITEM, null, "9月21日", "微信支付-餐饮", 56.80, false));items.add(new BillItem(BillItem.TYPE_HEADER, "2025年8月", null, null, 0, false));items.add(new BillItem(BillItem.TYPE_ITEM, null, "8月31日", "水电费", 320.50, false));items.add(new BillItem(BillItem.TYPE_ITEM, null, "8月30日", "房租", 2500.00, false));items.add(new BillItem(BillItem.TYPE_ITEM, null, "8月29日", "水电费", 320.50, false));items.add(new BillItem(BillItem.TYPE_ITEM, null, "8月28日", "房租", 2500.00, false));items.add(new BillItem(BillItem.TYPE_ITEM, null, "8月27日", "水电费", 320.50, false));items.add(new BillItem(BillItem.TYPE_ITEM, null, "8月26日", "房租", 2500.00, false));items.add(new BillItem(BillItem.TYPE_ITEM, null, "8月25日", "水电费", 320.50, false));items.add(new BillItem(BillItem.TYPE_ITEM, null, "8月24日", "房租", 2500.00, false));items.add(new BillItem(BillItem.TYPE_ITEM, null, "8月23日", "水电费", 320.50, false));items.add(new BillItem(BillItem.TYPE_ITEM, null, "8月22日", "房租", 2500.00, false));items.add(new BillItem(BillItem.TYPE_HEADER, "2025年7月", null, null, 0, false));items.add(new BillItem(BillItem.TYPE_ITEM, null, "7月29日", "旅游消费", 1200.00, false));items.add(new BillItem(BillItem.TYPE_ITEM, null, "7月28日", "电影票", 80.00, false));items.add(new BillItem(BillItem.TYPE_ITEM, null, "8月27日", "旅游消费", 320.50, false));items.add(new BillItem(BillItem.TYPE_ITEM, null, "8月26日", "房租", 2500.00, false));items.add(new BillItem(BillItem.TYPE_ITEM, null, "8月25日", "旅游消费", 320.50, false));items.add(new BillItem(BillItem.TYPE_ITEM, null, "8月24日", "房租", 2500.00, false));items.add(new BillItem(BillItem.TYPE_ITEM, null, "8月23日", "旅游消费", 320.50, false));items.add(new BillItem(BillItem.TYPE_ITEM, null, "8月22日", "房租", 2500.00, false));items.add(new BillItem(BillItem.TYPE_ITEM, null, "8月21日", "旅游消费", 320.50, false));items.add(new BillItem(BillItem.TYPE_ITEM, null, "8月20日", "房租", 2500.00, false));items.add(new BillItem(BillItem.TYPE_ITEM, null, "8月19日", "旅游消费", 320.50, false));items.add(new BillItem(BillItem.TYPE_ITEM, null, "8月18日", "房租", 2500.00, false));items.add(new BillItem(BillItem.TYPE_ITEM, null, "8月17日", "旅游消费", 320.50, false));items.add(new BillItem(BillItem.TYPE_ITEM, null, "8月16日", "房租", 2500.00, false));items.add(new BillItem(BillItem.TYPE_ITEM, null, "8月15日", "旅游消费", 320.50, false));items.add(new BillItem(BillItem.TYPE_ITEM, null, "8月14日", "房租", 2500.00, false));items.add(new BillItem(BillItem.TYPE_ITEM, null, "8月13日", "旅游消费", 320.50, false));items.add(new BillItem(BillItem.TYPE_ITEM, null, "8月12日", "房租", 2500.00, false));return items;}
}

完整的示例地址:
https://gitee.com/weicongxiang/imitation-we-chat-bill-list.git

拿走,拿走!!!

http://www.dtcms.com/a/422554.html

相关文章:

  • 建三江建设局网站网站建设自我评价怎么写比较好
  • 华为Fit4手表:个性化表盘,让生活更有温度
  • Spring Boot - 从PF4J到SBP:深入解析Java插件化架构的演进与实践
  • 河南做网站企起做平面什么网站的素材不侵权
  • 哪个网站做ppt模板赚钱手机棋牌游戏平台
  • 鸿蒙app开发中 拿到json文件数据进行动画的播放
  • 第三章 鸽巢原理
  • 智慧政务——解读57页清华大学:DeepSeek政务场景应用与解决方案【附全文阅读】
  • Transformer模型:深度解析自然语言处理的革命性架构
  • 声网AI逐字拆解问题,30天重塑口语清晰表达
  • Java异常简介
  • VSCode Web版本安装
  • 实用软件 | 实时监控andriod设备硬件状态-devcheck
  • 非关系型数据库(NoSQL):特性、类型与应用指南​
  • 性能革命的底层逻辑:深入理解 Spring Cloud Gateway 的 Reactor 核心
  • 2025 年 AI+BI 趋势下,Wyn 商业智能软件如何重构企业决策效率?
  • 网站开发合同印花税公司网站建设重点内容
  • CMake cmake_parse_arguments
  • 4、存储系统架构 - 从机械到闪存的速度革命
  • 淘宝店铺全量商品接口深度开发:从分页优化到数据完整性保障
  • 视频MixformerV2 onnx导出
  • winfrom 的 BindingSource ,ist<T> + LINQ,DataTable + DataView 自动刷新机制 优势劣势
  • Spring Statemachine 架构详解
  • 做网站大概费用给漫画网站做推广
  • Hadoop RPC深度解析:分布式通信的核心机制
  • 提升开发效率的RPC系统!
  • 微信小程序入门学习教程,从入门到精通,微信小程序页面交互 —— 知识点详解与案例实现(3)
  • 高端品牌网站建设电商网站设计常州的平台公司
  • 物联网存储选型避坑指南:SQLite/MySQL/InfluxDB深度对比(C#场景+性能测试+选型工具)
  • Sublime Text 4 下载 + 安装 + 汉化全流程教程(图文保姆级指南)