一、前言
页面内容超出屏幕是常态,scroll-view 就是小程序提供的"可滚动容器":给它一个固定的高度(或宽度),加上 scroll-y 或 scroll-x,容器内部就能独立于页面滚动。它比页面级滚动更可控——可以指定初始位置、监听滚动到顶/到底、监听滚动过程本身,下拉刷新和上拉加载几乎都建立在它之上。
本案例在一个页面里并排放两个滚动视图:上面是纵向滚动(三个整屏色块,初始定位到第二页),下面是横向滚动(三屏色块,初始偏移 50px),滚动过程中在控制台打印信息。案例还带出了几个容易被忽略的配套知识:skyline 渲染引擎不支持\n换行所以要切回 webview;横向滚动的三个 CSS 前提;以及用伪元素在色块正中叠加文字的经典定位写法。
二、核心概念
scroll-view 组件:可滚动视图区域。scroll-y="true"允许纵向滚动,scroll-x="true"允许横向滚动,二者都需要容器有确定的尺寸,否则滚不起来。
scroll-into-view:值为某子元素的 id,组件初始化或该值变化时自动滚动到该元素位置。案例设为 "page2",所以纵向视图一打开就停在第二页。id 不能以数字开头。
scroll-left:横向滚动条的初始位置,单位 px。案例设为 50,初始显示第一页的一部分和第二页的一小部分。对应纵向的是 scroll-top。
bindscrolltoupper / bindscrolltolower:滚动到顶部/左侧、底部/右侧时触发,是"下拉加载上一页、上拉加载下一页"的事件基础。
bindscroll:滚动过程中持续触发,事件对象里带 scrollLeft/scrollTop/scrollHeight 等,频率很高,回调里不宜做重活。
white-space: nowrap:横向滚动的 CSS 前提之一。子元素默认会自动换行,容器就"不需要滚了",所以必须禁止换行,让内容溢出。
display: inline-block:横向滚动子元素的排布方式。块级元素各占一行会换行,行内元素又设不了宽高,inline-block 两者兼得。
::before 伪元素:在元素内部最前面插入内容。案例给每个色块用content: '第一页'加绝对定位 + translate(-50%, -50%) 把文字钉在正中央。
webview 渲染引擎:文字里的\n换行符 skyline 引擎不支持,需在 app.json 中把 renderer 改为 webview——这是本案例建项目后的第一步。
三、项目文件结构
scroll-view-demo/ ├── app.js 小程序逻辑,注册小程序实例 ├── app.json 全局配置(renderer 改为 webview) ├── app.wxss 全局样式(.box、.title) ├── sitemap.json 微信索引配置 ├── project.config.json 项目配置(appid、编译设置等) ├── components/ │ └── navigation-bar/ 自定义导航栏组件 └── pages/ └── index/ ├── index.js 页面逻辑:upper/lower/scroll_y/scroll_x ├── index.json 页面配置:注册自定义组件 ├── index.wxml 页面结构:纵向 + 横向两个滚动视图 └── index.wxss 页面样式:色块尺寸、伪元素文字、不换行四、代码实现
1. pages/index/index.wxml【结构页面】
<navigation-bar title="scroll-view组件" back="{{false}}" color="black" background="#FFF"></navigation-bar> <scroll-view class="scrollarea" scroll-y type="list"> <view class="box"> <view class="title">滚动视图示例</view> <view class="page-section-title"> <text>Vertical Scroll\n纵向滚动</text> </view> <scroll-view scroll-y="true" style="height: 300rpx;" class="sheet" scroll-into-view="page2" bindscrolltoupper="upper" bindscrolltolower="lower" bindscroll="scroll_y"> <view id="page1" class="scroll-view-item-y page1"></view> <view id="page2" class="scroll-view-item-y page2"></view> <view id="page3" class="scroll-view-item-y page3"></view> </scroll-view> <view class="page-section-title"> <text>Horizontal Scroll\n横向滚动</text> </view> <scroll-view scroll-x="true" style="width: 100%;" scroll-left="50" class="scroll-view-x sheet" bindscroll="scroll_x"> <view id="page1" class="scroll-view-item-x page1"></view> <view id="page2" class="scroll-view-item-x page2"></view> <view id="page3" class="scroll-view-item-x page3"></view> <view id="page4" class="scroll-view-item-x page4"></view> </scroll-view> </view> </scroll-view>2. pages/index/index.js【页面数据】
// index.js Page({ /** * 事件处理函数 */ upper(e) { console.log('to upper'); }, /** * 事件处理函数 */ lower(e) { console.log('to lower'); }, /** * 事件处理函数 */ scroll_y(e) { console.log('vertical scrolling'); }, /** * 事件处理函数 */ scroll_x(e) { console.log('Horizontal Scrolling'); } })3. pages/index/index.wxss【页面样式】
/**index.wxss**/ page { height: 100vh; /* flex布局 */ display: flex; /* 按纵向排列 */ flex-direction: column; } .scrollarea { flex: 1; overflow-y: hidden; } .page-section-title { font-size: 40rpx; text-align: center; margin-top: 40rpx; margin-bottom: 20rpx; } .scroll-view-item-y { height: 300rpx; } .scroll-view-item-x { /* 创建了一个内联级块容器 */ display: inline-block; width: 100%; height: 300rpx; } .sheet { font-size: 50rpx; /* 字符间距 */ letter-spacing: 1.2em; } .page1 { /* 相对定位 */ position: relative; background-color: #1AAD19; color: #FFFFFF; } /* 伪类选择器 */ .page1::before { content: '第一页'; /* 绝对定位 */ position: absolute; /* 元素从父元素的顶部向下移动50% */ top: 50%; /* 元素从父元素的最左端往右移动50% */ left: 50%; /* 元素在水平方向上向左移动自身宽度的50%,向上移动自身宽度的50% */ transform: translate(-50%, -50%); } .page2 { position: relative; background-color: #2782D7; color: #FFFFFF; } .page2::before { content: '第二页'; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } .page3 { position: relative; background-color: purple; color: #FFFFFF; } .page3::before { content: '第三页'; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } /* 作业扩展:横向滚动视图新增的第四项 */ .page4 { position: relative; background-color: #FF9500; color: #FFFFFF; } .page4::before { content: '第四页'; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } .scroll-view-x { /* 不换行 */ white-space: nowrap; }4. app.wxss【全局样式】
/**app.wxss**/ .box { margin: 20rpx; padding: 20rpx; border: 2rpx solid silver; } .title { font-size: 40rpx; font-weight: bolder; text-align: center; margin-bottom: 30rpx; color: red; }5. app.json【全局配置】
新项目默认使用 skyline 渲染引擎,但它在文字中不支持\n这样的换行符,所以要删掉"renderer": "skyline"或改为 webview:
{ "pages": [ "pages/index/index" ], "window": { "navigationBarTextStyle": "black", "navigationStyle": "custom" }, "style": "v2", "renderer": "webview", "componentFramework": "glass-easel", "sitemapLocation": "sitemap.json", "lazyCodeLoading": "requiredComponents" }五、运行效果
页面顶部是自定义导航栏「scroll-view组件」,银色边框的盒子里是红色标题「滚动视图示例」。第一组标题"Vertical Scroll / 纵向滚动"分两行显示——这就是\n在 webview 引擎下的效果。标题下方是一个 300rpx 高的纵向滚动容器,打开页面时直接停在第二页(scroll-into-view="page2" 生效):蓝色底白字"第二页"铺满视口,上下各露出一点点绿色"第一页"和紫色"第三页"。用手指上下拖动,可以滚回第一页、滚到第三页;滚到最顶上时控制台打印to upper,滚到最底下打印to lower,拖动的全程持续打印vertical scrolling。
第二组标题"Horizontal Scroll / 横向滚动"下方是横向滚动容器,初始位置距最左侧 50px:视口里能看到第一页的大部分和第二页的一小条,横向拖动依次经过绿、蓝、紫、橙四个色块,每个色块正中都钉着"第一页"到"第四页"的白字——第四项就是作业扩展新增的,橙色 #FF9500,写法和前三项完全一样。横向拖动全程控制台持续打印Horizontal Scrolling。
六、知识点总结
1、scroll-view 能滚的两个前提缺一不可:容器有确定尺寸(纵向style="height: 300rpx",横向width: 100%)+滚动方向开关(scroll-y / scroll-x)。没有高度的 scroll-y 容器会被内容撑开,看起来"根本不需要滚"——这是初学者最常踩的坑。
2、scroll-into-view="page2"让视图初始就定位到第二页,它的值是子元素的 id且不能以数字开头("2page" 非法,"page2" 合法)。这个属性还有个进阶用法:把它的值绑到 data 上,值一变视图就滚过去,是"点击目录跳转到对应章节"这类需求的实现基础。注意它只在值变化时滚动,因此滚动后想再回到同一位置,要先清空再赋值。
3、本案例纵向和横向两个 scroll-view 的子元素用了重复的 id(都有 page1/page2/page3)。功能上没问题——scroll-into-view 只在自己容器内查找——但 id 重复不符合语义,也给全局选择器埋雷。作业扩展的 page4 只加在横向视图,正是为避免和纵向的 page4 撞名;如果两个视图都要四页,应把 id 设计成 v-page4 / h-page4。
4、横向滚动靠"一条 CSS 组合拳":容器white-space: nowrap禁止换行,子元素display: inline-block让它们既保持行内排列又能设宽高,两者缺一不可——少了 nowrap,子元素放不下会换行,容器就不滚了;少了 inline-block,块级子元素各占一行,同样不滚。此外子元素之间的换行和空格也会形成间隙(行内元素的固有特性),要求严丝合缝时应把子元素写在同一行或用 flex 布局。
5、scroll-left="50"的初始偏移单位是px而不是 rpx,纵向的 scroll-top 同理。混用单位是 scroll 类属性出错的常见原因:CSS 里写 rpx、属性里写 px,两套坐标不在一个体系里。
6、bindscroll 触发频率非常高——滚动期间基本每帧一次。回调里只打印一行没问题,但一旦开始做 setData 或复杂计算,滚动就会明显掉帧。需要"边滚边响应"时(如标题栏吸顶变色),正确做法是节流(throttle),或者改用节流版的 bindscroll 的差值判断。
7、色块正中的"第一页"文字用的是伪元素三件套:父元素position: relative建立定位上下文,伪元素position: absolute; top: 50%; left: 50%把左上角放到中心,再transform: translate(-50%, -50%)往回挪自身的一半——这是"水平垂直居中"的经典写法,比 flex 更老但兼容性更好。content属性是伪元素生效的必要条件,漏写整个伪元素不渲染。
8、切 webview 引擎的原因值得记住:skyline 不支持文字里的\n。这个知识点反过来也成立——如果页面在 skyline 下文字不换行,把\n换成两个 text 标签或改用 webview 都能解决。渲染引擎是很多"文档说支持但实际没效果"问题的根源。
9、page { height: 100vh; display: flex; }加.scrollarea { flex: 1 }的外层骨架,让最外层 scroll-view 占满视口剩余高度,页面级滚动由此交给了这个容器。100vh 是视口高度,flex: 1 是"除去导航栏后的剩余空间",两者配合才能正确撑起滚动区。
10、作业扩展 page4 的做法本身没有新知识——复制一个色块改个 id 和颜色——但检查点在于:新色块必须在滚动方向的末端被完整滚到。如果横向容器加了width: 100%之外的约束,或子元素宽度不是 100%,新增项可能挤在视口内而"根本感觉不到多了东西",验证扩展点要看滚动行为而不只是页面截图。
七、拓展思考
scroll-view 是小程序列表交互的地基:商品分类页的左侧分类 + 右侧商品区是两个联动的 scroll-view(点左侧分类改 scroll-into-view,滚右侧反查当前分类);聊天室是 scroll-view 加 scroll-top 钉底,新消息来了自动滚到最下;朋友圈式信息流是 bindscrolltolower 触发分页加载。这些场景共同的进阶话题都是性能——长列表要用虚拟渲染,图片要懒加载,滚动事件要节流。
还有两个官方给的"浓缩版"值得知道:onReachBottom/onPageScroll是页面级滚动的对应物,不需要 scroll-view 就能实现上拉加载,适合整页都是列表的简单场景;而 scroll-view 自带的refresher-enabled下拉刷新属性,可以把自定义 loading 状态和刷新逻辑直接挂在容器上。理解"页面级滚动"和"容器级滚动"的边界,才能在每种需求面前选对工具,而不是所有滚动都无脑套 scroll-view。