在这里插入图片描述

前言

有些东西看文字不如看视频来得直观,比如塑料瓶怎么压扁、厨余垃圾怎么沥干水分。视频教程页面就是提供这类可视化学习内容的地方。本文将详细介绍如何在Flutter for OpenHarmony环境下实现一个完整的视频教程页面。视频作为一种直观的学习方式,能够帮助用户更好地理解垃圾分类的具体操作方法,特别是对于一些需要动手操作的分类技巧,视频演示比文字描述更加清晰易懂。

技术要点概览

本页面涉及的核心技术点包括以下几个方面:

  • ListView.builder:高效的列表渲染,支持大量视频数据展示
  • Stack组件:缩略图和播放按钮的叠加效果
  • InkWell组件:点击时的水波纹反馈效果
  • Card组件:卡片式布局设计,视觉效果美观

视频数据结构

每个视频包含标题、时长和播放量:

class VideoGuidePage extends StatelessWidget {
  const VideoGuidePage({super.key});

  
  Widget build(BuildContext context) {
    final videos = [
      {'title': '垃圾分类入门指南', 'duration': '05:30', 'views': '12.5万'},
      {'title': '可回收物正确投放方法', 'duration': '03:45', 'views': '8.2万'},
      {'title': '有害垃圾处理注意事项', 'duration': '04:20', 'views': '6.8万'},
      {'title': '厨余垃圾减量小技巧', 'duration': '06:15', 'views': '9.1万'},
      {'title': '家庭垃圾分类实操演示', 'duration': '08:00', 'views': '15.3万'},
      {'title': '办公室垃圾分类指南', 'duration': '04:50', 'views': '5.6万'},
    ];

视频内容覆盖了从入门到进阶的各个方面,有基础知识也有实操演示。播放量数据能帮助用户判断哪些视频更受欢迎。

使用Model类管理数据

实际项目中建议使用Model类:

class VideoItem {
  final String id;
  final String title;
  final String duration;
  final int views;
  final String? thumbnailUrl;
  final String? videoUrl;
  final String? category;
  
  VideoItem({
    required this.id,
    required this.title,
    required this.duration,
    required this.views,
    this.thumbnailUrl,
    this.videoUrl,
    this.category,
  });
  
  String get formattedViews {
    if (views >= 10000) {
      return '${(views / 10000).toStringAsFixed(1)}万';
    }
    return '$views';
  }
}

页面布局

视频列表用卡片式布局,每张卡片左边是视频缩略图,右边是标题和播放量:

    return Scaffold(
      appBar: AppBar(title: const Text('视频教程')),
      body: ListView.builder(
        padding: EdgeInsets.all(16.w),
        itemCount: videos.length,
        itemBuilder: (context, index) {
          final video = videos[index];
          return Card(
            margin: EdgeInsets.only(bottom: 12.h),
            child: InkWell(
              onTap: () => _showVideoDialog(context, video['title']!),

InkWell包裹整个卡片,点击时会有水波纹效果,给用户明确的反馈。

视频缩略图区域

缩略图区域用Container模拟,中间放播放按钮,右下角显示时长:

              child: Padding(
                padding: EdgeInsets.all(12.w),
                child: Row(
                  children: [
                    Container(
                      width: 120.w,
                      height: 80.h,
                      decoration: BoxDecoration(
                        color: AppTheme.primaryColor.withOpacity(0.1),
                        borderRadius: BorderRadius.circular(8.r),
                      ),
                      child: Stack(
                        alignment: Alignment.center,
                        children: [
                          Icon(Icons.play_circle_fill, color: AppTheme.primaryColor, size: 40.sp),

设计说明:实际项目中这里应该是视频的真实缩略图。现在用纯色背景加播放图标来占位,视觉上也能让用户理解这是个视频入口。

时长标签

时长标签放在右下角:

                          Positioned(
                            bottom: 4.h,
                            right: 4.w,
                            child: Container(
                              padding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 2.h),
                              decoration: BoxDecoration(
                                color: Colors.black54,
                                borderRadius: BorderRadius.circular(4.r),
                              ),
                              child: Text(
                                video['duration']!,
                                style: TextStyle(color: Colors.white, fontSize: 10.sp),
                              ),
                            ),
                          ),
                        ],
                      ),
                    ),

时长用半透明黑色背景,白色文字,这是视频平台通用的设计语言。

视频信息区域

右边显示视频标题和播放量:

                    SizedBox(width: 12.w),
                    Expanded(
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Text(
                            video['title']!,
                            style: TextStyle(fontSize: 15.sp, fontWeight: FontWeight.w500),
                            maxLines: 2,
                            overflow: TextOverflow.ellipsis,
                          ),
                          SizedBox(height: 8.h),
                          Row(
                            children: [
                              Icon(Icons.visibility, size: 14.sp, color: Colors.grey),
                              SizedBox(width: 4.w),
                              Text(
                                '${video['views']}次播放',
                                style: TextStyle(fontSize: 12.sp, color: Colors.grey),
                              ),
                            ],
                          ),
                        ],
                      ),
                    ),
                  ],
                ),
              ),
            ),
          );
        },
      ),
    );
  }

