AndroidX RecyclerView实践-手写卡片式布局

2023-10-09 09:30

本文主要是介绍AndroidX RecyclerView实践-手写卡片式布局,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

    • 前言
    • 功能拆解
    • 代码实现
      • 准备工作
        • 添加依赖
        • MOCK数据
        • 创建适配器
      • 卡片布局实现
        • 思路分析
        • 初始配置
        • 自定义LayoutManager
      • 手势滑动移除实现
        • 思路分析
        • 自定义ItemTouchHelper.SimpleCallback
      • 点击按钮卡片飞出动画实现
        • 思路分析
        • 按钮点击监听
        • 自定义SimpleItemAnimator
    • 尾声

前言

RecyclerView系列总结:
《AndroidX RecyclerView总结-测量布局》
《AndroidX RecyclerView总结-Recycler》
《AndroidX RecyclerView总结-滑动处理》
《AndroidX RecyclerView总结-ItemTouchHelper》

RecyclerView除了可以展示线性、网格、瀑布流等常规列表布局,还支持自定义个性化的布局。这里实现卡片式滑动布局,效果如图:

最终效果图

功能拆解

最终实现效果是一个层叠卡片式布局,支持滑动拖拽移除,并且将移除的item再添加回数据集以便循环演示。点击对应按钮触发对应方向的自动滑出动画。当往左滑出的时候弹出"不喜欢"吐司,往右边滑出弹出"喜欢"吐司。

  • 首先实现卡片布局,很容易想到可以通过自定义LayoutManager来完成,重写onLayoutChildren中实现个性化布局逻辑。
  • 手势滑动移除,可以借助ItemTouchHelper实现。
  • 卡片飞出的动画效果,通过自定义SimpleItemAnimator,设置ItemAnimator。

代码实现

准备工作

添加依赖
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'com.google.android.material:material:1.1.0'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.appcompat:appcompat:1.1.0'
MOCK数据

使用本地图片资源。

