如何在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行内容排版问题
核心问题分析
- 阿拉伯文本是RTL(从右到左)方向,未设置文本方向导致排版错乱
- ListView会滚动所有分页,无法实现单页15行的固定展示
- 未控制行高与间距,导致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, // 右对齐 ); }), ), ),
二、实现左右拖拽翻页效果
核心逻辑
- 维护当前页码状态,拖拽时更新页码并重新查询对应页数据
- 用
PageView.builder实现原生拖拽翻页交互 - 优化数据库查询与字体加载逻辑,避免性能问题
完整代码调整
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), ), ), ), ), ); } }
额外优化建议
- 字体按需加载:604款字体全部打包会增大安装包体积,建议用动态加载字体文件的方式,仅加载当前页码的字体
- 预加载相邻页码:拖拽翻页时预加载前后1-2页的数据,提升翻页流畅度
- 文本样式精细化:调整行高、字间距,确保古兰经文本排版符合阅读习惯
内容的提问来源于stack exchange,提问作者NUlL
相关产品推荐
相关产品推荐

