diff --git a/lib/common/constants.dart b/lib/common/constants.dart new file mode 100644 index 00000000..ab9d0178 --- /dev/null +++ b/lib/common/constants.dart @@ -0,0 +1,7 @@ +import 'package:flutter/material.dart'; + +class StyleString { + static const double cardSpace = 8; + static BorderRadius mdRadius = BorderRadius.circular(6); + static const Radius imgRadius = Radius.circular(6); +} diff --git a/lib/pages/home/controller.dart b/lib/pages/home/controller.dart index e69de29b..e8d98bf4 100644 --- a/lib/pages/home/controller.dart +++ b/lib/pages/home/controller.dart @@ -0,0 +1,47 @@ +import 'package:flutter/cupertino.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/http/api.dart'; +import 'package:pilipala/http/init.dart'; + +class HomeController extends GetxController { + final ScrollController scrollController = ScrollController(); + int count = 12; + int _currentPage = 1; + int crossAxisCount = 2; + RxList videoList = [].obs; + bool isLoadingMore = false; + @override + void onInit() { + super.onInit(); + queryRcmdFeed('init'); + } + + // 获取推荐 + Future queryRcmdFeed(type) async { + var res = await Request().get( + Api.recommendList, + data: {'feed_version': "V3", 'ps': count, 'fresh_idx': _currentPage}, + ); + var data = res.data['data']['item']; + if (type == 'init') { + videoList.value = data; + } else if (type == 'onRefresh') { + videoList.insertAll(0, data); + } else if (type == 'onLoad') { + videoList.addAll(data); + } + _currentPage += 1; + isLoadingMore = false; + } + + // 下拉刷新 + Future onRefresh() async { + queryRcmdFeed('onRefresh'); + } + + // 上拉加载 + Future onLoad() async { + await Future.delayed(const Duration(milliseconds: 500)); + queryRcmdFeed('onLoad'); + } +} diff --git a/lib/pages/home/view.dart b/lib/pages/home/view.dart index 18f663a6..56494278 100644 --- a/lib/pages/home/view.dart +++ b/lib/pages/home/view.dart @@ -1,18 +1,151 @@ import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import './controller.dart'; +import 'package:pilipala/common/constants.dart'; +import 'package:pilipala/pages/home/widgets/app_bar.dart'; class HomePage extends StatefulWidget { - const HomePage({super.key}); + const HomePage({Key? key}) : super(key: key); @override State createState() => _HomePageState(); } -class _HomePageState extends State { +class _HomePageState extends State + with AutomaticKeepAliveClientMixin { + final HomeController _homeController = Get.put(HomeController()); + List videoList = []; + + @override + bool get wantKeepAlive => true; + + @override + void initState() { + super.initState(); + _homeController.videoList.listen((value) { + videoList = value; + setState(() {}); + }); + + _homeController.scrollController.addListener( + () { + if (_homeController.scrollController.position.pixels >= + _homeController.scrollController.position.maxScrollExtent - 200) { + if (!_homeController.isLoadingMore) { + _homeController.isLoadingMore = true; + _homeController.onLoad(); + } + } + }, + ); + } + @override Widget build(BuildContext context) { + super.build(context); return Scaffold( - appBar: AppBar( - title: const Text('推荐'), + // body: NestedScrollView( + // headerSliverBuilder: (context, innerBoxIsScrolled) => [ + // const HomeAppBar() + // ], + body: RefreshIndicator( + displacement: kToolbarHeight + MediaQuery.of(context).padding.top, + onRefresh: () async { + return await _homeController.onRefresh(); + }, + child: CustomScrollView( + controller: _homeController.scrollController, + slivers: [ + const HomeAppBar(), + // SliverPersistentHeader( + // delegate: MySliverPersistentHeaderDelegate(), + // pinned: true, + // ), + SliverPadding( + // 单列布局 EdgeInsets.zero + padding: _homeController.crossAxisCount == 1 + ? EdgeInsets.zero + : const EdgeInsets.fromLTRB( + StyleString.cardSpace, 0, StyleString.cardSpace, 8), + sliver: SliverGrid( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + // 行间距 + mainAxisSpacing: StyleString.cardSpace, + // 列间距 + crossAxisSpacing: StyleString.cardSpace, + // 列数 + crossAxisCount: _homeController.crossAxisCount, + mainAxisExtent: MediaQuery.of(context).size.width / + _homeController.crossAxisCount * + (10 / 16) + + 72), + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) { + return Container( + color: Theme.of(context).colorScheme.surfaceVariant, + child: Text(index.toString()), + ); + }, + childCount: videoList.isNotEmpty ? videoList.length : 10, + ), + ), + ), + const LoadingMore() + ], + ), + ), + // ), + ); + } +} + +class MySliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate { + @override + Widget build( + BuildContext context, double shrinkOffset, bool overlapsContent) { + return Container( + alignment: Alignment.center, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + border: Border( + bottom: BorderSide( + color: Theme.of(context).dividerColor.withOpacity(0.1), + ), + )), + child: const Text( + '我是一个SliverPersistentHeader', + ), + ); + } + + @override + double get maxExtent => 55.0; + + @override + double get minExtent => 55.0; + + @override + bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) => + true; // 如果内容需要更新,设置为true +} + +// loading more +class LoadingMore extends StatelessWidget { + const LoadingMore({super.key}); + + @override + Widget build(BuildContext context) { + return SliverToBoxAdapter( + child: Container( + height: MediaQuery.of(context).padding.bottom + 80, + padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom), + child: Center( + child: Text( + '加载中...', + style: TextStyle( + color: Theme.of(context).colorScheme.outline, fontSize: 13), + ), + ), ), ); } diff --git a/lib/pages/home/widgets/app_bar.dart b/lib/pages/home/widgets/app_bar.dart new file mode 100644 index 00000000..56a53861 --- /dev/null +++ b/lib/pages/home/widgets/app_bar.dart @@ -0,0 +1,58 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; + +class HomeAppBar extends StatelessWidget { + const HomeAppBar({super.key}); + + @override + Widget build(BuildContext context) { + return SliverAppBar( + // forceElevated: true, + scrolledUnderElevation: 0, + toolbarHeight: Platform.isAndroid + ? (MediaQuery.of(context).padding.top + 6) + : Platform.isIOS + ? MediaQuery.of(context).padding.top - 2 + : kToolbarHeight, + expandedHeight: kToolbarHeight + MediaQuery.of(context).padding.top, + automaticallyImplyLeading: false, + pinned: true, + floating: true, + primary: false, + flexibleSpace: LayoutBuilder( + builder: (context, constraints) { + return FlexibleSpaceBar( + background: Column( + children: [ + AppBar( + centerTitle: false, + title: const Text( + 'PiLiPaLa', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + letterSpacing: 1, + ), + ), + actions: [ + IconButton( + onPressed: () {}, + icon: const Icon(Icons.notifications_none_rounded), + ), + IconButton( + onPressed: () {}, + icon: const Icon(Icons.search_rounded), + ), + const SizedBox(width: 10) + ], + elevation: 0, + scrolledUnderElevation: 0, + ), + ], + ), + ); + }, + ), + ); + } +} diff --git a/lib/pages/main/controller.dart b/lib/pages/main/controller.dart index e69de29b..f7ef9e49 100644 --- a/lib/pages/main/controller.dart +++ b/lib/pages/main/controller.dart @@ -0,0 +1,30 @@ +import 'package:get/get.dart'; +import 'package:flutter/material.dart'; +import 'package:pilipala/pages/home/view.dart'; +import 'package:pilipala/pages/hot/view.dart'; +import 'package:pilipala/pages/mine/view.dart'; + +class MainController extends GetxController { + List pages = [ + const HomePage(), + const HotPage(), + const MinePage(), + ]; + List navigationBars = [ + { + 'icon': const Icon(Icons.home_outlined), + 'selectedIcon': const Icon(Icons.home), + 'label': "推荐", + }, + { + 'icon': const Icon(Icons.whatshot_outlined), + 'selectedIcon': const Icon(Icons.whatshot_rounded), + 'label': "热门", + }, + { + 'icon': const Icon(Icons.person_outline), + 'selectedIcon': const Icon(Icons.person), + 'label': "我的", + } + ]; +} diff --git a/lib/pages/main/view.dart b/lib/pages/main/view.dart index 5306bf5c..1aef1356 100644 --- a/lib/pages/main/view.dart +++ b/lib/pages/main/view.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:pilipala/pages/home/view.dart'; -import 'package:pilipala/pages/hot/view.dart'; -import 'package:pilipala/pages/mine/view.dart'; +import 'package:get/get.dart'; +import './controller.dart'; class MainApp extends StatefulWidget { const MainApp({super.key}); @@ -10,12 +9,35 @@ class MainApp extends StatefulWidget { State createState() => _MainAppState(); } -class _MainAppState extends State { +class _MainAppState extends State with SingleTickerProviderStateMixin { + final MainController _mainController = Get.put(MainController()); + late AnimationController? _animationController; + late Animation? _fadeAnimation; + late Animation? _slideAnimation; int selectedIndex = 0; + @override + void initState() { + super.initState(); + _animationController = AnimationController( + duration: const Duration(milliseconds: 800), + reverseDuration: const Duration(milliseconds: 0), + value: 1, + vsync: this, + ); + _fadeAnimation = + Tween(begin: 0.8, end: 1.0).animate(_animationController!); + _slideAnimation = + Tween(begin: 0.8, end: 1.0).animate(_animationController!); + } + void setIndex(int value) { if (selectedIndex != value) { selectedIndex = value; + _animationController!.reverse().then((_) { + selectedIndex = value; + _animationController!.forward(); + }); setState(() {}); } } @@ -23,33 +45,34 @@ class _MainAppState extends State { @override Widget build(BuildContext context) { return Scaffold( - body: IndexedStack( - index: selectedIndex, - children: const [ - HomePage(), - HotPage(), - MinePage(), - ], + body: FadeTransition( + opacity: _fadeAnimation!, + child: SlideTransition( + position: Tween( + begin: const Offset(0, 0.5), + end: Offset.zero, + ).animate( + CurvedAnimation( + parent: _slideAnimation!, + curve: Curves.fastOutSlowIn, + reverseCurve: Curves.linear, + ), + ), + child: IndexedStack( + index: selectedIndex, + children: _mainController.pages, + ), + ), ), bottomNavigationBar: NavigationBar( elevation: 1, - destinations: const [ - NavigationDestination( - icon: Icon(Icons.home_outlined), - selectedIcon: Icon(Icons.home), - label: "推荐", - ), - NavigationDestination( - icon: Icon(Icons.whatshot_outlined), - selectedIcon: Icon(Icons.whatshot_rounded), - label: "热门", - ), - NavigationDestination( - icon: Icon(Icons.person_outline), - label: "我的", - selectedIcon: Icon(Icons.person), - ), - ], + destinations: _mainController.navigationBars.map((e) { + return NavigationDestination( + icon: e['icon'], + selectedIcon: e['selectedIcon'], + label: e['label'], + ); + }).toList(), selectedIndex: selectedIndex, onDestinationSelected: (value) => setIndex(value), ),