H5 实现原生APP的页面右滑返回效果

一、前言

最近在开发 APP, 使用 ios 的webview 嵌套的h5实现的, 遇到了一个问题, 如何让 webview 内部的 h5 也实现滑动左侧边缘实现页面移开的效果, 这个效果其实在浏览器或者小程序里, 不需要程序员适配, 他内部就已经将这个功能做了适配,但是webview里没有这个效果。 需要自己手动实现一下。

开始是我在 webview 环境里面写代码判断页面栈有就执行浏览器的返回, 但是没有动画

然后又开始在 H5 上增加了动画, 但是无法控制页面的拖拽暂停

然后老板又非要在实现原生 APP 如下这种跳转功能, 既然 webview 那边无法实现只能在 H5 这一测实现了。

二、功能分析

实现这个功能前提就是需要页面之间具有层叠关系, 像 PC 的单页面肯定就无法实现, 因为我们项目是 Taro 编译的 H5 仔细一观察发现,为了和小程序保持统一, 他已经将页面的层级进行处理, 可以通过 Taro.getCurrentPages 获取到页面栈, 最多维护的栈为10层, 一想也是,本身这些dom都在页面中在维护, 栈数多了肯定影响性能。

有了这个前提后, 在实现我们的功能就容易多了, 实现之前我们先把页面切换的动画给他加上,Taro是支持开启动画的 app.config.js 中可以把动画开启,改一下参数

js 复制代码
 animation: {
    // 动画切换时间,单位毫秒
    duration: 300, // 动画切换时间,单位毫秒
    delay: 20,
  },

加上动画后, 发现了一个问题, 就是在切换的时候, Taro 的页面切换之前会有一个短暂的白屏, 效果很不好,通过断点调试,发现是在切换的时候,加了一个 display:none。 猜想是为了修复某个问题做的兼容吧

通过覆盖他的样式,先去掉这个问题, 在验证没发现什么问题

css 复制代码
  .taro_page_shade {
    display: block !important;
  }

  .taro_page_shade
    > .taro_page.taro_page_show.taro_page_stationed:not(.taro_page_shade):not(.taro_tabbar_page):not(:last-child) {
    display: block !important;
  }

分析后, 确定该功能可以实现

需求如下

  • 1、按住左侧边缘开始拖拽页面滑动
  • 2、滑动中禁用上下滚动
  • 3、性能方面,启动transform3d 和 will-change 进行加速
  • 4、按住右滑不到一半松开,恢复到起始位置, 按住右滑到超过一半时候松开,快速完成页面切换。调用返回上一层页面

三、功能实现

因为是直接操作的dom和css, 所以这里不需要使用 setState, 会产生性能问题

先初始化一些变量

js 复制代码
 let startTime = 0;
let currentX = 0;
let moving = false;
let startX = 0;

3-1、手指从左侧按下的操作

js 复制代码
const touchStartHandler = (e) => {
  const touchX = e.touches[0].clientX;
  const threshold = window.innerWidth * 0.1;
  if (touchX < threshold) {
    startTime = Date.now();
    const curPage = document.getElementById(currentPage.$taroPath) as HTMLElement;
    if (curPage.style) {
      curPage.style.willChange = 'transform';
      curPage.style.overflowY = 'hidden';
    }
    startX = touchX;
    moving = true;
  }
};

按下的时候,设置一下按下的时间和开启dom的willChange,禁用页面的overflow,moving记录当前是按下状态,可以进行拖动

3-2、手指滑动

js 复制代码
    const touchMoveHandler = throttle((e) => {
      if (moving) {
        const touchX = e.touches[0].clientX;
        currentX = touchX;
        const moveDistance = touchX - startX;
        const curPage = document.getElementById(currentPage.$taroPath) as HTMLElement;
        if (curPage.style) curPage.style.transform = `translate3d(${moveDistance}px, 0, 0)`;
      }
    }, 100);

按下滑动,通过页面栈拿到最上层的页面进行操作, 计算拖动距离,应用到dom上, 增加一个节流功能,减少页面的抖动

3-3、手指移开

js 复制代码
 const touchEndHandler = () => {
      if (moving) {
        const endTime = Date.now(); // 记录触摸结束的时间
        const elapsedTime = endTime - startTime; // 计算滑动时间
        moving = false;
        const curPage = document.getElementById(currentPage.$taroPath) as HTMLElement;
        curPage.style.transition = 'transform 0.1s ease';
        const moveDistance = currentX - startX;
        const halfWidth = window.innerWidth * 0.5;
        const speed = moveDistance / elapsedTime; // 计算滑动速度
        // 设置速度阈值,这里假设为 0.25(可根据实际情况调整)
        const speedThreshold = 0.25;
        // 滑动距离大于屏幕一半或滑动速度大于 0.25 时返回上一页
        if (moveDistance > halfWidth || speed > speedThreshold) {
          curPage.style.transform = 'translate3d(100%,0,0)';
          navigateBack();
        } else {
          curPage.style.transform = 'translate3d(0px,0,0)';
        }
        setTimeout(() => {
          curPage.style.willChange = '';
          curPage.style.transition = '';
          curPage.style.transform = '';
          curPage.style.overflowY = 'auto';
        }, 300);
      }
    };

