mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-07-08 10:10:13 +08:00
feat: sliver wrap (#1858)
* feat: sliver wrap * opt: list * update --------- Co-authored-by: dom <githubaccount56556@proton.me>
This commit is contained in:
committed by
GitHub
parent
01a74e191a
commit
c01318c066
@@ -1,8 +1,8 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:PiliPlus/common/widgets/disabled_icon.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart';
|
||||
import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart';
|
||||
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
|
||||
import 'package:PiliPlus/common/widgets/sliver_wrap.dart';
|
||||
import 'package:PiliPlus/http/loading_state.dart';
|
||||
import 'package:PiliPlus/models_new/search/search_rcmd/data.dart';
|
||||
import 'package:PiliPlus/pages/about/view.dart' show showImportExportDialog;
|
||||
@@ -27,6 +27,9 @@ class SearchPage extends StatefulWidget {
|
||||
class _SearchPageState extends State<SearchPage> {
|
||||
final _tag = Utils.generateRandomString(6);
|
||||
late final SSearchController _searchController;
|
||||
late ThemeData theme;
|
||||
late bool isPortrait;
|
||||
late EdgeInsets padding;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -37,109 +40,111 @@ class _SearchPageState extends State<SearchPage> {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
theme = Theme.of(context);
|
||||
padding = MediaQuery.viewPaddingOf(context);
|
||||
isPortrait = MediaQuery.sizeOf(context).isPortrait;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final isPortrait = MediaQuery.sizeOf(context).isPortrait;
|
||||
final trending = _searchController.enableTrending
|
||||
? _buildHotSearch()
|
||||
: null;
|
||||
final rcmd = _searchController.enableSearchRcmd
|
||||
? _buildHotSearch(isTrending: false)
|
||||
: null;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
shape: Border(
|
||||
bottom: BorderSide(
|
||||
color: theme.dividerColor.withValues(alpha: 0.08),
|
||||
width: 1,
|
||||
),
|
||||
appBar: _buildAppBar,
|
||||
body: Padding(
|
||||
padding: .only(left: padding.left, right: padding.right),
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
if (_searchController.searchSuggestion) _buildSearchSuggest(),
|
||||
if (isPortrait) ...[
|
||||
?trending,
|
||||
_buildHistory,
|
||||
?rcmd,
|
||||
] else if (_searchController.enableTrending ||
|
||||
_searchController.enableSearchRcmd)
|
||||
SliverCrossAxisGroup(
|
||||
slivers: [
|
||||
SliverMainAxisGroup(slivers: [?trending, ?rcmd]),
|
||||
_buildHistory,
|
||||
],
|
||||
)
|
||||
else
|
||||
_buildHistory,
|
||||
SliverPadding(padding: .only(bottom: padding.bottom)),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
Obx(
|
||||
() => _searchController.showUidBtn.value
|
||||
? IconButton(
|
||||
tooltip: 'UID搜索用户',
|
||||
icon: const Icon(Icons.person_outline, size: 22),
|
||||
onPressed: () => Get.toNamed(
|
||||
'/member?mid=${_searchController.controller.text}',
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: '清空',
|
||||
icon: const Icon(Icons.clear, size: 22),
|
||||
onPressed: _searchController.onClear,
|
||||
),
|
||||
IconButton(
|
||||
tooltip: '搜索',
|
||||
onPressed: _searchController.submit,
|
||||
icon: const Icon(Icons.search, size: 22),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
],
|
||||
title: TextField(
|
||||
autofocus: true,
|
||||
focusNode: _searchController.searchFocusNode,
|
||||
controller: _searchController.controller,
|
||||
textInputAction: TextInputAction.search,
|
||||
onChanged: _searchController.onChange,
|
||||
decoration: InputDecoration(
|
||||
visualDensity: .standard,
|
||||
hintText: _searchController.hintText ?? '搜索',
|
||||
border: InputBorder.none,
|
||||
),
|
||||
onSubmitted: (value) => _searchController.submit(),
|
||||
),
|
||||
),
|
||||
body: ListView(
|
||||
padding: MediaQuery.viewPaddingOf(context).copyWith(top: 0),
|
||||
children: [
|
||||
if (_searchController.searchSuggestion) _searchSuggest(),
|
||||
if (isPortrait) ...[
|
||||
if (_searchController.enableTrending) hotSearch(theme, isPortrait),
|
||||
_history(theme, isPortrait),
|
||||
if (_searchController.enableSearchRcmd)
|
||||
hotSearch(theme, isPortrait, isTrending: false),
|
||||
] else
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (_searchController.enableTrending ||
|
||||
_searchController.enableSearchRcmd)
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
if (_searchController.enableTrending)
|
||||
hotSearch(theme, isPortrait),
|
||||
if (_searchController.enableSearchRcmd)
|
||||
hotSearch(theme, isPortrait, isTrending: false),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(child: _history(theme, isPortrait)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _searchSuggest() {
|
||||
return Obx(
|
||||
() =>
|
||||
_searchController.searchSuggestList.isNotEmpty &&
|
||||
_searchController.searchSuggestList.first.term != null &&
|
||||
PreferredSizeWidget get _buildAppBar => AppBar(
|
||||
shape: Border(
|
||||
bottom: BorderSide(
|
||||
color: theme.dividerColor.withValues(alpha: 0.08),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
Obx(
|
||||
() => _searchController.showUidBtn.value
|
||||
? IconButton(
|
||||
tooltip: 'UID搜索用户',
|
||||
icon: const Icon(Icons.person_outline, size: 22),
|
||||
onPressed: () => Get.toNamed(
|
||||
'/member?mid=${_searchController.controller.text}',
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: '清空',
|
||||
icon: const Icon(Icons.clear, size: 22),
|
||||
onPressed: _searchController.onClear,
|
||||
),
|
||||
IconButton(
|
||||
tooltip: '搜索',
|
||||
onPressed: _searchController.submit,
|
||||
icon: const Icon(Icons.search, size: 22),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
],
|
||||
title: TextField(
|
||||
autofocus: true,
|
||||
focusNode: _searchController.searchFocusNode,
|
||||
controller: _searchController.controller,
|
||||
textInputAction: TextInputAction.search,
|
||||
onChanged: _searchController.onChange,
|
||||
decoration: InputDecoration(
|
||||
visualDensity: .standard,
|
||||
hintText: _searchController.hintText ?? '搜索',
|
||||
border: InputBorder.none,
|
||||
),
|
||||
onSubmitted: (value) => _searchController.submit(),
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildSearchSuggest() {
|
||||
return Obx(() {
|
||||
final list = _searchController.searchSuggestList;
|
||||
return list.isNotEmpty &&
|
||||
list.first.term != null &&
|
||||
_searchController.controller.text != ''
|
||||
? Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: _searchController.searchSuggestList
|
||||
? SliverList.list(
|
||||
children: list
|
||||
.map(
|
||||
(item) => InkWell(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
||||
borderRadius: const .all(.circular(4)),
|
||||
onTap: () => _searchController.onClickKeyword(item.term!),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 20,
|
||||
top: 9,
|
||||
bottom: 9,
|
||||
),
|
||||
padding: const .only(left: 20, top: 9, bottom: 9),
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
children: Em.regTitle(item.textRich)
|
||||
@@ -164,11 +169,13 @@ class _SearchPageState extends State<SearchPage> {
|
||||
)
|
||||
.toList(),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
);
|
||||
: const SliverToBoxAdapter();
|
||||
});
|
||||
}
|
||||
|
||||
Widget hotSearch(ThemeData theme, bool isPortrait, {bool isTrending = true}) {
|
||||
Widget _buildHotSearch({
|
||||
bool isTrending = true,
|
||||
}) {
|
||||
final text = Text(
|
||||
isTrending ? '大家都在搜' : '搜索发现',
|
||||
strutStyle: const StrutStyle(leading: 0, height: 1),
|
||||
@@ -184,7 +191,7 @@ class _SearchPageState extends State<SearchPage> {
|
||||
fontSize: 13,
|
||||
color: outline,
|
||||
);
|
||||
return Padding(
|
||||
return SliverPadding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
10,
|
||||
!isTrending && (isPortrait || _searchController.enableTrending)
|
||||
@@ -193,23 +200,28 @@ class _SearchPageState extends State<SearchPage> {
|
||||
4,
|
||||
25,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
sliver: SliverMainAxisGroup(
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.fromLTRB(6, 0, 6, 6),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
isTrending
|
||||
? Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
text,
|
||||
const SizedBox(width: 14),
|
||||
SizedBox(
|
||||
height: 34,
|
||||
child: TextButton(
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
isTrending
|
||||
? Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
text,
|
||||
const SizedBox(width: 14),
|
||||
TextButton(
|
||||
style: const ButtonStyle(
|
||||
visualDensity: .compact,
|
||||
tapTargetSize: .shrinkWrap,
|
||||
padding: WidgetStatePropertyAll(
|
||||
.symmetric(horizontal: 10),
|
||||
),
|
||||
),
|
||||
onPressed: () => Get.toNamed('/searchTrending'),
|
||||
child: Row(
|
||||
children: [
|
||||
@@ -229,16 +241,15 @@ class _SearchPageState extends State<SearchPage> {
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: text,
|
||||
SizedBox(
|
||||
height: 34,
|
||||
child: TextButton.icon(
|
||||
],
|
||||
)
|
||||
: text,
|
||||
TextButton.icon(
|
||||
style: const ButtonStyle(
|
||||
visualDensity: .compact,
|
||||
tapTargetSize: .shrinkWrap,
|
||||
padding: WidgetStatePropertyAll(
|
||||
EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
.symmetric(horizontal: 10),
|
||||
),
|
||||
),
|
||||
onPressed: isTrending
|
||||
@@ -258,8 +269,8 @@ class _SearchPageState extends State<SearchPage> {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Obx(
|
||||
@@ -275,14 +286,16 @@ class _SearchPageState extends State<SearchPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _history(ThemeData theme, bool isPortrait) {
|
||||
late final mainAxisExtent = 16 + MediaQuery.textScalerOf(context).scale(14);
|
||||
Widget get _buildHistory {
|
||||
return Obx(
|
||||
() {
|
||||
if (_searchController.historyList.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
final list = _searchController.historyList;
|
||||
if (list.isEmpty) {
|
||||
return const SliverToBoxAdapter();
|
||||
}
|
||||
final secondary = theme.colorScheme.secondary;
|
||||
return Padding(
|
||||
return SliverPadding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
10,
|
||||
!isPortrait
|
||||
@@ -293,67 +306,31 @@ class _SearchPageState extends State<SearchPage> {
|
||||
6,
|
||||
25,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
sliver: SliverMainAxisGroup(
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.fromLTRB(6, 0, 6, 6),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'搜索历史',
|
||||
strutStyle: const StrutStyle(leading: 0, height: 1),
|
||||
style: theme.textTheme.titleMedium!.copyWith(
|
||||
height: 1,
|
||||
fontWeight: FontWeight.bold,
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'搜索历史',
|
||||
strutStyle: const StrutStyle(leading: 0, height: 1),
|
||||
style: theme.textTheme.titleMedium!.copyWith(
|
||||
height: 1,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Obx(
|
||||
() {
|
||||
bool enable =
|
||||
_searchController.recordSearchHistory.value;
|
||||
return SizedBox(
|
||||
width: 34,
|
||||
height: 34,
|
||||
child: IconButton(
|
||||
iconSize: 22,
|
||||
tooltip: enable ? '记录搜索' : '无痕搜索',
|
||||
icon: DisabledIcon(
|
||||
disable: !enable,
|
||||
child: Icon(
|
||||
Icons.history,
|
||||
color: theme.colorScheme.onSurfaceVariant
|
||||
.withValues(alpha: 0.8),
|
||||
),
|
||||
),
|
||||
style: IconButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
onPressed: () {
|
||||
enable = !enable;
|
||||
_searchController.recordSearchHistory.value =
|
||||
enable;
|
||||
GStorage.setting.put(
|
||||
SettingBoxKey.recordSearchHistory,
|
||||
enable,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
_exportHistory(theme),
|
||||
const Spacer(),
|
||||
SizedBox(
|
||||
height: 34,
|
||||
child: TextButton.icon(
|
||||
const SizedBox(width: 12),
|
||||
_recordBtn,
|
||||
_exportBtn,
|
||||
const Spacer(),
|
||||
TextButton.icon(
|
||||
style: const ButtonStyle(
|
||||
visualDensity: .compact,
|
||||
tapTargetSize: .shrinkWrap,
|
||||
padding: WidgetStatePropertyAll(
|
||||
EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 6,
|
||||
),
|
||||
.symmetric(horizontal: 10),
|
||||
),
|
||||
),
|
||||
onPressed: _searchController.onClearHistory,
|
||||
@@ -364,27 +341,36 @@ class _SearchPageState extends State<SearchPage> {
|
||||
),
|
||||
label: Text(
|
||||
'清空',
|
||||
style: TextStyle(color: secondary),
|
||||
style: TextStyle(
|
||||
height: 1,
|
||||
color: secondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Wrap(
|
||||
SliverFixedWrap(
|
||||
mainAxisExtent: mainAxisExtent,
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
direction: Axis.horizontal,
|
||||
textDirection: TextDirection.ltr,
|
||||
children: _searchController.historyList
|
||||
.map(
|
||||
(item) => SearchText(
|
||||
text: item,
|
||||
onTap: _searchController.onClickKeyword,
|
||||
onLongPress: _searchController.onLongSelect,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
addAutomaticKeepAlives: false,
|
||||
addRepaintBoundaries: false,
|
||||
childCount: list.length,
|
||||
(context, index) => SearchText(
|
||||
text: list[index],
|
||||
onTap: _searchController.onClickKeyword,
|
||||
onLongPress: _searchController.onLongSelect,
|
||||
fontSize: 14,
|
||||
height: 1,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 11,
|
||||
vertical: 8,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -393,28 +379,58 @@ class _SearchPageState extends State<SearchPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _exportHistory(ThemeData theme) => SizedBox(
|
||||
width: 34,
|
||||
height: 34,
|
||||
child: IconButton(
|
||||
iconSize: 22,
|
||||
tooltip: '导入/导出历史记录',
|
||||
icon: Icon(
|
||||
Icons.import_export_outlined,
|
||||
color: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.8),
|
||||
),
|
||||
style: IconButton.styleFrom(padding: EdgeInsets.zero),
|
||||
onPressed: () => showImportExportDialog<List>(
|
||||
context,
|
||||
title: '历史记录',
|
||||
toJson: () => jsonEncode(_searchController.historyList),
|
||||
fromJson: (json) {
|
||||
final list = List<String>.from(json);
|
||||
_searchController.historyList.value = list;
|
||||
GStorage.historyWord.put('cacheList', list);
|
||||
return true;
|
||||
Widget get _recordBtn => Obx(
|
||||
() {
|
||||
bool enable = _searchController.recordSearchHistory.value;
|
||||
return IconButton(
|
||||
iconSize: 22,
|
||||
tooltip: enable ? '记录搜索' : '无痕搜索',
|
||||
icon: DisabledIcon(
|
||||
disable: !enable,
|
||||
child: Icon(
|
||||
Icons.history,
|
||||
color: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.8),
|
||||
),
|
||||
),
|
||||
style: const ButtonStyle(
|
||||
visualDensity: .comfortable,
|
||||
tapTargetSize: .shrinkWrap,
|
||||
padding: WidgetStatePropertyAll(.zero),
|
||||
),
|
||||
onPressed: () {
|
||||
enable = !enable;
|
||||
_searchController.recordSearchHistory.value = enable;
|
||||
GStorage.setting.put(
|
||||
SettingBoxKey.recordSearchHistory,
|
||||
enable,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Widget get _exportBtn => IconButton(
|
||||
iconSize: 22,
|
||||
tooltip: '导入/导出历史记录',
|
||||
icon: Icon(
|
||||
Icons.import_export_outlined,
|
||||
color: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.8),
|
||||
),
|
||||
style: const ButtonStyle(
|
||||
visualDensity: .comfortable,
|
||||
tapTargetSize: .shrinkWrap,
|
||||
padding: WidgetStatePropertyAll(.zero),
|
||||
),
|
||||
onPressed: () => showImportExportDialog<List>(
|
||||
context,
|
||||
title: '历史记录',
|
||||
toJson: () => jsonEncode(_searchController.historyList),
|
||||
fromJson: (json) {
|
||||
final list = List<String>.from(json);
|
||||
_searchController.historyList.value = list;
|
||||
GStorage.historyWord.put('cacheList', list);
|
||||
return true;
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@@ -423,23 +439,19 @@ class _SearchPageState extends State<SearchPage> {
|
||||
bool isTrending,
|
||||
) {
|
||||
return switch (loadingState) {
|
||||
Success(:final response) =>
|
||||
response.list?.isNotEmpty == true
|
||||
? LayoutBuilder(
|
||||
builder: (context, constraints) => HotKeyword(
|
||||
width: constraints.maxWidth,
|
||||
hotSearchList: response.list!,
|
||||
onClick: _searchController.onClickKeyword,
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
Error(:final errMsg) => errorWidget(
|
||||
Success(:final response) when (response.list?.isNotEmpty ?? false) =>
|
||||
SliverHotKeyword(
|
||||
hotSearchList: response.list!,
|
||||
onClick: _searchController.onClickKeyword,
|
||||
),
|
||||
Error(:final errMsg) => HttpError(
|
||||
safeArea: false,
|
||||
errMsg: errMsg,
|
||||
onReload: isTrending
|
||||
? _searchController.queryTrendingList
|
||||
: _searchController.queryRecommendList,
|
||||
),
|
||||
_ => const SizedBox.shrink(),
|
||||
_ => const SliverToBoxAdapter(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,87 +1,215 @@
|
||||
import 'package:PiliPlus/models_new/search/search_trending/list.dart';
|
||||
import 'package:PiliPlus/utils/extension/num_ext.dart';
|
||||
import 'package:PiliPlus/utils/extension/string_ext.dart';
|
||||
import 'package:PiliPlus/utils/image_utils.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart'
|
||||
show
|
||||
ContainerRenderObjectMixin,
|
||||
MultiChildLayoutParentData,
|
||||
RenderBoxContainerDefaultsMixin,
|
||||
BoxHitTestResult;
|
||||
|
||||
class HotKeyword extends StatelessWidget {
|
||||
final double width;
|
||||
class SliverHotKeyword extends StatelessWidget {
|
||||
final List<SearchTrendingItemModel> hotSearchList;
|
||||
final Function? onClick;
|
||||
const HotKeyword({
|
||||
const SliverHotKeyword({
|
||||
super.key,
|
||||
required double width,
|
||||
required this.hotSearchList,
|
||||
this.onClick,
|
||||
}) : width = width / 2 - 4;
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
late final style = TextStyle(
|
||||
fontSize: 14,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
color: ColorScheme.of(context).outline,
|
||||
);
|
||||
return Wrap(
|
||||
runSpacing: 0.4,
|
||||
spacing: 5.0,
|
||||
children: [
|
||||
for (final i in hotSearchList)
|
||||
SizedBox(
|
||||
width: width,
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(3)),
|
||||
child: InkWell(
|
||||
|
||||
late final cacheHeight = (MediaQuery.devicePixelRatioOf(context) * 15.0)
|
||||
.round();
|
||||
|
||||
return SliverToBoxAdapter(
|
||||
child: _HotKeywordGrid(
|
||||
mainAxisSpacing: 5,
|
||||
crossAxisSpacing: 0.4,
|
||||
crossAxisCount: 2,
|
||||
children: hotSearchList
|
||||
.map(
|
||||
(i) => Material(
|
||||
type: MaterialType.transparency,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(3)),
|
||||
onTap: () => onClick?.call(i.keyword),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 2, right: 10),
|
||||
child: Tooltip(
|
||||
message: i.keyword,
|
||||
child: Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(6, 5, 0, 5),
|
||||
child: Text(
|
||||
i.keyword!,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
style: const TextStyle(fontSize: 14),
|
||||
child: InkWell(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(3)),
|
||||
onTap: () => onClick?.call(i.keyword),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 2, right: 10),
|
||||
child: Tooltip(
|
||||
message: i.keyword,
|
||||
child: Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(6, 5, 0, 5),
|
||||
child: Text(
|
||||
i.keyword!,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!i.icon.isNullOrEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 4),
|
||||
child: CachedNetworkImage(
|
||||
height: 15,
|
||||
memCacheHeight: 15.cacheSize(context),
|
||||
imageUrl: ImageUtils.thumbnailUrl(i.icon!),
|
||||
placeholder: (_, _) => const SizedBox.shrink(),
|
||||
),
|
||||
)
|
||||
else if (i.showLiveIcon == true)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 4),
|
||||
child: Image.asset(
|
||||
'assets/images/live/live.gif',
|
||||
width: 48,
|
||||
height: 15,
|
||||
cacheHeight: 15.cacheSize(context),
|
||||
),
|
||||
)
|
||||
else if (i.recommendReason?.isNotEmpty == true)
|
||||
Text(i.recommendReason!, style: style),
|
||||
],
|
||||
if (!i.icon.isNullOrEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 4),
|
||||
child: CachedNetworkImage(
|
||||
height: 15,
|
||||
memCacheHeight: cacheHeight,
|
||||
imageUrl: ImageUtils.thumbnailUrl(i.icon!),
|
||||
placeholder: (_, _) => const SizedBox.shrink(),
|
||||
),
|
||||
)
|
||||
else if (i.showLiveIcon == true)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 4),
|
||||
child: Image.asset(
|
||||
'assets/images/live/live.gif',
|
||||
width: 48,
|
||||
height: 15,
|
||||
cacheHeight: cacheHeight,
|
||||
),
|
||||
)
|
||||
else if (i.recommendReason?.isNotEmpty == true)
|
||||
Text(i.recommendReason!, style: style),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _HotKeywordGrid extends MultiChildRenderObjectWidget {
|
||||
const _HotKeywordGrid({
|
||||
required this.crossAxisCount,
|
||||
this.mainAxisSpacing = 0.0,
|
||||
this.crossAxisSpacing = 0.0,
|
||||
required super.children,
|
||||
}) : assert(crossAxisCount > 0),
|
||||
assert(mainAxisSpacing >= 0.0),
|
||||
assert(crossAxisSpacing >= 0.0);
|
||||
|
||||
final int crossAxisCount;
|
||||
final double mainAxisSpacing;
|
||||
final double crossAxisSpacing;
|
||||
|
||||
@override
|
||||
RenderObject createRenderObject(BuildContext context) {
|
||||
return _RenderHotKeywordGrid(
|
||||
crossAxisCount: crossAxisCount,
|
||||
mainAxisSpacing: mainAxisSpacing,
|
||||
crossAxisSpacing: crossAxisSpacing,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(
|
||||
BuildContext context,
|
||||
_RenderHotKeywordGrid renderObject,
|
||||
) {
|
||||
renderObject
|
||||
..crossAxisCount = crossAxisCount
|
||||
..mainAxisSpacing = mainAxisSpacing
|
||||
..crossAxisSpacing = crossAxisSpacing;
|
||||
}
|
||||
}
|
||||
|
||||
class _RenderHotKeywordGrid extends RenderBox
|
||||
with
|
||||
ContainerRenderObjectMixin<RenderBox, MultiChildLayoutParentData>,
|
||||
RenderBoxContainerDefaultsMixin<RenderBox, MultiChildLayoutParentData> {
|
||||
_RenderHotKeywordGrid({
|
||||
required int crossAxisCount,
|
||||
required double mainAxisSpacing,
|
||||
required double crossAxisSpacing,
|
||||
}) : _crossAxisCount = crossAxisCount,
|
||||
_mainAxisSpacing = mainAxisSpacing,
|
||||
_crossAxisSpacing = crossAxisSpacing;
|
||||
|
||||
int _crossAxisCount;
|
||||
int get crossAxisCount => _crossAxisCount;
|
||||
set crossAxisCount(int value) {
|
||||
if (_crossAxisCount == value) return;
|
||||
_crossAxisCount = value;
|
||||
markNeedsLayout();
|
||||
}
|
||||
|
||||
double _mainAxisSpacing;
|
||||
double get mainAxisSpacing => _mainAxisSpacing;
|
||||
set mainAxisSpacing(double value) {
|
||||
if (_mainAxisSpacing == value) return;
|
||||
_mainAxisSpacing = value;
|
||||
markNeedsLayout();
|
||||
}
|
||||
|
||||
double _crossAxisSpacing;
|
||||
double get crossAxisSpacing => _crossAxisSpacing;
|
||||
set crossAxisSpacing(double value) {
|
||||
if (_crossAxisSpacing == value) return;
|
||||
_crossAxisSpacing = value;
|
||||
markNeedsLayout();
|
||||
}
|
||||
|
||||
@override
|
||||
void setupParentData(RenderBox child) {
|
||||
if (child.parentData is! MultiChildLayoutParentData) {
|
||||
child.parentData = MultiChildLayoutParentData();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void performLayout() {
|
||||
final constraints = this.constraints;
|
||||
final childWidth =
|
||||
(constraints.maxWidth - mainAxisSpacing * (crossAxisCount - 1)) /
|
||||
crossAxisCount;
|
||||
final c = BoxConstraints(maxWidth: childWidth);
|
||||
var child = firstChild;
|
||||
double? childHeight;
|
||||
int index = 0;
|
||||
while (child != null) {
|
||||
if (childHeight == null) {
|
||||
childHeight = (child..layout(c, parentUsesSize: true)).size.height;
|
||||
} else {
|
||||
child.layout(c);
|
||||
}
|
||||
final parentData = child.parentData as MultiChildLayoutParentData
|
||||
..offset = Offset(
|
||||
(childWidth + mainAxisSpacing) * (index % crossAxisCount),
|
||||
(childHeight + crossAxisSpacing) * (index ~/ crossAxisCount),
|
||||
);
|
||||
child = parentData.nextSibling;
|
||||
index++;
|
||||
}
|
||||
final row = (index / crossAxisCount).ceil();
|
||||
size = constraints.constrainDimensions(
|
||||
constraints.maxWidth,
|
||||
row * childHeight! + crossAxisSpacing * (row - 1),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
defaultPaint(context, offset);
|
||||
}
|
||||
|
||||
@override
|
||||
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
|
||||
return defaultHitTestChildren(result, position: position);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ class SearchText extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
late final colorScheme = Theme.of(context).colorScheme;
|
||||
late final colorScheme = ColorScheme.of(context);
|
||||
final hasLongPress = onLongPress != null;
|
||||
return Material(
|
||||
color: bgColor ?? colorScheme.onInverseSurface,
|
||||
|
||||
Reference in New Issue
Block a user