الرسوم المتحركة وتصميم الحركة

عناصر الرسوم الضمنية: AnimatedOpacity و AnimatedAlign و AnimatedSwitcher

15 دقيقة الدرس 3 من 13

عناصر الرسوم الضمنية: AnimatedOpacity و AnimatedAlign و AnimatedSwitcher

يأتي Flutter مزوّداً بمجموعة من عناصر الرسوم المتحركة الضمنية التي تتولى كل منطق الرسوم المتحركة داخلياً. تضع قيمةً جديدة، وتحدد duration ومنحنى اختياري curve، فيتكفّل Flutter بالانتقال السلس بين القيمة القديمة والجديدة. في هذا الدرس نركّز على ثلاثة أعضاء بارزين من تلك المجموعة: AnimatedOpacity وAnimatedAlign وAnimatedSwitcher.

AnimatedOpacity

يُغلّف AnimatedOpacity أيّ عنصر ويُحرّك شفافيته بين 0.0 (غير مرئي كلياً) و1.0 (معتم كلياً). وهو الأسلوب الأمثل لـتلاشي العناصر ظهوراً واختفاءً دون كتابة سطر واحد من كود متحكّم الرسوم المتحركة.

  • opacity — قيمة الشفافية المستهدفة؛ يبدأ الرسم المتحرك كلما تغيّرت.
  • duration — مدة الانتقال (مثل Duration(milliseconds: 500)).
  • curve — منحنى التسهيل؛ الافتراضي Curves.linear.
  • onEnd — رد نداء اختياري يُطلَق عند اكتمال الرسم المتحرك.
ملاحظة: العنصر غير المرئي (opacity: 0.0) لا يزال يشغل مساحة في التخطيط. استخدم Visibility أو التصيير الشرطي إذا كنت تريد إزالة العنصر من التخطيط أيضاً.

AnimatedOpacity — تلاشٍ ظهوراً واختفاءً عند الضغط على زر

class FadeDemo extends StatefulWidget {
  const FadeDemo({super.key});

  @override
  State<FadeDemo> createState() => _FadeDemoState();
}

class _FadeDemoState extends State<FadeDemo> {
  double _opacity = 1.0;

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        AnimatedOpacity(
          opacity: _opacity,
          duration: const Duration(milliseconds: 600),
          curve: Curves.easeInOut,
          child: Container(
            width: 200,
            height: 200,
            color: Colors.deepPurple,
            child: const Center(
              child: Text(
                'Flutter',
                style: TextStyle(color: Colors.white, fontSize: 28),
              ),
            ),
          ),
        ),
        const SizedBox(height: 24),
        ElevatedButton(
          onPressed: () {
            setState(() {
              _opacity = _opacity == 1.0 ? 0.0 : 1.0;
            });
          },
          child: const Text('تبديل الظهور'),
        ),
      ],
    );
  }
}

AnimatedAlign

يُحرّك AnimatedAlign موضع عنصره الفرعي داخل العنصر الأب عبر إيجاد قيمة وسيطة لـAlignment. تشمل حالات الاستخدام الشائعة: إبراز لوحة من الحافة، وإبراز مؤشر التبويب النشط، وتحريك زر الإجراء العائم إلى زاوية جديدة.

  • alignment — قيمة Alignment المستهدفة؛ يبدأ الرسم المتحرك عند تغييرها.
  • duration وcurve — نفس دلالاتهما في جميع العناصر الضمنية.
  • widthFactor / heightFactor — عوامل اختيارية لتحجيم العنصر نسبةً إلى عنصره الفرعي.
نصيحة: ادمج AnimatedOpacity وAnimatedAlign حول نفس العنصر الفرعي لإنشاء تأثير دخول "انزلاق وتلاشٍ" دون أي كود إضافي.

AnimatedAlign — الانزلاق بين الزوايا

class AlignDemo extends StatefulWidget {
  const AlignDemo({super.key});

  @override
  State<AlignDemo> createState() => _AlignDemoState();
}

class _AlignDemoState extends State<AlignDemo> {
  Alignment _alignment = Alignment.topLeft;

