paint-brush
Tạo hoạt ảnh so le trong Flutter: Hướng dẫn tương tác vi môtừ tác giả@nikkieke
460 lượt đọc
460 lượt đọc

Tạo hoạt ảnh so le trong Flutter: Hướng dẫn tương tác vi mô

từ tác giả 19m2024/06/04
Read on Terminal Reader

dài quá đọc không nổi

Khái niệm tương tác vi mô dựa trên ý tưởng rằng các chi tiết nhỏ có thể có tác động lớn đến trải nghiệm tổng thể của người dùng. Trong Flutter, bạn có thể xây dựng các Tương tác vi mô bằng cách tạo các hoạt ảnh tinh tế bằng cách sử dụng hoạt ảnh ẩn hoặc rõ ràng. Trong bài viết này, chúng tôi sẽ tạo hoạt ảnh so le để tạo hoạt ảnh cho trẻ em trong tiện ích cột khi trang đó được vuốt.
featured image - Tạo hoạt ảnh so le trong Flutter: Hướng dẫn tương tác vi mô
undefined HackerNoon profile picture
0-item

Hoạt ảnh so le bao gồm các hoạt ảnh tuần tự hoặc chồng chéo. Hoạt ảnh có thể hoàn toàn tuần tự, với một thay đổi xảy ra sau thay đổi tiếp theo hoặc có thể trùng lặp một phần hoặc hoàn toàn. Khi hoạt ảnh là tuần tự, các phần tử sẽ được tạo hoạt ảnh tuần tự với độ trễ nhỏ giữa thời điểm bắt đầu của mỗi phần tử. Điều này tạo ra hiệu ứng xếp tầng hoặc gợn sóng, trong đó hoạt ảnh dường như di chuyển qua các phần tử theo từng giai đoạn thay vì tất cả cùng một lúc.


Hoạt ảnh so le được coi là một loại tương tác vi mô vì chúng nâng cao trải nghiệm người dùng bằng cách cung cấp phản hồi tương tác tinh tế hướng dẫn người dùng thông qua một giao diện. Trong Flutter, bạn có thể xây dựng các tương tác vi mô bằng cách tạo các hoạt ảnh tinh tế bằng cách sử dụng hoạt ảnh ẩn hoặc rõ ràng.


Đối với ngữ cảnh, hoạt ảnh ngầm được thiết kế đơn giản và dễ sử dụng vì các chi tiết hoạt ảnh được trừu tượng hóa trong khi hoạt ảnh rõ ràng phù hợp hơn với các hoạt ảnh phức tạp vì chúng cung cấp khả năng kiểm soát hoàn toàn quá trình hoạt ảnh. Điều này đặc biệt được sử dụng khi bạn cần có quyền kiểm soát chi tiết hơn đối với hoạt ảnh.


Trong bài viết này, chúng ta sẽ đi sâu vào khái niệm về tương tác vi mô, sau đó đối với trường hợp sử dụng tương tác vi mô, chúng ta sẽ sử dụng hoạt ảnh rõ ràng để tạo hoạt ảnh so le để tạo hoạt ảnh cho các phần tử con có trong tiện ích cột.

Khái niệm về tương tác vi mô là gì?

Sản phẩm tuyệt vời là sản phẩm cung cấp tốt cả về tính năng và chi tiết. Các tính năng đưa mọi người đến với sản phẩm của bạn nhưng các chi tiết sẽ giữ chân họ. Những chi tiết này là những thứ làm cho ứng dụng của bạn nổi bật so với những ứng dụng khác. Với các tương tác vi mô, bạn có thể tạo những chi tiết này bằng cách cung cấp phản hồi thú vị cho người dùng của mình.


Khái niệm tương tác vi mô dựa trên ý tưởng rằng các chi tiết nhỏ có thể có tác động lớn đến trải nghiệm tổng thể của người dùng. Tương tác vi mô có thể được sử dụng để phục vụ các chức năng thiết yếu như truyền đạt phản hồi hoặc kết quả của một hành động. Ví dụ về các tương tác vi mô bao gồm:


  • Hoạt ảnh của nút: Nút thay đổi màu sắc hoặc kích thước khi được di chuột hoặc nhấn.


  • Chỉ báo đang tải: Ảnh động cho người dùng biết rằng một quá trình đang diễn ra.


  • Cử chỉ vuốt: Ảnh động phản hồi cử chỉ vuốt.


  • Chuyển tiếp điều hướng: Hình động mượt mà khi chuyển đổi giữa các màn hình.

