Files
MWClash/lib/widgets/tab.dart
chen08209 1477f9bd9c Fix windows tun issues
Optimize android get system dns

Optimize more details
2025-06-15 18:44:19 +08:00

1103 lines
33 KiB
Dart

import 'dart:math' as math;
import 'dart:math';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/physics.dart';
import 'package:flutter/rendering.dart';
const EdgeInsetsGeometry _kHorizontalItemPadding =
EdgeInsets.symmetric(vertical: 2, horizontal: 3);
const Radius _kCornerRadius = Radius.circular(9);
const Radius _kThumbRadius = Radius.circular(8);
const EdgeInsets _kThumbInsets = EdgeInsets.symmetric(horizontal: 1);
const double _kMinSegmentedControlHeight = 28.0;
const EdgeInsets _kSeparatorInset = EdgeInsets.symmetric(vertical: 5);
const double _kSeparatorWidth = 1;
const double _kMinThumbScale = 0.95;
const double _kSegmentMinPadding = 10;
const double _kTouchYDistanceThreshold = 50.0 * 50.0;
const double _kContentPressedMinOpacity = 0.2;
const double _kFontSize = 13.0;
const FontWeight _kFontWeight = FontWeight.w500;
const FontWeight _kHighlightedFontWeight = FontWeight.w600;
const Color _kDisabledContentColor = Color.fromARGB(115, 122, 122, 122);
final SpringSimulation _kThumbSpringAnimationSimulation = SpringSimulation(
const SpringDescription(mass: 1, stiffness: 503.551, damping: 44.8799),
0,
1,
0,
);
const Duration _kSpringAnimationDuration = Duration(milliseconds: 412);
const Duration _kOpacityAnimationDuration = Duration(milliseconds: 470);
const Duration _kHighlightAnimationDuration = Duration(milliseconds: 200);
class CommonTabBar<T extends Object> extends StatefulWidget {
CommonTabBar({
super.key,
required this.children,
required this.onValueChanged,
this.disabledChildren = const <Never>{},
this.groupValue,
required this.thumbColor,
this.padding = _kHorizontalItemPadding,
this.backgroundColor,
this.proportionalWidth = false,
}) : assert(children.length >= 2),
assert(
groupValue == null || children.keys.contains(groupValue),
'The groupValue must be either null or one of the keys in the children map.',
);
final Map<T, Widget> children;
final Set<T> disabledChildren;
final T? groupValue;
final ValueChanged<T?> onValueChanged;
final Color? backgroundColor;
final Color thumbColor;
final bool proportionalWidth;
final EdgeInsetsGeometry padding;
@override
State<CommonTabBar<T>> createState() => _CommonTabBarState<T>();
}
class _CommonTabBarState<T extends Object> extends State<CommonTabBar<T>>
with TickerProviderStateMixin<CommonTabBar<T>> {
late final AnimationController thumbController = AnimationController(
duration: _kSpringAnimationDuration,
value: 0,
vsync: this,
);
Animatable<Rect?>? thumbAnimatable;
late final AnimationController thumbScaleController = AnimationController(
duration: _kSpringAnimationDuration,
value: 0,
vsync: this,
);
late Animation<double> thumbScaleAnimation = thumbScaleController.drive(
Tween<double>(begin: 1, end: _kMinThumbScale),
);
final TapGestureRecognizer tap = TapGestureRecognizer();
final HorizontalDragGestureRecognizer drag =
HorizontalDragGestureRecognizer();
final LongPressGestureRecognizer longPress = LongPressGestureRecognizer();
final GlobalKey segmentedControlRenderWidgetKey = GlobalKey();
@override
void initState() {
super.initState();
final GestureArenaTeam team = GestureArenaTeam();
longPress.team = team;
drag.team = team;
team.captain = drag;
drag
..onDown = onDown
..onUpdate = onUpdate
..onEnd = onEnd
..onCancel = onCancel;
tap.onTapUp = onTapUp;
longPress.onLongPress = () {};
highlighted = widget.groupValue;
}
@override
void didUpdateWidget(CommonTabBar<T> oldWidget) {
super.didUpdateWidget(oldWidget);
if (!isThumbDragging && highlighted != widget.groupValue) {
thumbController.animateWith(_kThumbSpringAnimationSimulation);
thumbAnimatable = null;
highlighted = widget.groupValue;
}
}
@override
void dispose() {
thumbScaleController.dispose();
thumbController.dispose();
drag.dispose();
tap.dispose();
longPress.dispose();
super.dispose();
}
bool? _startedOnSelectedSegment;
bool _startedOnDisabledSegment = false;
bool get isThumbDragging =>
(_startedOnSelectedSegment ?? false) && !_startedOnDisabledSegment;
T segmentForXPosition(double dx) {
final BuildContext currentContext =
segmentedControlRenderWidgetKey.currentContext!;
final _RenderSegmentedControl<T> renderBox =
currentContext.findRenderObject()! as _RenderSegmentedControl<T>;
final int numOfChildren = widget.children.length;
assert(renderBox.hasSize);
assert(numOfChildren >= 2);
int segmentIndex = renderBox.getClosestSegmentIndex(dx);
switch (Directionality.of(context)) {
case TextDirection.ltr:
break;
case TextDirection.rtl:
segmentIndex = numOfChildren - 1 - segmentIndex;
}
return widget.children.keys.elementAt(segmentIndex);
}
bool _hasDraggedTooFar(DragUpdateDetails details) {
final RenderBox renderBox = context.findRenderObject()! as RenderBox;
assert(renderBox.hasSize);
final Size size = renderBox.size;
final Offset offCenter =
details.localPosition - Offset(size.width / 2, size.height / 2);
final double l2 =
math.pow(math.max(0.0, offCenter.dx.abs() - size.width / 2), 2) +
math.pow(math.max(0.0, offCenter.dy.abs() - size.height / 2), 2)
as double;
return l2 > _kTouchYDistanceThreshold;
}
void _playThumbScaleAnimation({required bool isExpanding}) {
thumbScaleAnimation = thumbScaleController.drive(
Tween<double>(
begin: thumbScaleAnimation.value,
end: isExpanding ? 1 : _kMinThumbScale),
);
thumbScaleController.animateWith(_kThumbSpringAnimationSimulation);
}
void onHighlightChangedByGesture(T newValue) {
if (highlighted == newValue) {
return;
}
setState(() {
highlighted = newValue;
});
thumbController.animateWith(_kThumbSpringAnimationSimulation);
thumbAnimatable = null;
}
void onPressedChangedByGesture(T? newValue) {
if (pressed != newValue) {
setState(() {
pressed = newValue;
});
}
}
void onTapUp(TapUpDetails details) {
if (isThumbDragging) {
return;
}
final T segment = segmentForXPosition(details.localPosition.dx);
onPressedChangedByGesture(null);
if (segment != widget.groupValue &&
!widget.disabledChildren.contains(segment)) {
widget.onValueChanged(segment);
}
}
void onDown(DragDownDetails details) {
final T touchDownSegment = segmentForXPosition(details.localPosition.dx);
_startedOnSelectedSegment = touchDownSegment == highlighted;
_startedOnDisabledSegment =
widget.disabledChildren.contains(touchDownSegment);
if (widget.disabledChildren.contains(touchDownSegment)) {
return;
}
onPressedChangedByGesture(touchDownSegment);
if (isThumbDragging) {
_playThumbScaleAnimation(isExpanding: false);
}
}
void onUpdate(DragUpdateDetails details) {
if (_startedOnDisabledSegment) {
return;
}
final T touchDownSegment = segmentForXPosition(details.localPosition.dx);
if (widget.disabledChildren.contains(touchDownSegment)) {
return;
}
if (isThumbDragging) {
onPressedChangedByGesture(touchDownSegment);
onHighlightChangedByGesture(touchDownSegment);
} else {
final T? segment = _hasDraggedTooFar(details)
? null
: segmentForXPosition(details.localPosition.dx);
onPressedChangedByGesture(segment);
}
}
void onEnd(DragEndDetails details) {
final T? pressed = this.pressed;
if (isThumbDragging) {
_playThumbScaleAnimation(isExpanding: true);
if (highlighted != widget.groupValue) {
widget.onValueChanged(highlighted);
}
} else if (pressed != null) {
onHighlightChangedByGesture(pressed);
assert(pressed == highlighted);
if (highlighted != widget.groupValue) {
widget.onValueChanged(highlighted);
}
}
onPressedChangedByGesture(null);
_startedOnSelectedSegment = null;
}
void onCancel() {
if (isThumbDragging) {
_playThumbScaleAnimation(isExpanding: true);
}
onPressedChangedByGesture(null);
_startedOnSelectedSegment = null;
}
T? highlighted;
T? pressed;
@override
Widget build(BuildContext context) {
assert(widget.children.length >= 2);
List<Widget> children = <Widget>[];
bool isPreviousSegmentHighlighted = false;
int index = 0;
int? highlightedIndex;
for (final MapEntry<T, Widget> entry in widget.children.entries) {
final bool isHighlighted = highlighted == entry.key;
if (isHighlighted) {
highlightedIndex = index;
}
if (index != 0) {
children.add(
_SegmentSeparator(
key: ValueKey<int>(index),
highlighted: isPreviousSegmentHighlighted || isHighlighted,
),
);
}
final TextDirection textDirection = Directionality.of(context);
final _SegmentLocation segmentLocation = switch (textDirection) {
TextDirection.ltr when index == 0 => _SegmentLocation.leftmost,
TextDirection.ltr when index == widget.children.length - 1 =>
_SegmentLocation.rightmost,
TextDirection.rtl when index == widget.children.length - 1 =>
_SegmentLocation.leftmost,
TextDirection.rtl when index == 0 => _SegmentLocation.rightmost,
TextDirection.ltr || TextDirection.rtl => _SegmentLocation.inbetween,
};
children.add(
Semantics(
button: true,
onTap: () {
if (widget.disabledChildren.contains(entry.key)) {
return;
}
widget.onValueChanged(entry.key);
},
inMutuallyExclusiveGroup: true,
selected: widget.groupValue == entry.key,
child: MouseRegion(
cursor: kIsWeb ? SystemMouseCursors.click : MouseCursor.defer,
child: _Segment<T>(
key: ValueKey<T>(entry.key),
highlighted: isHighlighted,
pressed: pressed == entry.key,
isDragging: isThumbDragging,
enabled: !widget.disabledChildren.contains(entry.key),
segmentLocation: segmentLocation,
child: entry.value,
),
),
),
);
index += 1;
isPreviousSegmentHighlighted = isHighlighted;
}
assert((highlightedIndex == null) == (highlighted == null));
switch (Directionality.of(context)) {
case TextDirection.ltr:
break;
case TextDirection.rtl:
children = children.reversed.toList(growable: false);
if (highlightedIndex != null) {
highlightedIndex = index - 1 - highlightedIndex;
}
}
return UnconstrainedBox(
constrainedAxis: Axis.horizontal,
child: Container(
clipBehavior: Clip.antiAlias,
padding: widget.padding.resolve(Directionality.of(context)),
decoration: ShapeDecoration(
shape: RoundedSuperellipseBorder(
borderRadius: const BorderRadius.all(_kCornerRadius),
),
color: widget.backgroundColor,
),
child: AnimatedBuilder(
animation: thumbScaleAnimation,
builder: (BuildContext context, Widget? child) {
return _CommonTabBarRenderWidget<T>(
proportionalWidth: widget.proportionalWidth,
key: segmentedControlRenderWidgetKey,
highlightedIndex: highlightedIndex,
thumbColor: widget.thumbColor,
thumbScale: thumbScaleAnimation.value,
state: this,
children: children,
);
},
),
),
);
}
}
class _Segment<T> extends StatefulWidget {
const _Segment({
required ValueKey<T> key,
required this.child,
required this.pressed,
required this.highlighted,
required this.isDragging,
required this.enabled,
required this.segmentLocation,
}) : super(key: key);
final Widget child;
final bool pressed;
final bool highlighted;
final bool enabled;
final _SegmentLocation segmentLocation;
final bool isDragging;
bool get shouldFadeoutContent => pressed && !highlighted && enabled;
bool get shouldScaleContent =>
pressed && highlighted && isDragging && enabled;
@override
_SegmentState<T> createState() => _SegmentState<T>();
}
class _SegmentState<T> extends State<_Segment<T>>
with TickerProviderStateMixin<_Segment<T>> {
late final AnimationController highlightPressScaleController;
late Animation<double> highlightPressScaleAnimation;
@override
void initState() {
super.initState();
highlightPressScaleController = AnimationController(
duration: _kOpacityAnimationDuration,
value: widget.shouldScaleContent ? 1 : 0,
vsync: this,
);
highlightPressScaleAnimation = highlightPressScaleController.drive(
Tween<double>(begin: 1.0, end: _kMinThumbScale),
);
}
@override
void didUpdateWidget(_Segment<T> oldWidget) {
super.didUpdateWidget(oldWidget);
assert(oldWidget.key == widget.key);
if (oldWidget.shouldScaleContent != widget.shouldScaleContent) {
highlightPressScaleAnimation = highlightPressScaleController.drive(
Tween<double>(
begin: highlightPressScaleAnimation.value,
end: widget.shouldScaleContent ? _kMinThumbScale : 1.0,
),
);
highlightPressScaleController
.animateWith(_kThumbSpringAnimationSimulation);
}
}
@override
void dispose() {
highlightPressScaleController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final Alignment scaleAlignment = switch (widget.segmentLocation) {
_SegmentLocation.leftmost => Alignment.centerLeft,
_SegmentLocation.rightmost => Alignment.centerRight,
_SegmentLocation.inbetween => Alignment.center,
};
return MetaData(
behavior: HitTestBehavior.opaque,
child: IndexedStack(
alignment: Alignment.center,
children: <Widget>[
AnimatedOpacity(
opacity:
widget.shouldFadeoutContent ? _kContentPressedMinOpacity : 1,
duration: _kOpacityAnimationDuration,
curve: Curves.ease,
child: AnimatedDefaultTextStyle(
style: DefaultTextStyle.of(context).style.merge(
TextStyle(
fontWeight: widget.highlighted
? _kHighlightedFontWeight
: _kFontWeight,
fontSize: _kFontSize,
color: widget.enabled ? null : _kDisabledContentColor,
),
),
duration: _kHighlightAnimationDuration,
curve: Curves.ease,
child: ScaleTransition(
alignment: scaleAlignment,
scale: highlightPressScaleAnimation,
child: widget.child,
),
),
),
DefaultTextStyle.merge(
style: const TextStyle(
fontWeight: _kHighlightedFontWeight, fontSize: _kFontSize),
child: widget.child,
),
],
),
);
}
}
class _SegmentSeparator extends StatefulWidget {
const _SegmentSeparator({
required ValueKey<int> key,
required this.highlighted,
}) : super(key: key);
final bool highlighted;
@override
_SegmentSeparatorState createState() => _SegmentSeparatorState();
}
class _SegmentSeparatorState extends State<_SegmentSeparator>
with TickerProviderStateMixin<_SegmentSeparator> {
late final AnimationController separatorOpacityController;
@override
void initState() {
super.initState();
separatorOpacityController = AnimationController(
duration: _kSpringAnimationDuration,
value: widget.highlighted ? 0 : 1,
vsync: this,
);
}
@override
void didUpdateWidget(_SegmentSeparator oldWidget) {
super.didUpdateWidget(oldWidget);
assert(oldWidget.key == widget.key);
if (oldWidget.highlighted != widget.highlighted) {
separatorOpacityController.animateTo(
widget.highlighted ? 0 : 1,
duration: _kSpringAnimationDuration,
curve: Curves.ease,
);
}
}
@override
void dispose() {
separatorOpacityController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: separatorOpacityController,
child: const SizedBox(width: _kSeparatorWidth),
builder: (BuildContext context, Widget? child) {
return Padding(
padding: _kSeparatorInset,
child: DecoratedBox(
decoration: BoxDecoration(
color: Colors.transparent,
),
child: child,
),
);
},
);
}
}
class _CommonTabBarRenderWidget<T extends Object>
extends MultiChildRenderObjectWidget {
const _CommonTabBarRenderWidget({
super.key,
super.children,
required this.highlightedIndex,
required this.thumbColor,
required this.thumbScale,
required this.state,
required this.proportionalWidth,
});
final int? highlightedIndex;
final Color thumbColor;
final double thumbScale;
final bool proportionalWidth;
final _CommonTabBarState<T> state;
@override
RenderObject createRenderObject(BuildContext context) {
return _RenderSegmentedControl<T>(
highlightedIndex: highlightedIndex,
thumbColor: thumbColor,
thumbScale: thumbScale,
proportionalWidth: proportionalWidth,
state: state,
);
}
@override
void updateRenderObject(
BuildContext context, _RenderSegmentedControl<T> renderObject) {
assert(renderObject.state == state);
renderObject
..thumbColor = thumbColor
..thumbScale = thumbScale
..highlightedIndex = highlightedIndex
..proportionalWidth = proportionalWidth;
}
}
class _SegmentedControlContainerBoxParentData
extends ContainerBoxParentData<RenderBox> {}
enum _SegmentLocation { leftmost, rightmost, inbetween }
class _RenderSegmentedControl<T extends Object> extends RenderBox
with
ContainerRenderObjectMixin<RenderBox,
ContainerBoxParentData<RenderBox>>,
RenderBoxContainerDefaultsMixin<RenderBox,
ContainerBoxParentData<RenderBox>> {
_RenderSegmentedControl({
required int? highlightedIndex,
required Color thumbColor,
required double thumbScale,
required bool proportionalWidth,
required this.state,
}) : _highlightedIndex = highlightedIndex,
_thumbColor = thumbColor,
_thumbScale = thumbScale,
_proportionalWidth = proportionalWidth;
final _CommonTabBarState<T> state;
Rect? currentThumbRect;
@override
void attach(PipelineOwner owner) {
super.attach(owner);
state.thumbController.addListener(markNeedsPaint);
}
@override
void detach() {
state.thumbController.removeListener(markNeedsPaint);
super.detach();
}
double get thumbScale => _thumbScale;
double _thumbScale;
set thumbScale(double value) {
if (_thumbScale == value) {
return;
}
_thumbScale = value;
if (state.highlighted != null) {
markNeedsPaint();
}
}
int? get highlightedIndex => _highlightedIndex;
int? _highlightedIndex;
set highlightedIndex(int? value) {
if (_highlightedIndex == value) {
return;
}
_highlightedIndex = value;
markNeedsPaint();
}
Color get thumbColor => _thumbColor;
Color _thumbColor;
set thumbColor(Color value) {
if (_thumbColor == value) {
return;
}
_thumbColor = value;
markNeedsPaint();
}
bool get proportionalWidth => _proportionalWidth;
bool _proportionalWidth;
set proportionalWidth(bool value) {
if (_proportionalWidth == value) {
return;
}
_proportionalWidth = value;
markNeedsLayout();
}
@override
void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
assert(debugHandleEvent(event, entry));
if (event is PointerDownEvent && !state.isThumbDragging) {
state.tap.addPointer(event);
state.longPress.addPointer(event);
state.drag.addPointer(event);
}
}
double get separatorWidth => _kSeparatorInset.horizontal + _kSeparatorWidth;
double get totalSeparatorWidth => separatorWidth * (childCount ~/ 2);
int getClosestSegmentIndex(double dx) {
int index = 0;
RenderBox? child = firstChild;
while (child != null) {
final _SegmentedControlContainerBoxParentData childParentData =
child.parentData! as _SegmentedControlContainerBoxParentData;
final double clampX = clampDouble(
dx,
childParentData.offset.dx,
child.size.width + childParentData.offset.dx,
);
if (dx <= clampX) {
break;
}
index++;
child = nonSeparatorChildAfter(child);
}
final int segmentCount = childCount ~/ 2 + 1;
return min(index, segmentCount - 1);
}
RenderBox? nonSeparatorChildAfter(RenderBox child) {
final RenderBox? nextChild = childAfter(child);
return nextChild == null ? null : childAfter(nextChild);
}
@override
double computeMinIntrinsicWidth(double height) {
final int childCount = this.childCount ~/ 2 + 1;
RenderBox? child = firstChild;
double maxMinChildWidth = 0;
while (child != null) {
final double childWidth = child.getMinIntrinsicWidth(height);
maxMinChildWidth = math.max(maxMinChildWidth, childWidth);
child = nonSeparatorChildAfter(child);
}
return (maxMinChildWidth + 2 * _kSegmentMinPadding) * childCount +
totalSeparatorWidth;
}
@override
double computeMaxIntrinsicWidth(double height) {
final int childCount = this.childCount ~/ 2 + 1;
RenderBox? child = firstChild;
double maxMaxChildWidth = 0;
while (child != null) {
final double childWidth = child.getMaxIntrinsicWidth(height);
maxMaxChildWidth = math.max(maxMaxChildWidth, childWidth);
child = nonSeparatorChildAfter(child);
}
return (maxMaxChildWidth + 2 * _kSegmentMinPadding) * childCount +
totalSeparatorWidth;
}
@override
double computeMinIntrinsicHeight(double width) {
RenderBox? child = firstChild;
double maxMinChildHeight = _kMinSegmentedControlHeight;
while (child != null) {
final double childHeight = child.getMinIntrinsicHeight(width);
maxMinChildHeight = math.max(maxMinChildHeight, childHeight);
child = nonSeparatorChildAfter(child);
}
return maxMinChildHeight;
}
@override
double computeMaxIntrinsicHeight(double width) {
RenderBox? child = firstChild;
double maxMaxChildHeight = _kMinSegmentedControlHeight;
while (child != null) {
final double childHeight = child.getMaxIntrinsicHeight(width);
maxMaxChildHeight = math.max(maxMaxChildHeight, childHeight);
child = nonSeparatorChildAfter(child);
}
return maxMaxChildHeight;
}
@override
double? computeDistanceToActualBaseline(TextBaseline baseline) {
return defaultComputeDistanceToHighestActualBaseline(baseline);
}
@override
void setupParentData(RenderBox child) {
if (child.parentData is! _SegmentedControlContainerBoxParentData) {
child.parentData = _SegmentedControlContainerBoxParentData();
}
}
double _getMaxChildHeight(BoxConstraints constraints, double childWidth) {
double maxHeight = _kMinSegmentedControlHeight;
RenderBox? child = firstChild;
while (child != null) {
final double boxHeight = child.getMaxIntrinsicHeight(childWidth);
maxHeight = math.max(maxHeight, boxHeight);
child = nonSeparatorChildAfter(child);
}
return maxHeight;
}
double _getMaxChildWidth(BoxConstraints constraints) {
final int childCount = this.childCount ~/ 2 + 1;
double childWidth =
(constraints.minWidth - totalSeparatorWidth) / childCount;
RenderBox? child = firstChild;
while (child != null) {
childWidth = math.max(
childWidth,
child.getMaxIntrinsicWidth(double.infinity) + 2 * _kSegmentMinPadding,
);
child = nonSeparatorChildAfter(child);
}
return math.min(
childWidth, (constraints.maxWidth - totalSeparatorWidth) / childCount);
}
List<double> _getChildWidths(BoxConstraints constraints) {
if (!proportionalWidth) {
final double maxChildWidth = _getMaxChildWidth(constraints);
final int segmentCount = childCount ~/ 2 + 1;
return List<double>.filled(segmentCount, maxChildWidth);
}
final List<double> segmentWidths = <double>[];
RenderBox? child = firstChild;
while (child != null) {
final double childWidth =
child.getMaxIntrinsicWidth(double.infinity) + 2 * _kSegmentMinPadding;
child = nonSeparatorChildAfter(child);
segmentWidths.add(childWidth);
}
final double totalWidth = segmentWidths.sum;
final double allowedMaxWidth = constraints.maxWidth - totalSeparatorWidth;
final double allowedMinWidth = constraints.minWidth - totalSeparatorWidth;
final double scale =
clampDouble(totalWidth, allowedMinWidth, allowedMaxWidth) / totalWidth;
if (scale != 1) {
for (int i = 0; i < segmentWidths.length; i++) {
segmentWidths[i] = segmentWidths[i] * scale;
}
}
return segmentWidths;
}
Size _computeOverallSize(BoxConstraints constraints) {
final double maxChildHeight =
_getMaxChildHeight(constraints, constraints.maxWidth);
return constraints.constrain(
Size(_getChildWidths(constraints).sum + totalSeparatorWidth,
maxChildHeight),
);
}
@override
double? computeDryBaseline(
covariant BoxConstraints constraints, TextBaseline baseline) {
final List<double> segmentWidths = _getChildWidths(constraints);
final double childHeight =
_getMaxChildHeight(constraints, constraints.maxWidth);
int index = 0;
BaselineOffset baselineOffset = BaselineOffset.noBaseline;
RenderBox? child = firstChild;
while (child != null) {
final BoxConstraints childConstraints = BoxConstraints.tight(
Size(segmentWidths[index], childHeight),
);
baselineOffset = baselineOffset.minOf(
BaselineOffset(child.getDryBaseline(childConstraints, baseline)),
);
child = nonSeparatorChildAfter(child);
index++;
}
return baselineOffset.offset;
}
@override
Size computeDryLayout(BoxConstraints constraints) {
return _computeOverallSize(constraints);
}
@override
void performLayout() {
final BoxConstraints constraints = this.constraints;
final List<double> segmentWidths = _getChildWidths(constraints);
final double childHeight = _getMaxChildHeight(constraints, double.infinity);
final BoxConstraints separatorConstraints = BoxConstraints(
minHeight: childHeight,
maxHeight: childHeight,
);
RenderBox? child = firstChild;
int index = 0;
double start = 0;
while (child != null) {
final BoxConstraints childConstraints = BoxConstraints.tight(
Size(segmentWidths[index ~/ 2], childHeight),
);
child.layout(index.isEven ? childConstraints : separatorConstraints,
parentUsesSize: true);
final _SegmentedControlContainerBoxParentData childParentData =
child.parentData! as _SegmentedControlContainerBoxParentData;
final Offset childOffset = Offset(start, 0);
childParentData.offset = childOffset;
start += child.size.width;
assert(
index.isEven ||
child.size.width == _kSeparatorWidth + _kSeparatorInset.horizontal,
'${child.size.width} != ${_kSeparatorWidth + _kSeparatorInset.horizontal}',
);
child = childAfter(child);
index += 1;
}
size = _computeOverallSize(constraints);
}
Rect? moveThumbRectInBound(Rect? thumbRect, List<RenderBox> children) {
assert(hasSize);
assert(children.length >= 2);
if (thumbRect == null) {
return null;
}
final Offset firstChildOffset =
(children.first.parentData! as _SegmentedControlContainerBoxParentData)
.offset;
final double leftMost = firstChildOffset.dx;
final double rightMost =
(children.last.parentData! as _SegmentedControlContainerBoxParentData)
.offset
.dx +
children.last.size.width;
assert(rightMost > leftMost);
return Rect.fromLTRB(
math.max(thumbRect.left, leftMost - _kThumbInsets.left),
firstChildOffset.dy - _kThumbInsets.top,
math.min(thumbRect.right, rightMost + _kThumbInsets.right),
firstChildOffset.dy + children.first.size.height + _kThumbInsets.bottom,
);
}
@override
void paint(PaintingContext context, Offset offset) {
final List<RenderBox> children = getChildrenAsList();
for (int index = 1; index < childCount; index += 2) {
_paintSeparator(context, offset, children[index]);
}
final int? highlightedChildIndex = highlightedIndex;
if (highlightedChildIndex != null) {
final RenderBox selectedChild = children[highlightedChildIndex * 2];
final _SegmentedControlContainerBoxParentData childParentData =
selectedChild.parentData! as _SegmentedControlContainerBoxParentData;
final Rect newThumbRect = _kThumbInsets.inflateRect(
childParentData.offset & selectedChild.size,
);
if (state.thumbController.isAnimating) {
final Animatable<Rect?>? thumbTween = state.thumbAnimatable;
if (thumbTween == null) {
final Rect startingRect =
moveThumbRectInBound(currentThumbRect, children) ?? newThumbRect;
state.thumbAnimatable =
RectTween(begin: startingRect, end: newThumbRect);
} else if (newThumbRect != thumbTween.transform(1)) {
final Rect startingRect =
moveThumbRectInBound(currentThumbRect, children) ?? newThumbRect;
state.thumbAnimatable = RectTween(
begin: startingRect,
end: newThumbRect,
).chain(CurveTween(curve: Interval(state.thumbController.value, 1)));
}
} else {
state.thumbAnimatable = null;
}
final Rect unscaledThumbRect =
state.thumbAnimatable?.evaluate(state.thumbController) ??
newThumbRect;
currentThumbRect = unscaledThumbRect;
final _SegmentLocation childLocation;
if (highlightedChildIndex == 0) {
childLocation = _SegmentLocation.leftmost;
} else if (highlightedChildIndex == children.length ~/ 2) {
childLocation = _SegmentLocation.rightmost;
} else {
childLocation = _SegmentLocation.inbetween;
}
final double delta = switch (childLocation) {
_SegmentLocation.leftmost =>
unscaledThumbRect.width - unscaledThumbRect.width * thumbScale,
_SegmentLocation.rightmost =>
unscaledThumbRect.width * thumbScale - unscaledThumbRect.width,
_SegmentLocation.inbetween => 0,
};
final Rect thumbRect = Rect.fromCenter(
center: unscaledThumbRect.center - Offset(delta / 2, 0),
width: unscaledThumbRect.width * thumbScale,
height: unscaledThumbRect.height * thumbScale,
);
_paintThumb(context, offset, thumbRect);
} else {
currentThumbRect = null;
}
for (int index = 0; index < children.length; index += 2) {
_paintChild(context, offset, children[index]);
}
}
final Paint separatorPaint = Paint();
void _paintSeparator(
PaintingContext context, Offset offset, RenderBox child) {
final _SegmentedControlContainerBoxParentData childParentData =
child.parentData! as _SegmentedControlContainerBoxParentData;
context.paintChild(child, offset + childParentData.offset);
}
void _paintChild(PaintingContext context, Offset offset, RenderBox child) {
final _SegmentedControlContainerBoxParentData childParentData =
child.parentData! as _SegmentedControlContainerBoxParentData;
context.paintChild(child, childParentData.offset + offset);
}
void _paintThumb(PaintingContext context, Offset offset, Rect thumbRect) {
// const List<BoxShadow> thumbShadow = <BoxShadow>[
// BoxShadow(color: Color(0x1F000000), offset: Offset(0, 3), blurRadius: 8),
// BoxShadow(color: Color(0x0A000000), offset: Offset(0, 3), blurRadius: 1),
// ];
final RRect thumbRRect =
RRect.fromRectAndRadius(thumbRect.shift(offset), _kThumbRadius);
// for (final BoxShadow shadow in thumbShadow) {
// context.canvas
// .drawRRect(thumbRRect.shift(shadow.offset), shadow.toPaint());
// }
context.canvas.drawRRect(
thumbRRect.inflate(0.5), Paint()..color = const Color(0x0A000000));
context.canvas.drawRRect(thumbRRect, Paint()..color = thumbColor);
}
@override
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
RenderBox? child = lastChild;
while (child != null) {
final _SegmentedControlContainerBoxParentData childParentData =
child.parentData! as _SegmentedControlContainerBoxParentData;
if ((childParentData.offset & child.size).contains(position)) {
return result.addWithPaintOffset(
offset: childParentData.offset,
position: position,
hitTest: (BoxHitTestResult result, Offset localOffset) {
assert(localOffset == position - childParentData.offset);
return child!.hitTest(result, position: localOffset);
},
);
}
child = childParentData.previousSibling;
}
return false;
}
}