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 | 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<VideoZonePage> { | @@ -99,7 +135,13 @@ class _VideoZonePageState extends State<VideoZonePage> { | ||
| 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 | ], |