标题最多显示两行,超出部分用省略号。播放量前面加了个眼睛图标,让信息更直观。

视频播放的处理

点击视频后弹出一个对话框,提示功能开发中:

  void _showVideoDialog(BuildContext context, String title) {
    showDialog(
      context: context,
      builder: (ctx) => AlertDialog(
        title: Text(title),
        content: const Text('视频功能开发中,敬请期待...'),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(ctx),
            child: const Text('确定'),
          ),
        ],
      ),
    );
  }
}

为什么不直接播放视频? 视频播放涉及到很多东西:视频源、播放器、缓存、全屏等等。这里先用对话框占位。

真实视频播放实现

使用video_player插件

import 'package:video_player/video_player.dart';

class VideoPlayerPage extends StatefulWidget {
  final String videoUrl;
  
  const VideoPlayerPage({super.key, required this.videoUrl});
  
  
  State<VideoPlayerPage> createState() => _VideoPlayerPageState();
}

class _VideoPlayerPageState extends State<VideoPlayerPage> {
  late VideoPlayerController _controller;
  
  
  void initState() {
    super.initState();
    _controller = VideoPlayerController.network(widget.videoUrl)
      ..initialize().then((_) {
        setState(() {});
        _controller.play();
      });
  }
  
  
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
  
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('视频播放')),
      body: Center(
        child: _controller.value.isInitialized
            ? AspectRatio(
                aspectRatio: _controller.value.aspectRatio,
                child: VideoPlayer(_controller),
              )
            : CircularProgressIndicator(),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          setState(() {
            _controller.value.isPlaying
                ? _controller.pause()
                : _controller.play();
          });
        },
        child: Icon(
          _controller.value.isPlaying ? Icons.pause : Icons.play_arrow,
        ),
      ),
    );
  }
}

使用chewie插件

chewie提供了更完善的播放控制UI:

import 'package:chewie/chewie.dart';

class ChewiePlayerPage extends StatefulWidget {
  final String videoUrl;
  
  const ChewiePlayerPage({super.key, required this.videoUrl});
  
  
  State<ChewiePlayerPage> createState() => _ChewiePlayerPageState();
}

class _ChewiePlayerPageState extends State<ChewiePlayerPage> {
  late VideoPlayerController _videoController;
  ChewieController? _chewieController;
  
  
  void initState() {
    super.initState();
    _initializePlayer();
  }
  
