auto_fit_text: نص Flutter يصغر ليناسب مكانه، مع TextScaler و RTL صحيحين - DEV Community

DEV Community

Cover image for auto_fit_text: نص Flutter يصغر ليناسب مكانه، مع TextScaler و RTL صحيحين
Abdeldjalil Chougui
Abdeldjalil Chougui

Posted on Originally published at abdeldjalilchougui.biz

auto_fit_text: نص Flutter يصغر ليناسب مكانه، مع TextScaler و RTL صحيحين

عنوان مناسب بالإنجليزية يتجاوز مساحته بالألمانية. وسعر يظهر كاملًا في هاتف وينكسر إلى سطرين في هاتف أصغر. ونص زر يُقطع عندما يكبّر المستخدم خط النظام. الجواب الشائع في Flutter هو widget يصغّر النص حتى يناسب مكانه، ولسنوات كان هذا الـ widget هو auto_size_text.

يصف الـ README الخاص بـ auto_fit_text أين تتعطل تلك الطريقة اليوم: textScaleFactor التي صارت deprecated، وقياس يعامل كل نص كأنه من اليسار إلى اليمين، وانهيار مع WidgetSpan، ومجموعات ترتعش، وتخطيطات مثل IntrinsicHeight لا يعمل فيها الـ widget. auto_fit_text بديل بنفس شكل الواجهة (API) يعالج هذه الحالات.

ماذا يقدّم

يراعي العرض والارتفاع وmaxLines. يجد أكبر حجم خط يناسب به النصُّ المساحة المتاحة، بين minFontSize وحجم الخط في الـ style.

بحث ثنائي مع تخزين مؤقت. يبحث بين الأحجام المرشّحة بالبحث الثنائي (binary search) ويحفظ النتائج في cache لكل قيود (constraints). وإذا كان النص مناسبًا من البداية، يكفي قياس واحد.

معالجة صحيحة لـ TextScaler. أحجام الخط منطقية. والـ TextScaler المحيط (أو المعطى) يُطبَّق عند القياس وعند الرسم معًا، فيعمل التكبير غير الخطي في Android 14. وtextScaleFactor القديمة غير مستعملة.

يقيس بنفس مدخلات Text. الـ Directionality المحيط، ولغة Localizations، وDefaultTextStyle (الـ style وmaxLines وtextAlign وsoftWrap وoverflow وtextWidthBasis وtextHeightBehavior)، وDefaultTextHeightBehavior، وMediaQuery.boldText.

نص قابل للتحديد. يعمل داخل SelectionArea، والمُنشئ AutoFitText.selectable يبني SelectableText مستقلًّا.

النص الغني (rich text). AutoFitText.rich يصغّر كل الـ spans بالمعامل نفسه، ويرث DefaultTextStyle، ولا ينهار مع WidgetSpan.

المجموعات. AutoFitTextGroup يجعل عدة نصوص تتشارك أصغر حجم ناتج. كل عضو يُقاس حسب قيوده هو فقط، فتستقر المجموعة بعد إطار إضافي واحد على الأكثر ولا تدخل أبدًا في حلقة إعادة بناء.

قراءة الحجم. onFontSizeResolved يعطيك الحجم المعروض بعد الإطار. واستدعاء setState داخله آمن.

التخطيطات الذاتية (intrinsic). يدعم الأحجام الذاتية و dry layout و dry baselines، فيعمل داخل IntrinsicHeight وIntrinsicWidth وعناوين Chip.

الحالات الحدّية. يعالج الصناديق ذات الحجم الصفري والنصوص الفارغة. والصندوق غير المحدود في الاتجاهين يُطلق assertion في وضع debug يشرح الحل.

طريقة الاستعمال

أضِفه من GitHub:

dependencies:
  auto_fit_text:
    git: https://github.com/AbdeldjalilChougui/auto_fit_text
Enter fullscreen mode Exit fullscreen mode
import 'package:auto_fit_text/auto_fit_text.dart';
Enter fullscreen mode Exit fullscreen mode

الحالة الأساسية:

