Commit 9475aa49a010750d35b003e2d1125ef430c47dcd
1 parent
7e98b732
feat: 优化视频专区列表布局与沉浸体验
Showing
1 changed file
with
118 additions
and
78 deletions
lib/pages/video/zone/video_zone_page.dart
| 1 | +import 'dart:async'; | |
| 2 | + | |
| 1 | 3 | import 'package:cached_network_image/cached_network_image.dart'; |
| 2 | 4 | import 'package:flutter/material.dart'; |
| 5 | +import 'package:flutter/services.dart'; | |
| 3 | 6 | import 'package:flutter_screenutil/flutter_screenutil.dart'; |
| 4 | 7 | import 'package:wow_english/common/dialogs/show_dialog.dart'; |
| 5 | 8 | import 'package:wow_english/common/request/dao/video_dao.dart'; |
| ... | ... | @@ -21,62 +24,95 @@ class _VideoZonePageState extends State<VideoZonePage> { |
| 21 | 24 | @override |
| 22 | 25 | void initState() { |
| 23 | 26 | super.initState(); |
| 27 | + unawaited( | |
| 28 | + SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky), | |
| 29 | + ); | |
| 24 | 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 | 41 | void _reload() { |
| 28 | - setState(() => _itemsFuture = VideoDao.list()); | |
| 42 | + setState(() { | |
| 43 | + _itemsFuture = VideoDao.list(); | |
| 44 | + }); | |
| 29 | 45 | } |
| 30 | 46 | |
| 31 | 47 | @override |
| 32 | 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<VideoZonePage> { |
| 99 | 135 | .toList(), |
| 100 | 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 | 182 | child: Column( |
| 141 | 183 | crossAxisAlignment: CrossAxisAlignment.start, |
| 142 | 184 | children: [ |
| 143 | - Expanded( | |
| 185 | + AspectRatio( | |
| 186 | + aspectRatio: 16 / 9, | |
| 144 | 187 | child: ClipRRect( |
| 145 | 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 | 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 | ], | ... | ... |