شرح إضافة تاثير على list في Flutter لعمل animation حركي اثناء scroll

شرح إضافة تاثير على list في Flutter لعمل animation حركي اثناء scroll

شرح إضافة تاثير على list في Flutter لعمل animation حركي اثناء scroll

في هذا المقال سوف نشرح لكم كيف تقوم بعمل animation جميل على ال list في flutter بطريقة بسيطة جدا وسهله ويمكن لاي شخص استعمالها في تطبيقه حتى ينفذ التصميم الذي يرغب به بكل سهوله , فكل ما عليك فعله لتنفيذ هذه العملية هو استخدام ال CustomScrollView ومنها سوف نقوم بعمل انميشن على ال list والامر بسيط ويمكنك نسخ الكود والتعديل عليه بشكل مباشر بدون مشاكل وايضا يوجد اكثر من شكل يمكنك تنفيذه موجود بنفس الكود الموجود بالاسفل حتى نسهل عليك عملية التنفيذ .


غالبًا ما تكون لغة Dart من Google هي الجانب الأكثر إثارة للخلاف وتميزًا في النظام الأساسي ، حيث تمثل عيبًا كبيرًا وفائدة ملحوظة لتطوير Flutter. Dart هي لغة أنشأتها Google لأول مرة في عام 2011 للاستخدام الداخلي. لكن اللغة لم تكتسب أي شكل من أشكال الشعبية لدى عامة الناس حتى ظهور Flutter. إنها الآن لغة تتوسع ببطء وبطء. اكتشف العديد من المطورين الذين يتعلمون Dart أنها تمثل تحديًا أقل جوهرية مما توقعوا. إنها تقنية سهلة التبديل من برمجة الويب أو JavaScript لأنها تم تطويرها بشكل أساسي خصيصًا للتطوير متعدد المنصات وتم تأسيسها على مبادئ وتقنيات اللغات ذات الصلة. فوائد كبيرة تأتي مع دعم جوجل.


Generate model class image


في البداية سوف نقوم بعمل ملف لل model ويكون عباره عن نص وصورة ويمكنك تنفيذ اي شيئ ترغب به ولكن هنا احتجت فقط لعمل نص و صورة فقط .


Generate model class image

model.dart


class DataModel {
  final String name;
  final String photo;

  DataModel({required this.name , required this.photo});

}

List<DataModel> listModel = [
  DataModel(name: 'image 1', photo: 'https://images.unsplash.com/photo-1664262283606-d4e198491656?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1112&q=80'),
  DataModel(name: 'image 2', photo: 'https://images.unsplash.com/photo-1664215795139-516f3a4ce81b?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1229&q=80'),
  DataModel(name: 'image 3', photo: 'https://images.unsplash.com/photo-1664236731665-b0d6b176d45d?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80'),
  DataModel(name: 'image 4', photo: 'https://images.unsplash.com/photo-1662581872342-3f8e0145668f?ixlib=rb-1.2.1&ixid=MnwxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1172&q=80'),
  DataModel(name: 'image 5', photo: 'https://images.unsplash.com/photo-1664204924910-36e0c2bb5c74?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80'),
  DataModel(name: 'image 6', photo: 'https://images.unsplash.com/photo-1664176062054-715f89b1974b?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=687&q=80'),
  DataModel(name: 'image 7', photo: 'https://images.unsplash.com/photo-1664096555683-3bd2d1ce9352?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=687&q=80'),
  DataModel(name: 'image 8', photo: 'https://images.unsplash.com/photo-1664136254860-26a42dbb2463?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=687&q=80'),
  DataModel(name: 'image 9', photo: 'https://images.unsplash.com/photo-1656427534508-60f8c7336a2d?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80'),
  DataModel(name: 'image 10', photo: 'https://images.unsplash.com/photo-1647003786841-2909cb0548a0?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80'),
  DataModel(name: 'image 11', photo: 'https://images.unsplash.com/photo-1664261421791-c25c5760f577?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80'),
];


How to create animation to list in Flutter project


هذا الجزء هو المسؤول عن تنفيذ الانميشن يمكنك نسخ الكود التالي واستخدامه في تطبيقك بدون مشاكل وايضا يمكنك التعديل على الانميشن والشكل ويوجد بعض الاشكال التي قمنا بعمل وسوف تلاحظ وجود علامة // بجانب العنصر الذي يمكنك تنفعيله ولكن لا تنسى اغلاق العنصر الاخر الذي يكون مرتبط بنفس الاسم حتى لا يحدث مشاكل ويمكنك التعديل في القيم للوصول الى الشكل الذي ترغب به , ايضا تستطيع وبكل سهوله استخدام hero widget كما هو ملاحظ حتى تتمكن من تنفيذ عملية الانتقال الى صفحة اخرى مع الانميشن .


