在小程序中实现swiper联动轮盘滚动

2,407 阅读4分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第6天,点击查看活动详情

前段时间,产品经理看了别人的小程序提了个新需求,要求小程序里实现一个轮盘效果。要轮盘滚动

参考

image.png

我一看好家伙这不是swiper么,但又不全是swiper,毕竟swiper不能弧线滑动。

最终效果

1231233.gif

一番思索时间得出一套可实现方案 原理如图

原理

初始版本

image.png

初始版本实线步骤

  1. 创建一个大的元素(图中的黑色实线)
  2. 按照大的元素的中心 创建数个小的元素 (途中灰色虚线,为了好解释没有画完,以此按照角度画到头)
  3. 在页面上加入swiper组件,绝对定位在最上层。

有了正确思路就能够开工了。 但是按照上面的图可以发现,虽然达到了轮转的效果,但是不够美观,页面上只会有一个元素,没法展示的更多。按照产品的需求还需要展示文字,一个页面上需要展示5个标题。于是有了下面的版本

1.0版本

image.png

可以看到文字代表的蓝色(实际上会依次按角度旋转到头,自行脑补)和图片代表的灰色不是同一个元素表示。

于是我把360度分成了4份,每份90度。在这90度中,放入一组完整的标题数组。然后4份就刚好凑成完整的圆形。

1.0版本实线步骤

  1. 创建一个大的元素(图中的黑色实线)
  2. 按照大的元素的中心 创建数个小的元素 (途中灰色虚线,为了好解释没有画完,以此按照角度画到头) 2.1 按照一定的角度
  3. 在页面上加入swiper组件,绝对定位在最上层。

按照这个思路,就可以写出具体实现方法

具体实现

1.创建一个容器元素(黑色实线部分)

我们都知道小程序的宽是 750rpx,所以需要将这个容器计算放到屏幕的中心 建议先定义为750 可以看到整体效果,后续再放大

2.创建灰色虚线和块的元素

这部分是图片的元素容器,页面上只会显示一个图片,所以可以将图片隔的比较开,节省渲染性能

我是用360度除图片数组的长度。 然后根据 index 按照角度显示到页面上。

2.1 创建蓝色虚线块

这部分是文字的元素容器,页面上会显示5个文字,所以要将360度分为4份,每份90度(自己定义份数、度数 相乘360度),然后根据 index * (90/num)按照角度显示到页面上。

上面两部的核心代码

  _setBanner() {
    // num代表服务器返回数组的长度
    const num = this.data.num;
    // 先创建 bannerNullArr 表示 将360度分为多少份 比如4份
    const bannerNullArr = Array(BANNER_CONFIG.ROUND_GROUP_NUM * num).fill("");
    // 组装数据,将每份的角度计算出来
    // 角度为一份(比如90度) 90/数组长度 
    const banner = bannerNullArr.map((el, i) => {
      return {
        rotate: i * (BANNER_CONFIG.ONE_GROUP_DEG / num),
        ...this.data.originBanner[i % num],
      };
    });

    // 图片长度的空数组
    const imgs = Array(num).fill("");
    // 图片计算角度
    const bannerImg = imgs.map((el, i) => {
      return {
        rotate: i * (360 / num),
        ...this.data.originBanner[i % num],
      };
    });
    this.setData({
      banner,
      bannerImg,
    });
  },

3 实现swiper与显示元素滚动的联动

这里实现的是swiper与轮盘的联动

利用了swiper的transition方法,来联动swiper和滚动的角度

  transition(e) {
    // 先计算滑动相对于总路程的进度
    const progress = this._getProcess(e);
    // 计算文字滚动的角度
    const textdeg = progress * (360 / BANNER_CONFIG.ROUND_GROUP_NUM); //每60度为份 共6份
    // 计算图片滚动的角度
    const imgDeg = progress * 1 * 360; //每360度为份 共1份

    const rotate = `transform: rotate(${-textdeg}deg)`;
    const rotateImg = `transform: rotate(${-imgDeg}deg)`;

    this.setData({
      rotate,
      rotateImg,
      animationfinish: false,
    });
  },
  
  _getProcess(e) {
    const num = this.data.num;
    const current = this.data.oldCurrent || this.data.current;
    let dx = e.detail.dx;
    // dx 表示走到页面宽度的距离 
    // 宽度需要通过 wx.getSystemInfoSync().windowWidth 获取
    dx = Math.ceil(dx);
    return ((dx / this.data.windowWidth + current) / num).toFixed(6);
  },

核心代码

wxml


	<view class="box">
		<!-- 用户滑动的区域 -->
		<swiper
		 current="{{current}}"
		 class="swiper"
		 indicator-dots="{{false}}"
		 autoplay="{{false}}"
		 circular="{{true}}"
		 interval="2000"
		 duration="500"
		 bindtransition="transition"
		 bindanimationfinish="animationfinish"
		>
			<block wx:for="{{banner}}" wx:key="index">
				<swiper-item>
					<view class="swiper-item" data-item="{{item}}" bind:tap="chooseBanner" />
					<view class="swiper-item-tabs">
						<view
						 wx:if="{{!oldCurrent}}"
						 wx:for="{{showIndex}}"
						 id="{{item}}"
						 bind:tap="jumpSwiper"
						 class="swiper-item-tabs"
						 wx:key="index"
						/>
					</view>
				</swiper-item>
			</block>
		</swiper>
		<view class="rotateBoxPort">
			<view style="{{rotate}}" class="rotateBox">
				<view
				 class="innerBox"
				 wx:for="{{banner}}"
				 wx:key="index"
				 style="transform: rotate({{item.rotate}}deg)"
				>
					<view class="rotateBoxItem">
						<view class="rotateBoxItemText">
							<!-- <view class="rotateBoxItemLine" style="left:{{BANNER_CONFIG.left}};transform: rotate({{BANNER_CONFIG.deg}}deg)" /> -->
							<view class="{{current === index?'rotateBoxItemTextActive':''}}">
								{{item.title}}
							</view>
						</view>
						<view wx:if="{{current === index}}" style="font-size:20rpx">
							{{item.englishTitle}}
						</view>
					</view>
				</view>
			</view>
		</view>
		<view class="rotateImgsBoxPort">
			<view style="{{rotateImg}}" class="rotateImgsBox">
				<view
				 class="innerBox"
				 wx:for="{{bannerImg}}"
				 wx:key="index"
				 style="transform: rotate({{item.rotate}}deg)"
				>
					<image class="rotateBoxItemImg" src="{{item.imgUrl}}" />
				</view>
			</view>
		</view>
	</view>

当我实线到这个步骤后,产品告诉我还要点击文字滚动到对应的图片,也就是要跳过某些元素。还有可能后台返回的数据不是一定长度的。要针对多种情况都在页面完美的显示5个标题。

下次再讲下实现

总结

  • 利用 swipertransition 方法来联动轮盘的滚动
  • 按照区域划分360度,计算出每个文字的角度
  • 按照360度划分图片的角度