Tạo ImageSlider trong ứng dụng Flutter với PageView

ImageSlider khá phổ biến trong thế giới Web. Và với ứng dụng Android, bạn có thể dễ dàng thực hiện điều tương tự với hướng dẫn của tôi ở đây. Còn Flutter thì sao? Đương nhiên là được. Bài viết này sẽ hướng dẫn các bạn mang ImageSlider vào ứng dụng Flutter để chạy tốt trên cả Android và iOS chỉ với một project duy nhất với PageView (tương ứng với ViewPager trong Android Support Lib). Và như thường lệ, xin mời các bạn tạo một Flutter Project mới với Android Studio và xin quyền INTERNET trong AndroidManifest trong thư mục android của project. Ngoài ra, chúng ta sẽ load ảnh từ unsplash.com thông qua constructor Image.network(String src).

1. Điều chỉnh main.dart:

Bạn xóa hết các class MyHomePage và _MyHomePageState có sẵn và điều chỉnh main.dart với nội dung mới như sau:

Ở đây, MyHomePage sẽ trở thành một StatelessWidget, còn ImageSlider của chúng ta sẽ là một StatefulWidget. Vì sao lại là StatefulWidget? Đơn giản là chúng ta sẽ còn phải cập nhật chỉ thị của các chấm biểu thị ví trị hiện tại của ImageSlider nữa. Và chúng ta sẽ không có cách nào tiến hành cập nhật với StatelessWidget. Nội dung của ImageSlider, đúng hơn là ImageSliderState sẽ gồm một Stack (tương tự FrameLayout của Android) gồm hai lớp: Lớp bên dưới là một PageView chứa hình ảnh, và lớp bên trên là (tạm cho là) một Row chứa các chấm chỉ thị vị trí. Và từ vị trí này trở xuống bên dưới, chúng ta sẽ chỉ thao tác với class ImageSliderState.

2. Dựng PageView:

PageView của chúng ta sẽ vô cùng đơn giản. Phần children gồm các Widget nằm bên trong nó ứng với mỗi trang (page) sẽ được định nghĩa trong hàm _buildPageViewChildren(BuildContext). Mỗi trang của chúng ta chỉ đơn giản là một Image.network(String src) mà thôi. Dưới đây là phần code của tôi chỉ trong ba dòng duy nhất:

Nếu bạn không hiểu List#map, thì bạn có thể diễn giải method trên ra vài dòng dễ hiểu như sau:

3. Dựng Dot indicator:

Đúng như bạn đang nghĩ, dot indicator sẽ chỉ là một Row chứa các “chấm” bên trong được định nghĩa thông qua buildDots(BuildContext). Mỗi chấm sẽ là một Container có shape là dạng BoxShape.circle và màu sẽ là colorPrimary nếu vị trí của nó trùng với vị trí hiện tại của PageView bên trên, mà ta sẽ cập nhật thông qua biến _pos, và màu colorAccent cho các vị trí khác. Vì vậy, chúng ta sẽ có code như sau:

Và cuối cùng, để cập nhật trạng thái của các “chấm” sao cho khớp với vị trí hiện tại của PageView, bạn chỉ cần setState trong onPageChanged() mà thôi. Tức là:

Cuối cùng, cho chạy thử, và 1, 2, 3… Bạn sẽ phát hiện một chi tiết rất khó chịu: Các chấm của chúng ta nằm ở góc trên bên trái chứ không phải ở vị trí chính giữa trên cạnh dưới màn hình vốn là vị trí luật bất thành văn mặc định cho đa số các chấm chỉ thị vị trí. Để “bưng” chúng về vị trí giữa đó, chúng ta sẽ phải điều chỉnh lại Widget mà buildDotIndicator(BuildContext) return.

4. Điều chỉnh vị trí của Dot indicator:

