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 的最终实现效果:

相关推荐
惜.己9 分钟前
javaScript基础(8个案例+代码+效果图)
开发语言·前端·javascript·vscode·css3·html5
奇客软件28 分钟前
iPhone使用技巧:如何恢复变砖的 iPhone 或 iPad
数码相机·macos·ios·电脑·笔记本电脑·iphone·ipad
什么鬼昵称32 分钟前
Pikachu-csrf-CSRF(get)
前端·csrf
长天一色1 小时前
【ECMAScript 从入门到进阶教程】第三部分:高级主题(高级函数与范式,元编程,正则表达式,性能优化)
服务器·开发语言·前端·javascript·性能优化·ecmascript
NiNg_1_2341 小时前
npm、yarn、pnpm之间的区别
前端·npm·node.js
秋殇与星河1 小时前
CSS总结
前端·css
BigYe程普2 小时前
我开发了一个出海全栈SaaS工具,还写了一套全栈开发教程
开发语言·前端·chrome·chatgpt·reactjs·个人开发
余生H2 小时前
前端的全栈混合之路Meteor篇:关于前后端分离及与各框架的对比
前端·javascript·node.js·全栈
程序员-珍2 小时前
使用openapi生成前端请求文件报错 ‘Token “Integer“ does not exist.‘
java·前端·spring boot·后端·restful·个人开发
axihaihai2 小时前
网站开发的发展(后端路由/前后端分离/前端路由)
前端