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

如何在Flutter中实现Usman Taha书法风格的15行古兰经翻页效果

问题描述

我想用指定的数据库和604款字体制作古兰经应用,目前遇到两个核心问题:

  • 无法完成单页15行内容的精准排版
  • 需要实现左右拖拽切换604个完整页码的效果(类似示例图的翻页交互)

附上我的Flutter代码:

import 'dart:ffi';
import 'dart:io';
import 'dart:typed_data';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import 'package:sqflite_common/sqlite_api.dart';

class Quran extends StatefulWidget {
  const Quran({Key? key});

  @override
  State<Quran> createState() => _QuranState();
}

class _QuranState extends State<Quran> {
  List<String> ayahs = [];
  double point = 100.0;

  @override
  void initState() {
    sql();
    super.initState();
  }

  void sql() async {
    sqfliteFfiInit();
    databaseFactory = databaseFactoryFfi;

    var documentsDirectory = await getApplicationDocumentsDirectory();
    var databasePath = '${documentsDirectory.path}/moshaf15.db';

    if (!(await File(databasePath).exists())) {
      ByteData data = await rootBundle.load('assets/db/moshaf15.db');
      List<int> bytes = data.buffer
          .asUint8List(data.offsetInBytes, data.lengthInBytes);
      await File(databasePath).writeAsBytes(bytes);
    }

    Database database = await openDatabase(databasePath);

    List<Map<String, dynamic>> result = await database.rawQuery(
      'SELECT * FROM glyph WHERE page_number = 264 ORDER BY line_number ASC',
    );

    for (var row in result) {
      String code = row['code'].toString();
      ayahs.add(code);
    }

    await database.close();
  }

  @override
  Widget build(BuildContext context) {
    double height = MediaQuery.of(context).size.height;
    double width = MediaQuery.of(context).size.width;
    double heightSunWidth = (height + width);

    return SafeArea(
      child: Scaffold(
        appBar: AppBar(
          title: Text(
            "بیت الاحزان",
            style: TextStyle(
              fontSize: heightSunWidth / 60,
              fontFamily: "Vazir-semibold",
            ),
          ),
          centerTitle: true,
          backgroundColor: Colors.green,
        ),
        body: ListView.builder(
          itemCount: (ayahs.length / 15).ceil(),
          itemBuilder: (context, index) {
            int startIndex = index * 15;
            int endIndex = startIndex + 15;
            List<String> subAyahs = ayahs.sublist(startIndex, endIndex);
            return Column(
              children: subAyahs.map((ayah) {
                return Text(
                  String.fromCharCode(int.parse(ayah)),
                  style: TextStyle(fontSize: 100, fontFamily: 'p264'),
                );
              }).toList(),
            );
          },
        ),
        bottomNavigationBar: BottomAppBar(
          clipBehavior: Clip.antiAlias,
          color: Colors.green,
          shape: CircularNotchedRectangle(),
          notchMargin: 5,
          height: 60,
          child: Row(
            mainAxisSize: MainAxisSize.max,
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              PointButton(heightSunWidth, "تپق", 0.5),
              PointButton(heightSunWidth, "اعراب", 1.5),
              PointButton(heightSunWidth, "کلمه", 3.0),
              SizedBox(
                width: width / 10,
              ),
              PointButton(heightSunWidth, "جمله", 4.0),
              PointButton(heightSunWidth, "سر آیه", 4.0),
              PointButton(heightSunWidth, "سر صفحه", 5.0),
            ],
          ),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () {},
          backgroundColor: Colors.green,
          child: Icon(Icons.check),
        ),
        floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
      ),
    );
  }

  Widget PointButton(
    double hw,
    String title,
    double point,
  ) {
    return Expanded(
      child: InkWell(
        onTap: () {
          setState(() {
            this.point -= point;
          });
        },
        child: Container(
          child: Center(
            child: Text(
              title,
              style: TextStyle(
                fontFamily: "Vazir-regular",
                color: Colors.white,
              ),
            ),
          ),
        ),
      ),
    );
  }
}

解决方案

一、修复15行内容排版问题

核心问题分析

  1. 阿拉伯文本是RTL(从右到左)方向,未设置文本方向导致排版错乱
  2. ListView会滚动所有分页,无法实现单页15行的固定展示
  3. 未控制行高与间距,导致15行无法适配屏幕

代码调整

替换原body中的ListView.builder为以下代码:

body: Container(
  height: MediaQuery.of(context).size.height - kToolbarHeight - 60, // 减去导航栏和底部栏高度
  padding: EdgeInsets.symmetric(horizontal: 16),
  child: Column(
    mainAxisAlignment: MainAxisAlignment.spaceEvenly, // 均匀分布15行
    children: List.generate(15, (index) {
      if (index >= ayahs.length) return SizedBox(); // 处理行数不足的情况
      return Text(
        String.fromCharCode(int.parse(ayahs[index])),
        style: TextStyle(fontSize: point, fontFamily: 'p264'),
        textDirection: TextDirection.rtl, // 设置RTL方向
        textAlign: TextAlign.right, // 右对齐
      );
    }),
  ),
),