Trong Flutter có một Widget rất hay là Positioned. Nó sẽ nằm tại vị trí tương đối với Widget đang chứa nó (tức parent widget) theo các constraints mà bạn sẽ chỉ định cho nó. Đối với các bạn code iOS với constraints hay Android với ConstraintLayout, hay Web với các styles { position: absolute, left, right, top, bottom, width, height } thì sẽ nắm vững về nó trong vòng hai hoặc ba nốt nhạc là tối đa. Còn đối với các bạn code Android nhưng ghét ConstraintLayout như tôi chẳng hạn, thì nó giúp bạn định vị trí View con với layout_gravity và margins trong FrameLayout, và bản thân Positioned cũng được khuyến nghị dùng chung với Stack ứng với FrameLayout.

Positioned có các properties bao gồm hai nhóm có class là double gồm [left, right, width], [top, bottom, height] tương tự như CSS. Trong mỗi nhóm, chỉ tối đa định nghĩa được hai props. Chẳng hạn, bạn phải buộc định nghĩa một prop, vd tôi định nghĩa left, thì tối đa chỉ có thể định nghĩa thêm một trong right và width không null. Nếu định nghĩa cả ba trong cùng nhóm thì sẽ báo lỗi không render được. Nếu cả ba prop trong cùng nhóm cùng null, thì Positioned sẽ căn cứ vào Stack.alignment để đặt child.

Bây giờ, chúng ta tiến hành wrap Row của buildDotIndicator(BuildContext) với một Positioned. Kết quả tương tự như sau:

Tiếp theo, để định nghĩa vị trí tương đối mà Positioned sẽ “đậu” vào, ở đây là bên dưới, thì ta sẽ thêm prop bottom có giá trị là 0.0, tức nó sẽ cách bottom của Widget mẹ của nó, chính là Stack, là 0.0. Tiếp theo là việc căn giữa. Ở đây, tôi sẽ dùng cùng lúc cả hai prop là left: 0.0 và right: 0.0 luôn. Và bạn nhấn Ctrl S để ứng dụng render lại.

Vấn đề chỉ được giải quyết phân nửa, tức là các chấm chỉ được di chuyển xuống cạnh dưới mà thôi. Còn vấn đề canh giữa thì vẫn chưa được giải quyết. Bạn tiếp tục wrap Row vào Center và điều chỉnh mainAxisSize như bên dưới là xong. Ở đây, MainAxisSize.min tương tự như wrap_content trong Android, còn mặc định MainAxisSize.max tương tự như match_parent:

Kết quả là bạn sẽ có được kết quả tương tự như sau, và ta đã tạo xong ImageSlider trong ứng dụng Flutter:

Bạn đã biết ImageSlider khá phổ biến trong thế giới Web. Nhưng bạn có biết chúng ta có thể dễ dàng ImageSlider mang lên ứng dụng Android? Hãy tham khảo cách thực hiện tại http://eitguide.net/tao-imageslider-trong-ung-dung-android-voi-viewpager/

Eitguide Androidさんの投稿 2019年1月24日木曜日

5. Hướng đi khác:

Có lẽ các bạn đã thao tác nhiều với các Widgets và nắm rõ về Stack và Positioned sẽ cho rằng tôi đi quá dài dòng trong việc định nghĩa vị trí của Dot indicator. Thực tế, chúng ta có thể đạt được điều hoàn toàn giống như trên chỉ với vài dòng còm sau:

Nếu bạn để ý, thì dĩ nhiên phần DotIndicator sẽ chịu ảnh hưởng của Alignment.bottomCenter rồi. Nhưng bên cạnh đó, PageView của chúng ta cũng chịu ảnh hưởng luôn. Trong bài này, vì kết cấu Widget khá đơn giản chỉ gồm hai thành phần PageView và DotIndicator nên kết quả của hai cách làm là như nhau. Sau này, bạn muốn thêm một Text nhưng muốn cho nó nằm chính giữa trong cả trục ngang và dọc, và thêm nhiều Widgets khác với vị trí khác nhau nữa, thì mặc định chúng sẽ chịu ảnh hưởng của bottomCenter theo mặc định, và để “hóa giải” thì bạn lại phải tốn công định nghĩa các constraints như tôi đã làm. Dù cách nào đi nữa, thì kết quả như bạn mong muốn là điều quan trọng nhất.Về PageView, Stack và Positioned, tôi sẽ có những bài viết riêng về chúng sau này. Chúc các bạn code vui vẻ.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.