当前位置: 首页 > article >正文

UniApp+Vue3实现高性能无限滚动卡片组件:垂直滑动、触摸拖拽与动态导航的完美结合

引言

在移动应用开发中,流畅且吸引人的用户界面对于提升用户体验至关重要。本文将详细介绍如何使用UniApp和Vue3框架构建一个具有垂直方向无限滚动卡片、触摸拖拽支持、同步导航栏和平滑动画效果的高级UI组件。我们将通过代码分析每个功能的实现细节,帮助开发者理解这类复杂交互组件的开发思路。

实现效果

我们开发的组件具有以下特点:

  1. 垂直方向的无限滚动卡片
  2. 流畅的触摸拖拽支持
  3. 右侧同步高亮的导航栏
  4. 卡片缩放动画效果
  5. 自动循环复位机制

效果预览

核心实现

1. 数据结构与状态管理

首先,我们通过Vue3的组合式API来管理组件的状态

// 导入原始规则数据
import rulesJson from "@/json/rules/index.json";

// 存储原始规则数据
const originalRulesData = ref(rulesJson.rules);

// 创建循环数组,包含三组相同的数据,用于实现无限滚动
const loopRulesData = computed(() => [
  ...originalRulesData.value,
  ...originalRulesData.value,
  ...originalRulesData.value
]);

// 单个卡片高度
const cardHeight = 688.43;

// 当前显示的实际索引(用于右侧导航)
const currentIndex = ref(0); 

// 显示索引(用于卡片位置计算)
const displayIndex = ref(originalRulesData.value.length);

// Y轴偏移量
const translateY = ref(-originalRulesData.value.length * cardHeight);

// 触摸事件相关状态
const startY = ref(0);
const moveY = ref(0);
const isDragging = ref(false);
const enableTransition = ref(true);

这里的关键点是我们创建了一个"循环数组"(loopRulesData),包含三组相同的数据。这是实现无限滚动的核心思路,允许用户在第一组数据之前和第三组数据之后继续滚动,然后在适当的时机无缝重置位置。

2. 垂直方向无限滚动实现

无限滚动的关键在于三个部分:

  1. 使用三倍数据长度的数组
  2. 初始化位置在中间组的起始位置
  3. 当滚动到边界时,无缝重置到中间组的对应位置
<view
  class="cards-container"
  :style="{ 
    transform: `translateY(${translateY}rpx)`,
    transition: enableTransition ? 'transform 0.3s ease-out' : 'none'
  }"
>
  <view
    v-for="(item, index) in loopRulesData"
    :key="index"
    class="rule-card"
    :class="{ 'rule-card-active': displayIndex === index }"
  >
    <!-- 卡片内容 -->
  </view>
</view>
// 触摸结束事件处理
const touchEnd = () => {
  if (!isDragging.value) return;
  isDragging.value = false;

  const diff = translateY.value - moveY.value;
  const direction = diff > 0 ? -1 : 1; // 确定滑动方向
  const minSwipeDistance = cardHeight * 0.15; // 最小滑动距离阈值

  if (Math.abs(diff) > minSwipeDistance) {
    enableTransition.value = true;
    displayIndex.value += direction;
    
    // 更新实际索引并处理循环
    let newIndex = currentIndex.value + direction;
    if (newIndex < 0) {
      newIndex = originalRulesData.value.length - 1;
    } else if (newIndex >= originalRulesData.value.length) {
      newIndex = 0;
    }
    currentIndex.value = newIndex;

    // 执行带动画的滑动
    translateY.value = -displayIndex.value * cardHeight;

    const length = originalRulesData.value.length;
    // 处理无限滚动的边界情况
    if (displayIndex.value >= length * 2) {
      // 当滑动到第三部分时,无动画地重置到中间部分
      setTimeout(() => {
        enableTransition.value = false;
        displayIndex.value = length;
        translateY.value = -length * cardHeight;
      }, 300);
    } else if (displayIndex.value < length) {
      // 当滑动到第一部分时,无动画地重置到中间部分
      setTimeout(() => {
        enableTransition.value = false;
        displayIndex.value = length * 2 - 1;
        translateY.value = -(length * 2 - 1) * cardHeight;
      }, 300);
    }
  } else {
    // 如果滑动距离不够,回弹到原位
    enableTransition.value = true;
    translateY.value = -displayIndex.value * cardHeight;
  }
};

这段代码的精髓在于:

  • 用户看到的是平滑的动画滚动
  • 但在滚动到边界后,我们在动画完成时(通过setTimeout配合动画持续时间)禁用过渡效果,并立即重置位置
  • 重置操作对用户不可见,因此创造了无限滚动的错觉

