uni-app 滚动视图scroll-view从入门到精通

发布于:2025-06-13 ⋅ 阅读:(17) ⋅ 点赞:(0)

uni-app 滚动视图scroll-view从入门到精通,这篇就够了!(内含保姆级教程和示例) 🚀

scroll-view官网

哈喽,各位小伙伴们! 👋

在开发 uni-app 的道路上,你是否也曾被 scroll-view 这个“磨人的小妖精”折磨得抓耳挠腮?明明代码照着文档敲了,可它就是不滚动!或者,你想实现一个炫酷的下拉刷新、一个丝滑的触底加载,却不知从何下手?

别慌!今天,我就带你把 scroll-view 的所有“脾气”都摸透,从基础用法到高阶技巧,一网打尽。看完这篇,保证你也能成为 scroll-view 大师!

黄金法则:开始前必须知道的两件事 ⚠️

在展示任何代码之前,请把下面这两条法则刻在你的DNA里,99% 的 scroll-view 不工作问题都源于此:

  1. 竖向滚动 (scroll-y):你必须<scroll-view> 组件设置一个明确的、固定的高度。无论是 height: 500px 还是 height: 100vh,总之不能让它“随心所欲”。因为它需要知道自己的可视区域有多大,才能判断内容是否超出了、何时该滚动。
  2. 横向滚动 (scroll-x):你必须<scroll-view>内部子元素们设置 white-space: nowrap; 样式,告诉它们:“兄弟们,不许换行,给我排成一队!”。通常我们还会让这些子元素 display: inline-block;

记住了吗?好,让我们开始实战!

场景一:基础竖向滚动与“无限加载” (触底加载) 📱

这是最常见的需求:一个长长的列表,用户滑到底部时,自动加载更多内容。

核心武器:

  • scroll-y: 开启竖向滚动。
  • @scrolltolower: 当滚动条接近底部时触发的“神仙事件”。
  • lower-threshold: 设置离底部多远时触发 @scrolltolower,避免用户真的要滑到最最最底才加载。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

直接上代码:

<template>
	<view class="container">
		<view class="header">
			<text class="title">Demo 1: 基础与触底加载</text>
			<text class="scroll-info">当前滚动位置: {{ currentScrollTop.toFixed(0) }}px</text>
			<button size="mini" @click="goTop">点我返回顶部</button>
		</view>

		<!-- 
			scroll-y: 开启纵向滚动
			:scroll-top: 动态绑定滚动条位置,用于程序化控制
			:scroll-with-animation: 使 scroll-top 的改变产生动画效果
			@scroll: 监听滚动事件,实时获取滚动信息
			@scrolltolower: 滚动到底部时触发
			lower-threshold: 距离底部20px时就触发 scrolltolower
		-->
		<scroll-view class="list-scroll" scroll-y :scroll-top="scrollTopValue" scroll-with-animation @scroll="onScroll"
			@scrolltolower="loadMore" :lower-threshold="20">
			<!-- 列表内容 -->
			<view v-for="(item, index) in dataList" :key="index" class="list-item">
				<text>第 {{ item }} 项数据</text>
			</view>

			<!-- 加载状态提示 -->l
			<view class="loading-status">
				<text v-if="loadingStatus === 'loading'">努力加载中...</text>
				<text v-if="loadingStatus === 'no-more'">--- 我是有底线的 ---</text>
			</view>
		</scroll-view>
	</view>
</template>

<script>
	export default {
		data() {
			return {
				dataList: [], // 列表数据
				page: 1, // 当前页码
				loadingStatus: 'loading', // 加载状态:'loading', 'no-more'
				scrollTopValue: 0, // 控制滚动条位置,0表示顶部
				currentScrollTop: 0 // 用于显示当前滚动位置
			}
		},
		onLoad() {
			// 页面加载时,获取初始数据
			this.fetchData();
		},
		methods: {
			// 模拟从服务器获取数据
			fetchData() {
				this.loadingStatus = 'loading';
				console.log(`正在请求第 ${this.page} 页数据...`);

				// 模拟网络请求
				setTimeout(() => {
					const newItems = [];
					for (let i = 0; i < 20; i++) {
						newItems.push((this.page - 1) * 20 + i + 1);
					}

					this.dataList = [...this.dataList, ...newItems];

					// 模拟数据加载完毕的情况
					if (this.page >= 5) {
						this.loadingStatus = 'no-more';
					} else {
						this.loadingStatus = ''; // 重置状态,准备下一次加载
					}

					console.log('数据加载完成!');
				}, 1000); // 模拟1秒延迟
			},

			/**
			 * @scrolltolower 事件触发的方法
			 * 滚动到底部时调用
			 */
			loadMore() {
				// 如果当前已经是'no-more'状态,或者正在加载中,则不执行任何操作
				if (this.loadingStatus === 'no-more' || this.loadingStatus === 'loading') {
					console.log('已无更多数据或正在加载,跳过本次请求。');
					return;
				}
				this.page++;
				this.fetchData();
			},

			/**
			 * @scroll 事件触发的方法
			 * 滚动过程中实时触发
			 */
			onScroll(event) {
				// event.detail 包含了滚动的所有信息 {scrollLeft, scrollTop, scrollHeight, scrollWidth, deltaX, deltaY}
				// console.log(event.detail);
				this.currentScrollTop = event.detail.scrollTop;
			},

			/**
			 * 返回顶部
			 */
			goTop() {
				// scroll-top 的一个技巧:如果连续设置相同的值,第二次不会生效。
				// 所以我们先设置为一个很小但不同的值,再在下一次渲染后设置为0.
				this.scrollTopValue = 0.1;
				this.$nextTick(() => {
					this.scrollTopValue = 0;
				});
			}
		}
	}