  static const List<Alignment> _positions = [
    Alignment.topLeft,
    Alignment.topRight,
    Alignment.bottomRight,
    Alignment.bottomLeft,
  ];
  int _index = 0;

  void _next() {
    setState(() {
      _index = (_index + 1) % _positions.length;
      _alignment = _positions[_index];
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        SizedBox(
          width: 300,
          height: 300,
          child: AnimatedAlign(
            alignment: _alignment,
            duration: const Duration(milliseconds: 500),
            curve: Curves.easeInOutCubic,
            child: Container(
              width: 60,
              height: 60,
              decoration: const BoxDecoration(
                color: Colors.teal,
                shape: BoxShape.circle,
              ),
            ),
          ),
        ),
        ElevatedButton(
          onPressed: _next,
          child: const Text('تحريك'),
        ),
      ],
    );
  }
}

AnimatedSwitcher

يُجري AnimatedSwitcher تلاشياً متقاطعاً (أو أي انتقال مخصص) بين عنصرَين فرعيَّين مختلفَين. كلما عُيِّن للخاصية child عنصرٌ ذو مفتاح مختلف، يؤدي العنصر القديم رسم خروج بينما يؤدي الجديد رسم دخول في الوقت ذاته.

  • duration — مدة انتقال العنصر الفرعي الداخل.
  • reverseDuration — مدة منفصلة اختيارية للعنصر الفرعي الخارج.
  • transitionBuilder — مصنع يُغلّف العنصر الفرعي في عنصر متحرك مخصص (الافتراضي: FadeTransition).
  • layoutBuilder — يتحكم في طريقة تكديس العنصرين القديم والجديد أثناء الانتقال.
تحذير: إذا كان العنصران الداخل والخارج من نفس النوع ولا Key صريح لهما، يعدّهما Flutter نفس العنصر ولا يطلق الانتقال. دائماً عيّن ValueKey يتغير مع المحتوى.

AnimatedSwitcher — تلاشٍ متقاطع بين عداد وأيقونة

class SwitcherDemo extends StatefulWidget {
  const SwitcherDemo({super.key});

  @override
  State<SwitcherDemo> createState() => _SwitcherDemoState();
}

class _SwitcherDemoState extends State<SwitcherDemo> {
  int _count = 0;
  bool _showIcon = false;

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        AnimatedSwitcher(
          duration: const Duration(milliseconds: 400),
          transitionBuilder: (child, animation) {
            return FadeTransition(
              opacity: animation,
              child: ScaleTransition(scale: animation, child: child),
            );
          },
          child: _showIcon
              ? const Icon(
                  Icons.check_circle,
                  key: ValueKey('icon'),
                  size: 80,
                  color: Colors.green,
                )
              : Text(
                  '$_count',
                  key: ValueKey(_count),
                  style: const TextStyle(fontSize: 72, fontWeight: FontWeight.bold),
                ),
        ),
        const SizedBox(height: 24),
        Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed: () => setState(() => _count++),
              child: const Text('زيادة'),
            ),
            const SizedBox(width: 12),
            ElevatedButton(
              onPressed: () => setState(() => _showIcon = !_showIcon),
              child: const Text('تبديل العرض'),
            ),
          ],
        ),
      ],
    );
  }
}

اختيار العنصر المناسب

  • استخدم AnimatedOpacity عندما تريد إخفاء وإظهار عنصر دون تحريكه.
  • استخدم AnimatedAlign عندما تريد إنزلاق عنصر إلى موضع جديد داخل أبيه.
  • استخدم AnimatedSwitcher عندما تريد استبدال عنصر بآخر مع تأثير انتقالي.

ملخص

تتبع العناصر الثلاثة نفس عقد الرسوم الضمنية: ضع قيمة خاصية جديدة داخل setState()، وفّر duration، وتكفّل Flutter بالباقي. يتحكّم AnimatedOpacity في الشفافية، ويتحكّم AnimatedAlign في الموضع القائم على المحاذاة، ويُحوّل AnimatedSwitcher بين عناصر فرعية مختلفة كلياً. إتقان هذه الثلاثة يغطي الغالبية العظمى من احتياجات حركة واجهة المستخدم اليومية دون الحاجة إلى واجهات AnimationController منخفضة المستوى.