3. 触摸拖拽支持

触摸拖拽通过三个关键事件实现:touchstart、touchmove和touchend:

// 触摸开始事件处理
const touchStart = (e: TouchEvent) => {
  isDragging.value = true;
  enableTransition.value = false; // 关闭过渡动画以实现流畅拖动
  startY.value = e.touches[0].clientY;
  moveY.value = translateY.value;
};

// 触摸移动事件处理
const touchMove = (e: TouchEvent) => {
  if (!isDragging.value) return;
  
  const currentY = e.touches[0].clientY;
  const diff = (currentY - startY.value) * 1.8; // 1.8为移动速度系数
  translateY.value = moveY.value + diff;
};

这里的关键点:

  • 在touchStart中禁用过渡动画,确保拖拽感觉更自然
  • 在touchMove中计算手指移动距离,并实时更新translateY
  • 使用系数1.8放大移动效果,使滚动感觉更加灵敏

4. 右侧同步导航栏

右侧导航栏需要与当前显示的卡片保持同步:

<view class="right-nav">
  <view
    v-for="(item, index) in originalRulesData"
    :key="index"
    class="nav-item"
    :class="{ 'nav-item-active': currentIndex === index }"
    @click="handleNavClick(index)"
  >
    {{ item.title }}
  </view>
</view>
// 右侧导航点击处理
const handleNavClick = (index: number) => {
  currentIndex.value = index;
  // 直接跳转到中间部分的对应位置
  displayIndex.value = originalRulesData.value.length + index;
  translateY.value = -displayIndex.value * cardHeight;
};

导航逻辑的关键点:

  • 使用currentIndex来跟踪实际的数据索引(而不是循环数组中的索引)
  • 在用户滑动卡片时更新currentIndex
  • 点击导航时,直接跳转到中间组的对应卡片位置

5. 卡片缩放动画效果

缩放动画通过CSS类和条件渲染实现

<view
  class="rule-card"
  :class="{ 'rule-card-active': displayIndex === index }"
>
  <!-- 卡片内容 -->
</view>
.rule-card {
  width: 447.76rpx;
  height: 688.43rpx;
  padding: 30rpx;
  position: relative;
  border-radius: 50rpx;
  opacity: 0.4;
  box-sizing: border-box;
  margin-bottom: 0;
  transition: all 0.3s;
  transform: scale(0.8);
  background-color: transparent;
}

.rule-card-active {
  opacity: 1;
  transform: scale(1);
}

动画效果的关键点:

  • 非活跃卡片应用了scale(0.8)缩小效果和降低透明度
  • 活跃卡片恢复正常大小和完全不透明
  • 使用CSS过渡(transition: all 0.3s)使变化平滑
  • 通过比较displayIndex和循环列表的索引来确定哪张卡片是当前活跃的

6. 自动循环复位

循环复位是无限滚动的关键部分,在touchEnd事件中实现:

// 省略前面的代码...
  const length = originalRulesData.value.length;
    // 处理无限滚动的边界情况
    if (displayIndex.value >= length * 2) {
      // 当滑动到第三部分时,无动画地重置到中间部分
      setTimeout(() => {
        enableTransition.value = false;
        displayIndex.value = length;
        translateY.value = -length * cardHeight;
      }, 300);
    } else if (displayIndex.value < length) {
      // 当滑动到第一部分时,无动画地重置到中间部分
      setTimeout(() => {
        enableTransition.value = false;
        displayIndex.value = length * 2 - 1;
        translateY.value = -(length * 2 - 1) * cardHeight;
      }, 300);
    }

复位机制的关键点:

  • 使用requestAnimationFrame和setTimeout确保在动画完成后再进行重置
  • 禁用过渡动画(enableTransition = false)使重置操作对用户不可见
  • 根据滚动方向决定重置到哪个位置

完整代码解析

以下是组件模板的关键部分:

