mirror of
https://github.com/HChaZZY/PiliPlus.git
synced 2025-12-06 09:13:48 +08:00
feat: new img grid view
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
462
lib/common/widgets/custom_layout.dart
Normal file
462
lib/common/widgets/custom_layout.dart
Normal file
@@ -0,0 +1,462 @@
|
|||||||
|
// 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<Object, RenderBox>? _idToChild;
|
||||||
|
Set<RenderBox>? _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(<DiagnosticsNode>[
|
||||||
|
ErrorSummary(
|
||||||
|
'The $this custom multichild layout delegate provided invalid box constraints for the child with id "$childId".',
|
||||||
|
),
|
||||||
|
DiagnosticsProperty<AssertionError>(
|
||||||
|
'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<RenderBox>('${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<Object, RenderBox>? previousIdToChild = _idToChild;
|
||||||
|
|
||||||
|
Set<RenderBox>? debugPreviousChildrenNeedingLayout;
|
||||||
|
assert(() {
|
||||||
|
debugPreviousChildrenNeedingLayout = _debugChildrenNeedingLayout;
|
||||||
|
_debugChildrenNeedingLayout = <RenderBox>{};
|
||||||
|
return true;
|
||||||
|
}());
|
||||||
|
|
||||||
|
try {
|
||||||
|
_idToChild = <Object, RenderBox>{};
|
||||||
|
RenderBox? child = firstChild;
|
||||||
|
while (child != null) {
|
||||||
|
final MultiChildLayoutParentData childParentData =
|
||||||
|
child.parentData! as MultiChildLayoutParentData;
|
||||||
|
assert(() {
|
||||||
|
if (childParentData.id == null) {
|
||||||
|
throw FlutterError.fromParts(<DiagnosticsNode>[
|
||||||
|
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(<DiagnosticsNode>[
|
||||||
|
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<DiagnosticsNode>(_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<RenderBox, MultiChildLayoutParentData>,
|
||||||
|
RenderBoxContainerDefaultsMixin<RenderBox, MultiChildLayoutParentData> {
|
||||||
|
/// Creates a render object that customizes the layout of multiple children.
|
||||||
|
RenderCustomMultiChildLayoutBox({
|
||||||
|
List<RenderBox>? 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;
|
||||||
|
}
|
||||||
274
lib/common/widgets/image/custom_grid_view.dart
Normal file
274
lib/common/widgets/image/custom_grid_view.dart
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of PiliPlus
|
||||||
|
*
|
||||||
|
* PiliPlus is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* PiliPlus is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with PiliPlus. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'dart:math' show min;
|
||||||
|
|
||||||
|
import 'package:PiliPlus/common/constants.dart';
|
||||||
|
import 'package:PiliPlus/common/widgets/badge.dart';
|
||||||
|
import 'package:PiliPlus/common/widgets/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';
|
||||||
|
import 'package:PiliPlus/utils/extension.dart';
|
||||||
|
import 'package:PiliPlus/utils/page_utils.dart';
|
||||||
|
import 'package:PiliPlus/utils/storage_pref.dart';
|
||||||
|
import 'package:flutter/material.dart'
|
||||||
|
hide CustomMultiChildLayout, MultiChildLayoutDelegate;
|
||||||
|
|
||||||
|
class ImageModel {
|
||||||
|
ImageModel({
|
||||||
|
required num? width,
|
||||||
|
required num? height,
|
||||||
|
required this.url,
|
||||||
|
this.liveUrl,
|
||||||
|
}) {
|
||||||
|
this.width = width == null || width == 0 ? 1 : width;
|
||||||
|
this.height = height == null || height == 0 ? 1 : height;
|
||||||
|
}
|
||||||
|
|
||||||
|
late num width;
|
||||||
|
late num height;
|
||||||
|
String url;
|
||||||
|
String? liveUrl;
|
||||||
|
bool? _isLongPic;
|
||||||
|
bool? _isLivePhoto;
|
||||||
|
|
||||||
|
bool get isLongPic => _isLongPic ??= (height / width) > _maxRatio;
|
||||||
|
bool get isLivePhoto =>
|
||||||
|
_isLivePhoto ??= enableLivePhoto && liveUrl?.isNotEmpty == true;
|
||||||
|
|
||||||
|
static bool enableLivePhoto = Pref.enableLivePhoto;
|
||||||
|
}
|
||||||
|
|
||||||
|
const double _maxRatio = 22 / 9;
|
||||||
|
|
||||||
|
class CustomGridView extends StatelessWidget {
|
||||||
|
const CustomGridView({
|
||||||
|
super.key,
|
||||||
|
this.space = 5,
|
||||||
|
required this.maxWidth,
|
||||||
|
required this.picArr,
|
||||||
|
this.onViewImage,
|
||||||
|
this.onDismissed,
|
||||||
|
this.callback,
|
||||||
|
});
|
||||||
|
|
||||||
|
final double maxWidth;
|
||||||
|
final double space;
|
||||||
|
final List<ImageModel> picArr;
|
||||||
|
final VoidCallback? onViewImage;
|
||||||
|
final ValueChanged<int>? onDismissed;
|
||||||
|
final Function(List<String>, int)? callback;
|
||||||
|
|
||||||
|
void onTap(int index) {
|
||||||
|
if (callback != null) {
|
||||||
|
callback!(picArr.map((item) => item.url).toList(), index);
|
||||||
|
} else {
|
||||||
|
onViewImage?.call();
|
||||||
|
PageUtils.imageView(
|
||||||
|
initialPage: index,
|
||||||
|
imgList: picArr.map(
|
||||||
|
(item) {
|
||||||
|
bool isLive = item.isLivePhoto;
|
||||||
|
return SourceModel(
|
||||||
|
sourceType: isLive
|
||||||
|
? SourceType.livePhoto
|
||||||
|
: SourceType.networkImage,
|
||||||
|
url: item.url,
|
||||||
|
liveUrl: isLive ? item.liveUrl : null,
|
||||||
|
width: isLive ? item.width.toInt() : null,
|
||||||
|
height: isLive ? item.height.toInt() : null,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
).toList(),
|
||||||
|
onDismissed: onDismissed,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static BorderRadius borderRadius(
|
||||||
|
int col,
|
||||||
|
int length,
|
||||||
|
int index, {
|
||||||
|
Radius r = StyleString.imgRadius,
|
||||||
|
}) {
|
||||||
|
if (length == 1) return StyleString.mdRadius;
|
||||||
|
|
||||||
|
final bool hasUp = index - col >= 0;
|
||||||
|
final bool hasDown = index + col < length;
|
||||||
|
|
||||||
|
final bool isRowStart = (index % col) == 0;
|
||||||
|
final bool isRowEnd = (index % col) == col - 1 || index == length - 1;
|
||||||
|
|
||||||
|
final bool hasLeft = !isRowStart;
|
||||||
|
final bool hasRight = !isRowEnd && (index + 1) < length;
|
||||||
|
|
||||||
|
return BorderRadius.only(
|
||||||
|
topLeft: !hasUp && !hasLeft ? r : Radius.zero,
|
||||||
|
topRight: !hasUp && !hasRight ? r : Radius.zero,
|
||||||
|
bottomLeft: !hasDown && !hasLeft ? r : Radius.zero,
|
||||||
|
bottomRight: !hasDown && !hasRight ? r : Radius.zero,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
double imageWidth;
|
||||||
|
double imageHeight;
|
||||||
|
final length = picArr.length;
|
||||||
|
final isSingle = length == 1;
|
||||||
|
final isFour = length == 4;
|
||||||
|
if (length == 2) {
|
||||||
|
imageWidth = imageHeight = (maxWidth - space) / 2;
|
||||||
|
} else {
|
||||||
|
imageHeight = imageWidth = (maxWidth - 2 * space) / 3;
|
||||||
|
if (isSingle) {
|
||||||
|
final img = picArr.first;
|
||||||
|
final width = img.width;
|
||||||
|
final height = img.height;
|
||||||
|
final ratioWH = width / height;
|
||||||
|
final ratioHW = height / width;
|
||||||
|
imageWidth = ratioWH > 1.5
|
||||||
|
? maxWidth
|
||||||
|
: (ratioWH >= 1 || (height > width && ratioHW < 1.5))
|
||||||
|
? 2 * imageWidth
|
||||||
|
: 1.5 * imageWidth;
|
||||||
|
if (width != 1) {
|
||||||
|
imageWidth = min(imageWidth, width.toDouble());
|
||||||
|
}
|
||||||
|
imageHeight = imageWidth * min(ratioHW, _maxRatio);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final int column = isFour ? 2 : 3;
|
||||||
|
final int row = isFour ? 2 : (length / 3).ceil();
|
||||||
|
late final placeHolder = Container(
|
||||||
|
width: imageWidth,
|
||||||
|
height: imageHeight,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.onInverseSurface.withValues(alpha: 0.4),
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/images/loading.png',
|
||||||
|
width: imageWidth,
|
||||||
|
height: imageHeight,
|
||||||
|
cacheWidth: imageWidth.cacheSize(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 6),
|
||||||
|
child: SizedBox(
|
||||||
|
width: maxWidth,
|
||||||
|
height: imageHeight * row + space * (row - 1),
|
||||||
|
child: CustomMultiChildLayout(
|
||||||
|
delegate: _CustomGridViewDelegate(
|
||||||
|
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);
|
||||||
|
return LayoutId(
|
||||||
|
id: index,
|
||||||
|
child: Hero(
|
||||||
|
tag: item.url,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => onTap(index),
|
||||||
|
child: Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: radius,
|
||||||
|
child: NetworkImgLayer(
|
||||||
|
radius: 0,
|
||||||
|
src: item.url,
|
||||||
|
width: imageWidth,
|
||||||
|
height: imageHeight,
|
||||||
|
isLongPic: item.isLongPic,
|
||||||
|
forceUseCacheWidth: item.width <= item.height,
|
||||||
|
getPlaceHolder: () => placeHolder,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (item.isLivePhoto)
|
||||||
|
const PBadge(
|
||||||
|
text: 'Live',
|
||||||
|
right: 8,
|
||||||
|
bottom: 8,
|
||||||
|
type: PBadgeType.gray,
|
||||||
|
)
|
||||||
|
else if (item.isLongPic)
|
||||||
|
const PBadge(
|
||||||
|
text: '长图',
|
||||||
|
right: 8,
|
||||||
|
bottom: 8,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CustomGridViewDelegate extends MultiChildLayoutDelegate {
|
||||||
|
_CustomGridViewDelegate({
|
||||||
|
required this.space,
|
||||||
|
required this.itemCount,
|
||||||
|
required this.column,
|
||||||
|
required this.width,
|
||||||
|
required this.height,
|
||||||
|
});
|
||||||
|
|
||||||
|
final double space;
|
||||||
|
final int itemCount;
|
||||||
|
final int column;
|
||||||
|
final double width;
|
||||||
|
final double height;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void performLayout(Size size) {
|
||||||
|
final constraints = BoxConstraints.loose(Size(width, height));
|
||||||
|
for (int i = 0; i < itemCount; i++) {
|
||||||
|
layoutChild(i, constraints);
|
||||||
|
positionChild(
|
||||||
|
i,
|
||||||
|
Offset(
|
||||||
|
(space + width) * (i % column),
|
||||||
|
(space + height) * (i ~/ column),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRelayout(_CustomGridViewDelegate oldDelegate) {
|
||||||
|
return space != oldDelegate.space || itemCount != oldDelegate.itemCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,200 +0,0 @@
|
|||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:PiliPlus/common/constants.dart';
|
|
||||||
import 'package:PiliPlus/common/widgets/badge.dart';
|
|
||||||
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
|
|
||||||
import 'package:PiliPlus/common/widgets/image/nine_grid_view.dart';
|
|
||||||
import 'package:PiliPlus/models/common/badge_type.dart';
|
|
||||||
import 'package:PiliPlus/models/common/image_preview_type.dart';
|
|
||||||
import 'package:PiliPlus/utils/extension.dart';
|
|
||||||
import 'package:PiliPlus/utils/page_utils.dart';
|
|
||||||
import 'package:PiliPlus/utils/storage_pref.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class ImageModel {
|
|
||||||
ImageModel({
|
|
||||||
required num? width,
|
|
||||||
required num? height,
|
|
||||||
required this.url,
|
|
||||||
this.liveUrl,
|
|
||||||
}) {
|
|
||||||
this.width = width == null || width == 0 ? 1 : width;
|
|
||||||
this.height = height == null || height == 0 ? 1 : height;
|
|
||||||
}
|
|
||||||
|
|
||||||
late num width;
|
|
||||||
late num height;
|
|
||||||
String url;
|
|
||||||
String? liveUrl;
|
|
||||||
bool? _isLongPic;
|
|
||||||
bool? _isLivePhoto;
|
|
||||||
|
|
||||||
bool get isLongPic => _isLongPic ??= (height / width) > _maxRatio;
|
|
||||||
bool get isLivePhoto =>
|
|
||||||
_isLivePhoto ??= enableLivePhoto && liveUrl?.isNotEmpty == true;
|
|
||||||
|
|
||||||
static bool enableLivePhoto = Pref.enableLivePhoto;
|
|
||||||
}
|
|
||||||
|
|
||||||
const double _maxRatio = 22 / 9;
|
|
||||||
|
|
||||||
Widget imageView(
|
|
||||||
double maxWidth,
|
|
||||||
List<ImageModel> picArr, {
|
|
||||||
VoidCallback? onViewImage,
|
|
||||||
ValueChanged<int>? onDismissed,
|
|
||||||
Function(List<String>, int)? callback,
|
|
||||||
}) {
|
|
||||||
double imageWidth = (maxWidth - 10) / 3;
|
|
||||||
double imageHeight = imageWidth;
|
|
||||||
if (picArr.length == 1) {
|
|
||||||
num width = picArr[0].width;
|
|
||||||
num height = picArr[0].height;
|
|
||||||
double ratioWH = width / height;
|
|
||||||
double ratioHW = height / width;
|
|
||||||
imageWidth = ratioWH > 1.5
|
|
||||||
? maxWidth
|
|
||||||
: (ratioWH >= 1 || (height > width && ratioHW < 1.5))
|
|
||||||
? 2 * imageWidth
|
|
||||||
: 1.5 * imageWidth;
|
|
||||||
if (width != 1) {
|
|
||||||
imageWidth = min(imageWidth, width.toDouble());
|
|
||||||
}
|
|
||||||
imageHeight = imageWidth * min(ratioHW, _maxRatio);
|
|
||||||
} else if (picArr.length == 2) {
|
|
||||||
imageWidth = imageHeight = 2 * imageWidth;
|
|
||||||
}
|
|
||||||
late final int row = picArr.length == 4 ? 2 : 3;
|
|
||||||
BorderRadius borderRadius(index) {
|
|
||||||
if (picArr.length == 1) {
|
|
||||||
return StyleString.mdRadius;
|
|
||||||
}
|
|
||||||
return BorderRadius.only(
|
|
||||||
topLeft:
|
|
||||||
index - row >= 0 ||
|
|
||||||
((index - 1) >= 0 && (index - 1) % row < index % row)
|
|
||||||
? Radius.zero
|
|
||||||
: StyleString.imgRadius,
|
|
||||||
topRight:
|
|
||||||
index - row >= 0 ||
|
|
||||||
((index + 1) < picArr.length && (index + 1) % row > index % row)
|
|
||||||
? Radius.zero
|
|
||||||
: StyleString.imgRadius,
|
|
||||||
bottomLeft:
|
|
||||||
index + row < picArr.length ||
|
|
||||||
((index - 1) >= 0 && (index - 1) % row < index % row)
|
|
||||||
? Radius.zero
|
|
||||||
: StyleString.imgRadius,
|
|
||||||
bottomRight:
|
|
||||||
index + row < picArr.length ||
|
|
||||||
((index + 1) < picArr.length && (index + 1) % row > index % row)
|
|
||||||
? Radius.zero
|
|
||||||
: StyleString.imgRadius,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
int parseSize(size) {
|
|
||||||
return switch (size) {
|
|
||||||
int() => size,
|
|
||||||
double() => size.round(),
|
|
||||||
String() => int.tryParse(size) ?? 1,
|
|
||||||
_ => 1,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
void onTap(int index) {
|
|
||||||
if (callback != null) {
|
|
||||||
callback(picArr.map((item) => item.url).toList(), index);
|
|
||||||
} else {
|
|
||||||
onViewImage?.call();
|
|
||||||
PageUtils.imageView(
|
|
||||||
initialPage: index,
|
|
||||||
imgList: picArr.map(
|
|
||||||
(item) {
|
|
||||||
bool isLive = item.isLivePhoto;
|
|
||||||
return SourceModel(
|
|
||||||
sourceType: isLive
|
|
||||||
? SourceType.livePhoto
|
|
||||||
: SourceType.networkImage,
|
|
||||||
url: item.url,
|
|
||||||
liveUrl: isLive ? item.liveUrl : null,
|
|
||||||
width: isLive ? parseSize(item.width) : null,
|
|
||||||
height: isLive ? parseSize(item.height) : null,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
).toList(),
|
|
||||||
onDismissed: onDismissed,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return NineGridView(
|
|
||||||
type: NineGridType.weiBo,
|
|
||||||
margin: const EdgeInsets.only(top: 6),
|
|
||||||
bigImageWidth: imageWidth,
|
|
||||||
bigImageHeight: imageHeight,
|
|
||||||
space: 5,
|
|
||||||
height: picArr.length == 1 ? imageHeight : null,
|
|
||||||
width: picArr.length == 1 ? imageWidth : maxWidth,
|
|
||||||
itemCount: picArr.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final item = picArr[index];
|
|
||||||
return Hero(
|
|
||||||
tag: item.url,
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: () => onTap(index),
|
|
||||||
child: Stack(
|
|
||||||
clipBehavior: Clip.none,
|
|
||||||
alignment: Alignment.center,
|
|
||||||
children: [
|
|
||||||
ClipRRect(
|
|
||||||
borderRadius: borderRadius(index),
|
|
||||||
child: NetworkImgLayer(
|
|
||||||
radius: 0,
|
|
||||||
src: item.url,
|
|
||||||
width: imageWidth,
|
|
||||||
height: imageHeight,
|
|
||||||
isLongPic: item.isLongPic,
|
|
||||||
forceUseCacheWidth: item.width <= item.height,
|
|
||||||
getPlaceHolder: () {
|
|
||||||
return Container(
|
|
||||||
width: imageWidth,
|
|
||||||
height: imageHeight,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.onInverseSurface.withValues(alpha: 0.4),
|
|
||||||
borderRadius: borderRadius(index),
|
|
||||||
),
|
|
||||||
child: Center(
|
|
||||||
child: Image.asset(
|
|
||||||
'assets/images/loading.png',
|
|
||||||
width: imageWidth,
|
|
||||||
height: imageHeight,
|
|
||||||
cacheWidth: imageWidth.cacheSize(context),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (item.isLivePhoto)
|
|
||||||
const PBadge(
|
|
||||||
text: 'Live',
|
|
||||||
right: 8,
|
|
||||||
bottom: 8,
|
|
||||||
type: PBadgeType.gray,
|
|
||||||
)
|
|
||||||
else if (item.isLongPic)
|
|
||||||
const PBadge(
|
|
||||||
text: '长图',
|
|
||||||
right: 8,
|
|
||||||
bottom: 8,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,595 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'dart:collection';
|
|
||||||
import 'dart:math' as math;
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author: Sky24n
|
|
||||||
* @GitHub: https://github.com/Sky24n
|
|
||||||
* @Description: NineGridView.
|
|
||||||
* @Date: 2020/06/16
|
|
||||||
*/
|
|
||||||
|
|
||||||
/// NineGridView Type.
|
|
||||||
enum NineGridType {
|
|
||||||
/// normal NineGridView.
|
|
||||||
normal,
|
|
||||||
|
|
||||||
/// like WeChat NineGridView.
|
|
||||||
weChat,
|
|
||||||
|
|
||||||
/// like WeiBo International NineGridView.
|
|
||||||
weiBo,
|
|
||||||
|
|
||||||
/// like WeChat group.
|
|
||||||
weChatGp,
|
|
||||||
|
|
||||||
/// like DingTalk group.
|
|
||||||
dingTalkGp,
|
|
||||||
|
|
||||||
/// like QQ group.
|
|
||||||
qqGp,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// big images size cache map.
|
|
||||||
Map<String, Rect> ngvBigImageSizeMap = HashMap();
|
|
||||||
|
|
||||||
/// NineGridView.
|
|
||||||
/// like WeChat, WeiBo International, WeChat group, DingTalk group, QQ group.
|
|
||||||
///
|
|
||||||
/// Another [NineGridView](https://github.com/flutterchina/flukit) in [flukit](https://github.com/flutterchina/flukit) UI Kit,using GridView implementation。
|
|
||||||
class NineGridView extends StatefulWidget {
|
|
||||||
/// create NineGridView.
|
|
||||||
/// If you want to show a single big picture.
|
|
||||||
/// It is recommended to use a medium-quality picture, because the original picture is too large and takes time to load.
|
|
||||||
/// 单张大图建议使用中等质量图片,因为原图太大加载耗时。
|
|
||||||
/// you need input (bigImageWidth + bigImageHeight) or (bigImage + bigImageUrl).
|
|
||||||
const NineGridView({
|
|
||||||
super.key,
|
|
||||||
this.width,
|
|
||||||
this.height,
|
|
||||||
this.space = 3,
|
|
||||||
this.arcAngle = 0,
|
|
||||||
this.initIndex = 1,
|
|
||||||
this.padding = EdgeInsets.zero,
|
|
||||||
this.margin = EdgeInsets.zero,
|
|
||||||
this.alignment,
|
|
||||||
this.color,
|
|
||||||
this.decoration,
|
|
||||||
this.type = NineGridType.weChat,
|
|
||||||
required this.itemCount,
|
|
||||||
required this.itemBuilder,
|
|
||||||
this.bigImageWidth,
|
|
||||||
this.bigImageHeight,
|
|
||||||
this.bigImage,
|
|
||||||
this.bigImageUrl,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// View width.
|
|
||||||
final double? width;
|
|
||||||
|
|
||||||
/// View height.
|
|
||||||
final double? height;
|
|
||||||
|
|
||||||
/// The number of logical pixels between each child.
|
|
||||||
final double space;
|
|
||||||
|
|
||||||
/// QQ group arc angle (0 ~ 180).
|
|
||||||
final double arcAngle;
|
|
||||||
|
|
||||||
/// QQ group init index (0 or 1). def 1.
|
|
||||||
final int initIndex;
|
|
||||||
|
|
||||||
/// View padding.
|
|
||||||
final EdgeInsets padding;
|
|
||||||
|
|
||||||
/// View margin.
|
|
||||||
final EdgeInsets margin;
|
|
||||||
|
|
||||||
/// Align the [child] within the container.
|
|
||||||
final AlignmentGeometry? alignment;
|
|
||||||
|
|
||||||
/// The color to paint behind the [child].
|
|
||||||
final Color? color;
|
|
||||||
|
|
||||||
/// The decoration to paint behind the [child].
|
|
||||||
final Decoration? decoration;
|
|
||||||
|
|
||||||
/// NineGridView type.
|
|
||||||
final NineGridType type;
|
|
||||||
|
|
||||||
/// The total number of children this delegate can provide.
|
|
||||||
final int itemCount;
|
|
||||||
|
|
||||||
/// Called to build children for the view.
|
|
||||||
final IndexedWidgetBuilder itemBuilder;
|
|
||||||
|
|
||||||
/// Single big picture width.
|
|
||||||
final double? bigImageWidth;
|
|
||||||
|
|
||||||
/// Single big picture height.
|
|
||||||
final double? bigImageHeight;
|
|
||||||
|
|
||||||
/// It is recommended to use a medium-quality picture, because the original picture is too large and takes time to load.
|
|
||||||
/// 单张大图建议使用中等质量图片,因为原图太大加载耗时。
|
|
||||||
/// Single big picture Image.
|
|
||||||
final Image? bigImage;
|
|
||||||
|
|
||||||
/// Single big picture url.
|
|
||||||
final String? bigImageUrl;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<StatefulWidget> createState() {
|
|
||||||
return _NineGridViewState();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// _NineGridViewState.
|
|
||||||
class _NineGridViewState extends State<NineGridView> {
|
|
||||||
/// init view size.
|
|
||||||
Rect _initSize(BuildContext context) {
|
|
||||||
EdgeInsets padding = widget.padding;
|
|
||||||
if (widget.itemCount == 0) {
|
|
||||||
return Rect.fromLTRB(0, 0, padding.horizontal, padding.vertical);
|
|
||||||
}
|
|
||||||
double width =
|
|
||||||
widget.width ??
|
|
||||||
(MediaQuery.sizeOf(context).width - widget.margin.horizontal);
|
|
||||||
width = width - padding.horizontal;
|
|
||||||
double space = widget.space;
|
|
||||||
double itemW;
|
|
||||||
if (widget.type == NineGridType.weiBo &&
|
|
||||||
(widget.itemCount == 1 || widget.itemCount == 2)) {
|
|
||||||
// || itemCount == 4
|
|
||||||
itemW = (width - space) / 2;
|
|
||||||
} else {
|
|
||||||
itemW = (width - space * 2) / 3;
|
|
||||||
}
|
|
||||||
bool fourGrid =
|
|
||||||
(widget.itemCount == 4 && widget.type != NineGridType.normal);
|
|
||||||
int column = fourGrid ? 2 : math.min(3, widget.itemCount);
|
|
||||||
int row = fourGrid ? 2 : (widget.itemCount / 3).ceil();
|
|
||||||
double realWidth =
|
|
||||||
itemW * column + space * (column - 1) + padding.horizontal;
|
|
||||||
double realHeight = itemW * row + space * (row - 1) + padding.vertical;
|
|
||||||
return Rect.fromLTRB(itemW, 0, realWidth, realHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// build nine grid view.
|
|
||||||
Widget _buildChild(BuildContext context, double itemW) {
|
|
||||||
double space = widget.space;
|
|
||||||
int column = (widget.itemCount == 4 && widget.type != NineGridType.normal)
|
|
||||||
? 2
|
|
||||||
: 3;
|
|
||||||
List<Widget> list = [];
|
|
||||||
for (int i = 0; i < widget.itemCount; i++) {
|
|
||||||
list.add(
|
|
||||||
Positioned(
|
|
||||||
top: (space + itemW) * (i ~/ column),
|
|
||||||
left: (space + itemW) * (i % column),
|
|
||||||
child: SizedBox(
|
|
||||||
width: itemW,
|
|
||||||
height: itemW,
|
|
||||||
child: widget.itemBuilder(context, i),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return Stack(
|
|
||||||
clipBehavior: Clip.none,
|
|
||||||
children: list,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// build one child.
|
|
||||||
Widget? _buildOneChild(BuildContext context) {
|
|
||||||
double? bigImgWidth = widget.bigImageWidth?.toDouble();
|
|
||||||
double? bigImgHeight = widget.bigImageHeight?.toDouble();
|
|
||||||
if (!_isZero(bigImgWidth) && !_isZero(bigImgHeight)) {
|
|
||||||
return _getOneChild(context, bigImgWidth!, bigImgHeight!);
|
|
||||||
} else if (widget.bigImage != null) {
|
|
||||||
String bigImageUrl = widget.bigImageUrl!;
|
|
||||||
Rect? bigImgRect = ngvBigImageSizeMap[bigImageUrl];
|
|
||||||
bigImgWidth = bigImgRect?.width;
|
|
||||||
bigImgHeight = bigImgRect?.height;
|
|
||||||
if (!_isZero(bigImgWidth) && !_isZero(bigImgHeight)) {
|
|
||||||
return _getOneChild(context, bigImgWidth!, bigImgHeight!);
|
|
||||||
} else {
|
|
||||||
_ImageUtil()
|
|
||||||
.getImageSize(widget.bigImage)
|
|
||||||
?.then((rect) {
|
|
||||||
ngvBigImageSizeMap[bigImageUrl] = rect;
|
|
||||||
if (!mounted) return;
|
|
||||||
setState(() {});
|
|
||||||
})
|
|
||||||
.catchError((e) {});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// get one child.
|
|
||||||
Widget _getOneChild(BuildContext context, double width, double height) {
|
|
||||||
Rect rect = _getBigImgSize(width, height);
|
|
||||||
return SizedBox(
|
|
||||||
width: rect.width,
|
|
||||||
height: rect.height,
|
|
||||||
child: widget.itemBuilder(context, 0),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// build weChat group.
|
|
||||||
Widget _buildWeChatGroup(BuildContext context) {
|
|
||||||
double width = widget.width! - widget.padding.horizontal;
|
|
||||||
double space = widget.space;
|
|
||||||
double itemW;
|
|
||||||
|
|
||||||
int column = widget.itemCount < 5 ? 2 : 3;
|
|
||||||
int row = 0;
|
|
||||||
if (widget.itemCount == 1) {
|
|
||||||
row = 1;
|
|
||||||
itemW = width;
|
|
||||||
} else if (widget.itemCount < 5) {
|
|
||||||
row = widget.itemCount == 2 ? 1 : 2;
|
|
||||||
itemW = (width - space) / 2;
|
|
||||||
} else if (widget.itemCount < 7) {
|
|
||||||
row = 2;
|
|
||||||
itemW = (width - space * 2) / 3;
|
|
||||||
} else {
|
|
||||||
row = 3;
|
|
||||||
itemW = (width - space * 2) / 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
int first = widget.itemCount % column;
|
|
||||||
List<Widget> list = [];
|
|
||||||
for (int i = 0; i < widget.itemCount; i++) {
|
|
||||||
double left;
|
|
||||||
if (first > 0 && i < first) {
|
|
||||||
left =
|
|
||||||
(width - itemW * first - space * (first - 1)) / 2 +
|
|
||||||
(itemW + space) * i;
|
|
||||||
} else {
|
|
||||||
left = (space + itemW) * ((i - first) % column);
|
|
||||||
}
|
|
||||||
|
|
||||||
int itemIndex = (first > 0 && i < first)
|
|
||||||
? 0
|
|
||||||
: (first > 0 ? (i + column - first) : i) ~/ column;
|
|
||||||
|
|
||||||
double top =
|
|
||||||
(width - itemW * row - space * (row - 1)) / 2 +
|
|
||||||
(space + itemW) * itemIndex;
|
|
||||||
|
|
||||||
list.add(
|
|
||||||
Positioned(
|
|
||||||
top: top,
|
|
||||||
left: left,
|
|
||||||
child: SizedBox(
|
|
||||||
width: itemW,
|
|
||||||
height: itemW,
|
|
||||||
child: widget.itemBuilder(context, i),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return Stack(
|
|
||||||
clipBehavior: Clip.none,
|
|
||||||
children: list,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// build dingTalk group.
|
|
||||||
Widget _buildDingTalkGroup(BuildContext context) {
|
|
||||||
double width = widget.width! - widget.padding.horizontal;
|
|
||||||
int itemCount = math.min(4, widget.itemCount);
|
|
||||||
double itemW = (width - widget.space) / 2;
|
|
||||||
List<Widget> children = [];
|
|
||||||
for (int i = 0; i < itemCount; i++) {
|
|
||||||
children.add(
|
|
||||||
Positioned(
|
|
||||||
top: (widget.space + itemW) * (i ~/ 2),
|
|
||||||
left:
|
|
||||||
(widget.space + itemW) *
|
|
||||||
(((itemCount == 3 && i == 2) ? i + 1 : i) % 2),
|
|
||||||
child: SizedBox(
|
|
||||||
width: itemCount == 1 ? width : itemW,
|
|
||||||
height:
|
|
||||||
(itemCount == 1 || itemCount == 2 || (itemCount == 3 && i == 0))
|
|
||||||
? width
|
|
||||||
: itemW,
|
|
||||||
child: widget.itemBuilder(context, i),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return ClipOval(
|
|
||||||
child: Stack(
|
|
||||||
clipBehavior: Clip.none,
|
|
||||||
children: children,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// build QQ group.
|
|
||||||
Widget _buildQQGroup(BuildContext context) {
|
|
||||||
double width = widget.width! - widget.padding.horizontal;
|
|
||||||
int itemCount = math.min(5, widget.itemCount);
|
|
||||||
if (itemCount == 1) {
|
|
||||||
return ClipOval(
|
|
||||||
child: SizedBox(
|
|
||||||
width: width,
|
|
||||||
height: width,
|
|
||||||
child: widget.itemBuilder(context, 0),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Widget> children = [];
|
|
||||||
double startDegree = 0;
|
|
||||||
double r = 0;
|
|
||||||
double r1 = 0;
|
|
||||||
double centerX = width / 2;
|
|
||||||
double centerY = width / 2;
|
|
||||||
switch (itemCount) {
|
|
||||||
case 2:
|
|
||||||
startDegree = 135;
|
|
||||||
r = width / (2 + 2 * math.sin(math.pi / 4));
|
|
||||||
r1 = r;
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
startDegree = 210;
|
|
||||||
r = width / (2 + 4 * math.sin(math.pi * (3 - 2) / (2 * 3)));
|
|
||||||
r1 = r / math.cos(math.pi * (3 - 2) / (2 * 3));
|
|
||||||
double R =
|
|
||||||
r *
|
|
||||||
(1 + math.sin(math.pi / itemCount)) /
|
|
||||||
math.sin(math.pi / itemCount);
|
|
||||||
double dy = 0.5 * (width - R - r * (1 + 1 / math.tan(math.pi / 3)));
|
|
||||||
centerY = dy + r + r1;
|
|
||||||
break;
|
|
||||||
case 4:
|
|
||||||
startDegree = 180;
|
|
||||||
r = width / 4;
|
|
||||||
r1 = r / math.cos(math.pi / 4);
|
|
||||||
break;
|
|
||||||
case 5:
|
|
||||||
startDegree = 126;
|
|
||||||
r = width / (2 + 4 * math.sin(math.pi * (5 - 2) / (2 * 5)));
|
|
||||||
r1 = r / math.cos(math.pi * (5 - 2) / (2 * 5));
|
|
||||||
double R =
|
|
||||||
r *
|
|
||||||
(1 + math.sin(math.pi / itemCount)) /
|
|
||||||
math.sin(math.pi / itemCount);
|
|
||||||
double dy = 0.5 * (width - R - r * (1 + 1 / math.tan(math.pi / 5)));
|
|
||||||
centerY = dy + r + r1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < itemCount; i++) {
|
|
||||||
double degree1 = (itemCount == 2 || itemCount == 4) ? (-math.pi / 4) : 0;
|
|
||||||
double x = centerX + r1 * math.sin(degree1 + i * 2 * math.pi / itemCount);
|
|
||||||
double y = centerY - r1 * math.cos(degree1 + i * 2 * math.pi / itemCount);
|
|
||||||
|
|
||||||
double degree = startDegree + i * 2 * 180 / itemCount;
|
|
||||||
if (degree >= 360) degree = degree % 360;
|
|
||||||
double previousX = r + 2 * r * math.sin(degree / 180 * math.pi);
|
|
||||||
double previousY = r - 2 * r * math.cos(degree / 180 * math.pi);
|
|
||||||
|
|
||||||
Widget child = Positioned.fromRect(
|
|
||||||
rect: Rect.fromCircle(center: Offset(x, y), radius: r),
|
|
||||||
child: ClipPath(
|
|
||||||
clipper: QQClipper(
|
|
||||||
total: itemCount,
|
|
||||||
index: i,
|
|
||||||
initIndex: widget.initIndex,
|
|
||||||
previousX: previousX,
|
|
||||||
previousY: previousY,
|
|
||||||
degree: degree,
|
|
||||||
arcAngle: widget.arcAngle,
|
|
||||||
space: widget.space,
|
|
||||||
),
|
|
||||||
child: widget.itemBuilder(context, i),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
children.add(child);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Stack(
|
|
||||||
clipBehavior: Clip.none,
|
|
||||||
children: children,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// double is zero.
|
|
||||||
bool _isZero(double? value) {
|
|
||||||
return value == null || value == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// get big image size.
|
|
||||||
Rect _getBigImgSize(double originalWidth, double originalHeight) {
|
|
||||||
double width =
|
|
||||||
widget.width ??
|
|
||||||
(MediaQuery.sizeOf(context).width - widget.margin.horizontal);
|
|
||||||
width = width - widget.padding.horizontal;
|
|
||||||
double itemW = (width - widget.space * 2) / 3;
|
|
||||||
|
|
||||||
// double devicePixelRatio = MediaQuery.devicePixelRatioOf(context);
|
|
||||||
double devicePixelRatio = 1.0;
|
|
||||||
double tempWidth = originalWidth / devicePixelRatio;
|
|
||||||
double tempHeight = originalHeight / devicePixelRatio;
|
|
||||||
double maxW = itemW * 2 + widget.space;
|
|
||||||
double minW = width / 2;
|
|
||||||
|
|
||||||
double relWidth = tempWidth >= maxW ? maxW : math.max(minW, tempWidth);
|
|
||||||
|
|
||||||
double relHeight;
|
|
||||||
double ratio = tempWidth / tempHeight;
|
|
||||||
if (tempWidth == tempHeight) {
|
|
||||||
relHeight = relWidth;
|
|
||||||
} else if (tempWidth > tempHeight) {
|
|
||||||
relHeight = relWidth / (math.min(ratio, 4 / 3));
|
|
||||||
} else {
|
|
||||||
relHeight = relWidth / (math.max(ratio, 3 / 4));
|
|
||||||
}
|
|
||||||
return Rect.fromLTRB(0, 0, relWidth, relHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
Widget? child;
|
|
||||||
double? realWidth = widget.width;
|
|
||||||
double? realHeight = widget.height;
|
|
||||||
switch (widget.type) {
|
|
||||||
case NineGridType.normal:
|
|
||||||
case NineGridType.weiBo:
|
|
||||||
case NineGridType.weChat:
|
|
||||||
Rect size = _initSize(context);
|
|
||||||
if (widget.itemCount == 1) {
|
|
||||||
child = _buildOneChild(context);
|
|
||||||
if (child == null) {
|
|
||||||
realWidth = size.right;
|
|
||||||
realHeight = size.bottom;
|
|
||||||
child = _buildChild(context, size.left);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
realWidth = size.right;
|
|
||||||
realHeight = size.bottom;
|
|
||||||
child = _buildChild(context, size.left);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case NineGridType.weChatGp:
|
|
||||||
child = _buildWeChatGroup(context);
|
|
||||||
break;
|
|
||||||
case NineGridType.dingTalkGp:
|
|
||||||
child = _buildDingTalkGroup(context);
|
|
||||||
break;
|
|
||||||
case NineGridType.qqGp:
|
|
||||||
child = _buildQQGroup(context);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return Container(
|
|
||||||
alignment: widget.alignment,
|
|
||||||
color: widget.color,
|
|
||||||
decoration: widget.decoration,
|
|
||||||
margin: widget.margin,
|
|
||||||
padding: widget.padding,
|
|
||||||
width: realWidth,
|
|
||||||
height: realHeight,
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// image util.
|
|
||||||
class _ImageUtil {
|
|
||||||
late ImageStreamListener listener;
|
|
||||||
late ImageStream imageStream;
|
|
||||||
|
|
||||||
/// get image size.
|
|
||||||
Future<Rect>? getImageSize(Image? image) {
|
|
||||||
if (image == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Completer<Rect> completer = Completer<Rect>();
|
|
||||||
listener = ImageStreamListener(
|
|
||||||
(ImageInfo info, bool synchronousCall) {
|
|
||||||
imageStream.removeListener(listener);
|
|
||||||
if (!completer.isCompleted) {
|
|
||||||
completer.complete(
|
|
||||||
Rect.fromLTWH(
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
info.image.width.toDouble(),
|
|
||||||
info.image.height.toDouble(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onError: (dynamic exception, StackTrace? stackTrace) {
|
|
||||||
imageStream.removeListener(listener);
|
|
||||||
if (!completer.isCompleted) {
|
|
||||||
completer.completeError(exception, stackTrace);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
imageStream = image.image.resolve(ImageConfiguration.empty);
|
|
||||||
imageStream.addListener(listener);
|
|
||||||
return completer.future;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// QQ Clipper.
|
|
||||||
class QQClipper extends CustomClipper<Path> {
|
|
||||||
QQClipper({
|
|
||||||
this.total = 0,
|
|
||||||
this.index = 0,
|
|
||||||
this.initIndex = 1,
|
|
||||||
this.previousX = 0,
|
|
||||||
this.previousY = 0,
|
|
||||||
this.degree = 0,
|
|
||||||
this.arcAngle = 0,
|
|
||||||
this.space = 0,
|
|
||||||
}) : assert(arcAngle >= 0 && arcAngle <= 180);
|
|
||||||
|
|
||||||
final int total;
|
|
||||||
final int index;
|
|
||||||
final int initIndex;
|
|
||||||
final double previousX;
|
|
||||||
final double previousY;
|
|
||||||
final double degree;
|
|
||||||
final double arcAngle;
|
|
||||||
final double space;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Path getClip(Size size) {
|
|
||||||
double r = size.width / 2;
|
|
||||||
Path path = Path();
|
|
||||||
List<Offset> points = [];
|
|
||||||
|
|
||||||
if (total == 2 && index == initIndex) {
|
|
||||||
path.addOval(Rect.fromLTRB(0, 0, size.width, size.height));
|
|
||||||
} else {
|
|
||||||
/// arcAngle and space, prefer to use arcAngle.
|
|
||||||
double spaceA = arcAngle > 0
|
|
||||||
? (arcAngle / 2)
|
|
||||||
: (math.acos((r - math.min(r, space)) / r) / math.pi * 180);
|
|
||||||
double startA = degree + spaceA;
|
|
||||||
double endA = degree - spaceA;
|
|
||||||
for (double i = startA; i <= 360 + endA; i = i + 1) {
|
|
||||||
double x1 = r + r * math.sin(d2r(i));
|
|
||||||
double y1 = r - r * math.cos(d2r(i));
|
|
||||||
points.add(Offset(x1, y1));
|
|
||||||
}
|
|
||||||
|
|
||||||
double spaceB =
|
|
||||||
math.atan(
|
|
||||||
r * math.sin(d2r(spaceA)) / (2 * r - r * math.cos(d2r(spaceA))),
|
|
||||||
) /
|
|
||||||
math.pi *
|
|
||||||
180;
|
|
||||||
double r1 = (2 * r - r * math.cos(d2r(spaceA))) / math.cos(d2r(spaceB));
|
|
||||||
double startB = degree - 180 - spaceB;
|
|
||||||
double endB = degree - 180 + spaceB;
|
|
||||||
List<Offset> pointsB = [];
|
|
||||||
for (double i = startB; i < endB; i = i + 1) {
|
|
||||||
double x1 = previousX + r1 * math.sin(d2r(i));
|
|
||||||
double y1 = previousY - r1 * math.cos(d2r(i));
|
|
||||||
pointsB.add(Offset(x1, y1));
|
|
||||||
}
|
|
||||||
points.addAll(pointsB.reversed);
|
|
||||||
path.addPolygon(points, true);
|
|
||||||
}
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// degree to radian.
|
|
||||||
double d2r(double degree) {
|
|
||||||
return degree / 180 * math.pi;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool shouldReclip(CustomClipper<Path> oldClipper) {
|
|
||||||
return this != oldClipper;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:PiliPlus/common/widgets/image/image_view.dart';
|
import 'package:PiliPlus/common/widgets/image/custom_grid_view.dart';
|
||||||
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
|
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
|
||||||
import 'package:PiliPlus/http/constants.dart';
|
import 'package:PiliPlus/http/constants.dart';
|
||||||
import 'package:PiliPlus/models/common/image_preview_type.dart';
|
import 'package:PiliPlus/models/common/image_preview_type.dart';
|
||||||
@@ -207,9 +207,9 @@ class OpusContent extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return imageView(
|
return CustomGridView(
|
||||||
maxWidth,
|
maxWidth: maxWidth,
|
||||||
element.pic!.pics!
|
picArr: element.pic!.pics!
|
||||||
.map(
|
.map(
|
||||||
(e) => ImageModel(
|
(e) => ImageModel(
|
||||||
width: e.width,
|
width: e.width,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// 内容
|
// 内容
|
||||||
import 'package:PiliPlus/common/widgets/custom_icon.dart';
|
import 'package:PiliPlus/common/widgets/custom_icon.dart';
|
||||||
import 'package:PiliPlus/common/widgets/image/image_view.dart';
|
import 'package:PiliPlus/common/widgets/image/custom_grid_view.dart';
|
||||||
import 'package:PiliPlus/common/widgets/text/text.dart' as custom_text;
|
import 'package:PiliPlus/common/widgets/text/text.dart' as custom_text;
|
||||||
import 'package:PiliPlus/models/dynamics/result.dart';
|
import 'package:PiliPlus/models/dynamics/result.dart';
|
||||||
import 'package:PiliPlus/pages/dynamics/widgets/rich_node_panel.dart';
|
import 'package:PiliPlus/pages/dynamics/widgets/rich_node_panel.dart';
|
||||||
@@ -80,9 +80,9 @@ Widget content(
|
|||||||
maxLines: isSave ? null : 6,
|
maxLines: isSave ? null : 6,
|
||||||
),
|
),
|
||||||
if (item.modules.moduleDynamic?.major?.opus?.pics?.isNotEmpty == true)
|
if (item.modules.moduleDynamic?.major?.opus?.pics?.isNotEmpty == true)
|
||||||
imageView(
|
CustomGridView(
|
||||||
maxWidth,
|
maxWidth: maxWidth,
|
||||||
item.modules.moduleDynamic!.major!.opus!.pics!
|
picArr: item.modules.moduleDynamic!.major!.opus!.pics!
|
||||||
.map(
|
.map(
|
||||||
(item) => ImageModel(
|
(item) => ImageModel(
|
||||||
width: item.width,
|
width: item.width,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'dart:io' show Platform;
|
import 'dart:io' show Platform;
|
||||||
|
|
||||||
import 'package:PiliPlus/common/widgets/image/image_view.dart';
|
import 'package:PiliPlus/common/widgets/image/custom_grid_view.dart';
|
||||||
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
|
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
|
||||||
import 'package:PiliPlus/http/dynamics.dart';
|
import 'package:PiliPlus/http/dynamics.dart';
|
||||||
import 'package:PiliPlus/http/search.dart';
|
import 'package:PiliPlus/http/search.dart';
|
||||||
@@ -247,9 +247,9 @@ TextSpan? richNode(
|
|||||||
..add(const TextSpan(text: '\n'))
|
..add(const TextSpan(text: '\n'))
|
||||||
..add(
|
..add(
|
||||||
WidgetSpan(
|
WidgetSpan(
|
||||||
child: imageView(
|
child: CustomGridView(
|
||||||
maxWidth,
|
maxWidth: maxWidth,
|
||||||
i.pics!
|
picArr: i.pics!
|
||||||
.map(
|
.map(
|
||||||
(item) => ImageModel(
|
(item) => ImageModel(
|
||||||
url: item.src ?? '',
|
url: item.src ?? '',
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:math' show pi, max;
|
import 'dart:math' show pi, max;
|
||||||
|
|
||||||
import 'package:PiliPlus/common/widgets/image/image_view.dart';
|
import 'package:PiliPlus/common/widgets/image/custom_grid_view.dart'
|
||||||
|
show ImageModel;
|
||||||
import 'package:PiliPlus/common/widgets/pendant_avatar.dart';
|
import 'package:PiliPlus/common/widgets/pendant_avatar.dart';
|
||||||
import 'package:PiliPlus/common/widgets/radio_widget.dart';
|
import 'package:PiliPlus/common/widgets/radio_widget.dart';
|
||||||
import 'package:PiliPlus/common/widgets/refresh_indicator.dart';
|
import 'package:PiliPlus/common/widgets/refresh_indicator.dart';
|
||||||
@@ -510,9 +511,7 @@ List<SettingsModel> get extraSettings => [
|
|||||||
leading: const Icon(Icons.image_outlined),
|
leading: const Icon(Icons.image_outlined),
|
||||||
setKey: SettingBoxKey.enableLivePhoto,
|
setKey: SettingBoxKey.enableLivePhoto,
|
||||||
defaultVal: true,
|
defaultVal: true,
|
||||||
onChanged: (value) {
|
onChanged: (value) => ImageModel.enableLivePhoto = value,
|
||||||
ImageModel.enableLivePhoto = value;
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
const SettingsModel(
|
const SettingsModel(
|
||||||
settingsType: SettingsType.sw1tch,
|
settingsType: SettingsType.sw1tch,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import 'dart:math';
|
|||||||
import 'package:PiliPlus/common/constants.dart';
|
import 'package:PiliPlus/common/constants.dart';
|
||||||
import 'package:PiliPlus/common/widgets/badge.dart';
|
import 'package:PiliPlus/common/widgets/badge.dart';
|
||||||
import 'package:PiliPlus/common/widgets/dialog/report.dart';
|
import 'package:PiliPlus/common/widgets/dialog/report.dart';
|
||||||
import 'package:PiliPlus/common/widgets/image/image_view.dart';
|
import 'package:PiliPlus/common/widgets/image/custom_grid_view.dart';
|
||||||
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
|
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
|
||||||
import 'package:PiliPlus/common/widgets/pendant_avatar.dart';
|
import 'package:PiliPlus/common/widgets/pendant_avatar.dart';
|
||||||
import 'package:PiliPlus/common/widgets/text/text.dart' as custom_text;
|
import 'package:PiliPlus/common/widgets/text/text.dart' as custom_text;
|
||||||
@@ -299,9 +299,9 @@ class ReplyItemGrpc extends StatelessWidget {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: padding,
|
padding: padding,
|
||||||
child: LayoutBuilder(
|
child: LayoutBuilder(
|
||||||
builder: (context, constraints) => imageView(
|
builder: (context, constraints) => CustomGridView(
|
||||||
constraints.maxWidth,
|
maxWidth: constraints.maxWidth,
|
||||||
replyItem.content.pictures
|
picArr: replyItem.content.pictures
|
||||||
.map(
|
.map(
|
||||||
(item) => ImageModel(
|
(item) => ImageModel(
|
||||||
width: item.imgWidth,
|
width: item.imgWidth,
|
||||||
|
|||||||
Reference in New Issue
Block a user