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项目中,创造出更加吸引人的移动应用界面。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/979263.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

使用Docker Compose部署 MySQL8

MySQL 8 是一个功能强大的关系型数据库管理系统,而 Docker 则是一个流行的容器化平台。结合使用它们可以极大地简化 MySQL 8 的部署过程,并且确保开发环境和生产环境的一致性。 安装 Docker 和 Docker Compose 首先,确保你的机器上已经安装了 Docker 和 Docker Compose。 …

计算机黑皮书191本分享pdf

“黑皮书”通常指的是由机械工业出版社出版的计算机科学丛书。这些书籍的封面通常是黑色的&#xff0c;因此得名“黑皮书”。这些书籍涵盖了计算机科学的各个领域&#xff0c;包括操作系统、计算机网络、软件工程、编译原理、数据库等。 获取链接&#xff1a;链接:https://pan…

LeapVAD:通过认知感知和 Dual-Process 思维实现自动驾驶的飞跃

25年1月来自浙江大学、上海AI实验室、慕尼黑工大、同济大学和中科大的论文“LeapVAD: A Leap in Autonomous Driving via Cognitive Perception and Dual-Process Thinking”。 尽管自动驾驶技术取得长足进步&#xff0c;但由于推理能力有限&#xff0c;数据驱动方法仍然难以应…

R语言+AI提示词:贝叶斯广义线性混合效应模型GLMM生物学Meta分析

全文链接&#xff1a;https://tecdat.cn/?p40797 本文旨在帮助0基础或只有简单编程基础的研究学者&#xff0c;通过 AI 的提示词工程&#xff0c;使用 R 语言完成元分析&#xff0c;包括数据处理、模型构建、评估以及结果解读等步骤&#xff08;点击文末“阅读原文”获取完整代…

面试八股文--数据库基础知识总结(2) MySQL

本文介绍关于MySQL的相关面试知识 一、关系型数据库 1、定义 关系型数据库&#xff08;Relational Database&#xff09;是一种基于关系模型的数据库管理系统&#xff08;DBMS&#xff09;&#xff0c;它将数据存储在表格&#xff08;表&#xff09;中&#xff0c;并通过表格…

入门基础项目(SpringBoot+Vue)

文章目录 1. css布局相关2. JS3. Vue 脚手架搭建4. ElementUI4.1 引入ElementUI4.2 首页4.2.1 整体框架4.2.2 Aside-logo4.2.3 Aside-菜单4.2.4 Header-左侧4.2.5 Header-右侧4.2.6 iconfont 自定义图标4.2.7 完整代码 4.3 封装前后端交互工具 axios4.3.1 安装 axios4.3.2 /src…

手机放兜里,支付宝“碰一下”被盗刷?

大家好&#xff0c;我是小悟。 近期&#xff0c;网络上关于“支付宝‘碰一下’支付易被盗刷”的传言甚嚣尘上&#xff0c;不少用户对此心生疑虑。 首先&#xff0c;要明确一点&#xff1a;“碰一下”支付并不会像某些传言中所描述的那样容易被隔空盗刷。这一观点已经得到了支付…

【多模态】Magma多模态AI Agent

1. 前言 微软杨建伟团队&#xff0c;最近在AI Agent方面动作连连&#xff0c;前两天开源了OmniParser V2&#xff0c;2月26日又开源了Magma&#xff0c;OmniParser专注在对GUI的识别解析&#xff0c;而Magma则是基于多模态技术&#xff0c;能够同时应对GUI和物理世界的交互&…

解决yarn run dev报错: TypeError: Cannot create property ‘-registry-npmmirror-com‘

一、问题描述 在使用yarn run dev启动项目时&#xff0c;遇到以下错误&#xff1a; error TypeError: Cannot create property -registry-npmmirror-com on string {"-registry-npmmirror-com":true}二、解决方案 使用npm config get registry和yarn config get r…

HONOR荣耀MagicBook 15 2021款 独显(BOD-WXX9,BDR-WFH9HN)原厂Win10系统