<template>
  <view class="page-wrapper">
    <!-- 背景图和顶部图片 -->
    <image class="bg-image" src="@/static/ruleBg.jpg" mode="aspectFill" />
    <image class="top-image" src="@/static/ruleTop.jpg" mode="aspectFit" />
    <image class="go-back" @click="handleGoBack" src="@/static/goback.png" />

    <!-- 主要内容区域 -->
    <view class="content-container">
      <!-- 左侧卡片区域,绑定触摸事件 -->
      <view
        class="left-content"
        @touchstart="touchStart"
        @touchmove="touchMove"
        @touchend="touchEnd"
      >
        <!-- 卡片容器,应用动态样式实现滚动效果 -->
        <view
          class="cards-container"
          :style="{ 
            transform: `translateY(${translateY}rpx)`,
            transition: enableTransition ? 'transform 0.3s ease-out' : 'none'
          }"
        >
          <!-- 渲染循环数组中的所有卡片 -->
          <view
            v-for="(item, index) in loopRulesData"
            :key="index"
            class="rule-card"
            :class="{ 'rule-card-active': displayIndex === index }"
          >
            <image
              class="rule-card-bg"
              src="@/static/ruleCoverBg.png"
              mode="scaleToFill"
            />
            <view class="rule-content">
              <view class="rule-title">{{ item.title }}</view>
              <view class="rule-desc">{{ item.content }}</view>
              <button class="play-btn" open-type="share">
                <image src="@/static/playThis.png" mode="aspectFit" />
              </button>
            </view>
          </view>
        </view>
      </view>

      <!-- 右侧导航 -->
      <view class="right-nav">
        <view
          v-for="(item, index) in originalRulesData"
          :key="index"
          class="nav-item"
          :class="{ 'nav-item-active': currentIndex === index }"
          @click="handleNavClick(index)"
        >
          {{ item.title }}
        </view>
      </view>
    </view>
  </view>
</template>

性能优化

这个组件实现中的几个性能优化点值得注意:

  1. 硬件加速:使用 transform: translateZ(0)-webkit-transform: translateZ(0) 触发GPU加速

    .cards-container {
      transform: translateZ(0);
      -webkit-transform: translateZ(0);
      will-change: transform;
      backface-visibility: hidden;
      -webkit-backface-visibility: hidden;
    }
  2. 平滑过渡:使用cubic-bezier曲线优化过渡动画

    transition: transform 0.25s cubic-bezier(0.33, 1, 0.68, 1);
  3. 性能提示:使用will-change属性告知浏览器元素属性将要发生变化

    will-change: transform;
  4. 滚动阈值:设置最小滑动距离阈值,避免微小移动触发滚动

    const minSwipeDistance = cardHeight * 0.15;

适配与样式细节

组件的样式设计也有一些值得关注的细节:

/* 防止底部出现生硬的白色边界 */
.left-content::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 100rpx;
  background: linear-gradient(to bottom, transparent, rgba(0, 0, 0, 0.1));
  pointer-events: none;
}

/* 激活状态的导航项样式 */
.nav-item-active {
  width: 243rpx;
  height: 104rpx;
  background: #ffffff;
  box-shadow: 0rpx 0rpx 22rpx 0rpx #ffae00;
  border-radius: 11rpx 0rpx 0rpx 11rpx;
  opacity: 1;
  font-size: 34rpx;
}

 这些细节包括:

  • 使用渐变遮罩使卡片与背景过渡更自然
  • 为活跃导航项添加阴影和尺寸变化,增强视觉反馈
  • 使用圆角和适当的间距提升整体美观度

无限滚动卡片的核心实现思路详解

无限滚动的本质与思路

无限滚动的本质是创造一种"无尽内容"的视觉错觉,让用户感觉可以无限地向一个方向滚动,而实际上是在有限的内容中循环。以下是实现这种效果的核心思路:

1. 三段式数据结构

无限滚动的基础是创建一个包含三倍数据量的视图:

  • 前段数据:原始数据的一个完整副本
  • 中段数据:原始数据的一个完整副本(主要显示区域)
  • 后段数据:原始数据的一个完整副本

这种结构允许用户在任何方向上都有足够的内容可以滚动。

2. 起始定位策略

组件初始化时,将视图定位到中段数据的起始位置。这确保用户无论向上还是向下滚动,都有一段完整的内容可以查看。

3. "幻觉"制造机制

无限滚动的核心是一个精巧的"幻觉"制造机制,包含以下步骤:

  • 可见滚动阶段:当用户滑动时,组件正常执行带有动画效果的滚动
  • 边界检测:持续监测是否滚动到了前段或后段数据的边界
  • 位置重置:一旦到达边界(用户看到的是前段或后段数据),等待当前滚动动画完成
  • 禁用动画过渡:关键的一步,临时关闭所有过渡动画效果
  • 瞬间跳转:将内容位置瞬间重置到中段数据中的对应位置
  • 恢复动画设置:重新启用过渡动画,为下一次滚动做准备

4. 双重索引系统

为了管理这种复杂的滚动逻辑,需要维护两个不同的索引:

  • 现实索引:追踪用户当前查看的是哪一条实际数据(0到原始数据长度-1)
  • 显示索引:追踪在三倍数组中的实际位置(用于计算偏移量)

当用户滚动时,两个索引都会更新,但使用不同的规则:现实索引循环变化,显示索引可以超出原始数据范围。

