Commit 9475aa49a010750d35b003e2d1125ef430c47dcd

Authored by 吴启风
1 parent 7e98b732

feat: 优化视频专区列表布局与沉浸体验

lib/pages/video/zone/video_zone_page.dart
  1 +import 'dart:async';
  2 +
1 import 'package:cached_network_image/cached_network_image.dart'; 3 import 'package:cached_network_image/cached_network_image.dart';
2 import 'package:flutter/material.dart'; 4 import 'package:flutter/material.dart';
  5 +import 'package:flutter/services.dart';
3 import 'package:flutter_screenutil/flutter_screenutil.dart'; 6 import 'package:flutter_screenutil/flutter_screenutil.dart';
4 import 'package:wow_english/common/dialogs/show_dialog.dart'; 7 import 'package:wow_english/common/dialogs/show_dialog.dart';
5 import 'package:wow_english/common/request/dao/video_dao.dart'; 8 import 'package:wow_english/common/request/dao/video_dao.dart';
@@ -21,62 +24,95 @@ class _VideoZonePageState extends State<VideoZonePage> { @@ -21,62 +24,95 @@ class _VideoZonePageState extends State<VideoZonePage> {
21 @override 24 @override
22 void initState() { 25 void initState() {
23 super.initState(); 26 super.initState();
  27 + unawaited(
  28 + SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky),
  29 + );
24 _itemsFuture = VideoDao.list(); 30 _itemsFuture = VideoDao.list();
25 } 31 }
26 32
  33 + @override
  34 + void dispose() {
  35 + unawaited(
  36 + SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky),
  37 + );
  38 + super.dispose();
  39 + }
  40 +
27 void _reload() { 41 void _reload() {
28 - setState(() => _itemsFuture = VideoDao.list()); 42 + setState(() {
  43 + _itemsFuture = VideoDao.list();
  44 + });
29 } 45 }
30 46
31 @override 47 @override
32 Widget build(BuildContext context) { 48 Widget build(BuildContext context) {
33 - return Scaffold(  
34 - backgroundColor: Colors.white,  
35 - appBar: const WEAppBar(  
36 - titleText: '视频专区',  
37 - centerTitle: false,  
38 - ),  
39 - body: FutureBuilder<List<VideoZoneItem>>(  
40 - future: _itemsFuture,  
41 - builder: (context, snapshot) {  
42 - if (snapshot.connectionState == ConnectionState.waiting) {  
43 - return const Center(child: CircularProgressIndicator());  
44 - }  
45 - if (snapshot.hasError) {  
46 - return _VideoLoadError(onRetry: _reload);  
47 - }  
48 - final items = snapshot.data ?? const <VideoZoneItem>[];  
49 - if (items.isEmpty) {  
50 - return const Center(  
51 - child: Text(  
52 - '暂无视频',  
53 - style: TextStyle(color: Color(0xFF999999), fontSize: 18), 49 + return MediaQuery.removePadding(
  50 + context: context,
  51 + removeTop: true,
  52 + child: Scaffold(
  53 + backgroundColor: Colors.white,
  54 + appBar: const WEAppBar(
  55 + titleText: '视频专区',
  56 + centerTitle: false,
  57 + ),
  58 + body: FutureBuilder<List<VideoZoneItem>>(
  59 + future: _itemsFuture,
  60 + builder: (context, snapshot) {
  61 + if (snapshot.connectionState == ConnectionState.waiting) {
  62 + return const Center(child: CircularProgressIndicator());
  63 + }
  64 + if (snapshot.hasError) {
  65 + return _VideoLoadError(onRetry: _reload);
  66 + }
  67 + final items = snapshot.data ?? const <VideoZoneItem>[];
  68 + if (items.isEmpty) {
  69 + return const Center(
  70 + child: Text(
  71 + '暂无视频',
  72 + style: TextStyle(color: Color(0xFF999999), fontSize: 18),
  73 + ),
  74 + );
  75 + }
  76 + return RefreshIndicator(
  77 + onRefresh: () async {
  78 + final future = VideoDao.list();
  79 + setState(() => _itemsFuture = future);
  80 + await future;
  81 + },
  82 + child: LayoutBuilder(
  83 + builder: (context, constraints) {
  84 + final horizontalPadding = 30.w;
  85 + final crossAxisSpacing = 12.w;
  86 + final cardWidth = (constraints.maxWidth -
  87 + horizontalPadding * 2 -
  88 + crossAxisSpacing * 2) /
  89 + 3;
  90 + final cardHeight = cardWidth * 9 / 16 + 5.h + 24.h;
  91 + return GridView.builder(
  92 + physics: const AlwaysScrollableScrollPhysics(),
  93 + padding: EdgeInsets.fromLTRB(
  94 + horizontalPadding,
  95 + 10.h,
  96 + horizontalPadding,
  97 + 16.h,
  98 + ),
  99 + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
  100 + crossAxisCount: 3,
  101 + mainAxisSpacing: 14.h,
  102 + crossAxisSpacing: crossAxisSpacing,
  103 + mainAxisExtent: cardHeight,
  104 + ),
  105 + itemCount: items.length,
  106 + itemBuilder: (context, index) => _VideoCard(
  107 + item: items[index],
  108 + onTap: () => _openVideo(items, index),
  109 + ),
  110 + );
  111 + },
54 ), 112 ),
55 ); 113 );
56 - }  
57 - return RefreshIndicator(  
58 - onRefresh: () async {  
59 - final future = VideoDao.list();  
60 - setState(() => _itemsFuture = future);  
61 - await future;  
62 - },  
63 - child: GridView.builder(  
64 - physics: const AlwaysScrollableScrollPhysics(),  
65 - padding: EdgeInsets.fromLTRB(14.w, 10.h, 14.w, 16.h),  
66 - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(  
67 - crossAxisCount: 4,  
68 - mainAxisSpacing: 14.h,  
69 - crossAxisSpacing: 12.w,  
70 - childAspectRatio: 1.55,  
71 - ),  
72 - itemCount: items.length,  
73 - itemBuilder: (context, index) => _VideoCard(  
74 - item: items[index],  
75 - onTap: () => _openVideo(items, index),  
76 - ),  
77 - ),  
78 - );  
79 - }, 114 + },
  115 + ),
80 ), 116 ),
81 ); 117 );
82 } 118 }
@@ -99,7 +135,13 @@ class _VideoZonePageState extends State&lt;VideoZonePage&gt; { @@ -99,7 +135,13 @@ class _VideoZonePageState extends State&lt;VideoZonePage&gt; {
99 .toList(), 135 .toList(),
100 'initialVideoId': item.id, 136 'initialVideoId': item.id,
101 }, 137 },
102 - ).then((_) => _reload()); 138 + ).then((_) {
  139 + if (!mounted) return;
  140 + unawaited(
  141 + SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky),
  142 + );
  143 + _reload();
  144 + });