How to create animation to list in Flutter project

ui.dart


const itemSize = 150.0;

class TestWidget extends StatefulWidget {
  const TestWidget({Key? key}) : super(key: key);

  @override
  State<TestWidget> createState() => _TestWidgetState();
}

class _TestWidgetState extends State<TestWidget> {
  final scrollController = ScrollController();

  onListen() {
    setState(() {

    });
  }

  @override
  void initState() {
    super.initState();
    scrollController.addListener(onListen);
  }

  @override
  void dispose() {
    scrollController.removeListener(onListen);
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {

    return Scaffold(
      appBar: AppBar(
        title: const Text('Custom Scroll List'),
        backgroundColor:Colors.black,
      ),
      body: Padding(
        padding: const EdgeInsets.all(15.0),
        child: CustomScrollView(
          physics: const BouncingScrollPhysics(),
          controller: scrollController,
          slivers: [

            SliverAppBar(
              title: const Text('Custom Scroll List'),
              backgroundColor: Colors.black,
              automaticallyImplyLeading: false,
              floating: true,
              snap: true,
              pinned: true,
              expandedHeight: 200,
              flexibleSpace: FlexibleSpaceBar(
                background: Image.network(
                  listModel[2].photo,
                  fit: BoxFit.cover),
              ),
            ),

            const SliverToBoxAdapter(
              child: SizedBox(
                height: 50,
              ),
            ),

            SliverList(delegate: SliverChildBuilderDelegate((context, index) {
              final listIndex = listModel[index];
              final itemPositionOffsets = index * (itemSize *0.5 );
              final difference = scrollController.offset - itemPositionOffsets;
              // final percent = 1 - (difference / itemSize).clamp(0.0, 1.0);
              final percent = 1 - (difference / (itemSize * 0.8 ));
              double opacity = percent;
              double scale = percent;
              if (opacity > 1.0) opacity = 1.0;
              if (opacity < 0.0) opacity = 0.0;
              if (percent > 1.0) scale = 1.0;



              return Align(
                heightFactor: 0.6,
                child: Opacity(
                  opacity: opacity,
                  child: Transform(
                    alignment: Alignment.center,

                    // transform: Matrix4.identity()
                    //   ..setEntry(3, 2, 0.001)
                    //   ..translate(0.0, 30.0 * (1 - percent), 0.0),

                    // transform: Matrix4.identity()..scale(scale,1.0),

                    transform: Matrix4.identity()..scale(scale,scale),
                    child: Card(
                      elevation: 10.0,
                      shape: const RoundedRectangleBorder(
                        borderRadius: BorderRadius.only(
                          topLeft: Radius.circular(20),
                          topRight: Radius.circular(20),
                        ),
                      ),
                      child: SizedBox(
                        height: itemSize,
                        child: GestureDetector(
                          onTap: () {
                            Navigator.push(
                              context,
                              MaterialPageRoute(
                                builder: (context) => PageImage(
                                  img: listIndex.photo,
                                ),
                              ),
                            );
                          },
                          child: Stack(
                            children: [
                              ClipRRect(
                                borderRadius: BorderRadius.circular(10),
                                child: Hero(
                                  tag: listIndex.photo,
                                  child: Image.network(
                                    listIndex.photo,
                                    width: double.infinity,
                                    fit: BoxFit.cover,
                                  ),
                                ),
                              ),
                              Padding(
                                padding: const EdgeInsets.all(15.0),
                                child: Text(listIndex.name,style: const TextStyle(color: Colors.white,fontSize: 20),),
                              ),
                            ],
                          ),
                        ),
                      ),
                    ),
                  ),
                ),
              );
            }, childCount: listModel.length)),
          ],
        ),
      )
    );
  }
}


Navigator to another page by image


هذا الجزء المسؤول عن عملية الانتقال وهنا ايضا قمنا باستخدام hero حتى يحدث انميشن في حالة فتح او اغلاق الصورة كما هو موضح بالصورة الخاصه بالمقال .


Navigator to another page by image

page.dart


class PageImage extends StatelessWidget {
  final String img;
  const PageImage({Key? key,required this.img}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: SizedBox(
          height: MediaQuery.of(context).size.height * 0.85,
          width: MediaQuery.of(context).size.width * 0.9,
          child: Hero(
            tag: img,
            child: ClipRRect(
                borderRadius: BorderRadius.circular(10),
                child: Image.network(img , fit: BoxFit.cover,)),
          ),
        ),
      ),
    );
  }
}


