尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Android自定义LayoutManager实现卡片堆叠滑动效果
上个月接了个需求产品经理从工位上探出头给我发了张gif一叠卡片像扑克牌一样摞在一起第一张完整露出后面的卡片只显示一截头部整叠卡片可以上下滑动翻看滑动过程里卡片还有轻微的缩放变化层次感特别强。他说得很轻松“照着做一个就行。”我打开gif看了三秒心想这玩意儿怕是没那么简单。先去GitHub翻了一圈android-pile-layout这类库确实有但大部分只做静态堆叠展示滑动的交互要么依赖PageSnapHelper那种一页一页翻要么干脆是Tinder式的左右滑出卡片完全不是我要的“整叠卡片组上下滚动但保持层叠”的体验。没辙只能自己动手。最终方案是自定义一个RecyclerView.LayoutManager把堆叠坐标计算、滑动消费、缩放联动都握在自己手里。这篇文章就把整个从零实现的过程摊开讲包括坐标公式、滑动边界、复用逻辑和后来踩过的几个坑希望给你省点时间。1. 先搞清楚要做什么这个卡堆效果到底是一种什么交互1.1 从产品需求说起我接到的需求大概是这样首页要展示一组“任务卡片”每张卡片上有标题、摘要和按钮一屏最多完整显示第一张卡片后面的卡片按顺序向下偏移一小段距离露出卡片顶部让用户感知到“后面还有内容”。整组卡片可以上下滑动但视觉上不是每张卡片独立滚动而是整叠纸牌一起移动。这个“一起移动”是理解整个需求的关键。如果做成每张卡片自己在RecyclerView里独立滚动那其实就是LinearLayoutManager加一点间距根本不值得单独写一篇文章。真正值得写的是一叠卡片作为一个整体运动但同时保持“层叠关系”以及滑动过程中前后卡片的视觉变化。产品经理不会说“要层叠”他只会说“要那种一叠一叠的感觉”这句话翻译成技术方案就是自定义LayoutManager里的坐标算法。1.2 为什么我放弃了几个现成库在决定自己写之前我确实分别试用过几个开源库失望的点各有不同有些库的堆叠效果是固定的布局完成后不能滑动只能通过点击按钮切换下一张交互完全被写死有些库实现了手势滑动但它是“划走”卡片不是“整体滚动”卡片会被移除出堆叠数据模型也要跟着变还有的库内部用了ViewPager2/RecyclerView嵌套能实现滑动但每张卡片的缩放、偏移量是写死的常量想改成需求里那种“露头一截的层叠”需要改源码。最烦人的是这类库往往不开放在滑动手势过程中回调每一张卡片位置变化的接口我想做“第一张卡片滑出去时第二张卡片逐渐放大补位”这种联动库本身支持不了。与其改一个不熟悉内部结构的库不如直接基于RecyclerView.LayoutManager写一套反正 LayoutManager 就是官方留给我们的扩展点布局逻辑完全可控。1.3 自定义LayoutManager的工作边界RecyclerView.LayoutManager是一个抽象类我们要做的核心事情归纳起来其实就三件在onLayoutChildren里决定每张卡片摆放的坐标在scrollVerticallyBy里消费纵向滑动距离更新整体的滚动偏移量在一个fill方法里完成“哪些卡片需要显示、哪些需要回收”的调度。这三件事做完一个自定义LayoutManager就能跑起来。RecyclerView自带的Recycler机制会帮我们处理ViewHolder的创建和复用我们唯一要关心的是“你告诉我哪个item放在哪个位置我来负责测量和摆放”。后续数据增删、刷新也是通过 RecyclerView 已有的notify机制驱动整个体系非常容易嵌进现有项目。2. 布局和测量的底层配合写LayoutManager前必懂的三件事2.1 generateDefaultLayoutParams 决定卡片测量基础写自定义LayoutManager最容易翻车的地方不是坐标公式多难而是generateDefaultLayoutParams()没重写。这个方法返回LayoutParams决定了RecyclerView在给子View设置布局参数时的基础值。如果不重写默认可能拿到一个宽高都是0或固定值的参数测量出来的尺寸完全不是你预期的。Override public RecyclerView.LayoutParams generateDefaultLayoutParams() { return new RecyclerView.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); }上面这段是标准写法。在堆叠卡片场景里我建议卡片宽度直接等于RecyclerView宽度减去左右padding高度按一个比例计算比如宽度乘以0.72这样可以保证卡片看起来比较“宽幅”露出头部的信息量足够。Adapter里实际上不需要指定宽高因为我们会在LayoutManager里自己算。2.2 measureChildWithMargins 与 layoutDecoratedWithMargins 的关系RecyclerView不会自动测量子View所有测量都要在布局阶段手动触发。measureChildWithMargins(view, 0, 0)就是用来测量一个子View的它考虑了RecyclerView的padding、子View的margin、以及当前的测量模式。测量之后你能通过view.getMeasuredWidth()拿到真实的测量宽度。但拿到测量值之后真正把View放到屏幕上的方法是layoutDecoratedWithMargins(view, left, top, right, bottom)。这两个方法容易被混在一起理解的时候可以类比ViewGroup的流程先measure量尺寸再layout定位置。少了任何一个卡片要么是没有尺寸要么是量了尺寸但没摆放。measureChildWithMargins(view, 0, 0); int cardWidth getWidth() - getPaddingLeft() - getPaddingRight(); int cardHeight (int) (cardWidth * mCardRatio); layoutDecoratedWithMargins(view, left, top, left cardWidth, top cardHeight);这里我故意没有完全依赖getMeasuredWidth来算宽度而是直接用RecyclerView可用宽度计算。原因是堆叠卡片希望宽度铺满容器如果按照WRAP_CONTENT测量不同卡片内容长短会导致宽度不一致堆叠效果会参差不齐。measure这一步走流程真实尺寸由我自己定。2.3 detachAndScrapAttachedViews 在纯自定义布局中的意义onLayoutChildren开头一般要调用detachAndScrapAttachedViews(recycler)把当前屏幕上的子View从RecyclerView层级里“摘下来”放到scrap缓存里。注意这里用的是detach不是removeAllViews区别非常大detach只是临时移出层级ViewHolder不会被重新绑定数据removeAllViews则会走完整的移除流程下一次addView的时候ViewHolder的数据要重新绑定。Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { super.onLayoutChildren(recycler, state); if (state.getItemCount() 0) { removeAndRecycleAllViews(recycler); return; } detachAndScrapAttachedViews(recycler); fill(recycler); }很多教程开头就removeAndRecycleAllViews那是为了省事代价是每次布局都会重新绑定item数据。如果你在onBindViewHolder里加载了图片或做了稍微重一点的IO这种写法在滑动时肉眼可见地卡顿。detachAndScrapAttachedViews则不会它只是把View暂存起来layout阶段addView之后数据还在体验会顺滑得多。3. 核心坐标计算堆叠偏移与滑动消费的数学关系3.1 堆叠位置公式每张卡片该摆在哪假设每张卡片的高度是cardHeight每张卡片之间在垂直方向露出的高度是visibleHeight。那么第i张卡片的top坐标就是int top getPaddingTop() i * visibleHeight - mScrollOffset; int bottom top cardHeight; int left getPaddingLeft() i * horizontalOffset; int right left cardWidth;这条公式是整个LayoutManager的地基。它的直观含义是第i张卡片的顶部位置永远比前一张往下偏移visibleHeight全局滑动偏移mScrollOffset则是把整组卡片一起向上推移。visibleHeight是一个需要调优的核心参数。它决定了用户能感知到“后面还有卡片”的强度。如果取得太大卡片之间的遮挡关系不明显视觉上退化成普通列表如果取得太小后面的卡片只露出一条边标题完全看不清伸手就想去滑。我实测下来visibleHeight取70~90dp比较合适卡片本身高度在200~240dp区间露出的那截刚好能容纳一行标题或一个按钮。horizontalOffset则是可选的。你可以让它为0卡片左对齐纯粹靠缩放和Y轴偏移体现层叠感也可以设成5~15dp让卡片在水平方向也形成一个阶梯视觉效果更像一叠被错开的纸牌。我最终用的是8dp走小阶梯路线。这里的滑动偏移mScrollOffset是从0开始的每滑动一段距离就增加最大到“内容总高度减去RecyclerView自身高度”。它和每张卡片的top是线性关系这也是为什么整个滑动过程看起来很“跟手”——因为没有任何曲线函数就是纯线性平移用户划多少内容就动多少。3.2 scrollVerticallyBy 里如何计算并返回真实消费距离自定义LayoutManager要支持滑动必须重写scrollVerticallyBy(int dy, Recycler recycler, State state)。参数dy是RecyclerView想移动的距离手指上滑时dy为正表示内容要向上移动手指下滑时dy为负。我们要做的是根据dy算出新的mScrollOffset重新布局然后返回“实际消费掉的距离”。Override public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { if (getItemCount() 0) return 0; int maxOffset Math.max(0, getTotalHeight() - getVerticalSpace()); int targetOffset mScrollOffset dy; int newOffset Math.max(0, Math.min(targetOffset, maxOffset)); int consumed newOffset - mScrollOffset; if (consumed 0) { return 0; } mScrollOffset newOffset; fill(recycler); return consumed; }这里有个细节值得展开我们返回的不是dy而是consumed。假如用户已经滑到最底部mScrollOffset已经是maxOffset此时继续上滑targetOffset会超过maxOffsetnewOffset被 clamp 到maxOffsetconsumed变成0。返回0告诉RecyclerView“这次滑动我没有消费掉任何距离”RecyclerView就能正确处理边界情况不会继续上报给嵌套滚动机制。3.3 边界控制首尾不能划过头边界控制看起来简单实际有两个容易写错的地方。第一最小偏移一定是0但不能写成Math.max(0, targetOffset)就完事因为还要考虑第一张卡片顶部的padding。如果你在布局时加了getPaddingTop()那么当mScrollOffset 0的时候第一张卡片的top正好在padding位置这是合理的初始状态。第二最大偏移不能直接等于getTotalHeight() - getVerticalSpace()。这里有一个“最后一张卡片底部是否露出”的问题。假设内容总高度为totalHeightRecyclerView高度为viewHeight最后一张卡片的底部在总高度的末端。如果totalHeight - viewHeight是最大偏移那么滑到最大时最后一张卡片的底边正好贴住RecyclerView底边。但如果你的卡片高度超过了屏幕高度极少见但可能这个公式就得调整。我在实现里额外加了一个保护最大偏移再减去最后一张卡片的“底部padding”保证始终能看到最后卡片的一部分。3.4 关于横向滑动的补充因为卡片之间有horizontalOffset理论上整叠卡片在水平方向也超出了屏幕。我实际测下来这个偏移很小8dp量级而且越往后的卡片被前面的遮挡右侧露出来的那点距离用户根本感知不到所以不需要额外处理横向滚动。如果你的horizontalOffset调得比较大比如超过40dp那就需要再写一个scrollHorizontallyBy逻辑和纵向完全对称把mScrollOffsetY换成一个二维向量即可。4. 让堆叠“活”起来缩放旋转与视差联动4.1 位置之外的第二个视觉要素层级感坐标偏移只能做出“阶梯感”但卡片和卡片之间没有“前后”的层次。一叠纸牌之所以看起来像一叠纸牌是因为后面的卡片不仅偏了位置还比前面的小一点、暗一点。这个层次感必须通过缩放scale和透明度alpha来补。float progress Math.min(1f, (float) (i * visibleHeight - mScrollOffset) / cardHeight); float scale 1f - progress * mScaleFactor; if (scale 0.75f) scale 0.75f; view.setScaleX(scale); view.setScaleY(scale); view.setAlpha(1f - progress * mAlphaFactor);这里的progress是一个0到1之间的值表示第i张卡片“离可视区最前面”的距离。mScaleFactor我习惯用0.03mAlphaFactor用0.06。也就是说第4张卡片缩放约0.88透明度约0.76依然能看清标题但明显比第一张“退后”。progress的动态计算很关键它让缩放和透明度在滑动的每一帧都连续变化而不是跳变。4.2 滑动过程中动态调整缩放和透明度如果只是静态给每张卡片一个固定缩放滑动时卡片之间就没有“联动”感觉。真正好看的堆叠效果是第一张卡片往上滑走时第二张卡片同时在放大、变清晰仿佛它正在走到聚光灯下。实现这个效果需要把缩放和mScrollOffset绑定也就是上面的代码里progress的算法。分析一下当mScrollOffset 0时第0张卡片的progress 0完整显示第1张卡片的progress ≈ visibleHeight / cardHeight稍微小一点。随着滑动增大第0张的progress从0逐渐增大第1张的progress也在变化但速率不同视觉上就形成了连续过渡。这个“不同速率”其实是一种简单的视差效果前面的卡片移动得快后面的移动得慢。如果你想让视差更夸张可以在top坐标计算时额外加一个基于progress的修正值比如int extraOffset (int) (progress * mParallaxFactor * visibleHeight); int top getPaddingTop() i * visibleHeight - mScrollOffset extraOffset;mParallaxFactor可以取0.1~0.3这样就算偏移量相同后面的卡片也会因为视差补偿而比前面的移动更慢层次感更强。4.3 回弹与滚动到指定卡片RecyclerView默认的滚动停止后不会自动帮你做吸附。自定义LayoutManager里onScrollStateChanged是一个必须重写的方法因为你需要在这里判断“滑动停下来之后要不要自动对齐到某一张卡片”。Override public void onScrollStateChanged(int state) { super.onScrollStateChanged(state); if (state RecyclerView.SCROLL_STATE_IDLE) { int targetIndex Math.round(mScrollOffset / (float) visibleHeight); int targetScroll targetIndex * visibleHeight; smoothScrollToPositionCompat(targetScroll); } }这里用Math.round把当前偏移近似到最近的卡片位置让滑动停止后卡片会“卡”在一张卡片完整露头的位置而不是停在两个卡片中间。如果产品不要求这种吸附效果这一块可以直接省略毕竟纯线性滚动也很自然。吸附的优点是浏览体验更规整缺点是如果你滑得很快卡片会被强制“吸”到最近的锚点可能滑起来有点“粘”。5. 性能与复用从全量重排到按可见区域裁剪5.1 全量布局的实现和代价第一版实现我在fill里把所有item从0到itemCount - 1全部布局了一遍。因为卡片之间有堆叠理论上每张卡片都需要出现在层级里才能保证压盖顺序所以当时第一反应是“全量遍历”。这个写法在itemCount小于20的时候完全没问题滑动很流畅。但数据一多问题就来了如果列表里有200张卡片每滑动一帧都要循环200次执行recycler.getViewForPosition和layoutDecoratedWithMargins即使ViewHolder是复用的也会有大量的布局计算和层级操作。在低端机上掉帧是必然的。5.2 按可见区域裁剪卡片优化方向很明确只布局“理论上可能出现在屏幕内”的卡片。根据坐标公式反推第一张可见卡片的position是int firstVisiblePosition Math.max(0, mScrollOffset / visibleHeight); int lastVisiblePosition Math.min(getItemCount() - 1, (mScrollOffset getVerticalSpace()) / visibleHeight 1);然后把fill里的for循环从0..itemCount改成firstVisiblePosition..lastVisiblePosition。这个改动对性能的提升是决定性的不管itemCount有多少每帧最多布局5~7张卡片因为超出可视区域的卡片根本不需要被创建和摆放。加上visibleHeight即使取很小同一个时刻屏幕上能完整看到的卡片也就是那几张后面的完全被盖住让它们存在于布局里反而浪费。裁剪时要注意一个细节卡片的position必须用它在Adapter里的真实位置来计算视觉参数而不是用它相对firstVisiblePosition的索引。否则滑动到列表中间时某张原本“靠后”的卡片如果恰好成了可视区的第一张它的缩放和透明度就会突然从很暗跳到正常视觉上会闪一下。反过来如果用原始position计算参数永远是连续的滑动效果就平滑了。5.3 优化后的增量复用思路按可见区域裁剪之后其实还能再进一步不要每次detachAndScrapAttachedViews全量重排而是复用当前已经显示的子View把滑出屏幕的暂时移除把新进入屏幕的用addViewInt加进来。这块逻辑会复杂一些因为需要维护当前屏幕上子View和position的映射关系不能简单地“先全摘下来再全放回去”。我的建议是先实现全量重排 可见裁剪保证功能正确和性能OK如果后续数据显示量真的很大比如几百张卡片再考虑增量复用。我在实际项目中先用全量重排交付后来数据量到了一百多张才优化成增量方案性能又提升了一倍。但增量复用的代码量几乎是全量版的三倍如果你不是确实遇到性能瓶颈不要提前“优化”。5.4 测量模式与自动测量RecyclerView.LayoutManager是否开启自动测量会影响RecyclerView在wrap_content时的表现。我们的场景里RecyclerView通常是占据固定高度区域的所以我重写了isAutoMeasureEnabled()返回true让RecyclerView在wrap_content时可以按测量值自动决定高度。但如果你的RecyclerView高度是固定的这个方法的返回值影响就不大。还有一点在onMeasure里要处理高度模式为UNSPECIFIED的情况。如果你在外面把RecyclerView放在ScrollView里高度是wrap_content自定义LayoutManager需要你自己算一个内容总高度并设置测量值否则卡片可能完全不显示。这块我在后面坑位总结里会再提到。6. 实测中遇到的几个坑与处理方式6.1 ItemAnimator导致的闪烁自定义LayoutManager和RecyclerView默认的DefaultItemAnimator很容易冲突。表现是notifyItemChanged之后卡片的位置跳动、闪烁甚至偶发crash。原因是默认动画会同时控制translationX/Y和 LayoutManager的布局坐标两者互相拉扯。最简单的解决方式是直接关闭recyclerView.setItemAnimator(null);如果你的产品确实需要动画建议只保留一个简单的alpha渐隐渐显不要开位移和缩放动画。因为自定义LayoutManager已经控制了完整坐标任何其他动画在坐标上动手脚都会造成不一致。6.2 数据更新时布局不刷新一个非常隐蔽的坑adapter.notifyDataSetChanged()之后RecyclerView并没有重新走onLayoutChildren。原因是你可能在某处重写了onAdapterChanged但没有让RecyclerView强制requestLayout。我的处理习惯是Override public void onAdapterChanged(RecyclerView.Adapter oldAdapter, RecyclerView.Adapter newAdapter) { super.onAdapterChanged(oldAdapter, newAdapter); mScrollOffset 0; requestLayout(); }这样当Adapter变化时滚动位置重置到顶部布局重新执行避免新数据配旧偏移量导致空白。6.3 View的压盖顺序与setZ坑堆叠卡片天然需要后面出现的卡片被前面的卡片压住。控制压盖有几种思路addView的顺序、view.bringToFront()、setZ。我最开始用addView顺序发现问题的关键在于RecyclerView内部的View回收复用可能导致顺序不稳定特别是快速滑动时addView的顺序偶发会乱压盖关系就跟着乱。换成setZ之后稳定了。具体做法是在fill里对每个child调用view.setZ(1f (getItemCount() - i) / 1000f);让前面的卡片Z值更大自然覆盖后面的卡片。setZ在Android 5.0以上有效如果你还在支持更低的API可以用translateZ或者老老实实用addView顺序并保证fill每次按position升序添加。实际上现在最低基本都是5.0了setZ是安全的。6.4 RecyclerView嵌入ScrollView时的高度测量问题如果这个卡片堆叠要放进一个ScrollViewRecyclerView的wrap_content会触发onMeasure里的UNSPECIFIED模式。自定义LayoutManager如果只依赖getVerticalSpace()和getHeight()计算可见区域在UNSPECIFIED模式下可能拿到0甚至负数结果一张卡片都不显示。解决方案有两个。简单版给RecyclerView设置一个固定的高度比如match_parent或明确dp这样不会有测量问题。通用版在onMeasure里判断MeasureSpec是否UNSPECIFIED如果是就按内容总高度设置测量值Override public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state, int widthSpec, int heightSpec) { super.onMeasure(recycler, state, widthSpec, heightSpec); if (MeasureSpec.getMode(heightSpec) MeasureSpec.UNSPECIFIED) { int totalHeight getTotalHeight(); setMeasuredDimension(getMeasuredWidth(), totalHeight); } }注意这样做的代价是卡片堆叠的“一屏显示一部分”效果会丢失因为整个RecyclerView会拉伸到内容完整高度视差感和滑动就没了。所以我的建议还是尽量别把卡片堆叠塞进ScrollView用根布局直接RecyclerView占满一屏。最后再分享一个小技巧调试自定义LayoutManager的时候onLayoutChildren和scrollVerticallyBy会高频打印大量日志如果用Log.d刷屏会非常痛苦。我的习惯是打日志只打mScrollOffset的变化并且只在边界值变化时打比如首次进入、滑动到顶部、滑动到底部这样能快速定位坐标问题又不会被日志淹没。另外在开发阶段给每张卡片设置一个随机背景色或者不同高度可以一眼看出复用有没有错乱——如果卡片背景色跟着位置乱跳那基本就是getViewForPosition和addView的顺序问题。其实写到这里核心实现已经完整覆盖了堆叠坐标公式、滑动消费逻辑、缩放视差、可见区域裁剪、常见坑位。这套方案承载了我这边两个线上项目的卡片堆叠需求稳定跑了大半年没出过问题。如果你也遇到类似需求不用急着找现成库照这条路径自己写一个LayoutManager你会意外发现它比想象中简单而且后续想加什么效果改坐标公式就行。
RELATED