二、实现左右拖拽翻页效果

核心逻辑

  1. 维护当前页码状态,拖拽时更新页码并重新查询对应页数据
  2. 用PageView.builder实现原生拖拽翻页交互
  3. 优化数据库查询与字体加载逻辑,避免性能问题

完整代码调整

class _QuranState extends State<Quran> {
  List<String> ayahs = [];
  double point = 100.0;
  int currentPage = 264; // 当前页码
  final Map<int, List<String>> _pageCache = {}; // 缓存已加载的页码数据

  @override
  void initState() {
    _fetchPageData(currentPage);
    super.initState();
  }

  // 异步查询指定页码数据,带缓存
  Future<void> _fetchPageData(int pageNum) async {
    if (_pageCache.containsKey(pageNum)) {
      setState(() => ayahs = _pageCache[pageNum]!);
      return;
    }

    sqfliteFfiInit();
    databaseFactory = databaseFactoryFfi;

    var documentsDirectory = await getApplicationDocumentsDirectory();
    var databasePath = '${documentsDirectory.path}/moshaf15.db';

    if (!(await File(databasePath).exists())) {
      ByteData data = await rootBundle.load('assets/db/moshaf15.db');
      List<int> bytes = data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
      await File(databasePath).writeAsBytes(bytes);
    }

    Database database = await openDatabase(databasePath);
    List<Map<String, dynamic>> result = await database.rawQuery(
      'SELECT * FROM glyph WHERE page_number = ? ORDER BY line_number ASC',
      [pageNum],
    );

    List<String> pageAyahs = result.map((row) => row['code'].toString()).toList();
    setState(() {
      ayahs = pageAyahs;
      _pageCache[pageNum] = pageAyahs;
    });

    await database.close();
  }

  @override
  Widget build(BuildContext context) {
    double height = MediaQuery.of(context).size.height;
    double width = MediaQuery.of(context).size.width;
    double heightSunWidth = (height + width);

    return SafeArea(
      child: Scaffold(
        appBar: AppBar(
          title: Text(
            "بیت الاحزان",
            style: TextStyle(
              fontSize: heightSunWidth / 60,
              fontFamily: "Vazir-semibold",
            ),
          ),
          centerTitle: true,
          backgroundColor: Colors.green,
        ),
        // 用PageView实现拖拽翻页
        body: PageView.builder(
          itemCount: 604, // 总页码数
          controller: PageController(initialPage: currentPage - 1), // 初始页码对应索引
          onPageChanged: (index) {
            setState(() => currentPage = index + 1);
            _fetchPageData(currentPage);
          },
          itemBuilder: (context, index) {
            int pageNum = index + 1;
            List<String> currentAyahs = _pageCache[pageNum] ?? [];

            return Container(
              height: height - kToolbarHeight - 60,
              padding: EdgeInsets.symmetric(horizontal: 16),
              child: Column(
                mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                children: List.generate(15, (lineIndex) {
                  if (lineIndex >= currentAyahs.length) return SizedBox();
                  return Text(
                    String.fromCharCode(int.parse(currentAyahs[lineIndex])),
                    style: TextStyle(fontSize: point, fontFamily: 'p$pageNum'),
                    textDirection: TextDirection.rtl,
                    textAlign: TextAlign.right,
                  );
                }),
              ),
            );
          },
        ),
        bottomNavigationBar: BottomAppBar(
          clipBehavior: Clip.antiAlias,
          color: Colors.green,
          shape: CircularNotchedRectangle(),
          notchMargin: 5,
          height: 60,
          child: Row(
            mainAxisSize: MainAxisSize.max,
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              PointButton(heightSunWidth, "تپق", 0.5),
              PointButton(heightSunWidth, "اعراب", 1.5),
              PointButton(heightSunWidth, "کلمه", 3.0),
              SizedBox(width: width / 10),
              PointButton(heightSunWidth, "جمله", 4.0),
              PointButton(heightSunWidth, "سر آیه", 4.0),
              PointButton(heightSunWidth, "سر صفحه", 5.0),
            ],
          ),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () {},
          backgroundColor: Colors.green,
          child: Icon(Icons.check),
        ),
        floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
      ),
    );
  }

  // 保留原PointButton方法
  Widget PointButton(double hw, String title, double point) {
    return Expanded(
      child: InkWell(
        onTap: () {
          setState(() {
            this.point -= point;
          });
        },
        child: Container(
          child: Center(
            child: Text(
              title,
              style: TextStyle(fontFamily: "Vazir-regular", color: Colors.white),
            ),
          ),
        ),
      ),
    );
  }
}

额外优化建议

  1. 字体按需加载:604款字体全部打包会增大安装包体积,建议用动态加载字体文件的方式,仅加载当前页码的字体
  2. 预加载相邻页码:拖拽翻页时预加载前后1-2页的数据,提升翻页流畅度
  3. 文本样式精细化:调整行高、字间距,确保古兰经文本排版符合阅读习惯

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 10:35:58