android sdk manager تحميل flutter developers applications create app android android studio mac

شكل اخر ولكن مع تحسين عملية ال scroll

شكل اخر ولكن مع تحسين عملية ال scroll


ui.dart


class MyMobileBody extends StatefulWidget {
  const MyMobileBody({Key? key}) : super(key: key);

  @override
  State<MyMobileBody> createState() => _MyMobileBodyState();
}

class _MyMobileBodyState extends State<MyMobileBody> {
  final CategoriesScroller categoriesScroller = CategoriesScroller();
  ScrollController controller = ScrollController();
  bool closeTopContainer = false;
  double topContainer = 0;
  List<Widget> itemsData = [];

  void getPostsData() {
    List<dynamic> responseList = FOOD_DATA;
    List<Widget> listItems = [];
    responseList.forEach((post) {
      listItems.add(Container(
          height: 150,
          margin: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
          decoration: BoxDecoration(borderRadius: BorderRadius.all(Radius.circular(20.0)), color: Colors.white, boxShadow: [
            BoxShadow(color: Colors.black.withAlpha(100), blurRadius: 10.0),
          ]),
          child: Padding(
            padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 10),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: <Widget>[
                Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    Text(
                      post["name"],
                      style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
                    ),
                    Text(
                      post["brand"],
                      style: const TextStyle(fontSize: 17, color: Colors.grey),
                    ),
                    SizedBox(
                      height: 10,
                    ),
                    Text(
                      "\$ ${post["price"]}",
                      style: const TextStyle(fontSize: 25, color: Colors.black, fontWeight: FontWeight.bold),
                    )
                  ],
                ),
                Image.asset(
                  post["image"],
                  height: 100,
                  width: 100,
                  fit: BoxFit.cover,
                )
              ],
            ),
          )));
    });
    setState(() {
      itemsData = listItems;
    });
  }

  @override
  void initState() {
    super.initState();
    getPostsData();
    controller.addListener(() {

      double value = controller.offset/119;

      setState(() {
        topContainer = value;
        closeTopContainer = controller.offset > 50;
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    final Size size = MediaQuery.of(context).size;
    final double categoryHeight = size.height*0.30;
    return SafeArea(
      child: Scaffold(
        backgroundColor: Colors.white,
        appBar: AppBar(
          elevation: 0,
          backgroundColor: Colors.white,
          leading: Icon(
            Icons.menu,
            color: Colors.black,
          ),
          actions: <Widget>[
            IconButton(
              icon: Icon(Icons.search, color: Colors.black),
              onPressed: () {},
            ),
            IconButton(
              icon: Icon(Icons.person, color: Colors.black),
              onPressed: () {},
            )
          ],
        ),
        body: Container(
          height: size.height,
          child: Column(
            children: <Widget>[
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceAround,
                children: <Widget>[
                  Text(
                    "Loyality Cards",
                    style: TextStyle(color: Colors.grey, fontWeight: FontWeight.bold, fontSize: 20),
                  ),
                  Text(
                    "Menu",
                    style: TextStyle(color: Colors.black, fontWeight: FontWeight.bold, fontSize: 20),
                  ),
                ],
              ),
              const SizedBox(
                height: 10,
              ),
              AnimatedOpacity(
                duration: const Duration(milliseconds: 200),
                opacity: closeTopContainer?0:1,
                child: AnimatedContainer(
                    duration: const Duration(milliseconds: 200),
                    width: size.width,
                    alignment: Alignment.topCenter,
                    height: closeTopContainer?0:categoryHeight,
                    child: categoriesScroller),
              ),
              Expanded(
                  child: ListView.builder(
                      controller: controller,
                      itemCount: itemsData.length,
                      physics: BouncingScrollPhysics(),
                      itemBuilder: (context, index) {
                        double scale = 1.0;
                        if (topContainer > 0.5) {
                          scale = index + 0.5 - topContainer;
                          if (scale < 0) {
                            scale = 0;
                          } else if (scale > 1) {
                            scale = 1;
                          }
                        }
                        return Opacity(
                          opacity: scale,
                          child: Transform(
                            transform:  Matrix4.identity()..scale(scale,scale),
                            alignment: Alignment.bottomCenter,
                            child: Align(
                                heightFactor: 0.7,
                                alignment: Alignment.topCenter,
                                child: itemsData[index]),
                          ),
                        );
                      })),
            ],
          ),
        ),
      ),
    );
  }
}

class CategoriesScroller extends StatelessWidget {
  const CategoriesScroller();

  @override
  Widget build(BuildContext context) {
    final double categoryHeight = MediaQuery.of(context).size.height * 0.30 - 50;
    return SingleChildScrollView(
      physics: BouncingScrollPhysics(),
      scrollDirection: Axis.horizontal,
      child: Container(
        margin: const EdgeInsets.symmetric(vertical: 20, horizontal: 20),
        child: FittedBox(
          fit: BoxFit.fill,
          alignment: Alignment.topCenter,
          child: Row(
            children: <Widget>[
              Container(
                width: 150,
                margin: EdgeInsets.only(right: 20),
                height: categoryHeight,
                decoration: BoxDecoration(color: Colors.orange.shade400, borderRadius: BorderRadius.all(Radius.circular(20.0))),
                child: Padding(
                  padding: const EdgeInsets.all(12.0),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: <Widget>[
                      Text(
                        "Most\nFavorites",
                        style: TextStyle(fontSize: 25, color: Colors.white, fontWeight: FontWeight.bold),
                      ),
                      SizedBox(
                        height: 10,
                      ),
                      Text(
                        "20 Items",
                        style: TextStyle(fontSize: 16, color: Colors.white),
                      ),
                    ],
                  ),
                ),
              ),
              Container(
                width: 150,
                margin: EdgeInsets.only(right: 20),
                height: categoryHeight,
                decoration: BoxDecoration(color: Colors.blue.shade400, borderRadius: BorderRadius.all(Radius.circular(20.0))),
                child: Container(
                  child: Padding(
                    padding: const EdgeInsets.all(12.0),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: <Widget>[
                        Text(
                          "Newest",
                          style: TextStyle(fontSize: 25, color: Colors.white, fontWeight: FontWeight.bold),
                        ),
                        SizedBox(
                          height: 10,
                        ),
                        Text(
                          "20 Items",
                          style: TextStyle(fontSize: 16, color: Colors.white),
                        ),
                      ],
                    ),
                  ),
                ),
              ),
              Container(
                width: 150,
                margin: EdgeInsets.only(right: 20),
                height: categoryHeight,
                decoration: BoxDecoration(color: Colors.lightBlueAccent.shade400, borderRadius: BorderRadius.all(Radius.circular(20.0))),
                child: Padding(
                  padding: const EdgeInsets.all(12.0),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: <Widget>[
                      Text(
                        "Super\nSaving",
                        style: TextStyle(fontSize: 25, color: Colors.white, fontWeight: FontWeight.bold),
                      ),
                      SizedBox(
                        height: 10,
                      ),
                      Text(
                        "20 Items",
                        style: TextStyle(fontSize: 16, color: Colors.white),
                      ),
                    ],
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

const FOOD_DATA = [
  {
    "name":"Burger",
    "brand":"Hawkers",
    "price":2.99,
    "image":"assets/img/background/drink2.png"
  },{
    "name":"Cheese Dip",
    "brand":"Hawkers",
    "price":4.99,
    "image":"assets/img/background/drink1.png"
  },
  {
    "name":"Cola",
    "brand":"Mcdonald",
    "price":1.49,
    "image":"assets/img/background/drink3.png"
  },
  {
    "name":"Fries",
    "brand":"Mcdonald",
    "price":2.99,
    "image":"assets/img/background/drink4.png"
  },
  {
    "name":"Ice Cream",
    "brand":"Ben & Jerry's",
    "price":9.49,
    "image":"assets/img/background/image-1.png"
  },
  {
    "name":"Noodles",
    "brand":"Hawkers",
    "price":4.49,
    "image":"assets/img/background/image-2.png"
  },
  {
    "name":"Pizza",
    "brand":"Dominos",
    "price":17.99,
    "image":"assets/img/background/image-3.jpg"
  },
  {
    "name":"Sandwich",
    "brand":"Hawkers",
    "price":2.99,
    "image":"assets/img/background/image-4.jpg"
  },
  {
    "name":"Wrap",
    "brand":"Subway",
    "price":6.99,
    "image":"assets/img/background/image-5.png"
  },
  {
    "name":"Wrap",
    "brand":"Subway",
    "price":6.99,
    "image":"assets/img/map/avatar-1.png"
  },
  {
    "name":"Wrap",
    "brand":"Subway",
    "price":6.99,
    "image":"assets/logo.png"
  }
];

تعليقات