</script>

<style>
	.container {
		height: 100vh;
		display: flex;
		flex-direction: column;
	}

	.header {
		padding: 20rpx;
		border-bottom: 1rpx solid #eee;
		background-color: #f8f8f8;
	}

	.title {
		font-size: 32rpx;
		font-weight: bold;
	}

	.scroll-info {
		font-size: 24rpx;
		color: #666;
		margin-left: 20rpx;
	}

	.list-scroll {
		/* 这是关键!必须给 scroll-view 一个固定的高度 */
		flex: 1;
		height: 0;
		/* flex:1 和 height:0 是一个经典的组合,让元素撑满剩余空间 */
		box-sizing: border-box;
	}

	.list-item {
		padding: 30rpx 20rpx;
		border-bottom: 1rpx solid #f0f0f0;
		background-color: #fff;
	}

	.loading-status {
		padding: 20rpx;
		text-align: center;
		color: #999;
		font-size: 24rpx;
	}
</style>

小贴士:代码里还演示了 @scroll (实时监听滚动) 和 scroll-top (JS控制滚动条位置) 的用法,比如做个“返回顶部”的按钮,用户体验UP!

场景二:自定义下拉刷新与横向滚动 🔄

想让你的App看起来更专业?自定义下拉刷新和横向滑动的商品分类绝对是加分项!

核心武器:

  • refresher-enabled: 开启下拉刷新功能。
  • :refresher-triggered: 一个双向绑定的“开关”,true 就显示刷新动画,false 就收起。这是控制的关键
  • @refresherrefresh: 当用户下拉到足够距离并松手后,触发这个事件,你就可以在这里请求新数据了。
  • scroll-x: 开启横向滚动。
  • scroll-into-view: 可以让 scroll-view 自动滚动到指定的子元素ID处。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
代码秀:

<template>
	<view class="container">
		<view class="header">
			<text class="title">Demo 2: 下拉刷新与横向滚动</text>
			<button size="mini" @click="goToGreenItem">滚动到绿色块</button>
		</view>

		<!-- 
			refresher-enabled: 开启下拉刷新
			:refresher-triggered: 控制刷新动画的显示/隐藏,true为显示
			@refresherrefresh: 核心!触发刷新时调用此方法
			refresher-default-style="none": 不使用默认的三个点样式,方便完全自定义
		-->
		<scroll-view class="main-scroll" scroll-y refresher-enabled :refresher-triggered="isRefreshing"
			@refresherrefresh="onRefresh">
			<!-- 自定义下拉刷新视图 -->
			<view v-if="isRefreshing" class="custom-refresher">
				<image class="loading-icon" src="/static/loading.gif"></image> <!-- 请准备一个loading.gif图 -->
				<text>正在刷新...</text>
			</view>

			<!-- 横向滚动区域 -->
			<view class="section-title">横向滚动区域</view>
			<!-- 
				scroll-x: 开启横向滚动
				:scroll-into-view: 值应为某子元素id,滚动到该元素
				show-scrollbar: 隐藏横向滚动条
			-->
			<scroll-view class="horizontal-scroll" scroll-x :scroll-into-view="targetItemId" scroll-with-animation
				:show-scrollbar="false">
				<view class="horizontal-item" v-for="item in horizontalList" :key="item.id" :id="item.id"
					@click="handleClick(item.id)" :style="{ backgroundColor: item.color }">
					<text>{{ item.text }}</text>
				</view>
			</scroll-view>

			<!-- 纵向列表内容 -->
			<view class="section-title">纵向列表</view>
			<view v-for="(item, index) in dataList" :key="index" class="list-item">
				<text>数据项 {{ item }}</text>
			</view>
		</scroll-view>
	</view>
</template>