SizedBox(
  width: 200,
  height: 60,
  child: AutoFitText(
    'The quick brown fox jumps over the lazy dog',
    style: const TextStyle(fontSize: 40),
    maxLines: 2,
    minFontSize: 10,
    overflowReplacement: const Text('…'),
    onFontSizeResolved: (size) => debugPrint('fitted at $size'),
  ),
)
Enter fullscreen mode Exit fullscreen mode

حجم الخط يبدأ من حجم الخط في الـ style (بسقف maxFontSize) وينزل إلى minFontSize بخطوات قدرها stepGranularity. وللاختيار من أحجام ثابتة بدل ذلك:

const AutoFitText(
  'Supercalifragilisticexpialidocious',
  presetFontSizes: [32, 24, 16],
  maxLines: 1,
)
Enter fullscreen mode Exit fullscreen mode

لمشاركة حجم واحد بين عدة widgets، احتفظ بمجموعة في حقل داخل State:

final group = AutoFitTextGroup();

Row(
  children: [
    Expanded(child: AutoFitText('OK', group: group, maxLines: 1)),
    Expanded(child: AutoFitText('Remind me later', group: group, maxLines: 1)),
  ],
)
Enter fullscreen mode Exit fullscreen mode

من دون المجموعة، يبقى "OK" كبيرًا بينما يصغر "Remind me later"، فيظهر الزرّان المتجاوران غير متناسقين.

النص الغني، حيث يصغر كل span بالمعامل نفسه:

const AutoFitText.rich(
  TextSpan(
    text: 'Total: ',
    children: [
      TextSpan(
        text: '1 250,00 DZD',
        style: TextStyle(fontSize: 40, fontWeight: FontWeight.bold),
      ),
    ],
  ),
  style: TextStyle(fontSize: 24),
  maxLines: 1,
)
Enter fullscreen mode Exit fullscreen mode

النص القابل للتحديد:

const SelectionArea(child: AutoFitText('Select me', maxLines: 1));
// or, standalone:
const AutoFitText.selectable('Select me', maxLines: 1);
Enter fullscreen mode Exit fullscreen mode

يحتاج الـ widget عرضًا أو ارتفاعًا محدودًا. داخل Row أو Column، ضعه في Expanded أو Flexible. ومجلد example/ فيه تطبيق معرض جاهز للتشغيل.

الانتقال من auto_size_text

في أغلب الحالات تغيّر اسم الـ widget والمجموعة فقط: AutoSizeText يصبح AutoFitText، وAutoSizeText.rich يصبح AutoFitText.rich، وAutoSizeGroup يصبح AutoFitTextGroup. المعاملات المسمّاة هي نفسها. وفي الـ README الجدول الكامل؛ وهذه هي الفروق:

  • textScaleFactor: 1.5 تصبح textScaler: TextScaler.linear(1.5)، والـ scalers غير الخطية مدعومة.
  • minFontSize لم يعد يجب أن تكون من مضاعفات stepGranularity، وstepGranularity تقبل أي قيمة موجبة.
  • presetFontSizes تُقبل بأي ترتيب.
  • wrapWords تستعمل قواعد كسر الأسطر في محرك النص، فالفراغ غير القابل للكسر (non-breaking space) يُبقي الكلمات معًا.
  • .rich تقبل أي InlineSpan.
  • معاملات جديدة: textWidthBasis وtextHeightBehavior وselectionColor وonFontSizeResolved، والمُنشئ AutoFitText.selectable.

وبعض تغييرات السلوك تستحق المراجعة بعد تغيير الاسم:

  • النص يُقاس بالـ Directionality المحيط وبـ TextAlign.start. ويذكر الـ README أن auto_size_text 3.0.0 كان يقيس كل نص كأنه من اليسار إلى اليمين ومحاذًى إلى اليسار.
  • AutoFitText.rich يقيس بعد دمج DefaultTextStyle في الـ style الجذر، كما يرسمه Text.rich.
  • يمكن ضبط overflow وoverflowReplacement معًا.
  • إذا كان العرض والارتفاع كلاهما غير محدود، ترمي نسخ debug الآن assertion يشرح الحل. ونسخ release تبقى ترسم النص بحجمه الكامل.