List<CardBean> data = new ArrayList<>();
data.add(new CardBean(R.mipmap.tu15));
data.add(new CardBean(R.mipmap.tu16));
data.add(new CardBean(R.mipmap.tu17));
data.add(new CardBean(R.mipmap.xiaotu_50));
data.add(new CardBean(R.mipmap.xiaotu_51));
data.add(new CardBean(R.mipmap.xiaotu_122));
data.add(new CardBean(R.mipmap.xiaotu_131));
data.add(new CardBean(R.mipmap.xiaotu_134));
CardRecycleAdapter adapter = new CardRecycleAdapter(this, data);
cardLayout.setAdapter(adapter);
public class CardBean {// 图片资源IDpublic int cover;public CardBean(int cover) {this.cover = cover;}
}
创建适配器
public class CardRecycleAdapter extends BaseRecyclerAdapter<CardBean> {public CardRecycleAdapter(Context context, List<CardBean> data) {super(context, data);putItemLayoutId(VIEW_TYPE_DEFAULT, R.layout.item_card);}@Overridepublic void onBind(final ViewHolder holder, final CardBean item, final int position) {// 设置图片holder.setImageResource(R.id.ivCover, item.cover);holder.getView(R.id.btnDelete).setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {// ···}});holder.getView(R.id.btnLike).setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {// ···}});}
}

完整代码见CardRecycleAdapter.java

卡片布局实现

思路分析

图解

仔细观察最终效果图,注意到每层Item View的重叠排列。从顶层往下,View会逐渐往下偏移露出底部一部分视图,并且会适当缩小一定比例,从视觉上看起来位于后方。当数据过多时,不会全部摆放到RecyclerView中,限制最多展示View个数。

初始配置

定义一个配置类:

public static class CardConfig {public static int CARD_SHOW_COUNT;    //最多同时显示个数public static float SCALE_GAP;  //缩放比例public static int TRANS_Y_GAP;  //偏移量public static void init(Context context) {CARD_SHOW_COUNT = 4;SCALE_GAP = 0.05f;TRANS_Y_GAP = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 15, context.getResources().getDisplayMetrics());}
}
  • CARD_SHOW_COUNT:就是在RecyclerView中摆放View的数量上限
  • SCALE_GAP:每层View间的缩放比例
  • TRANS_Y_GAP:每层View间的垂直偏移量
自定义LayoutManager

关键步骤,继承LayoutManager,实现onLayoutChildren方法:

public class CardSwipeLayoutManager extends RecyclerView.LayoutManager {@Overridepublic RecyclerView.LayoutParams generateDefaultLayoutParams() {return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);}@Overridepublic void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {super.onLayoutChildren(recycler, state);//解除所有子view,添加到scrap集合缓存detachAndScrapAttachedViews(recycler);// 取数据个数和CARD_SHOW_COUNT的较小值int count = Math.min(getItemCount(), CardConfig.CARD_SHOW_COUNT);if(count < 1) {return;}//遍历前count个itemView加载显示for (int i=0; i<count; i++) {// 获取缓存的ViewView child = recycler.getViewForPosition(i);//添加至头部,显示在底层addView(child, 0);//测量child的大小measureChildWithMargins(child, 0, 0);//获取child外边距=(recyclerview的宽度-child包含了decorate间距的总宽度) / 2int widthSpace = (getWidth()-getDecoratedMeasuredWidth(child)) / 2;int heightSpace = (getHeight()-getDecoratedMeasuredHeight(child)) / 2;//摆放child的位置(居中摆放)layoutDecorated(child, widthSpace, heightSpace,widthSpace+getDecoratedMeasuredWidth(child),heightSpace+getDecoratedMeasuredHeight(child));//设置Y轴偏移和长宽缩放,层叠错开显示int fraction = i;if(fraction == count-1) {//最后一个和倒数第二个的fraction一致fraction = count - 2;}// 设置View的Y轴偏移和缩放child.setTranslationY(CardConfig.TRANS_Y_GAP * fraction);child.setScaleX(1 - CardConfig.SCALE_GAP*fraction);child.setScaleY(1 - CardConfig.SCALE_GAP*fraction);}}
}

核心步骤就是依次取child,先测量child,再将child居中摆放,最后计算它的偏移和缩放。

其中注意几个细节,在布局前首先调用detachAndScrapAttachedViews将mChildren数组中View移除,并将View交由Recycler进行缓存。之后再依次从Recycler获取View,按FILO顺序加入mChildren数组,即后添加的View插入数组头部,使先遍历的View能显示在上层

最后一个View和倒数第二个View的偏移和缩放比例是一致的,即最后一个刚好被倒二个完整覆盖,这样是为了在拖拽时视觉效果更连贯

完整代码见CardSwipeLayoutManager.java

手势滑动移除实现

思路分析

通过ItemTouchHelper绑定RecyclerView,可以托管RecyclerView的手势事件,创建ItemTouchHelper.Callback可以处理自定义滑动、拖拽相关业务逻辑。

移除View逻辑可以在ItemTouchHelper.Callback#onSwiped中处理,当滑动达到临界值时会触发onSwiped回调。

当拖拽时,底层的View也会相应的偏移和缩放,以填充上层View的位置。可以在ItemTouchHelper.Callback#onChildDraw中处理相应逻辑。

自定义ItemTouchHelper.SimpleCallback
  1. 首先设置滑动方向
public class CardSwipeCallback extends ItemTouchHelper.SimpleCallback {private Context context;private CardRecycleAdapter adapter;public CardSwipeCallback(Context context, CardRecycleAdapter adapter) {// 设置支持的手势类型和方向super(0, ItemTouchHelper.LEFT|ItemTouchHelper.UP|ItemTouchHelper.RIGHT|ItemTouchHelper.DOWN);this.context = context;this.adapter = adapter;}@Overridepublic boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {// 不进行item交换操作return false;}// ···
}

在构造函数中,设置了不支持Drag拖拽类型操作,Swipe滑动操作支持上下左右四个方向

  1. 处理移出操作
public class CardSwipeCallback extends ItemTouchHelper.SimpleCallback {// ···@Overridepublic void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {// 判断方向if(direction==ItemTouchHelper.LEFT) {Toast.makeText(context, "不喜欢", Toast.LENGTH_SHORT).show();} else {Toast.makeText(context, "喜欢", Toast.LENGTH_SHORT).show();}// 移除滑出的item并添加到尾部CardBean item = adapter.getmData().remove(viewHolder.getLayoutPosition());adapter.getmData().add(item);adapter.notifyDataSetChanged();}// ···
}

在onSwiped中,首先判断移出方向,弹对应toast。接着从适配器数据集中移除对应item,再重新添加到数据集尾部,实现循环效果。