5. 临界点重置逻辑

当显示索引达到临界点时,触发重置逻辑:

  • 前段边界:如果滚动到前段数据内,在适当时机将位置重置到中段数据的末尾对应位置
  • 后段边界:如果滚动到后段数据内,在适当时机将位置重置到中段数据的起始对应位置

关键是这个重置过程必须是不可见的,用户不应察觉到任何"跳跃"。

6. 时机控制精确性

重置操作的时机控制至关重要:

  • 必须在当前滚动动画完全结束后进行
  • 必须在下一帧渲染前完成
  • 重置过程必须禁用所有视觉过渡效果
  • 重置完成后立即恢复过渡效果

7. 导航同步机制

当用户使用导航直接跳转时,无限滚动系统需要:

  • 立即更新现实索引
  • 始终跳转到中段数据的对应项
  • 避免不必要的边界检测和重置

这确保了导航操作的稳定性和一致性。

为什么说是"无限"?

这种滚动被称为"无限"是因为:

  1. 感知上的无限:从用户感知角度看,内容可以无限向任一方向滚动,没有起点或终点
  2. 循环无尽:内容会不断循环显示,永不结束
  3. 无感知边界:用户无法感知到内容的重置点,体验上没有"边界"的概念
  4. 数学上的循环:虽然实际数据量有限,但通过位置重置技术创造了一个拓扑上的环形结构

无限滚动的本质启示

从更深层次理解,无限滚动实际上是一个"环形数据结构"在线性界面上的映射:

  • 物理世界中,有限长度的内容不可能无限滚动
  • 但通过创造"跳转点"并隐藏跳转过程,我们将线性结构变成了环形结构
  • 用户在这个环上移动,永远不会到达"尽头"

这种实现方式的精妙之处在于,它不需要实际创建无限量的数据,而只需要有限的数据和精心设计的视觉错觉机制,就能创造出无限内容的体验。

无限滚动的思想本质是:通过在用户不察觉的情况下,巧妙地操纵视图位置,使有限变得无限。

总结

本文详细介绍了如何使用UniApp和Vue3实现一个具有垂直无限滚动、触摸拖拽、同步导航和动画效果的卡片界面。关键实现技术包括:

  1. 无限滚动:通过三倍数组和边界重置技术实现流畅的无限滚动
  2. 触摸交互:结合touchstart、touchmove和touchend事件实现自然的拖拽体验
  3. 视觉反馈:使用缩放和透明度变化为用户提供明确的视觉反馈
  4. 性能优化:利用CSS3硬件加速和动画优化提升滚动性能
  5. 状态同步:维护多个状态变量确保卡片滚动和导航选择同步

这种组件适用于各类需要展示规则、介绍或产品信息的应用场景,通过流畅的交互极大提升了用户体验。开发者可以根据自身需求调整卡片样式、动画效果和交互细节,打造更加个性化的滚动界面。

通过理解这些实现细节,开发者可以将类似的交互模式应用到自己的UniApp项目中,创造出更加吸引人的移动应用界面。


http://www.kler.cn/a/566879.html

相关文章:

  • SQL Server2022版+SSMS安装教程(保姆级)
  • MapReduce编程模型
  • 【AI+智造】在阿里云Ubuntu 24.04上部署DeepSeek R1 14B的完整方案
  • 更换k8s容器运行时环境为docker
  • 菜鸟之路Day18一一IO流综合练习
  • 处理AAL的.nii文件:python获取AAL的各个区域的质心坐标
  • STM32之影子寄存器
  • 【愚公系列】《Python网络爬虫从入门到精通》035-DataFrame数据分组统计整理
  • 一文掌握python中正则表达式的各种使用
  • 天佐.乾坤袋 基于抽屉式文件存储的NoSql数据库
  • Python安装环境变量
  • java项目之基于ssm的物流配送人员车辆调度管理系统的设计与实现(源码+文档)
  • 太速科技-887-基于 RFSoC 47DR的8T8R 100Gbps 软件无线电光纤前端卡
  • Mysql官网下载Windows、Linux各个版本
  • 48页PDF | GBT 36073-2018 数据管理能力成熟度评估模型 (附下载)
  • [讨论] oracle数据库游标更新时sql%rowcount影响数量记录的一个疑问
  • 本地部署AI大模型之PyTorch:如何使用whl文件安装PyTorch
  • Linux上用C++和GCC开发程序实现不同MySQL实例下单个Schema之间的稳定高效的数据迁移
  • 鸿蒙 ArkUI 实现 2048 小游戏
  • Spring系列学习之Spring CredHub