计算滑动时间和滑动距离, 通过速度判断是否是快速右滑, 如果不是快速右滑, 那么就判断当前滑动距离和屏幕尺寸进行比较, 大于一半完成返回操作, 小于一半,恢复页面的状态,重制变量标识

3-4、 完整 hooks 代码

js 复制代码
import Taro from '@tarojs/taro';
import { useEffect } from 'react';
import { useNavigate } from './useNavigate';
import { throttle } from '@/utils/utils';

let startTime = 0;
let currentX = 0;
let moving = false;
let startX = 0;
const useIosSlideBack = () => {
  // isIos 为 true 时z执行
  const pages = Taro.getCurrentPages();
  const currentPage = pages[pages.length - 1];
  const { navigateBack } = useNavigate();
  const isRunning = pages.length > 1 && window.__iosApp__;

  useEffect(() => {
    const touchStartHandler = (e) => {
      const touchX = e.touches[0].clientX;
      const threshold = window.innerWidth * 0.1;
      if (touchX < threshold && isRunning) {
        startTime = Date.now();
        const curPage = document.getElementById(currentPage.$taroPath) as HTMLElement;
        if (curPage.style) {
          curPage.style.willChange = 'transform';
          curPage.style.overflowY = 'hidden';
        }
        startX = touchX;
        moving = true;
      }
    };

    const touchMoveHandler = throttle((e) => {
      if (moving && isRunning) {
        const touchX = e.touches[0].clientX;
        currentX = touchX;
        const moveDistance = touchX - startX;
        const curPage = document.getElementById(currentPage.$taroPath) as HTMLElement;
        if (curPage.style) curPage.style.transform = `translate3d(${moveDistance}px, 0, 0)`;
      }
    }, 100);

    const touchEndHandler = () => {
      if (moving && isRunning) {
        const endTime = Date.now(); // 记录触摸结束的时间
        const elapsedTime = endTime - startTime; // 计算滑动时间
        moving = false;
        const curPage = document.getElementById(currentPage.$taroPath) as HTMLElement;
        curPage.style.transition = 'transform 0.1s ease';
        const moveDistance = currentX - startX;
        const halfWidth = window.innerWidth * 0.5;
        const speed = moveDistance / elapsedTime; // 计算滑动速度
        // 设置速度阈值,这里假设为 0.25(可根据实际情况调整)
        const speedThreshold = 0.25;
        // 滑动距离大于屏幕一半或滑动速度大于 0.25 时返回上一页
        if (moveDistance > halfWidth || speed > speedThreshold) {
          curPage.style.transform = 'translate3d(100%,0,0)';
          navigateBack();
        } else {
          curPage.style.transform = 'translate3d(0px,0,0)';
        }
        setTimeout(() => {
          curPage.style.willChange = '';
          curPage.style.transition = '';
          curPage.style.transform = '';
          curPage.style.overflowY = 'auto';
        }, 300);
      }
    };

    if (!isRunning) return;
    document.addEventListener('touchstart', touchStartHandler);
    document.addEventListener('touchmove', touchMoveHandler);
    document.addEventListener('touchend', touchEndHandler);

    return () => {
      document.removeEventListener('touchstart', touchStartHandler);
      document.removeEventListener('touchmove', touchMoveHandler);
      document.removeEventListener('touchend', touchEndHandler);
    };
  }, [isRunning]);

  return null;
};

export default useIosSlideBack;

四、总结

本文在 Taro 生成 H5 的页面栈逻辑的基础上, 实现了一个原生app 右滑返回页面的效果, 当然这个效果是基于特定场景,比如通过 webview 嵌套 h5 又想要原生翻页效果的时候用,如果不是这个场景, 比如小程序, 浏览器自身起始已经实现了这个功能, 不需要在实现, 如果用原生开发APP或者用框架开发, 也都是可以自定义转场动画的

嵌入 APP 的最终实现效果:

相关推荐
亿元程序员4 分钟前
为什么大家都那么喜欢用Shader?
前端
Codebee5 分钟前
OODER Studio设计工具导入体系深度解析
前端
weedsfly9 分钟前
前端开发中的命令模式——从点餐退单到异步任务调度
前端·javascript·面试
慢功夫10 分钟前
💡第四篇:编辑器内核, Monaco model、URI、dirty 状态和 tabs 是怎么协作的?
前端·visual studio code
小马过河R12 分钟前
微前端实践:从概念、原理到 qiankun 落地
前端·人工智能·前端框架
用户0595401744620 分钟前
AI记忆存储一致性踩坑实录:每天手工回归200条对话,直到我用pytest+Redis搭建自动化验证
前端·css
weedsfly21 分钟前
前端中的适配器模式 vs 外观模式
前端·javascript·面试
图扑软件22 分钟前
其域创新 × 图扑软件|3DGS 高斯泼溅智慧钢厂实景孪生
前端·低代码·3d·编辑器·图形渲染·可视化
labixiong23 分钟前
写给前端的 Generator 指南:从 yield 到 async/await 的完整进化
前端·javascript·前端框架
asdzx6724 分钟前
使用 Spire.WordJS 在 React 中实现在线 Word 文档编辑
前端·react.js·word