Tương tác vi mô trong các ứng dụng đời thực

Dưới đây, chúng ta có thể thấy các ứng dụng thực tế của tương tác vi mô, những hoạt ảnh tinh tế này được tạo trong Flutter để nâng cao trải nghiệm người dùng. 👇🏽

Tài liệu tham khảo thiết kế cho các ứng dụng này được lấy từ Dribbble

Cách tạo hoạt ảnh so le trong Flutter

Trong ví dụ này, chúng tôi sẽ tạo hoạt ảnh so le để tạo hoạt ảnh cho trẻ em trong tiện ích cột khi trang đó được vuốt. Điều này sẽ được xây dựng bằng cách sử dụng phương pháp hoạt ảnh rõ ràng vì trong trường hợp này, chúng ta sẽ cần có toàn quyền kiểm soát cách chúng ta muốn hoạt ảnh chạy.


Đây là hình ảnh động sẽ trông như thế nào khi chúng ta hoàn thành 👇🏽

Điều kiện tiên quyết

Để tận dụng tối đa hướng dẫn này, bạn nên có những điều sau:


  • Hiểu biết cơ bản về cách tạo hoạt ảnh trong Flutter
  • Nắm vững các nguyên tắc cơ bản của Flutter & Dart
  • Trình chỉnh sửa mã, VScode hoặc Android Studio
  • Trình mô phỏng hoặc thiết bị để xây dựng trên đó


Sau khi bạn đã kiểm tra tất cả các điều kiện tiên quyết của dự án, hãy bắt tay vào thực hiện.


Đầu tiên chúng ta sẽ xây dựng màn hình chính chứa hai trang này. Hai trang này sẽ được bao bọc trong một tiện ích xem trang để kiểm soát trang nào được hiển thị khi vuốt. Ngoài ra, ở cuối màn hình chính, chúng tôi có một chỉ báo hiển thị cho chúng tôi trang mà chúng tôi hiện đang truy cập.

 class MainScreen extends StatefulWidget { const MainScreen({super.key}); @override State<MainScreen> createState() => _MainScreenState(); } class _MainScreenState extends State<MainScreen>{ final controller = PageController(keepPage: true); @override Widget build(BuildContext context) { return Scaffold( body: SafeArea( child: PageView( controller: controller, children: [ Page1(pageController: controller,), Page2(pageController: controller,), ], ), ), bottomNavigationBar: Container( alignment: Alignment.topCenter, padding: const EdgeInsets.only(top: 10), height: 30, child: SmoothPageIndicator( controller: controller, count: 2, effect: const JumpingDotEffect( dotHeight: 10, dotWidth: 10, activeDotColor: Colors.grey, dotColor: Colors.black12, ), ), ) ); } }


Bạn có thể tìm thấy SmoothPageIndicator được sử dụng trong đoạn mã trên trên pub.dev . Ở đây, SmoothPageIndicator được sử dụng để hiển thị trang hiện tại đang được xem. Bạn có thể thêm gói vào pubspec.yaml của mình như vậy 👇🏽

 dependencies: flutter: sdk: flutter smooth_page_indicator: ^1.1.0


Trong hai trang, chúng ta sẽ có một tiện ích cột với một số tiện ích thẻ trống. Các tiện ích thẻ trống này sẽ được sử dụng để điền vào cột để chúng ta có thể xem và đánh giá đầy đủ hoạt ảnh so le. Chúng tôi sẽ tạo tiện ích thẻ trống dưới dạng thành phần có thể sử dụng lại để không phải xây dựng nó từ đầu ở mọi nơi chúng tôi sử dụng.

 class EmptyCard extends StatelessWidget { const EmptyCard({super.key, required this.width, required this.height}); final double width; final double height; @override Widget build(BuildContext context) { return Container( height: height, width: width, decoration: BoxDecoration( borderRadius: const BorderRadius.all(Radius.circular(10)), color: Colors.blue.shade200, boxShadow: const [ BoxShadow( color: Colors.black12, blurRadius: 4, offset: Offset(0,4), ) ] ), ); } }


