mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-07-05 00:30:19 +08:00
55
lib/common/widgets/sliver/trending_header.dart
Normal file
55
lib/common/widgets/sliver/trending_header.dart
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import 'package:flutter/foundation.dart' show clampDouble;
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/rendering.dart' show RenderSliverToBoxAdapter;
|
||||||
|
|
||||||
|
class TrendingHeader extends SliverToBoxAdapter {
|
||||||
|
const TrendingHeader({
|
||||||
|
super.key,
|
||||||
|
required this.offset,
|
||||||
|
required this.onScrollRatioChanged,
|
||||||
|
required super.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
final double offset;
|
||||||
|
final ValueChanged<double> onScrollRatioChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
RenderSliverToBoxAdapter createRenderObject(BuildContext context) {
|
||||||
|
return RenderTrendingHeader(
|
||||||
|
offset: offset,
|
||||||
|
onScrollRatioChanged: onScrollRatioChanged,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void updateRenderObject(
|
||||||
|
BuildContext context,
|
||||||
|
RenderTrendingHeader renderObject,
|
||||||
|
) {
|
||||||
|
renderObject.offset = offset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RenderTrendingHeader extends RenderSliverToBoxAdapter {
|
||||||
|
RenderTrendingHeader({
|
||||||
|
required this.offset,
|
||||||
|
required this.onScrollRatioChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
double offset;
|
||||||
|
double? _scrollRatio;
|
||||||
|
final ValueChanged<double> onScrollRatioChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void performLayout() {
|
||||||
|
super.performLayout();
|
||||||
|
final scrollOffset = constraints.scrollOffset;
|
||||||
|
final scrollRatio = clampDouble(scrollOffset / offset, 0.0, 1.0);
|
||||||
|
if (_scrollRatio != scrollRatio) {
|
||||||
|
_scrollRatio = scrollRatio;
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
onScrollRatioChanged(scrollRatio);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -262,6 +262,7 @@ abstract final class SearchHttp {
|
|||||||
|
|
||||||
static Future<LoadingState<SearchTrendingData>> searchTrending({
|
static Future<LoadingState<SearchTrendingData>> searchTrending({
|
||||||
int limit = 30,
|
int limit = 30,
|
||||||
|
bool needsTop = false,
|
||||||
}) async {
|
}) async {
|
||||||
final res = await Request().get(
|
final res = await Request().get(
|
||||||
Api.searchTrending,
|
Api.searchTrending,
|
||||||
@@ -270,7 +271,9 @@ abstract final class SearchHttp {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (res.data['code'] == 0) {
|
if (res.data['code'] == 0) {
|
||||||
return Success(SearchTrendingData.fromJson(res.data['data']));
|
return Success(
|
||||||
|
SearchTrendingData.fromJson(res.data['data'], needsTop: needsTop),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return Error(res.data['message']);
|
return Error(res.data['message']);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,23 +2,27 @@ import 'package:PiliPlus/models_new/search/search_rcmd/data.dart';
|
|||||||
import 'package:PiliPlus/models_new/search/search_trending/list.dart';
|
import 'package:PiliPlus/models_new/search/search_trending/list.dart';
|
||||||
|
|
||||||
class SearchTrendingData extends SearchRcmdData {
|
class SearchTrendingData extends SearchRcmdData {
|
||||||
List<SearchTrendingItemModel>? topList;
|
late int topCount;
|
||||||
|
|
||||||
SearchTrendingData({super.list, this.topList});
|
SearchTrendingData.fromJson(
|
||||||
|
Map<String, dynamic> json, {
|
||||||
factory SearchTrendingData.fromJson(Map<String, dynamic> json) =>
|
bool needsTop = false,
|
||||||
SearchTrendingData(
|
}) {
|
||||||
list: (json['list'] as List<dynamic>?)
|
list = (json['list'] as List<dynamic>?)
|
||||||
?.map(
|
?.map((e) => SearchTrendingItemModel.fromJson(e))
|
||||||
(e) =>
|
.toList();
|
||||||
SearchTrendingItemModel.fromJson(e as Map<String, dynamic>),
|
if (needsTop) {
|
||||||
)
|
final topList = (json['top_list'] as List<dynamic>?)
|
||||||
.toList(),
|
?.map((e) => SearchTrendingItemModel.fromJson(e))
|
||||||
topList: (json['top_list'] as List<dynamic>?)
|
.toList();
|
||||||
?.map(
|
topCount = topList?.length ?? 0;
|
||||||
(e) =>
|
if (topList != null && topList.isNotEmpty) {
|
||||||
SearchTrendingItemModel.fromJson(e as Map<String, dynamic>),
|
if (list != null) {
|
||||||
)
|
list!.insertAll(0, topList);
|
||||||
.toList(),
|
} else {
|
||||||
);
|
list = topList;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,14 +16,11 @@ class SearchTrendingController
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
List<SearchTrendingItemModel>? getDataList(SearchTrendingData response) {
|
List<SearchTrendingItemModel>? getDataList(SearchTrendingData response) {
|
||||||
List<SearchTrendingItemModel> topList =
|
topCount = response.topCount;
|
||||||
response.topList ?? <SearchTrendingItemModel>[];
|
return response.list;
|
||||||
topCount = topList.length;
|
|
||||||
return response.list == null ? topList : topList
|
|
||||||
..addAll(response.list ?? []);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<LoadingState<SearchTrendingData>> customGetData() =>
|
Future<LoadingState<SearchTrendingData>> customGetData() =>
|
||||||
SearchHttp.searchTrending();
|
SearchHttp.searchTrending(needsTop: true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:PiliPlus/common/widgets/flutter/list_tile.dart';
|
|||||||
import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart';
|
import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart';
|
||||||
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
|
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
|
||||||
import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart';
|
import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart';
|
||||||
|
import 'package:PiliPlus/common/widgets/sliver/trending_header.dart';
|
||||||
import 'package:PiliPlus/http/loading_state.dart';
|
import 'package:PiliPlus/http/loading_state.dart';
|
||||||
import 'package:PiliPlus/models_new/search/search_trending/list.dart';
|
import 'package:PiliPlus/models_new/search/search_trending/list.dart';
|
||||||
import 'package:PiliPlus/pages/search_trending/controller.dart';
|
import 'package:PiliPlus/pages/search_trending/controller.dart';
|
||||||
@@ -15,7 +16,6 @@ import 'package:PiliPlus/utils/extension/num_ext.dart';
|
|||||||
import 'package:PiliPlus/utils/extension/size_ext.dart';
|
import 'package:PiliPlus/utils/extension/size_ext.dart';
|
||||||
import 'package:PiliPlus/utils/image_utils.dart';
|
import 'package:PiliPlus/utils/image_utils.dart';
|
||||||
import 'package:cached_network_image_ce/cached_network_image.dart';
|
import 'package:cached_network_image_ce/cached_network_image.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart' hide ListTile;
|
import 'package:flutter/material.dart' hide ListTile;
|
||||||
import 'package:flutter/services.dart' show SystemUiOverlayStyle;
|
import 'package:flutter/services.dart' show SystemUiOverlayStyle;
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
@@ -33,28 +33,6 @@ class _SearchTrendingPageState extends State<SearchTrendingPage> {
|
|||||||
late double _offset;
|
late double _offset;
|
||||||
final RxDouble _scrollRatio = 0.0.obs;
|
final RxDouble _scrollRatio = 0.0.obs;
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_controller.scrollController.addListener(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_controller.scrollController.removeListener(listener);
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
void listener() {
|
|
||||||
if (_controller.scrollController.hasClients) {
|
|
||||||
_scrollRatio.value = clampDouble(
|
|
||||||
_controller.scrollController.position.pixels / _offset,
|
|
||||||
0.0,
|
|
||||||
1.0,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
@@ -63,14 +41,13 @@ class _SearchTrendingPageState extends State<SearchTrendingPage> {
|
|||||||
final maxWidth = size.width - padding.horizontal;
|
final maxWidth = size.width - padding.horizontal;
|
||||||
final width = size.isPortrait ? maxWidth : min(640.0, maxWidth * 0.6);
|
final width = size.isPortrait ? maxWidth : min(640.0, maxWidth * 0.6);
|
||||||
final height = width * 528 / 1125;
|
final height = width * 528 / 1125;
|
||||||
_offset = height - 56 - padding.top;
|
_offset = height - kToolbarHeight - padding.top;
|
||||||
listener();
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
extendBody: true,
|
extendBody: true,
|
||||||
extendBodyBehindAppBar: true,
|
extendBodyBehindAppBar: true,
|
||||||
resizeToAvoidBottomInset: false,
|
resizeToAvoidBottomInset: false,
|
||||||
appBar: PreferredSize(
|
appBar: PreferredSize(
|
||||||
preferredSize: const Size.fromHeight(56),
|
preferredSize: const Size.fromHeight(kToolbarHeight),
|
||||||
child: Obx(
|
child: Obx(
|
||||||
() {
|
() {
|
||||||
final scrollRatio = _scrollRatio.value;
|
final scrollRatio = _scrollRatio.value;
|
||||||
@@ -80,9 +57,7 @@ class _SearchTrendingPageState extends State<SearchTrendingPage> {
|
|||||||
opacity: scrollRatio,
|
opacity: scrollRatio,
|
||||||
child: Text(
|
child: Text(
|
||||||
'bilibili热搜',
|
'bilibili热搜',
|
||||||
style: TextStyle(
|
style: TextStyle(color: flag ? null : Colors.white),
|
||||||
color: flag ? null : Colors.white,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
backgroundColor: theme.colorScheme.surface.withValues(
|
backgroundColor: theme.colorScheme.surface.withValues(
|
||||||
@@ -92,15 +67,13 @@ class _SearchTrendingPageState extends State<SearchTrendingPage> {
|
|||||||
systemOverlayStyle: flag
|
systemOverlayStyle: flag
|
||||||
? null
|
? null
|
||||||
: const SystemUiOverlayStyle(
|
: const SystemUiOverlayStyle(
|
||||||
statusBarBrightness: Brightness.dark,
|
statusBarBrightness: .dark,
|
||||||
statusBarIconBrightness: Brightness.light,
|
statusBarIconBrightness: .light,
|
||||||
),
|
),
|
||||||
shape: scrollRatio == 1
|
shape: scrollRatio == 1
|
||||||
? Border(
|
? Border(
|
||||||
bottom: BorderSide(
|
bottom: BorderSide(
|
||||||
color: theme.colorScheme.outline.withValues(
|
color: theme.colorScheme.outline.withValues(alpha: 0.1),
|
||||||
alpha: 0.1,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
@@ -109,10 +82,7 @@ class _SearchTrendingPageState extends State<SearchTrendingPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: Padding(
|
body: Padding(
|
||||||
padding: EdgeInsets.only(
|
padding: .only(left: padding.left, right: padding.right),
|
||||||
left: padding.left,
|
|
||||||
right: padding.right,
|
|
||||||
),
|
|
||||||
child: Center(
|
child: Center(
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: width,
|
width: width,
|
||||||
@@ -122,7 +92,9 @@ class _SearchTrendingPageState extends State<SearchTrendingPage> {
|
|||||||
controller: _controller.scrollController,
|
controller: _controller.scrollController,
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverToBoxAdapter(
|
TrendingHeader(
|
||||||
|
offset: _offset,
|
||||||
|
onScrollRatioChanged: _scrollRatio.call,
|
||||||
child: Image.asset(
|
child: Image.asset(
|
||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
|
|||||||
Reference in New Issue
Block a user