From 036dbcaf21afda42919f8374ba8df74514bd6269 Mon Sep 17 00:00:00 2001 From: dom Date: Mon, 19 Jan 2026 17:36:21 +0800 Subject: [PATCH] opt image grid Signed-off-by: dom --- lib/common/widgets/flutter/custom_layout.dart | 462 ------------------ .../widgets/image/custom_grid_view.dart | 156 +++++- 2 files changed, 129 insertions(+), 489 deletions(-) delete mode 100644 lib/common/widgets/flutter/custom_layout.dart diff --git a/lib/common/widgets/flutter/custom_layout.dart b/lib/common/widgets/flutter/custom_layout.dart deleted file mode 100644 index 2dce8ff86..000000000 --- a/lib/common/widgets/flutter/custom_layout.dart +++ /dev/null @@ -1,462 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -// ignore_for_file: uri_does_not_exist_in_doc_import - -/// @docImport 'package:flutter/widgets.dart'; -/// -/// @docImport 'stack.dart'; -library; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; - -class CustomMultiChildLayout extends MultiChildRenderObjectWidget { - /// Creates a custom multi-child layout. - const CustomMultiChildLayout({ - super.key, - required this.delegate, - super.children, - }); - - /// The delegate that controls the layout of the children. - final MultiChildLayoutDelegate delegate; - - @override - RenderCustomMultiChildLayoutBox createRenderObject(BuildContext context) { - return RenderCustomMultiChildLayoutBox(delegate: delegate); - } - - @override - void updateRenderObject( - BuildContext context, - RenderCustomMultiChildLayoutBox renderObject, - ) { - renderObject.delegate = delegate; - } -} - -/// A delegate that controls the layout of multiple children. -/// -/// Used with [CustomMultiChildLayout] (in the widgets library) and -/// [RenderCustomMultiChildLayoutBox] (in the rendering library). -/// -/// Delegates must be idempotent. Specifically, if two delegates are equal, then -/// they must produce the same layout. To change the layout, replace the -/// delegate with a different instance whose [shouldRelayout] returns true when -/// given the previous instance. -/// -/// Override [getSize] to control the overall size of the layout. The size of -/// the layout cannot depend on layout properties of the children. This was -/// a design decision to simplify the delegate implementations: This way, -/// the delegate implementations do not have to also handle various intrinsic -/// sizing functions if the parent's size depended on the children. -/// If you want to build a custom layout where you define the size of that widget -/// based on its children, then you will have to create a custom render object. -/// See [MultiChildRenderObjectWidget] with [ContainerRenderObjectMixin] and -/// [RenderBoxContainerDefaultsMixin] to get started or [RenderStack] for an -/// example implementation. -/// -/// Override [performLayout] to size and position the children. An -/// implementation of [performLayout] must call [layoutChild] exactly once for -/// each child, but it may call [layoutChild] on children in an arbitrary order. -/// Typically a delegate will use the size returned from [layoutChild] on one -/// child to determine the constraints for [performLayout] on another child or -/// to determine the offset for [positionChild] for that child or another child. -/// -/// Override [shouldRelayout] to determine when the layout of the children needs -/// to be recomputed when the delegate changes. -/// -/// The most efficient way to trigger a relayout is to supply a `relayout` -/// argument to the constructor of the [MultiChildLayoutDelegate]. The custom -/// layout will listen to this value and relayout whenever the Listenable -/// notifies its listeners, such as when an [Animation] ticks. This allows -/// the custom layout to avoid the build phase of the pipeline. -/// -/// Each child must be wrapped in a [LayoutId] widget to assign the id that -/// identifies it to the delegate. The [LayoutId.id] needs to be unique among -/// the children that the [CustomMultiChildLayout] manages. -/// -/// {@tool snippet} -/// -/// Below is an example implementation of [performLayout] that causes one widget -/// (the follower) to be the same size as another (the leader): -/// -/// ```dart -/// // Define your own slot numbers, depending upon the id assigned by LayoutId. -/// // Typical usage is to define an enum like the one below, and use those -/// // values as the ids. -/// enum _Slot { -/// leader, -/// follower, -/// } -/// -/// class FollowTheLeader extends MultiChildLayoutDelegate { -/// @override -/// void performLayout(Size size) { -/// Size leaderSize = Size.zero; -/// -/// if (hasChild(_Slot.leader)) { -/// leaderSize = layoutChild(_Slot.leader, BoxConstraints.loose(size)); -/// positionChild(_Slot.leader, Offset.zero); -/// } -/// -/// if (hasChild(_Slot.follower)) { -/// layoutChild(_Slot.follower, BoxConstraints.tight(leaderSize)); -/// positionChild(_Slot.follower, Offset(size.width - leaderSize.width, -/// size.height - leaderSize.height)); -/// } -/// } -/// -/// @override -/// bool shouldRelayout(MultiChildLayoutDelegate oldDelegate) => false; -/// } -/// ``` -/// {@end-tool} -/// -/// The delegate gives the leader widget loose constraints, which means the -/// child determines what size to be (subject to fitting within the given size). -/// The delegate then remembers the size of that child and places it in the -/// upper left corner. -/// -/// The delegate then gives the follower widget tight constraints, forcing it to -/// match the size of the leader widget. The delegate then places the follower -/// widget in the bottom right corner. -/// -/// The leader and follower widget will paint in the order they appear in the -/// child list, regardless of the order in which [layoutChild] is called on -/// them. -/// -/// See also: -/// -/// * [CustomMultiChildLayout], the widget that uses this delegate. -/// * [RenderCustomMultiChildLayoutBox], render object that uses this -/// delegate. -abstract class MultiChildLayoutDelegate { - /// Creates a layout delegate. - /// - /// The layout will update whenever [relayout] notifies its listeners. - MultiChildLayoutDelegate({Listenable? relayout}) : _relayout = relayout; - - final Listenable? _relayout; - - Map? _idToChild; - Set? _debugChildrenNeedingLayout; - - /// True if a non-null LayoutChild was provided for the specified id. - /// - /// Call this from the [performLayout] method to determine which children - /// are available, if the child list might vary. - /// - /// This method cannot be called from [getSize] as the size is not allowed - /// to depend on the children. - bool hasChild(Object childId) => _idToChild![childId] != null; - - /// Ask the child to update its layout within the limits specified by - /// the constraints parameter. The child's size is returned. - /// - /// Call this from your [performLayout] function to lay out each - /// child. Every child must be laid out using this function exactly - /// once each time the [performLayout] function is called. - Size layoutChild(Object childId, BoxConstraints constraints) { - final RenderBox? child = _idToChild![childId]; - assert(() { - if (child == null) { - throw FlutterError( - 'The $this custom multichild layout delegate tried to lay out a non-existent child.\n' - 'There is no child with the id "$childId".', - ); - } - if (!_debugChildrenNeedingLayout!.remove(child)) { - throw FlutterError( - 'The $this custom multichild layout delegate tried to lay out the child with id "$childId" more than once.\n' - 'Each child must be laid out exactly once.', - ); - } - try { - assert(constraints.debugAssertIsValid(isAppliedConstraint: true)); - } on AssertionError catch (exception) { - throw FlutterError.fromParts([ - ErrorSummary( - 'The $this custom multichild layout delegate provided invalid box constraints for the child with id "$childId".', - ), - DiagnosticsProperty( - 'Exception', - exception, - showName: false, - ), - ErrorDescription( - 'The minimum width and height must be greater than or equal to zero.\n' - 'The maximum width must be greater than or equal to the minimum width.\n' - 'The maximum height must be greater than or equal to the minimum height.', - ), - ]); - } - return true; - }()); - child!.layout(constraints, parentUsesSize: true); - return child.size; - } - - /// Specify the child's origin relative to this origin. - /// - /// Call this from your [performLayout] function to position each - /// child. If you do not call this for a child, its position will - /// remain unchanged. Children initially have their position set to - /// (0,0), i.e. the top left of the [RenderCustomMultiChildLayoutBox]. - void positionChild(Object childId, Offset offset) { - final RenderBox? child = _idToChild![childId]; - assert(() { - if (child == null) { - throw FlutterError( - 'The $this custom multichild layout delegate tried to position out a non-existent child:\n' - 'There is no child with the id "$childId".', - ); - } - return true; - }()); - final MultiChildLayoutParentData childParentData = - child!.parentData! as MultiChildLayoutParentData; - childParentData.offset = offset; - } - - DiagnosticsNode _debugDescribeChild(RenderBox child) { - final MultiChildLayoutParentData childParentData = - child.parentData! as MultiChildLayoutParentData; - return DiagnosticsProperty('${childParentData.id}', child); - } - - void _callPerformLayout(Size size, RenderBox? firstChild) { - // A particular layout delegate could be called reentrantly, e.g. if it used - // by both a parent and a child. So, we must restore the _idToChild map when - // we return. - final Map? previousIdToChild = _idToChild; - - Set? debugPreviousChildrenNeedingLayout; - assert(() { - debugPreviousChildrenNeedingLayout = _debugChildrenNeedingLayout; - _debugChildrenNeedingLayout = {}; - return true; - }()); - - try { - _idToChild = {}; - RenderBox? child = firstChild; - while (child != null) { - final MultiChildLayoutParentData childParentData = - child.parentData! as MultiChildLayoutParentData; - assert(() { - if (childParentData.id == null) { - throw FlutterError.fromParts([ - ErrorSummary( - 'Every child of a RenderCustomMultiChildLayoutBox must have an ID in its parent data.', - ), - child!.describeForError('The following child has no ID'), - ]); - } - return true; - }()); - _idToChild![childParentData.id!] = child; - assert(() { - _debugChildrenNeedingLayout!.add(child!); - return true; - }()); - child = childParentData.nextSibling; - } - performLayout(size); - assert(() { - if (_debugChildrenNeedingLayout!.isNotEmpty) { - throw FlutterError.fromParts([ - ErrorSummary('Each child must be laid out exactly once.'), - DiagnosticsBlock( - name: - 'The $this custom multichild layout delegate forgot ' - 'to lay out the following ' - '${_debugChildrenNeedingLayout!.length > 1 ? 'children' : 'child'}', - properties: _debugChildrenNeedingLayout! - .map(_debugDescribeChild) - .toList(), - ), - ]); - } - return true; - }()); - } finally { - _idToChild = previousIdToChild; - assert(() { - _debugChildrenNeedingLayout = debugPreviousChildrenNeedingLayout; - return true; - }()); - } - } - - /// Override this method to return the size of this object given the - /// incoming constraints. - /// - /// The size cannot reflect the sizes of the children. If this layout has a - /// fixed width or height the returned size can reflect that; the size will be - /// constrained to the given constraints. - /// - /// By default, attempts to size the box to the biggest size - /// possible given the constraints. - Size getSize(BoxConstraints constraints) => constraints.biggest; - - /// Override this method to lay out and position all children given this - /// widget's size. - /// - /// This method must call [layoutChild] for each child. It should also specify - /// the final position of each child with [positionChild]. - void performLayout(Size size); - - /// Override this method to return true when the children need to be - /// laid out. - /// - /// This should compare the fields of the current delegate and the given - /// `oldDelegate` and return true if the fields are such that the layout would - /// be different. - bool shouldRelayout(covariant MultiChildLayoutDelegate oldDelegate); - - /// Override this method to include additional information in the - /// debugging data printed by [debugDumpRenderTree] and friends. - /// - /// By default, returns the [runtimeType] of the class. - @override - String toString() => objectRuntimeType(this, 'MultiChildLayoutDelegate'); -} - -/// Defers the layout of multiple children to a delegate. -/// -/// The delegate can determine the layout constraints for each child and can -/// decide where to position each child. The delegate can also determine the -/// size of the parent, but the size of the parent cannot depend on the sizes of -/// the children. -class RenderCustomMultiChildLayoutBox extends RenderBox - with - ContainerRenderObjectMixin, - RenderBoxContainerDefaultsMixin { - /// Creates a render object that customizes the layout of multiple children. - RenderCustomMultiChildLayoutBox({ - List? children, - required MultiChildLayoutDelegate delegate, - }) : _delegate = delegate { - addAll(children); - } - - @override - void setupParentData(RenderBox child) { - if (child.parentData is! MultiChildLayoutParentData) { - child.parentData = MultiChildLayoutParentData(); - } - } - - /// The delegate that controls the layout of the children. - MultiChildLayoutDelegate get delegate => _delegate; - MultiChildLayoutDelegate _delegate; - set delegate(MultiChildLayoutDelegate newDelegate) { - if (_delegate == newDelegate) { - return; - } - final MultiChildLayoutDelegate oldDelegate = _delegate; - if (newDelegate.runtimeType != oldDelegate.runtimeType || - newDelegate.shouldRelayout(oldDelegate)) { - markNeedsLayout(); - } - _delegate = newDelegate; - if (attached) { - oldDelegate._relayout?.removeListener(markNeedsLayout); - newDelegate._relayout?.addListener(markNeedsLayout); - } - } - - @override - void attach(PipelineOwner owner) { - super.attach(owner); - _delegate._relayout?.addListener(markNeedsLayout); - } - - @override - void detach() { - _delegate._relayout?.removeListener(markNeedsLayout); - super.detach(); - } - - Size _getSize(BoxConstraints constraints) { - assert(constraints.debugAssertIsValid()); - return constraints.constrain(_delegate.getSize(constraints)); - } - - // TODO(ianh): It's a bit dubious to be using the getSize function from the delegate to - // figure out the intrinsic dimensions. We really should either not support intrinsics, - // or we should expose intrinsic delegate callbacks and throw if they're not implemented. - - @override - double computeMinIntrinsicWidth(double height) { - final double width = _getSize( - BoxConstraints.tightForFinite(height: height), - ).width; - if (width.isFinite) { - return width; - } - return 0.0; - } - - @override - double computeMaxIntrinsicWidth(double height) { - final double width = _getSize( - BoxConstraints.tightForFinite(height: height), - ).width; - if (width.isFinite) { - return width; - } - return 0.0; - } - - @override - double computeMinIntrinsicHeight(double width) { - final double height = _getSize( - BoxConstraints.tightForFinite(width: width), - ).height; - if (height.isFinite) { - return height; - } - return 0.0; - } - - @override - double computeMaxIntrinsicHeight(double width) { - final double height = _getSize( - BoxConstraints.tightForFinite(width: width), - ).height; - if (height.isFinite) { - return height; - } - return 0.0; - } - - @override - @protected - Size computeDryLayout(covariant BoxConstraints constraints) { - return _getSize(constraints); - } - - @override - void performLayout() { - size = _getSize(constraints); - delegate._callPerformLayout(size, firstChild); - } - - @override - void paint(PaintingContext context, Offset offset) { - defaultPaint(context, offset); - } - - @override - bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { - return defaultHitTestChildren(result, position: position); - } - - @override - bool get isRepaintBoundary => true; -} diff --git a/lib/common/widgets/image/custom_grid_view.dart b/lib/common/widgets/image/custom_grid_view.dart index 1ecad26f0..0b0f7cde1 100644 --- a/lib/common/widgets/image/custom_grid_view.dart +++ b/lib/common/widgets/image/custom_grid_view.dart @@ -20,7 +20,6 @@ import 'dart:math' show min; import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/widgets/badge.dart'; -import 'package:PiliPlus/common/widgets/flutter/custom_layout.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/models/common/badge_type.dart'; import 'package:PiliPlus/models/common/image_preview_type.dart'; @@ -33,6 +32,12 @@ import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:flutter/material.dart' hide CustomMultiChildLayout, MultiChildLayoutDelegate; +import 'package:flutter/rendering.dart' + show + ContainerRenderObjectMixin, + RenderBoxContainerDefaultsMixin, + MultiChildLayoutParentData, + BoxHitTestResult; import 'package:flutter/services.dart' show HapticFeedback; import 'package:get/get_core/src/get_main.dart'; import 'package:get/get_navigation/get_navigation.dart'; @@ -244,14 +249,12 @@ class CustomGridView extends StatelessWidget { child: SizedBox( width: maxWidth, height: imageHeight * row + space * (row - 1), - child: CustomMultiChildLayout( - delegate: _CustomGridViewDelegate( - space: space, - itemCount: length, - column: column, - width: imageWidth, - height: imageHeight, - ), + child: ImageGrid( + space: space, + itemCount: length, + column: column, + width: imageWidth, + height: imageHeight, children: List.generate(length, (index) { final item = picArr[index]; final radius = borderRadius(column, length, index); @@ -310,8 +313,10 @@ class CustomGridView extends StatelessWidget { } } -class _CustomGridViewDelegate extends MultiChildLayoutDelegate { - _CustomGridViewDelegate({ +class ImageGrid extends MultiChildRenderObjectWidget { + const ImageGrid({ + super.key, + super.children, required this.space, required this.itemCount, required this.column, @@ -326,31 +331,128 @@ class _CustomGridViewDelegate extends MultiChildLayoutDelegate { final double height; @override - void performLayout(Size size) { - final constraints = BoxConstraints( + RenderObject createRenderObject(BuildContext context) { + return RenderImageGrid( + space: space, + itemCount: itemCount, + column: column, + width: width, + height: height, + ); + } + + @override + void updateRenderObject(BuildContext context, RenderImageGrid renderObject) { + renderObject + ..space = space + ..itemCount = itemCount + ..column = column + ..width = width + ..height = height; + } +} + +class RenderImageGrid extends RenderBox + with + ContainerRenderObjectMixin, + RenderBoxContainerDefaultsMixin { + RenderImageGrid({ + required double space, + required int itemCount, + required int column, + required double width, + required double height, + }) : _space = space, + _itemCount = itemCount, + _column = column, + _width = width, + _height = height; + + double _space; + double get space => _space; + set space(double value) { + if (_space == value) return; + _space = value; + markNeedsPaint(); + } + + int _itemCount; + int get itemCount => _itemCount; + set itemCount(int value) { + if (_itemCount == value) return; + _itemCount = value; + markNeedsPaint(); + } + + int _column; + int get column => _column; + set column(int value) { + if (_space == value) return; + _column = value; + markNeedsPaint(); + } + + double _width; + double get width => _width; + set width(double value) { + if (_width == value) return; + _width = value; + markNeedsPaint(); + } + + double _height; + double get height => _height; + set height(double value) { + if (_height == value) return; + _height = value; + markNeedsPaint(); + } + + @override + void setupParentData(RenderBox child) { + if (child.parentData is! MultiChildLayoutParentData) { + child.parentData = MultiChildLayoutParentData(); + } + } + + @override + void performLayout() { + size = constraints.constrain(constraints.biggest); + + final itemConstraints = BoxConstraints( minWidth: width, maxWidth: width, minHeight: height, maxHeight: height, ); - for (int i = 0; i < itemCount; i++) { - layoutChild(i, constraints); - positionChild( - i, - Offset( - (space + width) * (i % column), - (space + height) * (i ~/ column), - ), + RenderBox? child = firstChild; + while (child != null) { + final childParentData = child.parentData as MultiChildLayoutParentData; + final index = childParentData.id as int; + child.layout(itemConstraints, parentUsesSize: true); + childParentData.offset = Offset( + (space + width) * (index % column), + (space + height) * (index ~/ column), ); + child = childParentData.nextSibling; } } @override - bool shouldRelayout(_CustomGridViewDelegate oldDelegate) { - return space != oldDelegate.space || - itemCount != oldDelegate.itemCount || - column != oldDelegate.column || - width != oldDelegate.width || - height != oldDelegate.height; + void paint(PaintingContext context, Offset offset) { + RenderBox? child = firstChild; + while (child != null) { + final childParentData = child.parentData as MultiChildLayoutParentData; + context.paintChild(child, childParentData.offset + offset); + child = childParentData.nextSibling; + } } + + @override + bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { + return defaultHitTestChildren(result, position: position); + } + + @override + bool get isRepaintBoundary => true; }