尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

微信小程序书籍翻页效果实现:CSS 3D与手势交互

微信小程序书籍翻页效果实现:CSS 3D与手势交互 简介这是一套面向Android初、中级开发者的新闻阅读类项目源码重点演示了书籍翻页效果的实现与升级改进适合想要学习自定义View动画、页面切换交互的读者参考。资源包内共31个文件包含Java源码、class字节码、XML布局与资源、数据库db文件、图片资源以及工程配置文件还附带可安装的apk安装包方便直接运行体验效果。压缩包体积仅242KB短小精悍便于快速下载查看。目前已有316人学习使用属于轻量级但功能完整的教学型项目。通过阅读src目录下的源码可以了解翻页算法、手势监听以及新闻列表与详情页的衔接逻辑res目录中的布局和资源文件也展示了多密度适配的基本做法。作者还保留了编译生成的dex和apk文件省去重新构建的麻烦适合边看边调试。1. 拿到“新闻阅读 书籍翻页效果(升级版)”源码包先想清楚要改哪里如果只是做一个新闻列表上下滑动就够了但标题带“书籍翻页效果”的小程序源码多半是想在手机上复现纸质书/杂志的阅读体验。用户在新闻详情页里左右翻页而不是按返回键退出去换下一篇。“升级版”这三个字一般指在初版基础上补了翻页阴影、双页排版、阅读进度保存而不是换了一套翻页引擎。下面按我的习惯把这类源码包从解压到改造成自己能用的过程过一遍先用命令行把 rar 解出来再在微信开发者工具里跑起来然后定位翻页核心代码最后把新闻数据接进去。这篇内容适合刚接触小程序的开发者也适合在小程序里做图文阅读的老手。2. 先解压 .rar把项目完整导入微信开发者工具2.1 用 unar 替代 unrar省去中文文件名乱码问题遇到.rar后缀多数人第一反应是unrar x。在 Windows 上双击 WinRAR 没问题但到了 macOS 或 Linux 服务器上unrar默认不装。Debian/Ubuntu 系安装命令sudo apt install unrarmacOS 用户可以brew install unrar。装完执行unrar x 小程序源码-新闻阅读-书籍翻页效果-升级版.rar这一步最烦人的不是缺依赖而是文件名乱码。老牌打包工具在 Windows 下默认按 GBK 编码文件名macOS/Linux 终端按 UTF-8 解包解出来经常是一堆类似灏忕▼搴忓師鐮.js的乱码目录。这种目录名在微信开发者工具的project.config.json里一旦被引用编译阶段就会直接报“文件不存在”。我一般会改用unar它能自动识别压缩包内的文件名编码并转成当前系统编码# macOS brew install unar # Debian/Ubuntu sudo apt install unar unar 小程序源码-新闻阅读-书籍翻页效果-升级版.rarunar会生成一个以原压缩包名去掉扩展名命名的目录文件保持中文可读。如果手边只有 Windows用 7-Zip 打开.rar后在“选项”里把文件名编码切到 UTF-8 也能达到同样效果。2.2 核对 project.config.json 和 appid解压后不要先急着打开编辑器先确认project.config.json里的appid字段。这个文件是微信开发者工具识别项目的入口常见内容如下{ miniprogramRoot: ./, appid: touristappid, projectname: news-flip, setting: { urlCheck: true } }appid写touristappid表示工具会以游客模式运行很多 API 会被限制留空则导入时会弹出 AppID 配置窗口。我一般建议先把 AppID 改成自己的测试号这样云开发、订阅消息这类能力能提前验证。下面表格列出这类阅读源码包里最容易影响启动的文件按重要性排文件作用需要特别检查的点app.json页面注册、窗口外观、tabBar 配置pages里每个路径都必须真实存在project.config.json项目级别配置appid、miniprogramRootpages/reader/index.json阅读页单独配置是否声明了navigationStyle: customcomponents/page-flip/翻页组件usingComponents路径是否对得上不少源码包是从某个模板项目改的pages里可能残留与阅读无关的示例页面。先检查app.json的pages数组删掉不存在或与本项目无关的页面路径不然导入时会像这样报错未找到 app.json 中定义的 pages/xxx/index 对应的文件用jq可以快速验证 JSON 语法jq . app.json /dev/null echo json ok如果命令正常输出了json ok说明app.json基本没断句问题如果报 parse error就打开文件补上缺失的逗号或括号。2.3 先定位 project.config.json避免启动就报目录错误很多源码包解压后并不是一眼就能看到入口嵌套目录很常见。先执行find . -name project.config.json -maxdepth 3 -print如果结果在二级目录微信开发者工具的导入目录要选到那个目录而不是最外层。如果最外层也有project.config.json但miniprogramRoot指向了不存在的子目录工具会提示“miniprogramRoot 不存在”。此时打开文件把miniprogramRoot改成实际路径比如miniprogram/。这之后才是导入步骤微信开发者工具顶部菜单选择“项目 → 导入项目”目录选择找到的project.config.json所在目录AppID 选择“测试号”点击“确定”等依赖解析完成导入后第一件事不是点预览而是点“编译”看页面是否正常渲染。如果控制台报“找不到 app.json 中的页面”回 2.2 的pages数组检查。还有一部分老项目用的是旧版全局组件打开后会遇到“未启用增强编译”的提示先别急着开详情面板的“本地设置”里默认配置能跑通就优先保持默认。3. 书籍翻页效果背后的 CSS 3D 原理与触摸交互3.1 为什么升级版优先选择 CSS 3D 而不是 canvas 绘制“书籍翻页效果”在小程序里有三种常见实现方式。第一种是用canvas每帧绘制页面和阴影仿真度最高但代码量大、低端机掉帧明显且 canvas 的触摸坐标换算在小程序里还要手动处理 canvas 到页面的缩放。第二种是用movable-view做拖拽位移老实说那更像是刷卡不是翻书。第三种是 CSS 3D 变换把左右两半页面分别放在一个 3D 容器里通过改变rotateY让页面像书页一样立起来再倒下去。“升级版”源码包里十有八九采用第三种。原因很直接小程序渲染层对transform和opacity底层做了合成器优化不会触发主线程重排配合transition一条声明就能获得平滑动画不需要写定时器。CSS 3D 翻页的核心是三个属性perspective给容器设置透视值越小透视越强页面翻转时近大远小越明显transform-style: preserve-3d让子元素按 3D 空间呈现而不是被压扁到平面backface-visibility: hidden页面翻转到超过 90 度后隐藏避免出现背面镜像内容。3.2 最小翻页组件的 WXML 与 WXSS一个能演示动画的翻页组件只需要两个半页。WXML 结构如下view classbook catch:touchstartonTouchStart catch:touchendonTouchEnd view classpage left-page styletransform: rotateY({{leftAngle}}deg);/view view classpage right-page styletransform: rotateY({{rightAngle}}deg);/view view classshadow/view /view对应的核心样式.book { position: relative; width: 700rpx; height: 1000rpx; margin: 100rpx auto; perspective: 2000rpx; transform-style: preserve-3d; } .page { position: absolute; top: 0; width: 50%; height: 100%; backface-visibility: hidden; transition: transform 0.45s ease; background: #fff; } .left-page { left: 0; transform-origin: left center; /* 沿左边缘翻转 */ } .right-page { right: 0; transform-origin: right center; } .shadow { position: absolute; inset: 0; box-shadow: 4rpx 0 16rpx rgba(0,0,0,0.3); }这里left-page的旋转轴是左边缘当leftAngle从 0 变为 -180 时页面像左半页一样翻起right-page的旋转轴是右边缘从 0 变为 180 时右半页往左倒。transition: transform 0.45s ease决定动画用时想要更接近纸质书的手感可以改成cubic-bezier(0.22, 1, 0.36, 1)后半段回弹更干脆。3.3 touch 手势判定与动画锁只有 CSS 还不够升级版要能根据手指滑动方向决定翻哪一页。常见做法是在容器上监听touchstart、touchmove、touchend计算手指位移的方向和距离。这里给一个只依赖 start 和 end 的简化版本// reader.js 中管理翻页状态 const RIGHT_PAGE 1; const LEFT_PAGE 2; Page({ data: { leftAngle: 0, rightAngle: 0, currentPage: 1 }, onLoad() { this.animating false; // 动画锁避免连续触发 this.startX 0; this.startY 0; }, onTouchStart(e) { if (this.animating) return; const touch e.touches[0]; this.startX touch.clientX; this.startY touch.clientY; }, onTouchEnd(e) { if (this.animating) return; const touch e.changedTouches[0]; const dx touch.clientX - this.startX; const dy touch.clientY - this.startY; // 横向位移大于竖向且超过 40px 才触发翻页 if (Math.abs(dx) Math.abs(dy) Math.abs(dx) 40) { if (dx 0) this.flipPage(RIGHT_PAGE); else this.flipPage(LEFT_PAGE); } }, flipPage(direction) { this.animating true; if (direction RIGHT_PAGE) { this.setData({ leftAngle: -180, currentPage: this.data.currentPage 1 }); } else { this.setData({ rightAngle: 180, currentPage: Math.max(1, this.data.currentPage - 1) }); } setTimeout(() { this.animating false; this.setData({ leftAngle: 0, rightAngle: 0 }); }, 450); } });这段代码里touchstart只记录起点touchend才判断方向。Math.abs(dx) Math.abs(dy)保证用户上下滑动新闻时不会误触发翻页40px是一个比较稳的经验值太小容易误触太大则快速滑动没反应。animating是核心翻页动画进行中不响应任何新 touch否则动画还没结束下一次setData又来了页面会卡在半翻转状态。实际源码会在重置角度的setData里把新一页的文本和图片同时换掉上面只演示动画状态机。换内容的操作要放在flipPage那个 450ms 的setTimeout里和角度重置同一批次执行避免出现“页面翻过去了但内容还是旧的”的闪烁。3.4 升级版补的阴影、双页和进度记忆初版效果往往只有页面旋转升级版会在动画过程中补一个阴影层。更精细的做法是绑定transitionend事件在页面翻起时增加对侧页面的明亮度this.setData({ leftAngle: -180, shadowOpacity: 0.25 });阴影用渐变模拟.book-shadow { position: absolute; top: 0; left: 50%; width: 50%; height: 100%; background: linear-gradient(90deg, rgba(0,0,0,0.18), transparent); opacity: 0; pointer-events: none; }双页模式是把内容同时排到左右两半left-page放当前页左侧right-page放右侧翻页后页码 2。阅读进度记忆一般放在onHide和onUnload里写入本地存储这样用户退出小程序再进来还能回到上次停住的位置。这三样做完基本就配得上“升级版”三个字了。4. 新闻数据怎么塞进书页并保持分页稳定4.1 长新闻按页拆分的测量方案书籍翻页组件本身只负责动画内容既可以直接每页一条新闻也可以把一篇长新闻按高度拆成多页。后者是新闻阅读场景的难点新闻文本长度不定、图片大小不定拆错了要么最后一页空一截要么内容被截断。一个稳定可复现的思路是先把完整新闻渲染到一个看不见的容器里拿到总高度再除以单页高度。下面是封装在页面里的测量方法measureNewsToPages() { return new Promise((resolve) { const query wx.createSelectorQuery(); query.select(#article-hidden).fields({ size: true, scrollOffset: true }, (res) { if (!res) { resolve({ pages: 1 }); return; } const pageHeight this.data.readerHeight || 1; // 单页显示高度单位 px const totalHeight res.scrollHeight || res.height; const pages Math.ceil(totalHeight / pageHeight); resolve({ pages, totalHeight, pageHeight }); }).exec(); }); }解释一下#article-hidden是页面上一个固定宽度但高度设为auto的隐藏节点把新闻 HTML 通过rich-text渲染在里面。fields的scrollHeight能拿到内容滚动高度即使容器本身没有限制高度只要内容溢出了这个值就是内容的实际高度。得到pages后读者可以一次翻一页也可以一次翻一屏。注意这里必须等rich-text渲染完成后再测量所以调用measureNewsToPages前要先wx.nextTick。有一个容易被忽略的细节如果这个测量方法写在自定义组件里wx.createSelectorQuery()后面要加.in(this)否则 select 查的是整个页面而不是组件内部。页面里使用则不需要。4.2 列表到详情的消息传递与进度记录新闻列表到阅读页常见的数据流是跳转时带一个newsId阅读页根据newsId去本地缓存或服务端拉全文。这里不建议直接把整篇内容放到 URL 参数里小程序url长度有限内容一长会被截断。我一般会在新闻列表页里const currentNews this.data.newsList[index]; wx.setStorageSync(currentNews, currentNews); wx.navigateTo({ url: /pages/reader/index?newsId currentNews.id });阅读页onLoad拿到newsId后先从本地缓存恢复进度const saved wx.getStorageSync(reading_ newsId); if (saved saved.pageNo) { this.setData({ currentPage: saved.pageNo }); }这样每翻一页记录一次写进 Storage 的频率要控制不能每次setData都同步写。常见做法是用“节流”页码变化后只更新内存onHide或onUnload时一次性写盘。数据字段可以参考下面这张表覆盖新闻阅读的最小集合字段类型说明newsIdstring新闻唯一 ID用于进度 keytitlestring标题sourcestring来源名称publishTimestring发布时间contentstringHTML 片段coverImagesarray封面图 URL 列表pageNonumber记录当前翻到第几页如果新闻接口返回的是纯文本需要先转成简单的 HTML 结构用p包住每一个段落图片用img标签。rich-text组件对部分标签支持有限video、iframe这类不建议直接放进去。4.3 字号调整后的重排防抖新闻阅读器几乎都要支持调字号。字号一旦变化原先测量出的页数就作废必须重新测量。如果用户在快速调大的过程中连续点击“”测量函数可能被触发十几次最后拿到的高度还是旧字号渲染的结果。解决办法是加一个防抖每次调整字号只更新data等字号稳定 300ms 后再重新测量分页。小程序没有现成的 debounce可以这样写remeasure() { if (this.timer) clearTimeout(this.timer); this.timer setTimeout(() { this.setData({ pageContent: this.data.content }, () { wx.nextTick(() { this.measureNewsToPages().then(({ pages }) { this.setData({ totalPages: pages, currentPage: 1 }); this.animating false; }); }); }); }, 300); }这里的setData会触发rich-text重新渲染wx.nextTick保证在渲染完成后读取scrollHeight否则读到的还是上一次高度。按经验字号按钮放在屏幕右侧竖排每按一次就调用remeasure页面不会出现“内容翻到一半突然多出几页”的尴尬。图片加载也要做同样处理图片加载完成事件里重新触发一次remeasure否则图文混合的新闻会在图片加载前后页数不一致。5. 真机调试时专门验证翻页效果的四个习惯5.1 先看性能面板里的 FPS再决定要不要优化微信开发者工具的“性能 Warnings”面板只能参考最好用真机调试。打开调试图切到“性能”标签开始录制然后连翻十几页观察帧率曲线。如果低于 40 FPS先检查翻页动画里是否滚动到了setData。翻转过程中只应改transform像width、height、top这类属性一旦频繁变化浏览器会做代价更高的布局计算。5.2 用图片按需加载控制内存新闻页经常大图多翻页后不可见的页面仍然持有图片的渲染对象。可以用wx.createIntersectionObserver监听每个.book-page节点与屏幕的交叉状态离开视口后把图片src置空const observer wx.createIntersectionObserver(this); observer.relativeTo(.book).observe(.book-page, (res) { const isVisible res.intersectionRatio 0; const imgSelector #page- res.dataset.pageIndex -image; if (isVisible) { this.setData({ [imgSelector]: this.data.realSrc }); } else { this.setData({ [imgSelector]: }); } });这样不可见页面不会一直占着图片解码后的内存。注意dataset的值只能从 WXML 里的style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;" />
返回列表