103 } 145 }
104 } 146 }
105 147
@@ -140,43 +182,41 @@ class _VideoCard extends StatelessWidget { @@ -140,43 +182,41 @@ class _VideoCard extends StatelessWidget {
140 child: Column( 182 child: Column(
141 crossAxisAlignment: CrossAxisAlignment.start, 183 crossAxisAlignment: CrossAxisAlignment.start,
142 children: [ 184 children: [
143 - Expanded( 185 + AspectRatio(
  186 + aspectRatio: 16 / 9,
144 child: ClipRRect( 187 child: ClipRRect(
145 borderRadius: BorderRadius.circular(8.r), 188 borderRadius: BorderRadius.circular(8.r),
146 - child: Stack(  
147 - fit: StackFit.expand,  
148 - children: [  
149 - CachedNetworkImage(  
150 - imageUrl: item.coverUrl,  
151 - fit: BoxFit.cover,  
152 - placeholder: (_, __) => const ColoredBox(  
153 - color: Color(0xFFF2F2F2),  
154 - child: Center(child: CircularProgressIndicator()),  
155 - ),  
156 - errorWidget: (_, __, ___) => const ColoredBox(  
157 - color: Color(0xFFF2F2F2),  
158 - child: Icon(Icons.broken_image_outlined,  
159 - color: Color(0xFFAAAAAA)),  
160 - ),  
161 - ),  
162 - const Center(  
163 - child: Icon(Icons.play_circle_fill,  
164 - size: 42, color: Colors.white),  
165 - ),  
166 - ], 189 + child: CachedNetworkImage(
  190 + imageUrl: item.coverUrl,
  191 + fit: BoxFit.cover,
  192 + placeholder: (_, __) => const ColoredBox(
  193 + color: Color(0xFFF2F2F2),
  194 + child: Center(child: CircularProgressIndicator()),
  195 + ),
  196 + errorWidget: (_, __, ___) => const ColoredBox(
  197 + color: Color(0xFFF2F2F2),
  198 + child: Icon(Icons.broken_image_outlined,
  199 + color: Color(0xFFAAAAAA)),
  200 + ),
167 ), 201 ),
168 ), 202 ),
169 ), 203 ),
170 SizedBox(height: 5.h), 204 SizedBox(height: 5.h),
171 - Text(  
172 - item.title,  
173 - maxLines: 1,  
174 - overflow: TextOverflow.ellipsis,  
175 - style: TextStyle(  
176 - color: const Color(0xFF222222),  
177 - fontFamily: null,  
178 - fontSize: 17.sp,  
179 - fontWeight: FontWeight.w500, 205 + SizedBox(
  206 + height: 24.h,
  207 + child: Align(
  208 + alignment: Alignment.centerLeft,
  209 + child: Text(
  210 + item.title,
  211 + maxLines: 1,
  212 + overflow: TextOverflow.ellipsis,
  213 + style: TextStyle(
  214 + color: const Color(0xFF222222),
  215 + fontFamily: null,
  216 + fontSize: 17.sp,
  217 + fontWeight: FontWeight.w500,
  218 + ),
  219 + ),
180 ), 220 ),
181 ), 221 ),
182 ], 222 ],