相关推荐

AutoCAD零基础自学路线:从二维绘图命令到规范出图

AutoCAD零基础自学路线:从二维绘图命令到规范出图

很多人第一次打开AutoCAD,满屏的坐标轴、网格线和工具条,第一反应就是:这不就是个能精确画线的软件吗。这句话没错,但对于工科专业的人来说,它远远低估了AutoCAD真正的价值。它承载的不是“画得好看”,而是…

📅 2026/10/6 3:44:48
K8s Service到Pod流量路径:kube-proxy与iptables/IPVS原理详解

K8s Service到Pod流量路径:kube-proxy与iptables/IPVS原理详解

先说结论:K8s里Service到Pod的流量路径,本质上就是一次“虚拟IP寻址”到“真实Endpoint”的精准转发。整个过程由kube-proxy、内核网络栈和一组名为Endpoints(新版叫EndpointSlice)的对象协同完成。你可以把它理解成公司前台的电话…

📅 2026/10/6 3:44:48
纯Java实现YOLOv5推理:算子复现与精度反超实战

纯Java实现YOLOv5推理:算子复现与精度反超实战

1. 为什么在Java里“重新发明”YOLO轮子先交代一下背景。我所在的项目组常年做Java后端,服务的对象是政企客户,生产环境里跑着的全是Spring Boot、Dubbo这套东西,GPU基本是奢侈品,偶尔有几台带推理卡的机器,还是给隔壁…