Với tất cả các mã soạn sẵn này, bây giờ chúng ta sẽ bắt đầu xây dựng hoạt ảnh. Đầu tiên, chúng ta sẽ tạo một stateful widget mới mà chúng ta sẽ gọi là AnimateWidget . Nó sẽ có một số tham số để kiểm soát cách hoạt ảnh sẽ chạy.

 class AnimateWidget extends StatefulWidget { const AnimateWidget({super.key, required this.duration, required this.position, required this.horizontalOffset, required this.child, required this.controller, }); final Duration duration; final int position; final double? horizontalOffset; final Widget child; final PageController controller; @override State<AnimateWidget> createState() => _AnimateWidgetState(); }


Với các thông số của AnimateWidget như trong đoạn mã trên, chúng ta có thể kiểm soát thành công:

  • Thời lượng của hoạt ảnh.
  • pageController , khiến nó kích hoạt hoạt ảnh khi trang được vuốt.
  • Mức độ lệch ngang của phần tử hoạt hình.


Tiếp theo, trong AnimateWiget , chúng ta sẽ định nghĩa những điều sau:

  • AnimationController : Dùng để điều khiển chuỗi hoạt ảnh.

  • Trang hiện tại: sẽ được sử dụng để chứa dữ liệu trang hiện tại.

  • Bộ hẹn giờ: Sẽ được sử dụng để kích hoạt hoạt ảnh sau một thời gian trì hoãn; đây là điều sẽ mang lại hiệu ứng xếp tầng hoạt hình so le.


     class _AnimateWidgetState extends State<AnimateWidget> with AutomaticKeepAliveClientMixin, SingleTickerProviderStateMixin{ @override bool get wantKeepAlive => true; late AnimationController animationController; int currentPage = 0; Timer? _timer; @override void initState() { super.initState(); animationController = AnimationController(vsync: this, duration: widget.duration); _timer = Timer(getDelay(), animationController.forward); widget.controller.addListener(() { currentPage = widget.controller.page!.round(); if(currentPage == widget.controller.page){ _timer = Timer(getDelay(), animationController.forward); } }); } @override void dispose() { _timer?.cancel(); animationController.dispose(); super.dispose(); } Duration getDelay(){ var delayInMilliSec = widget.duration.inMilliseconds ~/ 6; int getStaggeredListDuration(){ return widget.position * delayInMilliSec; } return Duration(milliseconds: getStaggeredListDuration()); }

Phân tích đoạn mã trên, bạn sẽ nhận thấy rằng:

  • Chúng tôi đã sử dụng AnimationController để điều khiển chuỗi hoạt ảnh, vì điều này nên chúng tôi đã giới thiệu SingleTickerProviderStateMixin .


  • Trong phương thức init state , chúng ta đã khởi tạo AnimationController , sau đó kích hoạt hoạt ảnh bằng cách sử dụng animationController.forward , khi nhập trang và vuốt trang.


  • Chúng tôi đã sử dụng Bộ hẹn giờ để kiểm soát việc kích hoạt hoạt ảnh dựa trên độ trễ.


  • Trong phương pháp dispose , chúng tôi đã dọn sạch tài nguyên.


  • Chúng tôi đã sử dụng AutomaticKeepAliveClientMixin để duy trì trạng thái của tiện ích. Điều này nhằm ngăn chặn việc xử lý AnimationController khi một trang được vuốt và không còn hiển thị nữa.


  • Trong hàm getDelay , chúng tôi đã tính toán độ trễ trước khi hoạt ảnh được kích hoạt cho từng phần tử. Để đạt được điều này, chúng tôi chia thời lượng tính bằng mili giây cho 6 và ước tính kết quả, sau đó nhân với vị trí. Đây là vị trí (trong trường hợp này là chỉ mục) của phần tử.


Trong phương thức Build, chúng tôi trả về AnimatedBuilder . Trong trình tạo hoạt ảnh này, chúng tôi sẽ trả về một hàm tiện ích có tên _slideAnimation trả về tiện ích Transform.translate . Trong hàm _slideAnimation , chúng ta có hàm offsetAnimation .


Hàm offsetAnimation trả về thuộc tính Animation được sử dụng trong tiện ích Transform.translate . Tiện ích Transform.translate tạo hoạt ảnh cho tiện ích con, sử dụng giá trị từ hoạt ảnh.

 @override Widget build(BuildContext context) { super.build(context); return AnimatedBuilder( animation: animationController, builder: (context, child){ return _slideAnimation(animationController); } ); } Widget _slideAnimation(Animation<double> animationController){ Animation<double> offsetAnimation(double offset, Animation<double> animationController) { return Tween<double>(begin: offset, end: 0.0).animate( CurvedAnimation( parent: animationController, curve: const Interval(0.0, 1.0, curve: Curves.ease), ), ); } return Transform.translate( offset: Offset( widget.horizontalOffset == 0.0 ? 0.0 : offsetAnimation(widget.horizontalOffset!, animationController).value, 0.0, ), child: widget.child, ); }


Đây là mã đầy đủ cho Lớp AnimateWidget 👇🏽

 class AnimateWidget extends StatefulWidget { const AnimateWidget({super.key, required this.duration, required this.position, required this.horizontalOffset, required this.child, required this.controller, }); final Duration duration; final int position; final double? horizontalOffset; final Widget child; final PageController controller; @override State<AnimateWidget> createState() => _AnimateWidgetState(); } class _AnimateWidgetState extends State<AnimateWidget> with AutomaticKeepAliveClientMixin, SingleTickerProviderStateMixin{ @override bool get wantKeepAlive => true; late AnimationController animationController; int currentPage = 0; Timer? _timer; @override void initState() { super.initState(); animationController = AnimationController(vsync: this, duration: widget.duration); _timer = Timer(getDelay(), animationController.forward); widget.controller.addListener(() { currentPage = widget.controller.page!.round(); if(currentPage == widget.controller.page){ _timer = Timer(getDelay(), animationController.forward); } }); } @override void dispose() { _timer?.cancel(); animationController.dispose(); super.dispose(); } Duration getDelay(){ var delayInMilliSec = widget.duration.inMilliseconds ~/ 6; int getStaggeredListDuration(){ return widget.position * delayInMilliSec; } return Duration(milliseconds: getStaggeredListDuration()); } @override Widget build(BuildContext context) { super.build(context); return AnimatedBuilder( animation: animationController, builder: (context, child){ return _slideAnimation(animationController); } ); } Widget _slideAnimation(Animation<double> animationController){ Animation<double> offsetAnimation(double offset, Animation<double> animationController) { return Tween<double>(begin: offset, end: 0.0).animate( CurvedAnimation( parent: animationController, curve: const Interval(0.0, 1.0, curve: Curves.ease), ), ); } return Transform.translate( offset: Offset( widget.horizontalOffset == 0.0 ? 0.0 : offsetAnimation(widget.horizontalOffset!, animationController).value, 0.0, ), child: widget.child, ); } }

Tiếp theo, để sử dụng lớp AnimateWidget này trong một tiện ích cột, chúng ta sẽ tạo một lớp có phương thức tĩnh tên là toStaggeredList trả về một danh sách. Trong phương thức này, chúng ta chuyển tất cả các tham số cần thiết, bao gồm cả một danh sách children . Tham số children là nơi chúng ta chuyển danh sách các phần tử mà chúng ta sẽ tạo hoạt ảnh.


Tiếp theo, chúng ta sẽ ánh xạ các phần tử con, gói từng phần tử con bằng AnimateWidget .

 class AnimateList{ static List<Widget>toStaggeredList({ required Duration duration, double? horizontalOffset, required PageController controller, required List<Widget>children, })=> children .asMap() .map((index, widget){ return MapEntry( index, AnimateWidget( duration: duration, position: index, horizontalOffset: horizontalOffset, controller: controller, child: widget, ) ); }) .values .toList(); }


Trong AnimateWidget , chúng tôi chuyển các tham số cần thiết để tạo hoạt ảnh thành công cho từng thành phần con trong danh sách. Bằng cách sử dụng phương thức AnimateList.toStaggeredList , giờ đây chúng ta có thể triển khai nó trên hai trang mà chúng ta đang làm việc.

 class Page1 extends StatelessWidget { const Page1({super.key, required this.pageController}); final PageController pageController; @override Widget build(BuildContext context) { return SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( children: AnimateList.toStaggeredList( duration: const Duration(milliseconds: 375), controller: pageController, horizontalOffset: MediaQuery.of(context).size.width / 2, children: [ const EmptyCard(width: 250, height: 50,), const Padding( padding: EdgeInsets.only(top: 20), child: EmptyCard(width: 180, height: 80,), ), const Padding( padding: EdgeInsets.only(top: 20), child: EmptyCard(width: 270, height: 50,), ), const Padding( padding: EdgeInsets.symmetric(vertical: 20), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ EmptyCard(height: 50, width: 70), EmptyCard(height: 50, width: 70), EmptyCard(height: 50, width: 70), ], ), ), const EmptyCard(width: 250, height: 50,), const Padding( padding: EdgeInsets.only(top: 20), child: EmptyCard(width: 180, height: 80,), ), const Padding( padding: EdgeInsets.only(top: 20), child: EmptyCard(width: 270, height: 50,), ), const Padding( padding: EdgeInsets.symmetric(vertical: 20), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ EmptyCard(height: 50, width: 70), EmptyCard(height: 50, width: 70), EmptyCard(height: 50, width: 70), ], ), ), ], ), ), ); } } class Page2 extends StatelessWidget { const Page2({super.key, required this.pageController}); final PageController pageController; @override Widget build(BuildContext context) { return SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( children: AnimateList.toStaggeredList( duration: const Duration(milliseconds: 375), controller: pageController, horizontalOffset: MediaQuery.of(context).size.width / 2, children: [ const EmptyCard(width: 220, height: 70,), const Padding( padding: EdgeInsets.only(top: 20), child: EmptyCard(width: 300, height: 70,), ), const Padding( padding: EdgeInsets.only(top: 20), child: EmptyCard(width: 200, height: 50,), ), const Padding( padding: EdgeInsets.symmetric(vertical: 20), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ EmptyCard(height: 70, width: 70), EmptyCard(height: 70, width: 70), ], ), ), const EmptyCard(width: 220, height: 70,), const Padding( padding: EdgeInsets.only(top: 20), child: EmptyCard(width: 300, height: 70,), ), const Padding( padding: EdgeInsets.symmetric(vertical: 20), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ EmptyCard(height: 70, width: 70), EmptyCard(height: 70, width: 70), ], ), ), ], ), ), ); } }

