《Flutter篇第二章》MasonryGridView瀑布流列表
学习Flutter ,列表的布局是一个很重要的点,大部分常见的APP 都离不开列表,尤其是瀑布Feed流,其中,很多app 会在列表中嵌套图片、视频和AD 广告,所以今天用MasonryGridView实现了一个列表样式
先看效果:
1、配置
video_player: ^2.8.0chewie: ^1.5.0flutter_staggered_grid_view: ^0.7.0
2、卡片widget
import 'package:chewie/chewie.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';import '../../modules/main/tabs/home/feed_item.dart';
import '../../modules/main/tabs/home/home_controller.dart';class FeedItemCard extends StatelessWidget {final FeedItem item;const FeedItemCard({super.key, required this.item}); Widget build(BuildContext context) {return Card(margin: const EdgeInsets.all(0),shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),clipBehavior: Clip.antiAlias,child: Column(crossAxisAlignment: CrossAxisAlignment.start,children: [_buildMediaContent(),Padding(padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),child: Column(crossAxisAlignment: CrossAxisAlignment.start,children: [Text(item.description,style: const TextStyle(fontSize: 14),maxLines: 2,overflow: TextOverflow.ellipsis,),const SizedBox(height: 8),_buildInteractionRow(),],),),],),);}Widget _buildMediaContent() {final controller = Get.find<HomeController>();return AspectRatio(aspectRatio: item.aspectRatio,child: Stack(children: [if (item.type == 'video')_buildVideoPlayer(controller)elseImage.network(item.url,fit: BoxFit.cover,width: double.infinity,height: double.infinity,),if (item.type == 'video')Positioned(bottom: 8,right: 8,child: Container(padding: const EdgeInsets.all(4),decoration: BoxDecoration(color: Colors.black.withOpacity(0.5),borderRadius: BorderRadius.circular(4),),child: const Icon(Icons.play_arrow,color: Colors.white,size: 16,),),)],),);}Widget _buildVideoPlayer(HomeController ctrl) {return GetBuilder<HomeController>(builder: (context) {final chewieController = ctrl.videoControllers[item.id];if (chewieController == null) {ctrl.initializeVideoPlayer(item.url, item.id);return Container(color: Colors.black,child: const Center(child: CircularProgressIndicator(color: Colors.white)),);}return GestureDetector(onTap: () {if (chewieController.isPlaying) {ctrl.pauseVideo(item.id);} else {ctrl.setActiveVideo(item.id);}},child: Chewie(controller: chewieController),);},);}Widget _buildInteractionRow() {return Row(children: [Obx(() {final controller = Get.find<HomeController>();final itemIndex = controller.feedItems.indexWhere((i) => i.id == item.id);if (itemIndex == -1) return const SizedBox();final currentItem = controller.feedItems[itemIndex];return Row(children: [IconButton(icon: Icon(currentItem.isLiked ? Icons.favorite : Icons.favorite_border,color: currentItem.isLiked ? Colors.red : Colors.grey[700],size: 20,),onPressed: () => controller.toggleLike(item.id),padding: EdgeInsets.zero,constraints: const BoxConstraints(),),const SizedBox(width: 4),Text('${currentItem.likes}',style: const TextStyle(fontSize: 12),),],);}),const SizedBox(width: 16),const Icon(Icons.mode_comment_outlined, size: 20, color: Colors.grey),const SizedBox(width: 16),const Icon(Icons.send, size: 20, color: Colors.grey),],);}
}
3、逻辑加载controller
import 'package:chewie/chewie.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:video_player/video_player.dart';import 'feed_item.dart';class HomeController extends GetxController {var welcomeMessage = ''.obs;final feedItems = <FeedItem>[].obs;final activeVideoId = RxString('');final videoControllers = <String, ChewieController>{};final scrollController = ScrollController();void onInit() {super.onInit();Future.delayed(const Duration(seconds: 1), () {welcomeMessage.value = 'Welcome to Xiaohongshu!';_loadFeedData();});scrollController.addListener(_handleScroll);}void onClose() {for (var controller in videoControllers.values) {controller.dispose();}videoControllers.clear();scrollController.dispose();super.onClose();}void _loadFeedData() {feedItems.assignAll([FeedItem(id: '1',type: 'image',url: 'https://picsum.photos/400/600?random=1',aspectRatio: 400 / 600,description: '夏日海滩度假照片 #旅行 #夏天',likes: 243,),FeedItem(id: '2',type: 'video',url: 'http://vjs.zencdn.net/v/oceans.mp4',aspectRatio: 16 / 9,description: '蝴蝶飞舞的美丽瞬间 #自然 #动物',likes: 512,),FeedItem(id: '3',type: 'image',url: 'https://picsum.photos/400/800?random=2',aspectRatio: 400 / 800,description: '城市夜景 #摄影 #城市',likes: 187,),FeedItem(id: '4',type: 'image',url: 'https://picsum.photos/500/700?random=3',aspectRatio: 500 / 700,description: '美食探店 #美食 #周末',likes: 324,),FeedItem(id: '5',type: 'video',url: 'http://vjs.zencdn.net/v/oceans.mp4',aspectRatio: 16 / 9,description: '周末电影时光 #电影 #休闲',likes: 421,),FeedItem(id: '6',type: 'image',url: 'https://picsum.photos/450/650?random=4',aspectRatio: 450 / 650,description: '健身打卡 #健身 #健康生活',likes: 278,),FeedItem(id: '6',type: 'image',url: 'https://picsum.photos/450/650?random=4',aspectRatio: 450 / 650,description: '健身打卡 #健身 #健康生活',likes: 278,),FeedItem(id: '6',type: 'image',url: 'https://picsum.photos/450/650?random=4',aspectRatio: 450 / 650,description: '健身打卡 #健身 #健康生活',likes: 278,),FeedItem(id: '6',type: 'image',url: 'https://picsum.photos/450/650?random=4',aspectRatio: 450 / 650,description: '健身打卡 #健身 #健康生活',likes: 278,),FeedItem(id: '6',type: 'image',url: 'https://picsum.photos/450/650?random=4',aspectRatio: 450 / 650,description: '健身打卡 #健身 #健康生活',likes: 278,),FeedItem(id: '6',type: 'image',url: 'https://picsum.photos/450/650?random=4',aspectRatio: 450 / 650,description: '健身打卡 #健身 #健康生活',likes: 278,),]);}void toggleLike(String itemId) {final index = feedItems.indexWhere((item) => item.id == itemId);if (index != -1) {final item = feedItems[index];feedItems[index] = FeedItem(id: item.id,type: item.type,url: item.url,aspectRatio: item.aspectRatio,description: item.description,likes: item.isLiked ? item.likes - 1 : item.likes + 1,isLiked: !item.isLiked,);}}void setActiveVideo(String videoId) {if (activeVideoId.value != videoId) {if (activeVideoId.isNotEmpty) {final currentController = videoControllers[activeVideoId.value];currentController?.pause();}activeVideoId.value = videoId;final controller = videoControllers[videoId];controller?.play();}}void pauseVideo(String videoId) {if (activeVideoId.value == videoId) {activeVideoId.value = '';}final controller = videoControllers[videoId];controller?.pause();}Future<void> initializeVideoPlayer(String videoUrl, String videoId) async {if (videoControllers.containsKey(videoId)) return;final videoController = VideoPlayerController.network(videoUrl);await videoController.initialize();final chewieController = ChewieController(videoPlayerController: videoController,autoPlay: false,looping: true,showControls: false,allowFullScreen: true,materialProgressColors: ChewieProgressColors(playedColor: Colors.red,handleColor: Colors.red,backgroundColor: Colors.grey,bufferedColor: Colors.grey.withOpacity(0.5),));videoControllers[videoId] = chewieController;}void _handleScroll() {for (var item in feedItems) {if (item.type == 'video') {pauseVideo(item.id);}}}
}
5、页面
import 'package:flutter/material.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:get/get.dart';import '../../../../view/widgets/feed_item_card.dart';
import 'home_controller.dart';class HomePage extends GetView<HomeController> {const HomePage({super.key}); Widget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Obx(() => Text(controller.welcomeMessage.value)),actions: [IconButton(icon: const Icon(Icons.search),onPressed: () {},),IconButton(icon: const Icon(Icons.notifications_none),onPressed: () {},),],),body: Obx(() {if (controller.feedItems.isEmpty) {return const Center(child: CircularProgressIndicator());}return MasonryGridView.count(controller: controller.scrollController,crossAxisCount: 2,itemCount: controller.feedItems.length,itemBuilder: (context, index) {final item = controller.feedItems[index];return FeedItemCard(item: item);},mainAxisSpacing: 8,crossAxisSpacing: 8,padding: const EdgeInsets.all(8),);}),);}
}