简介这份资源面向Android开发者尤其是希望在电子书或多媒体应用中实现逼真翻页交互的中高级开发者聚焦「双开」形态的翻书效果即同时呈现相邻两页并模拟真实书页卷曲过渡。包内共63个文件以22个class与7个java源码为核心配合5个xml布局与配置、12个png及6个jpg图片素材另有apk安装包、dex与md说明文档等压缩包约3.97MB结构完整可直接导入研究。内容围绕视图布局、属性动画组合、OpenGL ES三维渲染、手势检测与性能优化等关键环节展开CurlActivity作为核心类承载翻页动画与触摸交互逻辑便于读者对照源码理解页面旋转、纹理映射与滑动响应的实现思路。目前已有483人学习下载适合需要快速掌握翻书动画原理、复用成熟实现或排查渲染性能问题的开发者参考借鉴。1. 翻书效果双开一个被低估的阅读类交互细节做阅读类 App 的同行大概都遇到过这种需求产品经理拿着某款阅读软件的截图过来说“这个翻页效果我们也做一个”。你打开一看页面从中间对折、书页背面透出下一章的影子、手指拖到一半松手还会自动回弹——这就是翻书效果业内常说的“双开”指的是左右两页同时呈现、以中缝为轴对折翻动的形态区别于单页平移或覆盖式翻页。它看起来只是动画实际牵扯到视图层级、触摸事件分发、贝塞尔曲线拟合、阴影渐变和状态机回弹五件事任何一环没对齐翻起来就是“纸片乱飞”而不是“书页翻动”。这份资源包围绕双开翻书效果给出了一套可运行的实现适合正在做阅读器、画册、电子杂志的 Android 开发者直接拆开用也适合想搞懂自定义 View 触摸驱动动画的人拿来当练手素材。2. 双开翻书的几何模型先想清楚纸怎么弯动手写代码之前得先把“翻”这件事在二维平面上描述清楚。很多人一上来就找动画库结果参数调了半天还是不像根子在于没建立几何模型。双开翻书的核心不是旋转而是沿中缝的镜像折叠当前页和中缝另一侧的页面构成一个“对折”关系手指拖动的位置决定折痕线折痕线两侧分别做镜像变换再叠加阴影模拟纸张厚度。2.1 折痕线、镜像点与触摸坐标的映射把屏幕看成一张摊开的纸中缝在 x width/2 处。手指按下的点 P(x, y) 与中缝构成一条折痕的候选方向实际实现里通常取“触摸点与当前页某个角点的连线”作为折痕。以右下角翻页为例折痕是触摸点与页面右下角连线的中垂线这条线把页面切成“翻起部分”和“留下部分”。翻起部分绕折痕做镜像镜像后的多边形就是书页背面的可见区域。这里有个容易翻车的地方镜像变换用Matrix做setScale(-1, 1)再平移浮点误差累积会让背面边缘和正面边缘对不齐出现一条 1px 的缝。血泪经验是镜像后统一用Path重新闭合别指望两次独立变换严丝合缝。// 以触摸点 touch 和角点 corner 求折痕中垂线返回折痕上的两个端点 private void computeFoldLine(PointF touch, PointF corner, PointF outA, PointF outB) { // 中点 float midX (touch.x corner.x) / 2f; float midY (touch.y corner.y) / 2f; // 连线方向 float dx corner.x - touch.x; float dy corner.y - touch.y; // 中垂线方向 连线方向旋转 90 度 float nx -dy; float ny dx; float len (float) Math.hypot(nx, ny); if (len 0) { len 1; } nx / len; ny / len; // 沿中垂线向两侧延伸足够长度保证覆盖整页 float extend 2000f; outA.set(midX nx * extend, midY ny * extend); outB.set(midX - nx * extend, midY - ny * extend); }这段代码只做一件事给定触摸点和被翻的角点算出折痕所在直线的两个远端点。extend取 2000 是个经验值只要大于屏幕对角线就能保证折痕贯穿整页取小了会出现折痕没画到头、背面缺一块的现象。Math.hypot比手写开方稳避免nx/ny归一化时除零。实际项目里我会把折痕计算封装成独立方法因为单页翻和双开翻只是角点来源不同几何逻辑可以复用。2.2 双开模式下左右页的坐标系拆分双开和单页最大的差别在于单页只有一个绘制区域双开要把画布拆成左右两个“半页”各自有独立的裁剪区和内容偏移。常见做法是在onDraw里根据当前翻页进度progress0 到 1决定左右页各自画什么左页画上一章的末页右页画当前章的首页翻动过程中左页逐渐被“翻走”右页逐渐露出。坐标系拆分的关键是内容坐标与屏幕坐标的换算。阅读器的每一页内容通常按固定宽度排版双开时每半页的可用宽度是screenWidth/2减去中缝留白。如果直接拿屏幕坐标去截取 Bitmap翻到一半会发现文字被中缝切掉半个字。我一般会预留 8 到 16dp 的中缝 gutter排版时按(screenWidth - gutter) / 2计算单页宽度绘制时左页偏移 0、右页偏移screenWidth/2 gutter/2。参数含义常见取值调错的表现gutter中缝留白8~16dp太小文字贴缝太大像两本分开的书foldExtend折痕延伸长度≥ 屏幕对角线折痕画不到头背面缺角shadowWidth折痕阴影宽度12~24dp太窄像刀切太宽像脏污cornerRadius翻角圆角0~6dp直角生硬过大失去纸张感这张表里的四个参数基本决定了翻书“像不像”。新手最容易忽略shadowWidth觉得阴影可有可无实际上纸张翻动的立体感八成来自折痕处那条渐变阴影没有它镜像出来的背面就是一块死板的色块。3. 触摸驱动与状态机让手指和动画不打架几何模型解决“画成什么样”状态机解决“什么时候画成什么样”。翻书交互的本质是一个受触摸驱动的状态迁移静止 → 拖拽 → 松手判定 → 回弹或翻页完成 → 静止。这套状态机写不好就会出现手指还在拖、动画自己往前跑或者松手后页面卡在半空不动的情况。3.1 用 GestureDetector 还是自己算取舍与事件分发很多教程直接上GestureDetector的onScroll我试过翻书场景下不推荐。原因是onScroll给的是增量位移而翻书需要的是绝对触摸位置来实时计算折痕增量累加会引入漂移拖久了折痕和手指对不上。更稳的做法是重写onTouchEvent在ACTION_DOWN记录起点、ACTION_MOVE直接读event.getX()/getY()算折痕、ACTION_UP根据当前进度决定去向。事件分发上有个坑如果翻书 View 外面套了ViewPager或RecyclerView横向滑动会被父容器抢走。解决办法是在onTouchEvent里对ACTION_DOWN返回 true 并调用getParent().requestDisallowInterceptTouchEvent(true)等判定为纵向滑动或翻页结束后再放开。别在onInterceptTouchEvent里做翻页判定父容器拦截的时机和你需要的时机经常错位。Override public boolean onTouchEvent(MotionEvent event) { switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: // 抢在父容器之前拿到事件避免被 ViewPager 截胡 getParent().requestDisallowInterceptTouchEvent(true); downX event.getX(); downY event.getY(); state STATE_DRAGGING; return true; case MotionEvent.ACTION_MOVE: if (state STATE_DRAGGING) { // 直接用绝对坐标算折痕不做增量累加 updateFold(event.getX(), event.getY()); invalidate(); } return true; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: // 进度过半判定为翻页完成否则回弹 if (currentProgress 0.5f) { startFlipForward(); } else { startRebound(); } return true; } return super.onTouchEvent(event); }ACTION_DOWN里那句requestDisallowInterceptTouchEvent(true)是保命代码少了它外层只要是个可滚动容器你的翻书 View 大概率收不到完整的 MOVE 序列。currentProgress是 0 到 1 的翻页进度由折痕位置归一化得到0.5 这个阈值不是死的阅读类产品里通常设成 0.3 到 0.4让用户轻轻一划就能翻过去符合“翻书比拖进度条更随意”的心理预期。3.2 回弹动画ValueAnimator 的插值器与进度归一化松手后的回弹或翻页完成用ValueAnimator驱动进度从当前值过渡到 0 或 1。这里有两个参数值得细说插值器和时长。翻页完成用DecelerateInterpolator模拟纸张惯性甩出去后减速回弹用OvershootInterpolator(1.2f)或者干脆AccelerateDecelerateInterpolator前者带一点回弹过冲更像真实纸张后者稳但略呆板。时长别超过 300ms超过用户会觉得“卡”。private void animateProgress(float from, float to, boolean isForward) { ValueAnimator animator ValueAnimator.ofFloat(from, to); animator.setDuration(isForward ? 260 : 200); animator.setInterpolator(isForward ? new DecelerateInterpolator() : new AccelerateDecelerateInterpolator()); animator.addUpdateListener(a - { currentProgress (float) a.getAnimatedValue(); invalidate(); }); animator.addListener(new AnimatorListenerAdapter() { Override public void onAnimationEnd(Animator animation) { if (isForward Math.abs(to - 1f) 0.001f) { // 翻页完成通知内容层切到下一页 onPageFlipped(); } state STATE_IDLE; } }); animator.start(); }onPageFlipped()是内容层和动画层的交界点翻页动画走完才真正切换章节数据动画过程中内容层保持不动避免“边翻边换字”的撕裂感。isForward区分翻页和回弹两者时长和插值器不同混用会让回弹显得拖沓。进度归一化统一用 0 到 1所有绘制逻辑都基于这个值算折痕位置好处是动画和拖拽共用同一套绘制代码不会出现“拖的时候一个样、松手后另一个样”的玄学问题。4. 避坑排查翻书效果最容易翻车的五个地方4.1 背面内容错位或镜像反了现象翻起的书页背面文字是反的或者背面内容和正面接不上。原因通常是镜像矩阵作用在了错误的坐标系上——先平移再镜像和先镜像再平移结果完全不同。解决镜像前把画布平移到折痕原点镜像后再平移回去用canvas.save()/restore()包住别在全局画布上直接setScale(-1,1)。4.2 快速连续翻页导致状态错乱现象用户快速连划两下页面翻到一半又往回弹或者直接跳到第三页。原因是上一次动画没结束就启动了新动画两个ValueAnimator同时改currentProgress。解决启动新动画前先animator.cancel()并在onAnimationEnd里判断isForward和当前进度是否匹配不匹配就丢弃这次回调。4.3 低端机上掉帧严重现象中低端设备翻页时明显卡顿阴影渐变尤其吃性能。原因是每帧都在onDraw里新建Paint、Path、LinearGradient。解决把这些对象提到成员变量里复用Path用reset()而不是new渐变只在折痕位置变化超过阈值时才重建。实测这样能把单帧绘制时间压掉三到四成。4.4 中缝处出现一条白线或黑线现象左右两页交界处有一条 1px 的缝。原因是左右页绘制区域按浮点计算canvas.clipRect取整后留下亚像素间隙。解决右页裁剪区左边界减 1px 与左页重叠或者干脆用gutter区域画一条与纸张同色的底把缝盖住。这是像素对齐的老问题别指望靠调参数根治。4.5 触摸点落在中缝附近时折痕抖动现象手指在中缝附近拖动折痕来回跳。原因是触摸点与角点距离趋近于零时中垂线方向对微小位移极度敏感。解决给触摸点设一个最小偏移量距离角点小于 20dp 时钳制到 20dp或者在中缝附近直接禁用翻页、改为点击翻页。这个坑不修用户会觉得“这翻页有毛病”。5. 从能跑到好用把翻书效果接进真实阅读器的三个技巧资源包里的实现跑起来只是起点接进真实阅读器还有几件事要做。第一件是预渲染相邻页翻页动画开始前把下一页的内容提前画到Bitmap缓存里动画过程中直接贴图避免每帧重新排版文字。我一般用LruCache存三页当前、前、后内存吃紧时按Bitmap字节数算缓存上限别按页数算不同分辨率下页数一样内存差好几倍。第二件是翻页方向与章节切换解耦。双开模式下右页翻完意味着进入下一章但用户可能只是想翻到本章最后一页。做法是把“翻页”和“换章”分成两个事件onPageFlipped只负责页内推进推进到边界才触发onChapterChanged。这样动画层永远只关心几何内容层自己管章节逻辑两边互不污染。第三件是用录制回放验证手感。翻书效果好不好光看代码看不出来我会用adb shell screenrecord录一段慢动作逐帧看折痕移动是否跟手、阴影过渡是否连续。下面这个命令录 10 秒、限制码率方便反复对比调参前后的差异# 录制 10 秒屏幕输出到本地用于逐帧检查翻页动画 adb shell screenrecord --time-limit 10 --bit-rate 4000000 /sdcard/flip.mp4 adb pull /sdcard/flip.mp4 ./flip.mp4--time-limit别设太长10 秒足够覆盖两三次完整翻页--bit-rate给到 4Mbps 能看清阴影渐变再低就糊了。录完用播放器的逐帧功能看重点看松手那一帧折痕有没有跳变、回弹结束那一帧有没有突然停住。验证项观察方法合格标准跟手性慢放拖拽段折痕与手指偏差 8dp阴影连续性逐帧看折痕两侧无突变、无闪烁回弹自然度看松手后 200ms无过冲或过冲 5%边界切换翻到章节末页无白屏、无重复页这套验证流程走下来基本能把“看着像”和“用着顺”之间的差距补上。从那以后我每次做翻书类交互都强制先录一段慢动作过一遍这张表再谈参数微调——毕竟翻书效果的玄学多半是没量化的手感问题。希望帮到你。本文还有配套的精品资源点击获取
企业数字化 ERP 产品动态
相关推荐
容器镜像仓库:架构解析与生产实践指南 1. 镜像仓库:容器生态的基石作为一名长期奋战在容器化一线的工程师,我深刻体会到镜像仓库的重要性。如果把容器比作现代软件交付的"集装箱",那么镜像仓库就是确保这些集装箱高效流通的"港口系统"。没有稳定可靠的镜像仓库… · 2026/11/5 4:43:54
中文语音输入技术:深度学习与多模态优化实践 1. 中文语音输入的现状与痛点作为一个每天要处理大量文字工作的内容创作者,我使用过市面上几乎所有主流的中文语音输入工具。从早期的讯飞输入法到后来的百度语音、搜狗听写,再到现在的微信语音转文字功能,每次切换工具都带着"这次应该能… · 2026/11/5 4:43:54
PPT组合按钮灰色无法组合?一文讲透原因与六步排查解法 做PPT的人大概都遇到过这种让人抓狂的瞬间:辛辛苦苦把几张图片、几个文本框、一个形状箭头摆成了自己满意的版式,框选之后右键,结果“组合”那一项是灰的,点不动。你以为是软件卡了,重启一遍,还是灰的&… · 2026/11/5 4:43:54
用 Dropbear 为 Buildroot ARM64 设备配置密钥登录与端口转发 用 Dropbear 为 Buildroot ARM64 设备配置密钥登录与端口转发
前几篇已经在 Ubuntu 20.04、Buildroot 2025.02.8 和 QEMU ARM64 环境中完成了服务启动、权限隔离、日志轮转与 TCP 重连实验。接下来解决设备远程维护中的一个基础问题:怎样通过 SSH 安全登录目标系统&… · 2026/11/5 5:32:08
企业名录关键词和行业条件的组合检索思路 企业名录的关键词检索基于名称和经营范围的文本匹配,下面从检索逻辑出发,拆解关键词和行业条件的组合方法。按产品关键词找客户,为什么经常搜不准?▌搜不准主要有三个原因:搜成了同行、一词多义、关键词太泛或太窄&… · 2026/11/5 5:32:08
theHarvester 入门教程:从装好到看懂输出 第一次跑 theHarvester,我敲的是这一句:
theHarvester -d example.com -b all屏幕上开始刷。先是横幅,接着一行行 Searching,然后隔几行冒出一句 [!] Missing API key for xxx。等了几分钟,翻上去一看,大半… · 2026/11/5 5:32:08
【Java】包装类泛型 包装类为什么要有包装类Java 里8 种基本数据类型(byte/short/int/long/float/double/char/boolean)不是对象,不继承Object。但是集合、泛型只能存对象,不能直接放基本类型,所以 Java 为每个基本类型提供了对应的包装类… · 2026/11/5 5:32:08
360度评估工具计费方式解析:四种口径、四个必问项与成本折算方法 更新时间:2026 年 10 月摘要
360度评估工具的计费方式可归纳为四种口径。选择哪种取决于企业用量的形状——被评人数、年度轮次与评价人构成三者共同决定成本结构。本文给出四种口径的适用边界、采购阶段必须确认的四项条款,以及用人力成本折算投入产出的… · 2026/11/5 5:31:08
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 3:16:49
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 6:59:29
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 7:02:38
多智能体协作系统架构设计与实践优化 1. 多智能体协作系统的时代背景2017年Transformer架构的诞生彻底改变了人工智能的发展轨迹。当行业还沉浸在单一大模型带来的性能突破时,Anthropic的研究团队已经将目光投向了更前沿的领域——如何让多个智能体通过协作产生超越单体能力的系统智能。这就像交响乐团中… · 2026/11/5 0:00:39
大模型系统性入门实战指南:从原理到部署微调的全链路路线 提到大模型的系统性入门,我先说个结论:这事真不难,难的是资料太散、路线太多,今天这个框架明天那个工具,新人很容易在“收藏夹吃灰”和“教程看到一半放弃”之间反复横跳。市面上关于大模型的内容少说有几十万篇&#… · 2026/11/5 0:00:39
鸿蒙文件系统沙箱机制解析与开发实践 1. 鸿蒙文件系统沙箱机制深度解析作为一名从Android转向鸿蒙开发的工程师,我深刻体会到文件系统访问是迁移过程中最先遇到的"拦路虎"。在Android上运行良好的文件读写代码,在鸿蒙真机环境却频频出现文件写入后神秘消失或直接抛出权限拒绝错误的… · 2026/11/5 0:00:39
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 3:16:49
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 6:59:29
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 7:02:38