移动端中需要使用swiper插件实现tab切换和手势滑动的(类似于今日头条资讯列表),在各种APP上我们可以很常见,但在小程序上实现这个看起来有点难,swiper插件滑动到下一屏的时候位置总是会回到跟上一屏相同的位置。我记得微信刚出来不久,有一个这样子的需求,那时候是另外一个同事在跟,跟我说到过这个功能无法实现。最近我也接到这个需求,于是我认真的看了一下微信组件文档,发现swiper+scroll-view组件结合是可以实现这个功能。
需要说的是,我的每个swiper-item数据不是固定的,每个swiper-item列表数据都有滚动底部会无线加载,所以说我无法在一开始就确定了所有的子项的高度,另外每个swiper-item都需要滚动,这就是说它们的滚动到的位置每个都不是一样的。每次滑动到下一屏的时候,要确定最外层的scroll-view的scroll-top值。
自适应高度
由于swiper组件并不是自适应高度的,而我们每个swiper-item的高度并不是一样的,所以第一步就是计算每个swiper-item的高度,并赋值给swiper组件。xml代码如下:
<swiper current="{{current}}" style="height:{{list[curListId].swiperHeight}}rpx;" class="swiper-box" duration="300" bindchange="bindchange">
<block wx:for="{{nav}}">
<swiper-item>
<block wx:if="{{list[item.columnId].data.length>0}}">
<view class="list">
<block wx:for="{{list[item.columnId].data}}" wx:for-item="art">
<view class="item">
<navigator url="../art/art?id={{art.id}}" hover-class="none" open-type="navigate">
<view class="img">
<image mode="" src="{{art.imageUrl}}"></image>
<view class="meta">
<view class="avatar">
<image src="{{art.userImage}}"></image>
</view>
<view class="nickName">
<text>{{art.userName}}</text>
</view>
</view>
</view>
<view class="title">{{art.title}}</view>
<view class="des">{{art.summary}}</view>
</navigator>
</view>
</block>
</view>
</block>
</swiper-item>
</block>
</swiper>
可以看到我里面定义了一个循环,把所有子项的数据都放在list里面。当然前提我们已经知道要判断的list[item.columnId]是否已经存在值,如果没有我们需要先请求数据。而每个对象对应的下标我们已经知道,所以我们一开始进来就确认了所有swiper-item要加载的内容,后续滑动加载内容时如果没有,我们就会去请求,如果有则使用,并且滑动到底部时,我们根据curListId知道当前需要请求那个对象。把剩下的数据合并在同一个对象里面。
计算swiper-item的高度,可以使用微信提供的一个 api createSelectorQuery,我这里没用,因为我的列表每一个字内容都是固定好高度的。所以我只要知道获取的数据数组的长度是多少,然后计算每个子项的高度,就能得到swiper-item的高度了。
记录滚动值
记录滚动值,这个很简单,因为我们的页面最外层就是一个scroll-view组件,所以我们只要给这个scroll-view一个bindscroll事件,在滚动的过程中,不断的记录更新每一个子项它最后滚动到的位置,下次进入这一屏,就看看数据里面有没有这个滚动值,没有的话,就是第一次进入,默认为0,如果有值,说明之前我们已经滚动过一次,则赋值给scroll-view的scroll-top。
scroll: function(e) {
var self = this;
setTimeout(function() {
console.log(e.detail.scrollTop);
var list = self.data.list;
if (list[self.data.curListId]) {
list[self.data.curListId].scrollTop = e.detail.scrollTop;
self.setData({
list
})
}
}, 300);
},
预览和代码下载
就是实现这种常见的资讯tab列表,需要滑动加载数据,切换自动回到上次滚动的位置。
JS:
// pages/live/live.js
const app = getApp()
const config = require('../../pc.config.js');
Page({
/**
* 页面的初始数据
*/
data: {
nav: [{
"columnName": '全部',
"columnId": "000081525"
}, {
"columnName": '好物种草',
"columnId": "000084624"
}, {
"columnName": '收纳支招',
"columnId": "000084644"
}, {
"columnName": '细节控',
"columnId": "000084645"
}, {
"columnName": '小改造',
"columnId": "000084625"
}, {
"columnName": '洗刷刷',
"columnId": "000084626"
}, {
"columnName": '睡眠研究',
"columnId": "000084646"
}],
navPosition: [],
list: {},
curListId: 0,
endTipHidden: false,
endTip: '正在加载',
current: 0,
scrollLeft: 0,
px2rpx: 2,
winWidth: 375,
scrollTop: 0
},
/**
* 生命周期函数--监听页面加载
*/
onLoad: function(options) {
var index = options.index || 0;
this.getSystem();
this.getNav(index);
},
timer: null,
getSystem: function() {
var self = this;
wx.getSystemInfo({
success: function(res) {
console.log(res);
self.setData({
winWidth: res.windowWidth,
px2rpx: 750 / res.windowWidth
})
},
});
},
//获取导航和节点
getNav: function(index) {
var self = this;
wx.request({
url: config.getAPI('liveNav'),
success: function(res) {
// console.log(res);
self.setData({
nav: res.data.livingColumn,
current: index
});
//获取导航的初始位置
const query = wx.createSelectorQuery()
query.selectAll('.toc').boundingClientRect();
// query.selectViewport().scrollOffset()
query.exec(function(res) {
console.log(res);
console.log(res[index]);
self.setData({
navPosition: res[0]
})
if (index >= 4) {
self.setData({
scrollLeft: res[0][index].left
})
}
})
self.getList(res.data.livingColumn[index].columnId);
}
})
},
//滑到底部加载更多
loadMoreList: function() {
var list = this.data.list;
var cid = this.data.curListId;
if (list[cid].pageNo < list[cid].pageCount) {
this.getList(cid);
}
},
//请求列表
getList: function(cid) {
var self = this;
this.setData({
curListId: cid
})
var list = this.data.list;
if (!list[cid]) {
wx.request({
url: config.getAPI('liveList') + `?columnType=livingColumn&columnId=${cid}&pageSize=10&pageNo=1`,
success: function(res) {
// console.log(res.data);
var obj = {};
obj.pageNo = res.data.pageNo;
obj.pageCount = Math.ceil(res.data.total / res.data.pageSize);
obj.total = res.data.total;
obj.data = res.data.data;
obj.swiperHeight = res.data.total > res.data.pageSize * res.data.pageNo ? res.data.pageSize * res.data.pageNo * 518 + 102 : res.data.total * 518 + 102;
if (res.data.pageNo * res.data.pageSize >= res.data.total) {
obj.endTip = '没有更多了';
obj.endTipHidden = true;
} else {
obj.endTip = '正在加载';
}
list[cid] = obj;
self.setData({
list
})
}
})
} else {
if (list[cid].pageNo < list[cid].pageCount) {
wx.request({
url: config.getAPI('liveList') + `?columnType=livingColumn&columnId=${cid}&pageSize=10&pageNo=${list[cid].pageNo+1}`,
success: function(res) {
var obj = {};
obj.pageNo = res.data.pageNo;
obj.pageCount = Math.ceil(res.data.total / res.data.pageSize);
obj.total = res.data.total;
obj.data = list[cid].data.concat(res.data.data);
if (res.data.pageNo * res.data.pageSize >= res.data.total) {
obj.endTip = '没有更多了';
obj.endTipHidden = true;
} else {
obj.endTip = '正在加载';
}
obj.swiperHeight = res.data.total > res.data.pageSize * res.data.pageNo ? res.data.pageSize * res.data.pageNo * 518 + 102 : res.data.total * 518 + 102;
list[cid] = obj;
self.setData({
list
})
}
})
}
}
},
//切换导航(包含滑动swiper和切换导航跳转)
switchNav: function(index) {
if (index && this.data.current == index) return;
console.log('switchtab');
var cid = this.data.nav[index].columnId;
var self = this;
var scrollLeft = 0;
if (self.data.navPosition[index].right * self.data.px2rpx + 62 >= 750) {
scrollLeft = self.data.navPosition[index].left;
}
var list = self.data.list;
var scrollTop = 0;
if (list[cid] && list[cid].scrollTop) {
scrollTop = list[cid].scrollTop
}
if (!list[cid]) {
clearTimeout(self.timer);
self.timer = null;
self.timer = setTimeout(function() {
self.setData({
curListId: cid,
current: index,
scrollLeft,
scrollTop
})
self.getList(cid);
}, 500);
} else {
self.setData({
curListId: cid,
current: index,
scrollLeft,
scrollTop
})
}
},
//切换导航
changeTab: function(e) {
console.log('changetab');
var index = e.currentTarget.dataset.index;
this.switchNav(index);
},
//滑动swiper
bindchange: function(e) {
console.log('changeswiper');
var index = e.detail.current;
//加上这个避免swiper过程,swiper-item会发生滑动混乱,滑动过快就会一直在闪动,新的API属性,touch说明是用户接触滑动,而不是自动滑动
if (e.detail.source && e.detail.source =='touch'){
this.switchNav(index);
}
},
//滚动记录之前的滚动位置
scroll: function(e) {
var self = this;
setTimeout(function() {
console.log(e.detail.scrollTop);
var list = self.data.list;
if (list[self.data.curListId]) {
list[self.data.curListId].scrollTop = e.detail.scrollTop;
self.setData({
list
})
}
}, 300);
},
})
xml
<!--pages/live/live.wxml-->
<scroll-view class="g-doc" scroll-y="true" bindscrolltolower="loadMoreList" scroll-top="{{scrollTop}}" bindscroll="scroll">
<view class="tab">
<view class="tab-inner">
<scroll-view class="scroll-bangdan" scroll-x="true" scroll-left="{{scrollLeft}}">
<view class="ctrl" style="width:1054rpx">
<block wx:for="{{nav}}">
<view class="toc{{current==index?' cur':''}}" bindtap="changeTab" data-index="{{index}}">
<view class="text">
<text>{{item.columnName}}</text>
</view>
</view>
</block>
</view>
</scroll-view>
<view class="mask"></view>
</view>
</view>
<view class="tab-container">
<swiper current="{{current}}" style="height:{{list[curListId].swiperHeight}}rpx;" class="swiper-box" duration="300" bindchange="bindchange">
<block wx:for="{{nav}}">
<swiper-item>
<block wx:if="{{list[item.columnId].data.length>0}}">
<view class="list">
<!-- <view class="nodata">这里空空如也</view> -->
<block wx:for="{{list[item.columnId].data}}" wx:for-item="art">
<view class="item">
<navigator url="../art/art?id={{art.id}}" hover-class="none" open-type="navigate">
<view class="img">
<image mode="" src="{{art.imageUrl}}"></image>
<view class="meta">
<view class="avatar">
<image src="{{art.userImage}}"></image>
</view>
<view class="nickName">
<text>{{art.userName}}</text>
</view>
</view>
</view>
<view class="title">{{art.title}}</view>
<view class="des">{{art.summary}}</view>
</navigator>
</view>
</block>
</view>
</block>
</swiper-item>
</block>
</swiper>
<view class="m-end" hidden="{{list[curListId].endTipHidden}}">
<block wx:if="{{endTip=='正在加载'}}">
<view class="icon"></view>
</block>
{{endTip}}
</view>
<view class="m-end" hidden="{{!list[curListId].endTipHidden}}">没有更多了</view>
</view>
</scroll-view>
CSS
/* pages/live/live.wxss */
page {
height: 100%;
overflow: hidden;
}
.g-doc {
height: 100%;
}
.tab {
position: fixed;
top: 0;
left: 0;
border-bottom: 1px solid #f8f8f8;
background: #fff;
z-index: 1;
width: 100%;
height: 100rpx;
line-height: 100rpx;
}
.tab-inner{
height: 100rpx;
overflow: hidden;
}
/* .tab-con{width: 100%;} */
.scroll-bangdan {
width: 100%;
position: relative;
height: 130rpx;
}
.tab .ctrl {
display: -webkit-box;
display: -moz-box;
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
flex-flow: row wrap;
-webkit-flex: row wrap;
font-size: 28rpx;
height: 100rpx;
line-height: 100rpx;
}
.tab .ctrl .toc {
text-align: center;
margin-left: 50rpx;
white-space: nowrap;
}
.tab .ctrl .toc:first-child {
margin-left: 40rpx;
}
.tab .ctrl .toc:last-child {
margin-right: 40rpx;
}
.tab .ctrl .toc .text {
display: inline-block;
position: relative;
}
.tab .ctrl .toc.cur .text::before {
content: "\20";
display: block;
position: absolute;
height: 10rpx;
background: #fbe251;
left: 0;
bottom: 30rpx;
width: 100%;
z-index: 0;
}
.tab .ctrl .toc .text text {
position: relative;
z-index: 2;
}
.tab .mask {
background: url(https://www1.pchouse.com.cn/2018/weixinminipro/mask.png) no-repeat top right;
background-size: 113rpx;
width: 83rpx;
height: 100rpx;
position: fixed;
right: 0;
top: 0;
z-index: 50;
}
.list {
padding-top: 100rpx;
}
.list .item {
padding: 30rpx 40rpx 40rpx;
}
.list .item .img {
width: 670rpx;
height: 336rpx;
overflow: hidden;
position: relative;
}
.list .item .meta {
position: absolute;
left: 0;
bottom: 0;
height: 84rpx;
width: 100%;
padding-top: 16rpx;
line-height: 60rpx;
color: #fff;
font-size: 28rpx;
overflow: hidden;
background: url(https://www1.pchouse.com.cn/2018/weixinminipro/pic-modal.png?v2) repeat-x center bottom;
background-size: auto 99rpx;
}
.list .item .meta .avatar {
width: 60rpx;
height: 60rpx;
float: left;
margin-left: 24rpx;
margin-right: 24rpx;
}
.list .item .meta .avatar image {
width: 60rpx;
height: 60rpx;
border-radius: 100%;
}
.list .item .meta .nickName {
float: left;
}
.list .item .img image {
width: 670rpx;
height: 336rpx;
border-radius: 16rpx;
}
.list .item .title {
height: 73rpx;
line-height: 73rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 36rpx;
}
.list .item .des {
height: 40rpx;
overflow: hidden;
font-size: 24rpx;
color: #aaa;
text-overflow: ellipsis;
white-space: nowrap;
}
.m-end {
padding: 20rpx 0 34rpx;
line-height: 28rpx;
}
效果可以上微信小程序搜索:装修设计攻略指南(已上线)
(截止2018.10.31,scroll-view官网组件可能存在一点bug,page设置100%时,高度有时候没法自适应,官网回应正在修复)