  1. 处理下层View动画
public class CardSwipeCallback extends ItemTouchHelper.SimpleCallback {// ···@Overridepublic void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);// 设置滑动临界值,用于计算偏移和缩放差值,避免无限偏移缩放double maxDistance = recyclerView.getWidth() / 2;// 当前滑动距离(勾股定理)double distance = Math.sqrt(dX*dX + dX*dX);// 计算偏移缩放比例double ratio = distance / maxDistance;if(ratio > 1) {// 限制不超过1ratio = 1;}// 获取当前recyclerview显示item的个数,遍历计算偏移和缩放int count = recyclerView.getChildCount();for (int i=0; i<count; i++) {View child = recyclerView.getChildAt(i);// 越前面越底层int level = count - i - 1;// 判断非最底层的view才需要改变if(level != count-1) {// 以原来的位置加上偏移量和缩放比child.setTranslationY((float) (CardConfig.TRANS_Y_GAP * (level-ratio)));child.setScaleX((float) (1 - CardConfig.SCALE_GAP * level + CardConfig.SCALE_GAP*ratio));child.setScaleY((float) (1 - CardConfig.SCALE_GAP * level + CardConfig.SCALE_GAP*ratio));}}}
}

在onChildDraw方法中,首先利用滑动距离计算一个差值比例,在遍历child,依次计算child的偏移和缩放,达到上下移动和放大缩小的效果。

关于计算偏移和缩放比例说明:
在前面自定义LayoutManager#onLayoutChildren中,以原child索引值作为fraction计算偏移和缩放,偏移量和缩放比例依次递增,之后child都是插入mChildren数组头部。因此在此处自定义SimpleCallback#onChildDraw中遍历child时,先取到的child是最底层的,这里通过倒序求出原fraction,计算原偏移和缩放,再加上滑动差值比例计算的偏移和缩放,进行在原基础上的位移和放大缩小。

完整代码见CardSwipeCallback.java

点击按钮卡片飞出动画实现

思路分析

给item布局中的对应按钮增加点击事件监听,移除对应数据并触发适配器更新。动画效果通过自定义ItemAnimator(这里只需要自定义Remove动画),之后通过RecyclerView#setItemAnimator应用ItemAnimator。

按钮点击监听
public class CardRecycleAdapter extends BaseRecyclerAdapter<CardBean> {// ···@Overridepublic void onBind(final ViewHolder holder, final CardBean item, final int position) {holder.setImageResource(R.id.ivCover, item.cover);holder.getView(R.id.btnDelete).setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {Toast.makeText(mContext, "不喜欢", Toast.LENGTH_SHORT).show();// 给itemView设置tag,标记当前触发往左边滑出的动画holder.itemView.setTag(SwipeItemAnimator.SWIPE_REMOVE_LEFT);// 移除数据,并触发notifyItemRemovedremove(item);// 将数据添加回数据集,以便循环演示mData.add(item);}});holder.getView(R.id.btnLike).setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {Toast.makeText(mContext, "喜欢", Toast.LENGTH_SHORT).show();// 给itemView设置tag,标记当前触发往右边滑出的动画holder.itemView.setTag(SwipeItemAnimator.SWIPE_REMOVE_RIGHT);remove(item);mData.add(item);}});}
}

这里给两个按钮设置了点击监听,其中分别会给itemView设置tag来标记不同方向的滑出。
完整代码见CardRecycleAdapter.java

自定义SimpleItemAnimator

RecyclerView中默认有一个DefaultItemAnimator,实现了Add、Remove、Move、Change操作的动画。如果不是为了区分不同方向的移出动画(点击"不喜欢"按钮往左滑出、点击"喜欢"按钮往右滑出),使用默认动画即可。这里直接拷贝DefaultItemAnimator源码,仅修改其中Remove动画的实现。

public class SwipeItemAnimator extends SimpleItemAnimator {public static final int SWIPE_REMOVE_LEFT = 1;  // 标记左滑移除public static final int SWIPE_REMOVE_RIGHT = 2; // 标记右滑移除// ···private void animateRemoveImpl(final RecyclerView.ViewHolder holder) {final View view = holder.itemView;final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view);mRemoveAnimations.add(holder);//设置偏移量---向左还是向右float translateX = 0;// 判断点击时给view设置的tag,判断滑出方向if((int)view.getTag() == SWIPE_REMOVE_LEFT) {translateX = -view.getWidth();} else if((int)view.getTag() == SWIPE_REMOVE_RIGHT) {translateX = view.getWidth();}animation.setDuration(getRemoveDuration()).translationX(translateX).alpha(0).setListener(new VpaListenerAdapter() {@Overridepublic void onAnimationStart(View view) {dispatchRemoveStarting(holder);}@Overridepublic void onAnimationEnd(View view) {animation.setListener(null);//动画结束还原位置ViewCompat.setTranslationX(view, 0);ViewCompat.setAlpha(view, 1);dispatchRemoveFinished(holder);mRemoveAnimations.remove(holder);dispatchFinishedWhenDone();}}).start();}// ···
}

完整代码见SwipeItemAnimator.java

尾声

至此,完成了开头效果中的卡片式滑动布局。

完整代码见CardSwipeDemo

这篇关于AndroidX RecyclerView实践-手写卡片式布局的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/171905

相关文章

HarmonyOS学习(七)——UI(五)常用布局总结

自适应布局 1.1、线性布局(LinearLayout) 通过线性容器Row和Column实现线性布局。Column容器内的子组件按照垂直方向排列,Row组件中的子组件按照水平方向排列。 属性说明space通过space参数设置主轴上子组件的间距,达到各子组件在排列上的等间距效果alignItems设置子组件在交叉轴上的对齐方式,且在各类尺寸屏幕上表现一致,其中交叉轴为垂直时,取值为Vert

基于MySQL Binlog的Elasticsearch数据同步实践

一、为什么要做 随着马蜂窝的逐渐发展,我们的业务数据越来越多,单纯使用 MySQL 已经不能满足我们的数据查询需求,例如对于商品、订单等数据的多维度检索。 使用 Elasticsearch 存储业务数据可以很好的解决我们业务中的搜索需求。而数据进行异构存储后,随之而来的就是数据同步的问题。 二、现有方法及问题 对于数据同步,我们目前的解决方案是建立数据中间表。把需要检索的业务数据,统一放到一张M

系统架构师考试学习笔记第三篇——架构设计高级知识(20)通信系统架构设计理论与实践

本章知识考点:         第20课时主要学习通信系统架构设计的理论和工作中的实践。根据新版考试大纲,本课时知识点会涉及案例分析题(25分),而在历年考试中,案例题对该部分内容的考查并不多,虽在综合知识选择题目中经常考查,但分值也不高。本课时内容侧重于对知识点的记忆和理解,按照以往的出题规律,通信系统架构设计基础知识点多来源于教材内的基础网络设备、网络架构和教材外最新时事热点技术。本课时知识

Prometheus与Grafana在DevOps中的应用与最佳实践

Prometheus 与 Grafana 在 DevOps 中的应用与最佳实践 随着 DevOps 文化和实践的普及,监控和可视化工具已成为 DevOps 工具链中不可或缺的部分。Prometheus 和 Grafana 是其中最受欢迎的开源监控解决方案之一,它们的结合能够为系统和应用程序提供全面的监控、告警和可视化展示。本篇文章将详细探讨 Prometheus 和 Grafana 在 DevO

springboot整合swagger2之最佳实践

来源:https://blog.lqdev.cn/2018/07/21/springboot/chapter-ten/ Swagger是一款RESTful接口的文档在线自动生成、功能测试功能框架。 一个规范和完整的框架,用于生成、描述、调用和可视化RESTful风格的Web服务,加上swagger-ui,可以有很好的呈现。 SpringBoot集成 pom <!--swagge

lvgl8.3.6 控件垂直布局 label控件在image控件的下方显示

在使用 LVGL 8.3.6 创建一个垂直布局,其中 label 控件位于 image 控件下方,你可以使用 lv_obj_set_flex_flow 来设置布局为垂直,并确保 label 控件在 image 控件后添加。这里是如何步骤性地实现它的一个基本示例: 创建父容器:首先创建一个容器对象,该对象将作为布局的基础。设置容器为垂直布局:使用 lv_obj_set_flex_flow 设置容器

vue2实践:el-table实现由用户自己控制行数的动态表格

需求 项目中需要提供一个动态表单,如图: 当我点击添加时,便添加一行;点击右边的删除时,便删除这一行。 至少要有一行数据,但是没有上限。 思路 这种每一行的数据固定,但是不定行数的,很容易想到使用el-table来实现,它可以循环读取:data所绑定的数组,来生成行数据,不同的是: 1、table里面的每一个cell,需要放置一个input来支持用户编辑。 2、最后一列放置两个b

Apache Tiles 布局管理器

陈科肇 =========== 1.简介 一个免费的开源模板框架现代Java应用程序。  基于该复合图案它是建立以简化的用户界面的开发。 对于复杂的网站,它仍然最简单,最优雅的方式来一起工作的任何MVC技术。 Tiles允许作者定义页面片段可被组装成在运行一个完整的网页。  这些片段,或Tiles,可以用于为了降低公共页面元素的重复,简单地包括或嵌入在其它瓦片,制定了一系列可重复使用

【CSS in Depth 2 精译_023】第四章概述 + 4.1 Flexbox 布局的基本原理

当前内容所在位置(可进入专栏查看其他译好的章节内容) 第一章 层叠、优先级与继承(已完结) 1.1 层叠1.2 继承1.3 特殊值1.4 简写属性1.5 CSS 渐进式增强技术1.6 本章小结 第二章 相对单位(已完结) 2.1 相对单位的威力2.2 em 与 rem2.3 告别像素思维2.4 视口的相对单位2.5 无单位的数值与行高2.6 自定义属性2.7 本章小结 第三章 文档流与盒模型(已

【HarmonyOS】-TaskPool和Worker的对比实践

ArkTS提供了TaskPool与Worker两种多线程并发方案,下面我们将从其工作原理、使用效果对比两种方案的差异,进而选择适用于ArkTS图片编辑场景的并发方案。 TaskPool与Worker工作原理 TaskPool与Worker两种多线程并发能力均是基于 Actor并发模型实现的。Worker主、子线程通过收发消息进行通信;TaskPool基于Worker做了更多场景化的功能封装,例