  Future<void> _initializePlayer() async {
    _videoController = VideoPlayerController.network(widget.videoUrl);
    await _videoController.initialize();
    
    _chewieController = ChewieController(
      videoPlayerController: _videoController,
      autoPlay: true,
      looping: false,
      aspectRatio: _videoController.value.aspectRatio,
      placeholder: Container(color: Colors.black),
      autoInitialize: true,
    );
    
    setState(() {});
  }
  
  
  void dispose() {
    _videoController.dispose();
    _chewieController?.dispose();
    super.dispose();
  }
  
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('视频播放')),
      body: Center(
        child: _chewieController != null
            ? Chewie(controller: _chewieController!)
            : CircularProgressIndicator(),
      ),
    );
  }
}

视频列表的优化方向

1. 分类筛选

按垃圾类型或难度分类:

final categories = ['全部', '入门', '可回收物', '有害垃圾', '厨余垃圾', '其他垃圾'];
final selectedCategory = '全部'.obs;

Widget _buildCategoryFilter() {
  return SizedBox(
    height: 40.h,
    child: ListView.builder(
      scrollDirection: Axis.horizontal,
      itemCount: categories.length,
      itemBuilder: (context, index) {
        final category = categories[index];
        return Obx(() => Padding(
          padding: EdgeInsets.only(right: 8.w),
          child: ChoiceChip(
            label: Text(category),
            selected: selectedCategory.value == category,
            onSelected: (selected) {
              if (selected) selectedCategory.value = category;
            },
          ),
        ));
      },
    ),
  );
}

2. 搜索功能

final searchController = TextEditingController();
final filteredVideos = <VideoItem>[].obs;

void _filterVideos(String keyword) {
  if (keyword.isEmpty) {
    filteredVideos.value = videos;
  } else {
    filteredVideos.value = videos.where((v) {
      return v.title.contains(keyword);
    }).toList();
  }
}

3. 播放历史

class VideoHistoryService {
  static final _history = <String>[].obs;
  
  static void addToHistory(String videoId) {
    _history.remove(videoId);
    _history.insert(0, videoId);
    if (_history.length > 50) {
      _history.removeLast();
    }
    _saveToStorage();
  }
  
  static bool isWatched(String videoId) {
    return _history.contains(videoId);
  }
}

4. 收藏功能

final favoriteVideos = <String>{}.obs;

void toggleFavorite(String videoId) {
  if (favoriteVideos.contains(videoId)) {
    favoriteVideos.remove(videoId);
  } else {
    favoriteVideos.add(videoId);
  }
  _saveFavorites();
}

5. 离线下载

class VideoDownloadService {
  static Future<void> downloadVideo(VideoItem video) async {
    final dio = Dio();
    final savePath = await _getLocalPath(video.id);
    
    await dio.download(
      video.videoUrl!,
      savePath,
      onReceiveProgress: (received, total) {
        final progress = received / total;
        _updateProgress(video.id, progress);
      },
    );
  }
}

性能优化

1. 缩略图懒加载

CachedNetworkImage(
  imageUrl: video.thumbnailUrl!,
  placeholder: (context, url) => Container(
    color: Colors.grey.shade200,
    child: Icon(Icons.play_circle_outline),
  ),
  errorWidget: (context, url, error) => Icon(Icons.error),
)

2. 列表项使用Key

return Card(
  key: ValueKey(video.id),
  // ...
);

总结

视频教程是个很好的学习形式,特别是对于操作类的内容。本文介绍的实现方案包括:

  1. 列表布局:卡片式视频列表
  2. 缩略图设计:播放按钮和时长标签
  3. 视频播放:video_player和chewie的使用
  4. 功能扩展:分类、搜索、历史、收藏

虽然现在只是个列表页面,但框架搭好了,后续接入真实视频就很方便了。视频教程作为一种直观的学习方式,能够有效帮助用户掌握垃圾分类的实操技巧。

总结

本文详细介绍了视频教程页面的实现方案,从基础的列表展示到视频播放器的集成都有涉及。通过合理的界面设计,可以为用户提供优质的视频学习体验。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