📅 2026/10/6 3:44:48
MORE NEWS

更多资讯

📰

微信小程序护肤购物系统实践:数据建模与2MB主包优化

1. 项目概述与设计思路1.1 这个选题解决了什么问题先聊点实在的。做毕业设计或者个人项目选型,最难的不是实现本身,而是“这个题目最后能不能作为一个完整的故事讲出来”。护肤购物系统这个题目,名字里三个关键词缺一不可:微信小程…

📰

嵌入式Linux入门:从裸机到命令行,开发者必须掌握的实用命令与调试技巧

从单片机裸机开发转向嵌入式Linux,第一道坎往往不是C语言,也不是中断、寄存器这些老熟人,而是那个黑乎乎的终端界面。串口工具连上开发板,光标停在#符号前面,你突然发现自己连“看看目录里有什么”都做不到&#xff0c…

📰

莫以skill小而不为:AI Agent技能虽小却有大能量

大概两年前,我第一次在AI工具里看到"skill"这个词的时候,心里想的是:这不就是一段提示词打包成文件吗,能有什么技术含量。直到后来一个几十KB的小skill,让我在项目里少写了两百行逻辑,我才意识到…

📰

多智能体协作触达监控框架Agent-Reach:设计、指标与踩坑实践

最近我把自己搭的一个多智能体协作框架翻出来做了一次大的重构,顺手把所有"触达"相关的问题收敛成了一个独立模块,项目代号暂时就叫Agent-Reach。可能有人一听这个名字会以为是个网络探测或者渠道触达的工具,但其实不是&#xff0c…

📰

AI编程超级能力:本地化开发工具链的范式迁移

1. “Superpowers”不是功能,是开发者工具链的范式迁移最近在几个技术社区和内部分享里,反复听到一个词——“superpowers”。它既不是某个新发布的开源库,也不是某家大厂刚推出的SaaS服务,更不是什么玄学概念。它本质上是一类以A…

📰

基于Hadoop的智能图书推荐系统:从用户行为日志到协同过滤的完整实践

简介:基于Hadoop框架与用户行为特征感知的智能图书推荐系统设计的学士学位毕业论文,原为西南财经大学毕业论文,主要面向计算机科学与技术、软件工程等专业的本科、专科毕业生,也适合对大数据处理与个性化推荐感兴趣的学习者。论文…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