小程序tab 列表完美解决方案

今天在做百度小程序的转换,发现真机上用之前的swiper-item结合scroll-view 的效果不理想,于是我重新思考,发现了一种更合适的方案。

之前的缺陷

  1. swiper-item里面的内容使用的是view组件,导致每次切换到新的swiper-item时,历史定位都重置了。这样导致了我每次切换到另外一个swiper-item时要计算他的滚动位置和他的全部元素高度。
  2. 我还需要频繁记录每次滚动的定位,保存起来,以便下次用的时候来拿,使用scroll事件很卡。
  3. 上面返回历史位置时,最外层的scroll-view组件都要重新赋值scrollTop值,导致内容每次都要从头滚动,很消耗性能,并且还不是实时的,比如百度小程序(响应不及时)里就放大了这个bug了,当我切换到下一屏时,内容已经生成了,但是位置没有定位,要等零点几秒才能定位到那个位置,就是说你能看到内容在从头滚动。

解决方法

https://github.com/zhongjie-chen/wx-scrollable-tab-view 在网上找到这个人使用touch事件,我试着用他的方法,很好用,但是有一点缺陷,就是手指切换有时候不灵敏,有时候我已经滑动很长了,但它没有切换到下一屏,还在当前屏停留。然后我认真的看了他的实现,发现他里面解决了这个scroll-view定位由于滑动到下一屏会重置的问题。

他是怎么解决的呢?就是每一个切换里面又加多了一个scroll-view。这样子就避免了切换时历史滚动位置需要重置的问题。于是我想到了既然用他这种做法解决了我前面的缺陷,那我把两个结合起来,不就完美解决了。

首先是使用swiper组件解决切换问题,然后就是每个swiper-item里面加一个scroll-view组件,这样子就不需要每次切换都要计算历史滚动位置了。效果非常流畅,JS只需要几个常规的函数就解决了,不需要hack,完美的解决方案。

代码如下

wxml:

<!--pages/live/live.wxml-->
<view class="g-doc">
	<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 s-for="item, index in nav" s-key="{{item.columnId}}">
						<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}}" class="swiper-box" duration="300" bindchange="bindchange">
			<block s-for="item, index in nav" s-key="{{item.columnId}}">
				<swiper-item>
					<block s-if="list[item.columnId].data.length>0">
						<scroll-view style="height:100%" scroll-y="true" bindscrolltolower="loadMoreList">
							<view class="list">
								<block s-for="art, index in list[item.columnId].data" s-key="{{art.id}}">
									<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 class="m-end" hidden="{{list[curListId].endTipHidden}}">
									<block s-if="endTip=='正在加载'">
										<view class="icon"></view>
									</block>
									{{endTip}}
								</view>
								<view class="m-end" hidden="{{!list[curListId].endTipHidden}}">没有更多了</view>
							</view>
						</scroll-view>
					</block>
				</swiper-item>
			</block>
		</swiper>
	</view>
</view>

js:

// pages/live/live.js
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"
    }],
    list: {},
    curListId: 0,
    endTipHidden: false,
    endTip: '正在加载',
    current: 0,
    scrollLeft: 0,
    loading: false
  },

  /**
   * 生命周期函数--监听页面加载
   */
  onLoad: function (options) {
    var index = options.index || 0;
    this.getNav(index);
  },
  timer: null,

  //获取导航和节点
  getNav: function (index) {
    var self = this;
    swan.request({
      url: config.getAPI('liveNav'),
      success: function (res) {
        //   console.log(res);
        self.setData({
          nav: res.data.livingColumn,
          current: index
        });
        //获取导航的初始位置
        if (index > res.data.livingColumn.length / 2) {
          self.setData({
            scrollLeft: 1054 / 2
          });
        }

        self.getList(res.data.livingColumn[index].columnId);
      }
    });
  },
  //滑到底部加载更多
  loadMoreList: function () {
    var self = this;
    clearTimeout(self.timer);
    self.timer = null;
    self.timer = setTimeout(function () {
      var list = self.data.list;
      var cid = self.data.curListId;
      if (list[cid].pageNo < list[cid].pageCount) {
        self.getList(cid);
      }
    }, 300);
  },
  //请求列表
  getList: function (cid) {
    var self = this;
    this.setData({
      curListId: cid
    });
    var list = this.data.list;
    if (this.data.loading) return;
    this.setData({
      loading: true
    })
    if (!list[cid] || (list[cid] && (list[cid].pageNo < list[cid].pageCount))) {
      var pageNo = !list[cid] ? 1 : list[cid].pageNo + 1;
      swan.request({
        url: config.getAPI('liveList') + `?columnType=livingColumn&columnId=${cid}&pageSize=10&pageNo=${pageNo}`,
        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 = !list[cid] ? res.data.data : list[cid].data.concat(res.data.data);
          obj.swiperHeight = 1110;
          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,
            loading: false
          });
        },
        fail: function () {
          swan.showModal({
            title: '提示',
            content: '加载不成功,是否重新加载?',
            confirmText: "重新加载",
            success: function (res) {
              self.setData({
                loading: false
              })
              if (res.confirm) {
                self.loadMoreList();
              } else if (res.cancel) {
                // console.log('用户点击取消')
              }
            }
          });
        }
      });
    }
  },
  switchNav: function (index) {
    if (index && this.data.current == index) return;
    var cid = this.data.nav[index].columnId;
    var self = this;
    var scrollLeft = 0;
    if (index > this.data.nav.length / 2) {
      scrollLeft = 1054 / 2;
    }
    var list = self.data.list;
    if (!list[cid]) {
      clearTimeout(self.timer);
      self.timer = null;
      self.timer = setTimeout(function () {
        self.setData({
          curListId: cid,
          current: index,
          scrollLeft
        });
        self.getList(cid);
      }, 500);
    } else {
      self.setData({
        curListId: cid,
        current: index,
        scrollLeft
      });
    }
  },
  //切换导航
  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;
    if (e.detail.source && e.detail.source == 'touch') {
      this.switchNav(index);
    }
  },

  /**
   * 用户点击右上角分享
   */
  onShareAppMessage: function () {
    return {
      path: '/pages/live/live?index=' + this.data.current
    };
  }
});
/* pages/live/live.css */

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;
}
.swiper-box{position: absolute; left: 0; width: 100%; height: 100%; top: 0;}
.list {
    padding-top: 100rpx;
}

.list .item {
    padding: 30rpx 40rpx 40rpx;
}

.list .item .img {
    width: 670rpx;
    height: 336rpx;
    overflow: hidden;
    position: relative;
    border-radius: 16rpx;
}

.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;
}

关注我

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

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

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