mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-06-01 00:28:18 +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
@@ -3,17 +3,19 @@ import 'package:flutter_svg/flutter_svg.dart';
|
|||||||
|
|
||||||
class HttpError extends StatelessWidget {
|
class HttpError extends StatelessWidget {
|
||||||
const HttpError({
|
const HttpError({
|
||||||
|
super.key,
|
||||||
this.isSliver = true,
|
this.isSliver = true,
|
||||||
this.errMsg,
|
this.errMsg,
|
||||||
this.onReload,
|
this.onReload,
|
||||||
this.btnText,
|
this.btnText,
|
||||||
super.key,
|
this.safeArea = true,
|
||||||
});
|
});
|
||||||
|
|
||||||
final bool isSliver;
|
final bool isSliver;
|
||||||
final String? errMsg;
|
final String? errMsg;
|
||||||
final VoidCallback? onReload;
|
final VoidCallback? onReload;
|
||||||
final String? btnText;
|
final String? btnText;
|
||||||
|
final bool safeArea;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -57,6 +59,7 @@ class HttpError extends StatelessWidget {
|
|||||||
style: TextStyle(color: theme.colorScheme.primary),
|
style: TextStyle(color: theme.colorScheme.primary),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (safeArea)
|
||||||
SizedBox(height: 40 + MediaQuery.viewPaddingOf(context).bottom),
|
SizedBox(height: 40 + MediaQuery.viewPaddingOf(context).bottom),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
352
lib/common/widgets/sliver_wrap.dart
Normal file
352
lib/common/widgets/sliver_wrap.dart
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
class SliverFixedWrap extends SliverMultiBoxAdaptorWidget {
|
||||||
|
final double mainAxisExtent;
|
||||||
|
final double spacing;
|
||||||
|
final double runSpacing;
|
||||||
|
|
||||||
|
const SliverFixedWrap({
|
||||||
|
super.key,
|
||||||
|
required super.delegate,
|
||||||
|
required this.mainAxisExtent,
|
||||||
|
this.spacing = 0,
|
||||||
|
this.runSpacing = 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
SliverWrapElement createElement() =>
|
||||||
|
SliverWrapElement(this, replaceMovedChildren: true);
|
||||||
|
|
||||||
|
@override
|
||||||
|
RenderSliverFixedWrap createRenderObject(BuildContext context) {
|
||||||
|
return RenderSliverFixedWrap(
|
||||||
|
childManager: context as SliverWrapElement,
|
||||||
|
mainAxisExtent: mainAxisExtent,
|
||||||
|
spacing: spacing,
|
||||||
|
runSpacing: runSpacing,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void updateRenderObject(
|
||||||
|
BuildContext context,
|
||||||
|
RenderSliverFixedWrap renderObject,
|
||||||
|
) {
|
||||||
|
renderObject
|
||||||
|
..mainAxisExtent = mainAxisExtent
|
||||||
|
..spacing = spacing
|
||||||
|
..runSpacing = runSpacing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SliverWrapParentData extends SliverMultiBoxAdaptorParentData {
|
||||||
|
double crossAxisOffset = 0.0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'crossAxisOffset=$crossAxisOffset; ${super.toString()}';
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Row {
|
||||||
|
final int startIndex;
|
||||||
|
final int endIndex;
|
||||||
|
final List<double> childWidths;
|
||||||
|
|
||||||
|
_Row({
|
||||||
|
required this.startIndex,
|
||||||
|
required this.endIndex,
|
||||||
|
required this.childWidths,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class RenderSliverFixedWrap extends RenderSliverMultiBoxAdaptor {
|
||||||
|
RenderSliverFixedWrap({
|
||||||
|
required super.childManager,
|
||||||
|
required double mainAxisExtent,
|
||||||
|
double spacing = 0.0,
|
||||||
|
double runSpacing = 0.0,
|
||||||
|
}) : _mainAxisExtent = mainAxisExtent,
|
||||||
|
_spacing = spacing,
|
||||||
|
_runSpacing = runSpacing {
|
||||||
|
assert(mainAxisExtent > 0.0 && mainAxisExtent.isFinite);
|
||||||
|
}
|
||||||
|
|
||||||
|
double _mainAxisExtent;
|
||||||
|
double get mainAxisExtent => _mainAxisExtent;
|
||||||
|
set mainAxisExtent(double value) {
|
||||||
|
if (_mainAxisExtent == value) return;
|
||||||
|
_mainAxisExtent = value;
|
||||||
|
markRowsDirty();
|
||||||
|
markNeedsLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
double _spacing;
|
||||||
|
double get spacing => _spacing;
|
||||||
|
set spacing(double value) {
|
||||||
|
if (_spacing == value) return;
|
||||||
|
_spacing = value;
|
||||||
|
markRowsDirty();
|
||||||
|
markNeedsLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
double _runSpacing;
|
||||||
|
double get runSpacing => _runSpacing;
|
||||||
|
set runSpacing(double value) {
|
||||||
|
if (_runSpacing == value) return;
|
||||||
|
_runSpacing = value;
|
||||||
|
markRowsDirty();
|
||||||
|
markNeedsLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<_Row> _rows = [];
|
||||||
|
|
||||||
|
void markRowsDirty() {
|
||||||
|
_rows.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void setupParentData(RenderObject child) {
|
||||||
|
if (child.parentData is! SliverWrapParentData) {
|
||||||
|
child.parentData = SliverWrapParentData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
double childCrossAxisPosition(RenderBox child) {
|
||||||
|
return (child.parentData as SliverWrapParentData).crossAxisOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
double _childCrossExtent(RenderBox child) {
|
||||||
|
assert(child.hasSize);
|
||||||
|
return switch (constraints.axis) {
|
||||||
|
Axis.horizontal => child.size.height,
|
||||||
|
Axis.vertical => child.size.width,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderBox _getOrCreateChildAtIndex(
|
||||||
|
int index,
|
||||||
|
BoxConstraints constraints,
|
||||||
|
RenderBox? child,
|
||||||
|
) {
|
||||||
|
assert(firstChild != null);
|
||||||
|
|
||||||
|
if (index < indexOf(firstChild!)) {
|
||||||
|
do {
|
||||||
|
child = insertAndLayoutLeadingChild(constraints, parentUsesSize: true);
|
||||||
|
assert(child != null);
|
||||||
|
} while (indexOf(child!) > index);
|
||||||
|
|
||||||
|
assert(indexOf(child) == index);
|
||||||
|
|
||||||
|
return child;
|
||||||
|
} else if (index > indexOf(lastChild!)) {
|
||||||
|
do {
|
||||||
|
child = insertAndLayoutChild(
|
||||||
|
constraints,
|
||||||
|
after: lastChild,
|
||||||
|
parentUsesSize: true,
|
||||||
|
);
|
||||||
|
assert(child != null);
|
||||||
|
} while (indexOf(child!) < index);
|
||||||
|
|
||||||
|
assert(indexOf(child) == index);
|
||||||
|
|
||||||
|
return child;
|
||||||
|
} else {
|
||||||
|
child = firstChild;
|
||||||
|
while (indexOf(child!) < index) {
|
||||||
|
child = childAfter(child);
|
||||||
|
}
|
||||||
|
if (indexOf(child) == index) {
|
||||||
|
child.layout(constraints, parentUsesSize: true);
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
throw RangeError.value(index, 'index', 'Value not included in children');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _buildNextRow(int start, BoxConstraints childConstraints) {
|
||||||
|
final int childCount = childManager.childCount;
|
||||||
|
|
||||||
|
if (start >= childCount) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final crossAxisExtent = constraints.crossAxisExtent;
|
||||||
|
|
||||||
|
final List<double> widths = [];
|
||||||
|
int idx = start;
|
||||||
|
RenderBox? child;
|
||||||
|
for (var totalWidth = -_spacing; idx < childCount; idx++) {
|
||||||
|
child = _getOrCreateChildAtIndex(idx, childConstraints, child);
|
||||||
|
final childWidth = _childCrossExtent(child);
|
||||||
|
totalWidth += childWidth + _spacing;
|
||||||
|
|
||||||
|
if (totalWidth <= crossAxisExtent) {
|
||||||
|
widths.add(childWidth);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_rows.add(_Row(startIndex: start, endIndex: idx - 1, childWidths: widths));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void performLayout() {
|
||||||
|
childManager
|
||||||
|
..didStartLayout()
|
||||||
|
..setDidUnderflow(false);
|
||||||
|
|
||||||
|
final constraints = this.constraints;
|
||||||
|
final childCount = childManager.childCount;
|
||||||
|
|
||||||
|
final rowHeight = _mainAxisExtent + _runSpacing;
|
||||||
|
|
||||||
|
final scrollOffset = constraints.scrollOffset;
|
||||||
|
|
||||||
|
final firstCacheOffset = scrollOffset + constraints.cacheOrigin;
|
||||||
|
final lastCacheOffset = scrollOffset + constraints.remainingCacheExtent;
|
||||||
|
|
||||||
|
final firstNeededRow = math.max(0, firstCacheOffset ~/ rowHeight);
|
||||||
|
final lastNeededRow = math.max(0, lastCacheOffset ~/ rowHeight);
|
||||||
|
|
||||||
|
if (firstChild == null) {
|
||||||
|
if (!addInitialChild()) {
|
||||||
|
geometry = SliverGeometry.zero;
|
||||||
|
childManager.didFinishLayout();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
firstChild!.layout(
|
||||||
|
constraints.toFixedConstraints(_mainAxisExtent),
|
||||||
|
parentUsesSize: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (_rows.length <= lastNeededRow) {
|
||||||
|
final int startIndex = _rows.isEmpty ? 0 : _rows.last.endIndex + 1;
|
||||||
|
if (!_buildNextRow(
|
||||||
|
startIndex,
|
||||||
|
constraints.toFixedConstraints(_mainAxisExtent),
|
||||||
|
)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(firstNeededRow >= 0);
|
||||||
|
|
||||||
|
final int firstKeptRow = firstNeededRow.clamp(0, _rows.length - 1);
|
||||||
|
final int lastKeptRow = lastNeededRow.clamp(0, _rows.length - 1);
|
||||||
|
|
||||||
|
final int firstKeptIndex = _rows[firstKeptRow].startIndex;
|
||||||
|
final int lastKeptIndex = _rows[lastKeptRow].endIndex;
|
||||||
|
|
||||||
|
collectGarbage(
|
||||||
|
calculateLeadingGarbage(firstIndex: firstKeptIndex),
|
||||||
|
calculateTrailingGarbage(lastIndex: lastKeptIndex),
|
||||||
|
);
|
||||||
|
|
||||||
|
RenderBox? child;
|
||||||
|
for (var r = firstKeptRow; r <= lastKeptRow; r++) {
|
||||||
|
final row = _rows[r];
|
||||||
|
final rowStartOffset = r * rowHeight;
|
||||||
|
double crossOffset = 0.0;
|
||||||
|
for (var i = row.startIndex; i <= row.endIndex; i++) {
|
||||||
|
child = _getOrCreateChildAtIndex(
|
||||||
|
i,
|
||||||
|
constraints.toFixedConstraints(_mainAxisExtent),
|
||||||
|
child,
|
||||||
|
);
|
||||||
|
(child.parentData as SliverWrapParentData)
|
||||||
|
..layoutOffset = rowStartOffset
|
||||||
|
..crossAxisOffset = crossOffset;
|
||||||
|
crossOffset += row.childWidths[i - row.startIndex] + _spacing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final endOffset = _rows.last.endIndex == childCount - 1
|
||||||
|
? (_rows.length * rowHeight)
|
||||||
|
: (_rows.last.startIndex + 1) * rowHeight;
|
||||||
|
|
||||||
|
final double estimatedMaxScrollOffset;
|
||||||
|
if (_rows.length <= lastNeededRow || childCount == 0) {
|
||||||
|
estimatedMaxScrollOffset = childManager.estimateMaxScrollOffset(
|
||||||
|
constraints,
|
||||||
|
firstIndex: firstKeptIndex,
|
||||||
|
lastIndex: lastKeptIndex,
|
||||||
|
leadingScrollOffset: firstKeptRow * rowHeight,
|
||||||
|
trailingScrollOffset: endOffset,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
estimatedMaxScrollOffset = _rows.length * rowHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
final double paintExtent = calculatePaintOffset(
|
||||||
|
constraints,
|
||||||
|
from: firstKeptRow * rowHeight,
|
||||||
|
to: endOffset,
|
||||||
|
);
|
||||||
|
final double cacheExtent = calculateCacheOffset(
|
||||||
|
constraints,
|
||||||
|
from: firstCacheOffset,
|
||||||
|
to: lastCacheOffset,
|
||||||
|
);
|
||||||
|
|
||||||
|
geometry = SliverGeometry(
|
||||||
|
scrollExtent: estimatedMaxScrollOffset,
|
||||||
|
paintExtent: paintExtent,
|
||||||
|
cacheExtent: cacheExtent,
|
||||||
|
maxPaintExtent: estimatedMaxScrollOffset,
|
||||||
|
hasVisualOverflow:
|
||||||
|
endOffset >
|
||||||
|
constraints.scrollOffset + constraints.remainingPaintExtent,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (estimatedMaxScrollOffset <= endOffset) {
|
||||||
|
childManager.setDidUnderflow(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
childManager.didFinishLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
markRowsDirty();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SliverWrapElement extends SliverMultiBoxAdaptorElement {
|
||||||
|
SliverWrapElement(SliverFixedWrap super.widget, {super.replaceMovedChildren});
|
||||||
|
|
||||||
|
@override
|
||||||
|
void performRebuild() {
|
||||||
|
(renderObject as RenderSliverFixedWrap).markRowsDirty();
|
||||||
|
super.performRebuild();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension on SliverConstraints {
|
||||||
|
BoxConstraints toFixedConstraints(double mainAxisExtent) {
|
||||||
|
switch (axis) {
|
||||||
|
case Axis.horizontal:
|
||||||
|
return BoxConstraints(
|
||||||
|
minHeight: 0,
|
||||||
|
maxHeight: crossAxisExtent,
|
||||||
|
minWidth: mainAxisExtent,
|
||||||
|
maxWidth: mainAxisExtent,
|
||||||
|
);
|
||||||
|
case Axis.vertical:
|
||||||
|
return BoxConstraints(
|
||||||
|
minWidth: 0,
|
||||||
|
maxWidth: crossAxisExtent,
|
||||||
|
minHeight: mainAxisExtent,
|
||||||
|
maxHeight: mainAxisExtent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:PiliPlus/common/widgets/disabled_icon.dart';
|
import 'package:PiliPlus/common/widgets/disabled_icon.dart';
|
||||||
import 'package:PiliPlus/common/widgets/flutter/layout_builder.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/sliver_wrap.dart';
|
||||||
import 'package:PiliPlus/http/loading_state.dart';
|
import 'package:PiliPlus/http/loading_state.dart';
|
||||||
import 'package:PiliPlus/models_new/search/search_rcmd/data.dart';
|
import 'package:PiliPlus/models_new/search/search_rcmd/data.dart';
|
||||||
import 'package:PiliPlus/pages/about/view.dart' show showImportExportDialog;
|
import 'package:PiliPlus/pages/about/view.dart' show showImportExportDialog;
|
||||||
@@ -27,6 +27,9 @@ class SearchPage extends StatefulWidget {
|
|||||||
class _SearchPageState extends State<SearchPage> {
|
class _SearchPageState extends State<SearchPage> {
|
||||||
final _tag = Utils.generateRandomString(6);
|
final _tag = Utils.generateRandomString(6);
|
||||||
late final SSearchController _searchController;
|
late final SSearchController _searchController;
|
||||||
|
late ThemeData theme;
|
||||||
|
late bool isPortrait;
|
||||||
|
late EdgeInsets padding;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -37,12 +40,52 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
theme = Theme.of(context);
|
||||||
|
padding = MediaQuery.viewPaddingOf(context);
|
||||||
|
isPortrait = MediaQuery.sizeOf(context).isPortrait;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final trending = _searchController.enableTrending
|
||||||
final isPortrait = MediaQuery.sizeOf(context).isPortrait;
|
? _buildHotSearch()
|
||||||
|
: null;
|
||||||
|
final rcmd = _searchController.enableSearchRcmd
|
||||||
|
? _buildHotSearch(isTrending: false)
|
||||||
|
: null;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
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)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
PreferredSizeWidget get _buildAppBar => AppBar(
|
||||||
shape: Border(
|
shape: Border(
|
||||||
bottom: BorderSide(
|
bottom: BorderSide(
|
||||||
color: theme.dividerColor.withValues(alpha: 0.08),
|
color: theme.dividerColor.withValues(alpha: 0.08),
|
||||||
@@ -86,60 +129,22 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
),
|
),
|
||||||
onSubmitted: (value) => _searchController.submit(),
|
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() {
|
Widget _buildSearchSuggest() {
|
||||||
return Obx(
|
return Obx(() {
|
||||||
() =>
|
final list = _searchController.searchSuggestList;
|
||||||
_searchController.searchSuggestList.isNotEmpty &&
|
return list.isNotEmpty &&
|
||||||
_searchController.searchSuggestList.first.term != null &&
|
list.first.term != null &&
|
||||||
_searchController.controller.text != ''
|
_searchController.controller.text != ''
|
||||||
? Column(
|
? SliverList.list(
|
||||||
mainAxisSize: MainAxisSize.min,
|
children: list
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: _searchController.searchSuggestList
|
|
||||||
.map(
|
.map(
|
||||||
(item) => InkWell(
|
(item) => InkWell(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
borderRadius: const .all(.circular(4)),
|
||||||
onTap: () => _searchController.onClickKeyword(item.term!),
|
onTap: () => _searchController.onClickKeyword(item.term!),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding: const .only(left: 20, top: 9, bottom: 9),
|
||||||
left: 20,
|
|
||||||
top: 9,
|
|
||||||
bottom: 9,
|
|
||||||
),
|
|
||||||
child: Text.rich(
|
child: Text.rich(
|
||||||
TextSpan(
|
TextSpan(
|
||||||
children: Em.regTitle(item.textRich)
|
children: Em.regTitle(item.textRich)
|
||||||
@@ -164,11 +169,13 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
)
|
)
|
||||||
: const SizedBox.shrink(),
|
: const SliverToBoxAdapter();
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget hotSearch(ThemeData theme, bool isPortrait, {bool isTrending = true}) {
|
Widget _buildHotSearch({
|
||||||
|
bool isTrending = true,
|
||||||
|
}) {
|
||||||
final text = Text(
|
final text = Text(
|
||||||
isTrending ? '大家都在搜' : '搜索发现',
|
isTrending ? '大家都在搜' : '搜索发现',
|
||||||
strutStyle: const StrutStyle(leading: 0, height: 1),
|
strutStyle: const StrutStyle(leading: 0, height: 1),
|
||||||
@@ -184,7 +191,7 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: outline,
|
color: outline,
|
||||||
);
|
);
|
||||||
return Padding(
|
return SliverPadding(
|
||||||
padding: EdgeInsets.fromLTRB(
|
padding: EdgeInsets.fromLTRB(
|
||||||
10,
|
10,
|
||||||
!isTrending && (isPortrait || _searchController.enableTrending)
|
!isTrending && (isPortrait || _searchController.enableTrending)
|
||||||
@@ -193,11 +200,11 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
4,
|
4,
|
||||||
25,
|
25,
|
||||||
),
|
),
|
||||||
child: Column(
|
sliver: SliverMainAxisGroup(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
slivers: [
|
||||||
children: [
|
SliverPadding(
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.fromLTRB(6, 0, 6, 6),
|
padding: const EdgeInsets.fromLTRB(6, 0, 6, 6),
|
||||||
|
sliver: SliverToBoxAdapter(
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
@@ -207,9 +214,14 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
children: [
|
children: [
|
||||||
text,
|
text,
|
||||||
const SizedBox(width: 14),
|
const SizedBox(width: 14),
|
||||||
SizedBox(
|
TextButton(
|
||||||
height: 34,
|
style: const ButtonStyle(
|
||||||
child: TextButton(
|
visualDensity: .compact,
|
||||||
|
tapTargetSize: .shrinkWrap,
|
||||||
|
padding: WidgetStatePropertyAll(
|
||||||
|
.symmetric(horizontal: 10),
|
||||||
|
),
|
||||||
|
),
|
||||||
onPressed: () => Get.toNamed('/searchTrending'),
|
onPressed: () => Get.toNamed('/searchTrending'),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -229,16 +241,15 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
: text,
|
: text,
|
||||||
SizedBox(
|
TextButton.icon(
|
||||||
height: 34,
|
|
||||||
child: TextButton.icon(
|
|
||||||
style: const ButtonStyle(
|
style: const ButtonStyle(
|
||||||
|
visualDensity: .compact,
|
||||||
|
tapTargetSize: .shrinkWrap,
|
||||||
padding: WidgetStatePropertyAll(
|
padding: WidgetStatePropertyAll(
|
||||||
EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
.symmetric(horizontal: 10),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onPressed: isTrending
|
onPressed: isTrending
|
||||||
@@ -258,10 +269,10 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
Obx(
|
Obx(
|
||||||
() => _buildHotKey(
|
() => _buildHotKey(
|
||||||
isTrending
|
isTrending
|
||||||
@@ -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(
|
return Obx(
|
||||||
() {
|
() {
|
||||||
if (_searchController.historyList.isEmpty) {
|
final list = _searchController.historyList;
|
||||||
return const SizedBox.shrink();
|
if (list.isEmpty) {
|
||||||
|
return const SliverToBoxAdapter();
|
||||||
}
|
}
|
||||||
final secondary = theme.colorScheme.secondary;
|
final secondary = theme.colorScheme.secondary;
|
||||||
return Padding(
|
return SliverPadding(
|
||||||
padding: EdgeInsets.fromLTRB(
|
padding: EdgeInsets.fromLTRB(
|
||||||
10,
|
10,
|
||||||
!isPortrait
|
!isPortrait
|
||||||
@@ -293,11 +306,11 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
6,
|
6,
|
||||||
25,
|
25,
|
||||||
),
|
),
|
||||||
child: Column(
|
sliver: SliverMainAxisGroup(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
slivers: [
|
||||||
children: [
|
SliverPadding(
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.fromLTRB(6, 0, 6, 6),
|
padding: const EdgeInsets.fromLTRB(6, 0, 6, 6),
|
||||||
|
sliver: SliverToBoxAdapter(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
@@ -309,51 +322,15 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Obx(
|
_recordBtn,
|
||||||
() {
|
_exportBtn,
|
||||||
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(),
|
const Spacer(),
|
||||||
SizedBox(
|
TextButton.icon(
|
||||||
height: 34,
|
|
||||||
child: TextButton.icon(
|
|
||||||
style: const ButtonStyle(
|
style: const ButtonStyle(
|
||||||
|
visualDensity: .compact,
|
||||||
|
tapTargetSize: .shrinkWrap,
|
||||||
padding: WidgetStatePropertyAll(
|
padding: WidgetStatePropertyAll(
|
||||||
EdgeInsets.symmetric(
|
.symmetric(horizontal: 10),
|
||||||
horizontal: 10,
|
|
||||||
vertical: 6,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onPressed: _searchController.onClearHistory,
|
onPressed: _searchController.onClearHistory,
|
||||||
@@ -364,27 +341,36 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
),
|
),
|
||||||
label: Text(
|
label: Text(
|
||||||
'清空',
|
'清空',
|
||||||
style: TextStyle(color: secondary),
|
style: TextStyle(
|
||||||
|
height: 1,
|
||||||
|
color: secondary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Wrap(
|
),
|
||||||
|
SliverFixedWrap(
|
||||||
|
mainAxisExtent: mainAxisExtent,
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
runSpacing: 8,
|
runSpacing: 8,
|
||||||
direction: Axis.horizontal,
|
delegate: SliverChildBuilderDelegate(
|
||||||
textDirection: TextDirection.ltr,
|
addAutomaticKeepAlives: false,
|
||||||
children: _searchController.historyList
|
addRepaintBoundaries: false,
|
||||||
.map(
|
childCount: list.length,
|
||||||
(item) => SearchText(
|
(context, index) => SearchText(
|
||||||
text: item,
|
text: list[index],
|
||||||
onTap: _searchController.onClickKeyword,
|
onTap: _searchController.onClickKeyword,
|
||||||
onLongPress: _searchController.onLongSelect,
|
onLongPress: _searchController.onLongSelect,
|
||||||
|
fontSize: 14,
|
||||||
|
height: 1,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 11,
|
||||||
|
vertical: 8,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -393,17 +379,48 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _exportHistory(ThemeData theme) => SizedBox(
|
Widget get _recordBtn => Obx(
|
||||||
width: 34,
|
() {
|
||||||
height: 34,
|
bool enable = _searchController.recordSearchHistory.value;
|
||||||
child: IconButton(
|
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,
|
iconSize: 22,
|
||||||
tooltip: '导入/导出历史记录',
|
tooltip: '导入/导出历史记录',
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
Icons.import_export_outlined,
|
Icons.import_export_outlined,
|
||||||
color: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.8),
|
color: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.8),
|
||||||
),
|
),
|
||||||
style: IconButton.styleFrom(padding: EdgeInsets.zero),
|
style: const ButtonStyle(
|
||||||
|
visualDensity: .comfortable,
|
||||||
|
tapTargetSize: .shrinkWrap,
|
||||||
|
padding: WidgetStatePropertyAll(.zero),
|
||||||
|
),
|
||||||
onPressed: () => showImportExportDialog<List>(
|
onPressed: () => showImportExportDialog<List>(
|
||||||
context,
|
context,
|
||||||
title: '历史记录',
|
title: '历史记录',
|
||||||
@@ -415,7 +432,6 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildHotKey(
|
Widget _buildHotKey(
|
||||||
@@ -423,23 +439,19 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
bool isTrending,
|
bool isTrending,
|
||||||
) {
|
) {
|
||||||
return switch (loadingState) {
|
return switch (loadingState) {
|
||||||
Success(:final response) =>
|
Success(:final response) when (response.list?.isNotEmpty ?? false) =>
|
||||||
response.list?.isNotEmpty == true
|
SliverHotKeyword(
|
||||||
? LayoutBuilder(
|
|
||||||
builder: (context, constraints) => HotKeyword(
|
|
||||||
width: constraints.maxWidth,
|
|
||||||
hotSearchList: response.list!,
|
hotSearchList: response.list!,
|
||||||
onClick: _searchController.onClickKeyword,
|
onClick: _searchController.onClickKeyword,
|
||||||
),
|
),
|
||||||
)
|
Error(:final errMsg) => HttpError(
|
||||||
: const SizedBox.shrink(),
|
safeArea: false,
|
||||||
Error(:final errMsg) => errorWidget(
|
|
||||||
errMsg: errMsg,
|
errMsg: errMsg,
|
||||||
onReload: isTrending
|
onReload: isTrending
|
||||||
? _searchController.queryTrendingList
|
? _searchController.queryTrendingList
|
||||||
: _searchController.queryRecommendList,
|
: _searchController.queryRecommendList,
|
||||||
),
|
),
|
||||||
_ => const SizedBox.shrink(),
|
_ => const SliverToBoxAdapter(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +1,42 @@
|
|||||||
import 'package:PiliPlus/models_new/search/search_trending/list.dart';
|
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/extension/string_ext.dart';
|
||||||
import 'package:PiliPlus/utils/image_utils.dart';
|
import 'package:PiliPlus/utils/image_utils.dart';
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/rendering.dart'
|
||||||
|
show
|
||||||
|
ContainerRenderObjectMixin,
|
||||||
|
MultiChildLayoutParentData,
|
||||||
|
RenderBoxContainerDefaultsMixin,
|
||||||
|
BoxHitTestResult;
|
||||||
|
|
||||||
class HotKeyword extends StatelessWidget {
|
class SliverHotKeyword extends StatelessWidget {
|
||||||
final double width;
|
|
||||||
final List<SearchTrendingItemModel> hotSearchList;
|
final List<SearchTrendingItemModel> hotSearchList;
|
||||||
final Function? onClick;
|
final Function? onClick;
|
||||||
const HotKeyword({
|
const SliverHotKeyword({
|
||||||
super.key,
|
super.key,
|
||||||
required double width,
|
|
||||||
required this.hotSearchList,
|
required this.hotSearchList,
|
||||||
this.onClick,
|
this.onClick,
|
||||||
}) : width = width / 2 - 4;
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
late final style = TextStyle(
|
late final style = TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Theme.of(context).colorScheme.outline,
|
color: ColorScheme.of(context).outline,
|
||||||
);
|
);
|
||||||
return Wrap(
|
|
||||||
runSpacing: 0.4,
|
late final cacheHeight = (MediaQuery.devicePixelRatioOf(context) * 15.0)
|
||||||
spacing: 5.0,
|
.round();
|
||||||
children: [
|
|
||||||
for (final i in hotSearchList)
|
return SliverToBoxAdapter(
|
||||||
SizedBox(
|
child: _HotKeywordGrid(
|
||||||
width: width,
|
mainAxisSpacing: 5,
|
||||||
child: Material(
|
crossAxisSpacing: 0.4,
|
||||||
|
crossAxisCount: 2,
|
||||||
|
children: hotSearchList
|
||||||
|
.map(
|
||||||
|
(i) => Material(
|
||||||
type: MaterialType.transparency,
|
type: MaterialType.transparency,
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(3)),
|
borderRadius: const BorderRadius.all(Radius.circular(3)),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
@@ -57,7 +64,7 @@ class HotKeyword extends StatelessWidget {
|
|||||||
padding: const EdgeInsets.only(left: 4),
|
padding: const EdgeInsets.only(left: 4),
|
||||||
child: CachedNetworkImage(
|
child: CachedNetworkImage(
|
||||||
height: 15,
|
height: 15,
|
||||||
memCacheHeight: 15.cacheSize(context),
|
memCacheHeight: cacheHeight,
|
||||||
imageUrl: ImageUtils.thumbnailUrl(i.icon!),
|
imageUrl: ImageUtils.thumbnailUrl(i.icon!),
|
||||||
placeholder: (_, _) => const SizedBox.shrink(),
|
placeholder: (_, _) => const SizedBox.shrink(),
|
||||||
),
|
),
|
||||||
@@ -69,7 +76,7 @@ class HotKeyword extends StatelessWidget {
|
|||||||
'assets/images/live/live.gif',
|
'assets/images/live/live.gif',
|
||||||
width: 48,
|
width: 48,
|
||||||
height: 15,
|
height: 15,
|
||||||
cacheHeight: 15.cacheSize(context),
|
cacheHeight: cacheHeight,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
else if (i.recommendReason?.isNotEmpty == true)
|
else if (i.recommendReason?.isNotEmpty == true)
|
||||||
@@ -80,8 +87,129 @@ class HotKeyword extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
.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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
late final colorScheme = Theme.of(context).colorScheme;
|
late final colorScheme = ColorScheme.of(context);
|
||||||
final hasLongPress = onLongPress != null;
|
final hasLongPress = onLongPress != null;
|
||||||
return Material(
|
return Material(
|
||||||
color: bgColor ?? colorScheme.onInverseSurface,
|
color: bgColor ?? colorScheme.onInverseSurface,
|
||||||
|
|||||||
Reference in New Issue
Block a user