ويسرد الـ README أيضًا، بأرقامها، الـ issues المفتوحة في متتبّع auto_size_text التي تعالجها هذه الحزمة بطريقة مختلفة: التخطيطات الذاتية والـ chips، وانهيار WidgetSpan، والنص القابل للتحديد، وtextScaleFactor القديمة، وارتعاش المجموعات وحلقات إعادة البناء، وقراءة حجم الخط، وغياب TextHeightBehavior وTextWidthBasis، وتجاوز النص مع MediaQuery.boldText، وكائنات TextPainter التي لم يكن يُتخلَّص منها، وقيود wrapWords وminFontSize. لكل واحدة اختبار، ما عدا التخلص من TextPainter فهو معالَج في الكود.

ما يجب معرفته

  • البحث يفترض نصًّا "منتظمًا". البحث الثنائي يفترض أن الخط الأصغر لا يحتاج أبدًا مساحة أكبر من الخط الأكبر. هذا صحيح للنص العادي. الخطوط غير المألوفة أو بعض قيم StrutStyle قد تخالفه.
  • WidgetSpan يُقدَّر تقديرًا. لا يمكن قياسه قبل التخطيط، فيُقاس كمربع ضلعه حجم الخط. والـ widgets ذات الحجم المختلف قد تجعل الملاءمة غير دقيقة.
  • المجموعات تحتاج إطارًا واحدًا. المجموعة تستقر في الإطار الذي يلي أول تخطيط لأعضائها، فقد يُظهر الإطار الأول كل عضو بحجمه الخاص.
  • التصغير فقط. الملاءمة تبدأ من حجم الخط في الـ style. لملء صندوق كبير، أعطِ fontSize كبيرًا في الـ style.
  • مكان المؤشّر. AutoFitText.selectable يفترض عرض المؤشّر الافتراضي في SelectableText، وهو 2 px، عند حجز مكان له.
  • إذا لم يناسب أي حجم. إن لم يكفِ حتى minFontSize، يُقصّ النص حسب overflow، إلا إذا ضُبط overflowReplacement.
  • ما تم اختباره. الاختبارات تستعمل خط اختبارات Flutter، وكل حرف فيه مربع بحجم em واحد. الخطوط الحقيقية تمر بنفس مسارات TextPainter، لكن الملاءمة لم تُراجَع بصور golden على الأجهزة.

لماذا بنيته

تصغير النص ليناسب مكانه يبدو مشكلة محلولة، وهو كذلك تقريبًا في تطبيق من اليسار إلى اليمين بحجم الخط الافتراضي. لكن الثغرات التي يسردها الـ README تظهر بالضبط في التطبيقات التي تحتاج هذا الـ widget أكثر من غيرها.

الواجهة العربية من اليمين إلى اليسار، وقياسها كأنها من اليسار إلى اليمين يعطي جوابًا مختلفًا عمّا يُرسم. والمستخدم الذي يكبّر خط النظام في Android 14 يحصل على تكبير غير خطي، لا يصفه معامل تكبير واحد. وإعداد الخط الغامق في خيارات الوصول يجعل النص أعرض مما قيس. والسعر داخل بطاقة غالبًا نص غني، وصف الأزرار يحتاج حجمًا مشتركًا واحدًا لا يرتعش.

أردت widget يقيس بنفس المدخلات التي يرسم بها Text، حتى يكون ما قيس هو ما يظهر. وأردت أن يكون الانتقال رخيصًا للكود الموجود، ولهذا يتبع شكل الواجهة وأسماء المعاملات ما في auto_size_text، ويشرح الـ README كل فرق.

الكود

الكود، وجدول الانتقال، وتطبيق المعرض، والاختبارات موجودة على GitHub: https://github.com/AbdeldjalilChougui/auto_fit_text. الرخصة MIT، والـ issues والـ pull requests مرحّب بها.


نُشر في الأصل على abdeldjalilchougui.biz.

Top comments (0)