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

相关推荐
y先森13 分钟前
CSS3中的伸缩盒模型(弹性盒子、弹性布局)之伸缩容器、伸缩项目、主轴方向、主轴换行方式、复合属性flex-flow
前端·css·css3
前端Hardy13 分钟前
纯HTML&CSS实现3D旋转地球
前端·javascript·css·3d·html
susu108301891116 分钟前
vue3中父div设置display flex,2个子div重叠
前端·javascript·vue.js
IT女孩儿1 小时前
CSS查缺补漏(补充上一条)
前端·css
吃杠碰小鸡2 小时前
commitlint校验git提交信息
前端
虾球xz3 小时前
游戏引擎学习第20天
前端·学习·游戏引擎
我爱李星璇3 小时前
HTML常用表格与标签
前端·html
疯狂的沙粒3 小时前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
小镇程序员3 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
野槐3 小时前
前端图像处理(一)
前端