小程序tab实现之swiper自适应高度并记录滚动位置

移动端中需要使用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%时,高度有时候没法自适应,官网回应正在修复)


关注我

我的微信公众号:前端开发博客,在后台回复以下关键字可以获取资源。

  • 回复「小抄」,领取Vue、JavaScript 和 WebComponent 小抄 PDF
  • 回复「Vue脑图」获取 Vue 相关脑图
  • 回复「思维图」获取 JavaScript 相关思维图
  • 回复「简历」获取简历制作建议
  • 回复「简历模板」获取精选的简历模板
  • 回复「加群」进入500人前端精英群
  • 回复「电子书」下载我整理的大量前端资源,含面试、Vue实战项目、CSS和JavaScript电子书等。
  • 回复「知识点」下载高清JavaScript知识点图谱

每日分享有用的前端开发知识,加我微信:caibaojian89 交流