赞
踩
scroll-view 是一个可以滚动的视图区域的容器组件。
scroll-view 的滚动属性,实现了两套功能
scroll-x
允许横向滚动scroll-y
允许纵向滚动纵向滚动
<scroll-view scroll-y style="height: 300rpx;">
<view id="demo1" class="scroll-view-item demo-text-1"></view>
<view id="demo2" class="scroll-view-item demo-text-2"></view>
<view id="demo3" class="scroll-view-item demo-text-3"></view>
</scroll-view>
横向滚动
<scroll-view class="scroll-view_H" scroll-x style="width: 100%">
<view id="demo1" class="scroll-view-item_H demo-text-1"></view>
<view id="demo2" class="scroll-view-item_H demo-text-2"></view>
<view id="demo3" class="scroll-view-item_H demo-text-3"></view>
</scroll-view>
双向滚动
<scroll-view bindscroll="onScroll" class="scroll-view_H" scroll-x scroll-y style="width: 100%;height:200rpx;">
<view id="demo1" class="scroll-view-item_H demo-text-1">1</view>
<view id="demo2" class="scroll-view-item_H demo-text-2">2</view>
<view id="demo3" class="scroll-view-item_H demo-text-3">3</view>
</scroll-view>
.scroll-view_H { white-space: nowrap; } .scroll-view-item { height: 300rpx; } .scroll-view-item_H { display: inline-block; width: 100%; height: 300rpx; } .demo-text-1 { background-color: #E6E6FA; } .demo-text-2 { background-color: #E1FFFF; } .demo-text-3 { background-color: #FDF5E6; }
scroll-top 、 scroll-left
这两个属性,它们都是可以通过属性绑定、控制组件行为的属性。
如果我们想让内部的滚动实体滚动到某个位置,
并不能直接去调用它的类似于scrollTo的方法。
我们只能在js里面动态改变scroll-top,scroll-left 这两个属性绑定的变量。
然后在视图渲染更新以后,组件会自动发生滚动
scroll-into-view
滚动到某个元素,值应为某子元素id。假如同时开启scroll-x、scroll-y横纵这两个方向的滚动,
当通过scroll-into-view滚动时,
那么它的滚动行为是怎样变化的呢?

通过测试结果来看,结论很不明确,
如果不加scroll-with-animation的话,
也就是不开启动画,可以同时在x、y方向上瞬时移动到目标位置。
如果开启动画,同一时间只能在一个方向上滚动,
有时候在x方向滚动,有时候在y方向滚动,行为很不明确。
scroll-x scroll-y 最好不要同时开启,
如果功能需要,我们可以基于view实现同样的功能
或者是先在x方向上开启,完成移动后,再在y方向上开启,依次进行
<view class="page-section">
<view class="page-section-title">
<text>片9 测试scroll-into-view滚动</text>
</view>
<view class="page-section-spacing">
<scroll-view enable-flex scroll-into-view="{{scrollIntoViewId}}" bindscroll="onScroll" scroll-y scroll-x scroll-with-animation="{{false}}" style="width: 100%;height:300rpx;">
<view id="childview{{item}}0" wx:for="{{[1, 2, 3, 4, 5, 6, 7, 8, 9,10]}}" class="scroll-row">{{item}}
<view wx:for="{{[0, 1, 2, 3, 4, 5,6,7,8,9]}}" class="scroll-item" id="childview{{item}}{{item2}}" wx:for-item="item2">{{item}}:{{item2}}</view>
</view>
</scroll-view>
</view>
<view class="btn-area">
<button type="primary" bindtap="scrollToView1">滚动到子组件2</button>
</view>
</view>
data: {
scrollIntoViewId: '',
},
scrollToView1() {
viewId += 2
this.setData({
scrollIntoViewId: 'childview' + viewId
})
console.log(this.data.scrollIntoViewId)
},
scroll-anchoring
滚动锚定,默认false,控制滚动位置不随内容的变化而抖动的。滚动锚定是什么? 假设是一个图片瀑布流的页面, 当用户浏览瀑布流页面时, 加入由于网速的原因。 在看下面图片的时候, 上面图片突然加载进来, 这时候会使下方的图片自动往下跑, 这个体验肯定很不好。 滚动锚定的策略 通过一个CSS样式控制滚动实体在内容变化的时候不发生滚动。 scroll-anchoring 就是干这个的。 `这个属性某种情况下,可能会给开发者带来意想不到的bug`, 这个页面可能会陷入一种自循环,表现出一种`抖动不止的现象`。 当出现这个现象的时候,简单的解决方法就是`关闭滚动锚定策略`, 或者`设置一个具有相同效果的样式` overflow-anchor:none; scroll-anchoring 该属性目前`小程序只支持IOS`,`android手机需要自己通过CSS处理 overflow-anchor:auto;`
下拉更新属性
- upper-threshold
- lower-threshold
这两个属性是为了控制scrolltoupper、scrolltolowe事件何时派发的。默认都是50px
- bindscrolltoupper
- bindscrolltolower
这两个事件是状态事件,upper-threshold为50的时候,当scroll-top小于50的时候,
只要滚动行为发生着bindscrolltoupper事件会多次派发,并且这种派发基本上是随心所欲的,
派发基本是没有规律的,所以`说基于crolltoupper、scrolltolower这两个事件写业务逻辑的时候,
我们要注意特别判断一下,是否已经处理过了,以免造成重复的处理。
不是说滚动一次派发一次,有可能是滚动一次派发多次`。
- bindscroll
<!-- 测试scrolltoupper的随心所欲 --> <view class="page-section"> <view class="page-section-title"> <text>片12 测试scrolltoupper的随心所欲</text> </view> <view class="page-section-spacing"> <scroll-view upper-threshold="50" bindscrolltoupper="viewScrollToUpperEvent" scroll-y style="height: 300rpx"> <view id="demo1" class="scroll-view-item demo-text-1"></view> <view id="demo2" class="scroll-view-item demo-text-2"></view> <view id="demo3" class="scroll-view-item demo-text-3"></view> </scroll-view> </view> </view> upper-threshold="50" scroll-top在小于等于这个值的时候派发bindscrolltoupper事件
.page-section-spacing { margin-top: 60rpx; } .scroll-view-item { height: 300rpx; } .demo-text-1 { background-color: #E6E6FA; } .demo-text-2 { background-color: #E1FFFF; } .demo-text-3 { background-color: #FDF5E6; }
viewScrollToUpperEvent(e) {
console.log('测试scrolltoupper事件', e.detail);
},
自定义下拉刷新相关的属性
:- refresher-enabled boolean,默认false,是否开启自定义下拉刷新
- refresher-threshold number,触发下拉更新的临界值
- refresher-triggered boolean,默认false,它是为了在更新后取消下拉更新的状态,当组件处于下拉更新的状态后,它的值变为true,此时程序要去做一些异步耗时的事情,例如网络加载,待处理完成了,再将这个值设置为false
- bindrefresherpulling
- bindrefresherrefresh
- bindrefresherrestore
- bindrefresherabort 后面4个事件是自定义实现下拉动画效果的关键
使用wxs实现自定义下拉刷新
bindrefresherpulling 手指按住往下拉的过程中派发的,自定义的动画效果要在这个事件里面处理
当向下拉动时,区域慢慢的放大,同时箭头图标有一个方向的翻转。
主要是做了三件事情,
第一,计算拉到哪里了,占总量高度80的多少,找到icon图标,设置它的旋转角度
第二,找到下拉动画的容器,设置它的缩放,达到看起来越往下拉,容器越来越大的一个效果。
第三,当拉到refresher-threshold 临界值时,改变下拉更新的提示文本
<!-- 自定义下拉更新 --> <!-- 使用wxs自定义实现下拉刷新--> <!--module="refresh"可以让我们在WXML中引用--> <wxs module="refresh"> var pullingMessage = "下拉刷新" module.exports = { // onPulling 下拉的过程当中我们干什么事情 /* 主要是做了三件事情, 第一,计算拉到哪里了,占总量高度80的多少,找到icon图标,设置它的旋转角度 第二,找到下拉动画的容器,设置它的缩放,达到看起来越往下拉,容器越来越大的一个效果。 第三,当拉到refresher-threshold 临界值时,改变下拉更新的提示文本 */ onPulling: function (e, instance) { // instance 传进来的页面的实例对象 // 80的高度,因为refresher-threshold设置的是80,手指按住往下拉的状态 var p = Math.min(e.detail.dy / 80, 1) // 目前拉倒哪里了,进度 不大于1 // console.log(p) // 这里在视图层,不怕频繁操作DOM var icon = instance.selectComponent('#refresherIcon') // 图标 WeUi组件 icon.setStyle({ opacity: p, // 透明度,越往下拉越清晰 transform: "rotate(" + (90 + p * 180) + "deg)" // 旋转角度 }) var view = instance.selectComponent('.refresh-container') // 拉动的动画本身的容器 view.setStyle({ opacity: p, transform: "scale(" + p + ")" // 设置缩放 }) //拉到80的高度,可以释放状态 if (e.detail.dy >= 80) { if (pullingMessage == "下拉刷新") { pullingMessage = "释放更新" instance.callMethod("setData", { // wxs 调用js的setData方法 pullingMessage }) } } }, // 此时手拉开了,进入了加载中的状态 onRefresh: function (e, instance) { // 此时手拉开了,进入了加载中的状态 pullingMessage = "更新中" console.log(pullingMessage) instance.callMethod("setData", { pullingMessage: pullingMessage, refresherTriggered: true }) instance.callMethod("willCompleteRefresh", {}) //调用js方法 //willCompleteRefresh 方法 }, onAbort: function (e, instance) { // 异常状态,例如被事件突然打断,事件包括电话等,被迫松手了 pullingMessage = "下拉刷新" console.log(pullingMessage) }, onRestore: function (e, instance) { // 回去了,松手了,恢复原位了,不刷了 pullingMessage = "下拉刷新" console.log(pullingMessage) }, } </wxs>
这是一段WXS代码,是在视图层执行的,在这里基本上可以肆意操作更新视图,
而不用担心更新频繁操作导致开销太大影响性能
在我们的代码里面之所以用callMethod方法,调用页面主体的setData方法,
就是为了曲线救国,达到更新视图的目的。
每个WXS代码里的事件句柄函数,在执行的时候都有两个参数传递进来,
事件对象与当前页面的实例对象,如果没有这两个参数,动画就实现不了。
WXS是在视图层里面执行的,js文件中的js代码是在逻辑层执行的。
WXS是WeXin Script的简写,它有自己的语法。严格按照官方文档去写。
<view class="page-section"> <view class="page-section-title">自定义下拉刷新</view> <!-- bindrefresherpulling 下拉的过程当中我们干什么事情 bindrefresherrefresh 当它拉到可以松手的状态的时候 refresher-triggered 为true是小程序设定的,异步操作完成之后,要设置为false,下拉状态就自己回去了 --> <scroll-view scroll-y style="width: 100%; height: 400px;overflow-anchor:auto;" bindscroll="onScroll" bindscrolltoupper="onScrolltoupper" scroll-top="{{scrollTopValue}}" scroll-into-view="{{scrollIntoViewId}}" scroll-with-animation enable-back-to-top enable-flex scroll-anchoring refresher-enabled refresher-threshold="{{80}}" refresher-default-style="none" refresher-background="#FFF" bindrefresherpulling="{{refresh.onPulling}}" bindrefresherrefresh="{{refresh.onRefresh}}" bindrefresherrestore="{{refresh.onRestore}}" bindrefresherabort="{{refresh.onAbort}}" refresher-triggered="{{refresherTriggered}}"> <!-- slot="refresher" 写死的名字,写其他的不行 --> <view slot="refresher" class="refresh-container" style="display: block; width: 100%; height: 80px; background: #F8f8f8; display: flex; align-items: center;"> <view class="view1" style="position: absolute; text-align: center; width: 100%;display:flex;align-items:center;justify-content:center;color:#888;"> <mp-icon id="refresherIcon" icon="arrow" color="#888" size="{{20}}" style="margin-right:5px;transform:rotate(90deg)"></mp-icon> <text style="min-width:80px;text-align:left;">{{pullingMessage}}</text> </view> </view> <view wx:for="{{arr}}" id="view{{item+1}}" style="display: flex;height: 100px;"> <text style="position:relative;top:5px;left:5px;color:black;">{{item+1}}</text> <image src="https://p.qqan.com/up/2021-6/16232893151517011.jpg"></image> <image src="https://p.qqan.com/up/2021-6/16232893724729414.jpg"></image> <image src="https://p.qqan.com/up/2021-6/16232893157513212.jpg"></image> </view> </scroll-view> <view class="btn-area"> <button bindtap="plusScrollUpValue">向上滚动</button> <button bindtap="scrollToView1">滚动到子视图</button> <button bindtap="unshiftOnePic">顶部添加一张图</button> </view> </view>
bindrefresherrestore 事件,是状态恢复了,是设置了refresher-triggered 为false动画完成以后派发的事件。
bindrefresherabort 是下拉行为被打断时派发的事件,正常情况下这种事件是不会收到的
data: { pullingMessage: '下拉刷新', //下拉刷新,释放更新,加新中... refresherTriggered: false, // }, willCompleteRefresh() { console.log('更新中') // ... 的动画 let intervalId = setInterval(() => { let pullingMessage = this.data.pullingMessage console.log(pullingMessage, pullingMessage == '更新中') if (pullingMessage.length < 7) { pullingMessage += '.' } else { pullingMessage = '更新中' } this.setData({ pullingMessage }) }, 500) // 2s 1.清理定时器 2.setData setTimeout(() => { console.log('更新完成了') clearInterval(intervalId) this.setData({ pullingMessage: "已刷新", refresherTriggered: false, // 为true是小程序设定的,异步操作完成之后,要设置为false,下拉状态就自己回去了 }) }, 2000) },
bindrefresherrefresh事件,它是组件进入更新中状态时派发的事件,我们需要一个定时器,模拟网络异步加载,
但是WXS没有定时器,它只有一个页面实例对象的requestAnimationFrame函数,要么使用requestAnimationFrame方法模拟一个定时器,要么在js中实现
在js中定义willCompleteRefresh的方法,然后在WXS里面,在合适的时机,通过callMethod去调用它。
在这个组件中,willCompleteRefresh主要做了两件事情,
第一,使用一个定时器模拟实现 "更新中..." 后面的 ... 跳动的动画
第二,通过一个延时定时器在两秒以后设置刷新完成
关于下拉刷新组件有两个开源项目,
mescroll https://github.com/mescroll/mescroll
minirefresh https://github.com/minirefresh/minirefresh
使用scroll-view组件有几点需要我们注意:
第一点,启用scroll-anchoring 属性时,同时添加一个overflow-anchor:auto;的样式,来应对Android机型不兼容的情况, 第二点,任何时候只开启一个方向的滚动,scroll-x 或者 scroll-y 只取其一, 当开启scroll-y时,必须给组件一个高度,子组件的高度之和一定要大于这个高度, 当开启scroll-x时,必须给组件一个宽度,一般这个值是100%,等于屏宽,子组件的宽度之和要大于屏宽, 在启用scroll-x时,宽度为100%,如果出现不滚动的现象,可以尝试给滚动容器添加两个这样的样式 white-space:nowrap; 不换行 display:inline-block; 行内块元素 目的是让子元素在横向上排列成一排, 第三点,开启enable-flex,这个属性是在scroll-view组件上启用Flex布局的,相当于添加了一个display:flex这样的样式,但是如果是我们自己添加的话,是添加在了外围的容器上,只有通过这个属性添加才能加到内部真正的容器上, 第四点,如果需要使用refresher-enabled 启用下拉动画的自定义,自定义可以很方便实现一些有创意的交互效果, 下拉动画容器的slot属性要标记为refresher。 第五点,下拉动画组件的背景色用#F8F8F8,前景色包括图标与文本用#888888这个颜色,符合微信设计规范 第六点,尽量不在js代码里面在scroll事件的句柄函数中直接更新视图,`把相关的频繁的更新视图的代码,放在WXS模块中`, `在大列表视图中`,尤其要这么做。
//更新二维数组
const updateList = `tabs[${activeTab}].list[${page}]`
const updatePage = `tabs[${activeTab}].page`
this.setData({
[updateList]:res.data,
[updatePage]:page + 1
})
<view wx-for="{{gameListWrap}}" wx:for-item="gameList">
......
</view>
上面的代码中,作者是想实现一个多tab页的功能,
数据是tabs,gameListWrap是对tabs子数据访问的再封装,
activeTab、page是模板字符串中的变量
updateList、updatePage是setData更新的时候用的key,因为是变量,所以需要用中括号
let tabData = this.data.tabs[activeTab];
tabData.list.push(res.data);
tabData.page=page + 1;
let key = `tabs[${activeTab}]`
this.setData({
[key]:tabData
})
作者为什么不直接使用push方法呢? 当有新数据进来的时候,直接往tab页数据的底部推入新数据,这样不就可以了吗? 但这种操作有一个问题,setData受限于视图层与逻辑层之间,用于传话的evaluateJavascript函数, 每次携带的数据大小,官方要求在文本序列化以后,大小不能超过256KB, 如果每个tab页是一个瀑布流页面,它的tabData.list可能是一个越来越大的数据,很有可能就超过256KB。 将tab数据与页面数据分开,在当前页面循环渲染时,按照pages[activeTab].page的数字循环, 取数据的时候依照page当前的值,从gameListData[activeTab]中查取, gameListData这个时候在形式上相当于一个数据,但实际上它是一个map。 另外在渲染长列表的时候,微信在WeUI扩展组件库中,给出了一个长列表组件recycle-view,它用于渲染无限长的列表。 `那么这个问题怎么解决呢?` 使用`recycle-view扩展组件`: https://developers.weixin.qq.com/miniprogram/dev/extended/component-plus/recycle-view.html 这个长列表实现的原理也很简单,通过监听scroll事件, 只渲染当前视图窗口的内的list列表,看不见的地方用空白的占位符代替。
在使用recycle-view扩展组件的时候,batch属性的值必须为batchSetRecycleData,这是由组件自动管理的。
在js代码中调用createRecycleContext时,传入的dataKey:recycleList,
这个名称必须与WXML中的wx:for指定的数据名称一致,
如果一个页面中还使用了另外一个长列表,则需要再换一个名字。
<view class="page-section">
<view class="page-section-title">使用recycle-view扩展组件</view>
<recycle-view height="200" batch="{{batchSetRecycleData}}" id="recycleId" batch-key="batchSetRecycleData" style="background:white;">
<recycle-item wx:for="{{recycleList}}" wx:key="index" class='item'>
<view>
{{item.id}}: {{item.name}}
</view>
</recycle-item>
</recycle-view>
</view>
const createRecycleContext = require('miniprogram-recycle-view') function rpx2px(rpx) { return (rpx / 750) * wx.getSystemInfoSync().windowWidth } onReady: function () { var ctx = createRecycleContext({ id: 'recycleId', dataKey: 'recycleList', page: this, itemSize: { width: rpx2px(650), height: rpx2px(100) } }) let newList = [] for (let i = 0; i < 20; i++) { newList.push({ id: i, name: `标题${i + 1}` }) } ctx.append(newList) },
总结
当从后端拉取大数据渲染长列表的时候,大多数情况下卡顿并不是手机真的卡了,
这个时候如果打开App就会发现很流畅。
很可能这个时候只是试图渲染不及时,
影响小程序渲染效率的罪魁祸首就是底层的evaluateJavascript这个通信函数,
它可以说是逻辑层与视图层之间的一个很小的独木桥,
他无法承载过大的数据量,
所以我们要尽量减少大数据的渲染,在视图中的互动操作要尽量在WXS代码中去完成
从效果图看,需要实现两个功能,
第一,单击左侧菜单,右侧区域自动滚动到相应的位置,
第二,在右侧滚动的时候,左侧菜单自动同步并高亮显示。
<!--实现小程序页面分类选择物品页面 -->
<!-- 左侧菜单 -->
<scroll-view class='nav' scroll-y='true'>
<view wx-for='{{list}}' wx:key='{{item.id}}' id='{{item.id}}'
class='navList{{currentIndex == index ? "active":""}}' bindTap='menuListOnClick' data-index='{{index}}'
>{{item.name}}</view>
</scroll-view>
<!-- 右侧内容 -->
<scroll-view scroll-y='true' scroll-into-view='{{activeViewId}}' bindscroll='scrollFunc'>
<view class='fishList' wx:for='{{content}}' id='{{item.id}}' wx:key='{{item.id}}'>
<p>{{item.name}}</p>
</view>
</scroll-view>
//单击左侧菜单 menuListOnClick(e){ let me = this; me.setData({ activeViewId:e.target.id, currentIndex:e.target.dataset.index }) }, //滚动时触发,计算当前滚动到的位置对应的菜单是哪个 scrollFunc(e){ //看向上滚动了多少 this.setData({ scrollTop:e.detail.scrollTop }); // 右侧的内容区域有一个heightList // 每一块区域高度我们一个一个去对比 // 看目前滚动大概是处于哪一个高度的范围之内 //我们要事先在渲染的时候,heightList区域高度要事先计算出来并存储出来 for(let i= 0;i<this.data.heightList.length;i++){ let height1 = this.data.heightList[i]; let height2 = this.data.heightList[i + 1]; if(!height2 || (e.detail.scrollTop >= height1 && e.detail.scrollTop < height2) ){ this.setData({ currentIndex:i // 设置当前选择的是哪一个 }); } return ; } this.setData({ currentIndex: 0 }); }
vtabs.wxml
<mp-vtabs
vtabs="{{vtabs}}"
activeTab="{{activeTab}}"
bindtabclick="onTabCLick"
bindchange="onChange"
class="test"
>
<block wx:for="{{vtabs}}" wx:key="title" >
<mp-vtabs-content tabIndex="{{index}}">
<view class="vtabs-content-item">我是第{{index + 1}}项: {{item.title}}</view>
</mp-vtabs-content>
</block>
</mp-vtabs>
vtabs.js
Page({ data: { vtabs: [], activeTab: 0, }, onLoad() { const titles = ['热搜推荐', '手机数码', '家用电器', '生鲜果蔬', '酒水饮料', '生活美食', '美妆护肤', '个护清洁', '女装内衣', '男装内衣', '鞋靴箱包', '运动户外', '生活充值', '母婴童装', '玩具乐器', '家居建材', '计生情趣', '医药保健', '时尚钟表', '珠宝饰品', '礼品鲜花', '图书音像', '房产', '电脑办公'] const vtabs = titles.map(item => ({title: item})) this.setData({vtabs}) }, onTabCLick(e) { const index = e.detail.index console.log('tabClick', index) }, onChange(e) { const index = e.detail.index console.log('change', index) } })
vtabs.json
{
"usingComponents": {
"mp-vtabs": "../../components/vtabs/index",
"mp-vtabs-content": "../../components/vtabs-content/index"
}
}
vtabs.wxss
@import '../common.wxss';
page{
background-color: #FFFFFF;
height: 100%;
}
.vtabs-content-item {
width: 100%;
height: 300px;
box-sizing: border-box;
border-bottom: 1px solid #ccc;
padding-bottom: 20px;
}
common.wxss
@import '../components/weui-wxss/dist/style/weui.wxss'; page{ background-color: #F8F8F8; font-size: 16px; font-family: -apple-system-font,Helvetica Neue,Helvetica,sans-serif; } .page__hd { padding: 40px; } .page__bd { padding-bottom: 40px; } .page__bd_spacing { padding-left: 15px; padding-right: 15px; } .page__ft{ padding-bottom: 10px; text-align: center; } .page__title { text-align: left; font-size: 20px; font-weight: 400; } .page__desc { margin-top: 5px; color: #888888; text-align: left; font-size: 14px; } .weui-cell_example:before{ left:52px; } /* .weui-btn{width:184px;} */
组件源码
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。