You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter中为InAppWebView Epub阅读器添加翻页卷曲效果问题

解决InAppWebView中EPUB翻页卷曲动画后页面切换失败问题

问题描述

我在应用中使用InAppWebView展示EPUB文件,已通过JavaScript实现字体样式及设置修改。添加JavaScript翻页卷曲效果实现前后翻页时,动画可正常播放,但动画结束后无法切换页面,且下方显示空白背景而非目标页面,求解决办法。

关联Dart代码

InAppWebView(
  onProgressChanged: (s, p) {
    progress = p / 100;
    setState(() {});
  },
  onWebViewCreated: (s) {
    webViewController = s;
    setState(() {});

    webViewController?.platform.clearAllCache();
    if (Platform.isAndroid) {
      webViewController?.platform
          .clearClientCertPreferences();
    }
    styleWebView =
        StyleWebView(controller: webViewController!);
    webViewController?.addJavaScriptHandler(
        handlerName: "share",
        callback: (args) async {
          final text = (args[0])['text'];
          if (Utils.token.isEmpty) {
            Alerts.dialog(context,
                child: const LoginDialog());
            return;
          }
          Alerts.bottomSheet(context,
              child: ShareBottomSheet(
                cubit: cubit,
                text: text,
                id: widget.id,
              ));
        });

    webViewController?.addJavaScriptHandler(
        handlerName: "translate",
        callback: (args) async {
          log(args[0].runtimeType.toString());
          final text = (args[0])['text'];
          Alerts.dialog(
            context,
            child: TranslationDialog(
              text: text,
            ),
          );
        });
    webViewController?.addJavaScriptHandler(
        handlerName: "current_chapter",
        callback: (args) async {
          final text = (args[0][0]);
          isCurrentBookMark = false;
          currentChapter = ChapterWebview.fromJson(text);
          setState(() {});
        });
    webViewController?.addJavaScriptHandler(
        handlerName: "reader_customization",
        callback: (args) async {
          final data = (args[0]);
          textStyle = textStyle.copyWith(
              fontFamily: data['font_family'],
              fontSize:
                  double.tryParse(data['font_size']),
              height:
                  double.tryParse(data['line_height']));
          textAlign = styleWebView.textAlignFromName(
              data['text_alignment'] ?? "center");

          backgroundColor = Color(int.parse(
                  data['background'].substring(1, null),
                  radix: 16) +
              0xFF000000);

          if (data['background'] == "#333333") {
            brightness = Brightness.dark;
          }
          setState(() {});
        });

    webViewController?.addJavaScriptHandler(
        handlerName: "add_note",
        callback: (args) async {
          final data = args[0];
          Alerts.dialog(context,
              child: AddNoteDialog(
                bookId: widget.id,
                chapterName: data["chapter_name"] ?? "",
                content: data['text'] ?? "",
                path: data['path'] ?? "",
              ));
        });
    webViewController?.addJavaScriptHandler(
        handlerName: "current_bookmark",
        callback: (args) async {
          isCurrentBookMark = true;
          setState(() {});
        });

    webViewController?.addJavaScriptHandler(
        handlerName: "chapters",
        callback: (args) async {
          log("here chapters $args");
          final chapters = (args[0]);
          chaptersWebView = [];
          for (var element in chapters) {
            chaptersWebView
                ?.add(ChapterWebview.fromJson(element));
          }
          setState(() {});
          print(chaptersWebView?.length);
        });
  },
  contextMenu: ContextMenu(
      onCreateContextMenu: (s) {
        webViewController?.evaluateJavascript(
            source: openMenu);
      },
      settings: ContextMenuSettings(
          hideDefaultSystemContextMenuItems: true)),
  initialUrlRequest: URLRequest(url: webUri),
  gestureRecognizers: <Factory<
      OneSequenceGestureRecognizer>>{
    Factory<OneSequenceGestureRecognizer>(
      () => EagerGestureRecognizer(),
    ),
    Factory<LongPressGestureRecognizer>(
      () => LongPressGestureRecognizer(),
    ),
    Factory<TapGestureRecognizer>(
      () => TapGestureRecognizer()
        ..onTap = () {
          print("tapped");
        },
    ),
    Factory<TapAndHorizontalDragGestureRecognizer>(
      () => TapAndHorizontalDragGestureRecognizer()
        ..onDragStart = (details) {
          print(details);
        },
    ),
    Factory<HorizontalDragGestureRecognizer>(
      () => HorizontalDragGestureRecognizer()
        ..onUpdate = (details) {
          log(details.delta.toString());
        },
    ),
    Factory<PanGestureRecognizer>(
      () => PanGestureRecognizer()
        ..onUpdate = (details) {
          log(details.delta.toString());
        },
    )
  },
),

解决方案

  • 修复动画回调中的页面切换逻辑
    动画结束后无页面切换,通常是animationend事件的回调逻辑缺失或错误。确保动画完成后正确替换页面内容:

    // 示例:绑定动画结束事件
    const flipContainer = document.getElementById('flip-container');
    flipContainer.addEventListener('animationend', () => {
      // 确认目标页面DOM已加载
      const targetPage = document.getElementById('target-page');
      const currentPage = document.getElementById('current-page');
      if (targetPage?.innerHTML.trim()) {
        // 切换页面显示状态
        currentPage.style.display = 'none';
        targetPage.style.display = 'block';
        // 通知Flutter更新当前章节信息
        window.flutter_inappwebview.callHandler('current_chapter', [newChapterData]);
      }
    });
    
  • 提前预加载目标页面内容
    空白背景说明目标页面未加载完成。在翻页动画启动前,提前加载下/上一章内容到隐藏容器:

    // 预加载下一章
    async function preloadNextChapter(nextChapterHref) {
      const response = await fetch(nextChapterHref);
      const html = await response.text();
      document.getElementById('target-page').innerHTML = html;
    }
    
  • 简化手势识别器避免冲突
    代码中过多手势识别器可能拦截JS侧的翻页事件,保留核心手势并传递事件给JS:

    gestureRecognizers: <Factory<OneSequenceGestureRecognizer>>{
      Factory<HorizontalDragGestureRecognizer>(
        () => HorizontalDragGestureRecognizer()
          ..onUpdate = (details) {
            // 将手势事件传递给JS处理
            webViewController?.evaluateJavascript(
              source: `handleDragUpdate(${details.delta.dx})`
            );
          },
      ),
    },
    
  • 验证Flutter-JS通信逻辑
    检查JS侧是否在页面切换后正确调用window.flutter_inappwebview.callHandler('current_chapter', ...),同时确保Flutter侧current_chapter回调能正确解析数据,避免因数据格式错误导致状态更新失败。

  • 重置EPUB页面样式
    部分EPUB章节的固定定位、溢出隐藏样式会导致页面无法正常显示,添加样式重置:

    document.querySelectorAll('.epub-chapter').forEach(chapter => {
      chapter.style.position = 'relative';
      chapter.style.overflow = 'visible';
      chapter.style.display = 'none';
    });
    // 默认显示当前章节
    document.querySelector('.current-chapter').style.display = 'block';
    

内容的提问来源于stack exchange,提问作者Hamza Kaleem

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 13:27:32