<script>
	export default {
		data() {
			return {
				isRefreshing: false, // 控制下拉刷新状态
				dataList: ["初始数据1", "初始数据2", "初始数据3", "初始数据4", "初始数据5"],
				horizontalList: [{
						id: 'red-item',
						text: '红色块',
						color: '#F56C6C'
					},
					{
						id: 'orange-item',
						text: '橙色块',
						color: '#E6A23C'
					},
					{
						id: 'yellow-item',
						text: '黄色块',
						color: '#F2D33D'
					},
					{
						id: 'green-item',
						text: '绿色块',
						color: '#67C23A'
					}, // 目标元素
					{
						id: 'blue-item',
						text: '蓝色块',
						color: '#409EFF'
					},
					{
						id: 'purple-item',
						text: '紫色块',
						color: '#9049DE'
					},
				],
				targetItemId: ''
			}
		},
		methods: {
			/**
			 * @refresherrefresh 触发的刷新方法
			 */
			onRefresh() {
				if (this.isRefreshing) return;
				console.log('开始刷新...');
				this.isRefreshing = true;

				// 模拟网络请求
				setTimeout(() => {
					const newData = `新数据 ${new Date().toLocaleTimeString()}`;
					this.dataList.unshift(newData); // 在列表顶部插入新数据

					// 关键:刷新完成后,必须手动把 isRefreshing 设置为 false,收起刷新动画
					this.isRefreshing = false;
					console.log('刷新完成!');
					uni.showToast({
						title: '刷新成功',
						icon: 'success'
					});
				}, 1500);
			},

			/**
			 * 滚动到指定横向元素
			 */
			goToGreenItem() {
				this.targetItemId = 'green-item';
				// 小技巧:使用后清空,以便下次还能滚动到同一个id
				setTimeout(() => {
					this.targetItemId = '';
				}, 200);
			},
			handleClick(id) {
				this.targetItemId = id;
			}
		}
	}
</script>

<style>
	/* 通用样式,与上例部分重叠 */
	page {
		height: 100%;
		overflow: hidden;
	}

	.container {
		height: 100%;
		display: flex;
		flex-direction: column;
	}

	.header {
		padding: 20rpx;
		border-bottom: 1rpx solid #eee;
		background-color: #f8f8f8;
		display: flex;
		justify-content: space-between;
		align-items: center;
	}

	.title {
		font-size: 32rpx;
		font-weight: bold;
	}

	.main-scroll {
		flex: 1;
		height: 0;
	}

	.list-item {
		padding: 30rpx 20rpx;
		border-bottom: 1rpx solid #f0f0f0;
	}

	.section-title {
		padding: 20rpx;
		background-color: #f5f5f5;
		color: #666;
		font-size: 28rpx;
	}

	/* --- 本示例新增样式 --- */
	.custom-refresher {
		display: flex;
		align-items: center;
		justify-content: center;
		padding: 20rpx;
		color: #666;
	}

	.loading-icon {
		width: 40rpx;
		height: 40rpx;
		margin-right: 15rpx;
	}

	.horizontal-scroll {
		/* 这是关键!white-space:nowrap 防止子元素换行 */
		white-space: nowrap;
		width: 100%;
		padding: 20rpx 0;
		background-color: #fff;
	}

	.horizontal-item {
		/* 这是关键!inline-block 使元素在一行内排列 */
		display: inline-block;
		width: 250rpx;
		height: 180rpx;
		margin: 0 15rpx;
		border-radius: 10rpx;
		display: inline-flex;
		align-items: center;
		justify-content: center;
		color: #fff;
		font-weight: bold;
	}
</style>

划重点:下拉刷新的逻辑就是:@refresherrefresh 里把 isRefreshing 设为 true,等数据加载完了再把它设回 false。就这么简单!

场景三:那些不常用但超有用的“黑魔法”属性 ✨

还有一些属性,平时可能用得少,但在特定场景下能救你一命。

  • enable-back-to-top: (仅App和部分小程序) 用户点击手机顶部状态栏,或者双击安卓标题栏时,滚动条能自动回到顶部。开启它,能让你的App更像原生App。
  • enable-flex: (仅微信小程序) 默认情况下 scroll-view 不支持作为 flex 容器。开启它之后,就可以在 scroll-view 上使用 display: flex 了,布局更自由!

这些属性都是布尔值,直接在标签里写上就行,比如 <scroll-view scroll-y enable-back-to-top>

终极属性/事件速查表(建议收藏)

属性/事件 功能一句话总结
scroll-x / scroll-y 开启横/纵向滚动。
@scrolltolower 滚动到底部,用来加载更多
@scrolltoupper 滚动到顶部时触发。
@scroll 只要在滚,就一直触发,信息最全。
scroll-top / scroll-left JS控制滚动条位置,做返回顶部
scroll-into-view 滚动到指定ID的子元素,做锚点定位
refresher-enabled 开启自定义下拉刷新。
:refresher-triggered 控制下拉刷新动画的显示和隐藏(true/false)。
@refresherrefresh 执行下拉刷新逻辑的地方。

总结

scroll-view 看似复杂,其实只要掌握了核心规则和几个关键的属性事件,就能玩得转。

记住:

  1. 给高度!给高度!给高度! (竖向滚动)
  2. 不换行!不换行!不换行! (横向滚动)
  3. @scrolltolower 做上拉加载。
  4. refresher 全家桶做下拉刷新,refresher-triggered 是控制动画的开关。

希望这篇保姆级的教程能帮你彻底扫清 scroll-view 的障碍。现在,去你的项目里大展身手吧!

如果觉得有帮助,别忘了点赞、收藏哦!有什么问题,也欢迎在评论区留言讨论!👇


网站公告

今日签到

点亮在社区的每一天
去签到