适用型号&#xff1a;【BOD-WXX9】 MagicBook 15 2021款 i7 独显 MX450 16GB512GB (BDR-WFE9HN) MagicBook 15 2021款 i5 独显 MX450 16GB512GB (BDR-WFH9HN) MagicBook 15 2021款 i5 集显 16GB512GB (BDR-WFH9HN) 链接&#xff1a;https://pan.baidu.com/s/1S6L57ADS18fnJZ1…

车载电源管理新标杆NCV8460ADR2G 在汽车电子负载开关中的应用

NCV8460ADR2G是一款完全保护的高压侧驱动器&#xff0c;可用于开关各种负载&#xff0c;如灯泡、电磁阀和其他致动器。该器件可以通过有源电流限制和高温关断针对过载情况进行内部保护。 诊断状态输出引脚提供了高温以及开关状态开路负载情况的数字故障指示。 NCV8460ADR2G产品…

MYSQL数据库创建命令

1.创建数据库 2.查看数据库 3.切换数据库 4.删除数据库 5.查看数据库 6.练习 导出文件 可在文件里查看到

因子有效性的审判使者——回测分析【量化实践】

我叫补三补四&#xff0c;很高兴见到大家&#xff0c;欢迎一起学习交流和进步 今天来讲一讲alpha策略制定后的测试问题 因子回测的方法公说公有理&#xff0c;婆说婆有理&#xff0c;笔者在这里也不会盖棺定论&#xff0c;就像学模型的时候经常听到的老生常谈&#xff1a;从哥白…

SpringBoot 整合mongoDB并自定义连接池,实现多数据源配置

要想在同一个springboot项目中使用多个数据源&#xff0c;最主要是每个数据源都有自己的mongoTemplate和MongoDbFactory。mongoTemplate和MongoDbFactory是负责对数据源进行交互的并管理链接的。 spring提供了一个注解EnableMongoRepositories 用来注释在某些路径下的MongoRepo…

spark的一些指令

一&#xff0c;复制和移动 1、复制文件 格式&#xff1a;cp 源文件 目标文件 示例&#xff1a;把file1.txt 复制一份得到file2.txt 。那么对应的命令就是&#xff1a;cp file1.txt file2.txt 2、复制目录 格式&#xff1a;cp -r 源文件 目标文件夹 示例&#xff1a;把目…

linux之crosstool-NG(1)生成toolchain

Linux之crosstool-NG(1)生成交叉编译Toolchain Author: Once Day Date: 2025年2月25日 一位热衷于Linux学习和开发的菜鸟&#xff0c;试图谱写一场冒险之旅&#xff0c;也许终点只是一场白日梦… 本文相关内容翻译自crosstool-NG官方文档。 漫漫长路&#xff0c;有人对你微笑…

Vue 项目中配置代理的必要性与实现指南

Vue 项目中配置代理的必要性与实现指南 在 Vue 前端项目的开发过程中&#xff0c;前端与后端地址通常不同&#xff0c;可能引发跨域问题。为了在开发环境下顺畅地请求后端接口&#xff0c;常常会通过配置**代理&#xff08;proxy&#xff09;**来解决问题。这篇文章将详细解析…

【AI论文】MoM: 使用混合记忆(Mixture-of-Memories)的线性序列建模

摘要&#xff1a;线性序列建模方法&#xff0c;如线性注意力、状态空间建模和线性循环神经网络&#xff08;RNNs&#xff09;&#xff0c;通过降低训练和推理的复杂性&#xff0c;显著提高了效率。然而&#xff0c;这些方法通常将整个输入序列压缩成一个固定大小的单一记忆状态…

鸿蒙app 开发中的 横线 竖线 line

实现下面中线 可以使用 line 而不用前端 类似的 盒子的边框来实现 这种实现 方式 更加的灵活 参考的官方文档

腿足机器人之十四-强化学习SAC算法

腿足机器人之十四-强化学习SAC算法 核心原理关键结构输入输出规范&#xff08;以 Humanoid-v5 为例&#xff09; Soft Actor-Critic&#xff08;SAC&#xff09;是一种基于属于 Actor-Critic 框架的算法&#xff0c;属于最大熵的强化学习算法&#xff0c;最大熵的特点就是不仅考…