Trong phần tử con của tiện ích cột, chúng ta sẽ chuyển AnimateList.toStaggeredList và chuyển các tham số cần thiết, bao gồm cả các tiện ích sẽ được hiển thị trong cột. Với điều này, chúng tôi đã tạo thành công hoạt ảnh so le được kích hoạt khi vuốt. Bạn có thể kiểm tra mã đầy đủ ở đây .


Đây là kết quả cuối cùng của chúng tôi:

Phần kết luận

Chúng ta đã đi đến phần cuối của hướng dẫn này. Tại thời điểm này, chúng tôi đã đề cập đến khái niệm tương tác vi mô và tác động của nó đến trải nghiệm người dùng. Chúng tôi cũng đã trải qua quá trình xây dựng hoạt ảnh so le của các mục trong tiện ích cột được kích hoạt khi vuốt trang.


Có rất nhiều loại tương tác vi mô mà bạn có thể thêm vào ứng dụng của mình để cải thiện trải nghiệm người dùng trong dự án của mình; bạn có thể thử nghiệm bằng cách xây dựng nhiều hoạt ảnh tương tác hơn trong Flutter.


Nếu bạn thấy bài viết này hữu ích, bạn có thể hỗ trợ nó bằng cách để lại một lượt thích hoặc bình luận. Bạn cũng có thể theo dõi tôi để biết thêm các bài viết liên quan.

Người giới thiệu

Gói hoạt hình so le Flutter