《Flutter篇第二章》MasonryGridView瀑布流列表

学习Flutter ,列表的布局是一个很重要的点,大部分常见的APP 都离不开列表,尤其是瀑布Feed流,其中,很多app 会在列表中嵌套图片、视频和AD 广告,所以今天用MasonryGridView实现了一个列表样式

先看效果:

1、配置

dart 复制代码
  video_player: ^2.8.0
  chewie: ^1.5.0
  flutter_staggered_grid_view: ^0.7.0
 

2、卡片widget

dart 复制代码
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});

  @override
  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)
          else
            Image.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

dart 复制代码
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();

  @override
  void onInit() {
    super.onInit();
    Future.delayed(const Duration(seconds: 1), () {
      welcomeMessage.value = 'Welcome to Xiaohongshu!';
      _loadFeedData();
    });
    scrollController.addListener(_handleScroll);
  }

  @override
  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、页面

dart 复制代码
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});

  @override
  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),
        );
      }),
    );
  }
}
相关推荐
stevenzqzq11 小时前
android Initializer 启动入门
android
小雨下雨的雨11 小时前
Flutter 框架跨平台鸿蒙开发 —— Center 控件之完美居中之道
flutter·ui·华为·harmonyos·鸿蒙
·云扬·11 小时前
系统与MySQL核心监控指标及操作指南
android·数据库·mysql
小雨下雨的雨12 小时前
Flutter 框架跨平台鸿蒙开发 —— Icon 控件之图标交互美学
flutter·华为·交互·harmonyos·鸿蒙系统
小雨下雨的雨12 小时前
Flutter 框架跨平台鸿蒙开发 —— Placeholder 控件之布局雏形美学
flutter·ui·华为·harmonyos·鸿蒙系统
冬奇Lab12 小时前
【Kotlin系列01】Kotlin快速入门:环境搭建与Hello World
android·kotlin·android studio
君莫啸ོ12 小时前
Android 自定义View-圆圈扩散动画
android
行者9612 小时前
OpenHarmony Flutter弹出菜单组件深度实践:从基础到高级的完整指南
flutter·harmonyos·鸿蒙
stevenzqzq12 小时前
android启动和注入理解1
android