using System; using System.Collections.Generic; using System.Linq; using Unity.UIWidgets.animation; using Unity.UIWidgets.ui; using Unity.UIWidgets.foundation; using Unity.UIWidgets.gestures; using Unity.UIWidgets.widgets; using Unity.UIWidgets.painting; using Unity.UIWidgets.rendering; using UnityEngine; using Color = Unity.UIWidgets.ui.Color; using Rect = Unity.UIWidgets.ui.Rect; using TextStyle = Unity.UIWidgets.painting.TextStyle; namespace Unity.UIWidgets.cupertino { class CupertinoSegmentedControlsUtils { public static readonly EdgeInsetsGeometry _kHorizontalItemPadding = EdgeInsets.symmetric(horizontal: 16.0f); public const float _kMinSegmentedControlHeight = 28.0f; public static readonly TimeSpan _kFadeDuration = TimeSpan.FromMilliseconds(165); } public class CupertinoSegmentedControl : StatefulWidget { public CupertinoSegmentedControl( Key key = null, Dictionary children = null, ValueChanged onValueChanged = null, T groupValue = default, Color unselectedColor = null, Color selectedColor = null, Color borderColor = null, Color pressedColor = null, EdgeInsetsGeometry padding = null ) :base(key: key) { D.assert(children != null); D.assert(children.Count >= 2); D.assert(onValueChanged != null); D.assert( groupValue == null || children.Keys.Any((T child) => child.Equals(groupValue)), () => "The groupValue must be either null or one of the keys in the children map." ); this.children = children; this.onValueChanged = onValueChanged; this.groupValue = groupValue; this.unselectedColor = unselectedColor; this.selectedColor = selectedColor; this.borderColor = borderColor; this.pressedColor = pressedColor; this.padding = padding; } public readonly Dictionary children; public readonly T groupValue; public readonly ValueChanged onValueChanged; public readonly Color unselectedColor; public readonly Color selectedColor; public readonly Color borderColor; public readonly Color pressedColor; public readonly EdgeInsetsGeometry padding; public override State createState() { return new _SegmentedControlState(); } } class _SegmentedControlState : TickerProviderStateMixin> { T _pressedKey; public readonly List _selectionControllers = new List(); public readonly List _childTweens = new List(); ColorTween _forwardBackgroundColorTween; ColorTween _reverseBackgroundColorTween; ColorTween _textColorTween; Color _selectedColor; Color _unselectedColor; Color _borderColor; Color _pressedColor; AnimationController createAnimationController() { var controller = new AnimationController( duration: CupertinoSegmentedControlsUtils._kFadeDuration, vsync: this ); controller.addListener(()=> { setState(() =>{ // State of background/text colors has changed }); }); return controller; } bool _updateColors() { D.assert(mounted, ()=>"This should only be called after didUpdateDependencies"); bool changed = false; Color selectedColor = widget.selectedColor ?? CupertinoTheme.of(context).primaryColor; if (_selectedColor != selectedColor) { changed = true; _selectedColor = selectedColor; } Color unselectedColor = widget.unselectedColor ?? CupertinoTheme.of(context).primaryContrastingColor; if (_unselectedColor != unselectedColor) { changed = true; _unselectedColor = unselectedColor; } Color borderColor = widget.borderColor ?? CupertinoTheme.of(context).primaryColor; if (_borderColor != borderColor) { changed = true; _borderColor = borderColor; } Color pressedColor = widget.pressedColor ?? CupertinoTheme.of(context).primaryColor.withOpacity(0.2f); if (_pressedColor != pressedColor) { changed = true; _pressedColor = pressedColor; } _forwardBackgroundColorTween = new ColorTween( begin: _pressedColor, end: _selectedColor ); _reverseBackgroundColorTween = new ColorTween( begin: _unselectedColor, end: _selectedColor ); _textColorTween = new ColorTween( begin: _selectedColor, end: _unselectedColor ); return changed; } void _updateAnimationControllers() { D.assert(mounted, ()=>"This should only be called after didUpdateDependencies"); foreach ( AnimationController controller in _selectionControllers) { controller.dispose(); } _selectionControllers.Clear(); _childTweens.Clear(); foreach ( T key in widget.children.Keys) { AnimationController animationController = createAnimationController(); if (widget.groupValue.Equals(key)) { _childTweens.Add(_reverseBackgroundColorTween); animationController.setValue(1.0f); } else { _childTweens.Add(_forwardBackgroundColorTween); } _selectionControllers.Add(animationController); } } public override void didChangeDependencies() { base.didChangeDependencies(); if (_updateColors()) { _updateAnimationControllers(); } } public override void didUpdateWidget(StatefulWidget oldWidget) { oldWidget = (CupertinoSegmentedControl) oldWidget; base.didUpdateWidget(oldWidget); if (_updateColors() || ((CupertinoSegmentedControl) oldWidget).children.Count != widget.children.Count) { _updateAnimationControllers(); } if (!((CupertinoSegmentedControl)oldWidget).groupValue.Equals(widget.groupValue)) { int index = 0; foreach ( T key in widget.children.Keys) { if (widget.groupValue.Equals(key)) { _childTweens[index] = _forwardBackgroundColorTween; _selectionControllers[index].forward(); } else { _childTweens[index] = _reverseBackgroundColorTween; _selectionControllers[index].reverse(); } index += 1; } } } public override void dispose() { foreach( AnimationController animationController in _selectionControllers) { animationController.dispose(); } base.dispose(); } void _onTapDown(T currentKey) { if (_pressedKey == null && !currentKey.Equals(widget.groupValue)) { setState(()=> { _pressedKey = currentKey; }); } } void _onTapCancel() { setState(()=> { _pressedKey = default(T); }); } void _onTap(T currentKey) { if (!currentKey.Equals( _pressedKey)) return; if (!currentKey.Equals(widget.groupValue)) { widget.onValueChanged(currentKey); } _pressedKey = default; } Color getTextColor(int index, T currentKey) { if (_selectionControllers[index].isAnimating) return _textColorTween.evaluate(_selectionControllers[index]); if (widget.groupValue.Equals(currentKey)) return _unselectedColor; return _selectedColor; } Color getBackgroundColor(int index, T currentKey) { if (_selectionControllers[index].isAnimating) return _childTweens[index].evaluate(_selectionControllers[index]); if (widget.groupValue.Equals(currentKey)) return _selectedColor; if (_pressedKey.Equals(currentKey)) return _pressedColor; return _unselectedColor; } public override Widget build(BuildContext context) { List _gestureChildren = new List(); List _backgroundColors = new List(); int index = 0; int selectedIndex = 0; int pressedIndex = 0; foreach ( T currentKey in widget.children.Keys) { selectedIndex = (widget.groupValue.Equals(currentKey)) ? index : selectedIndex; pressedIndex = (_pressedKey.Equals(currentKey)) ? index : pressedIndex; TextStyle textStyle = DefaultTextStyle.of(context).style.copyWith( color: getTextColor(index, currentKey) ); IconThemeData iconTheme = new IconThemeData( color: getTextColor(index, currentKey) ); Widget child = new Center( child: widget.children[currentKey] ); child = new GestureDetector( onTapDown: (TapDownDetails _event)=> { _onTapDown(currentKey); }, onTapCancel: _onTapCancel, onTap: ()=> { _onTap(currentKey); }, child: new IconTheme( data: iconTheme, child: new DefaultTextStyle( style: textStyle, child: child /*Semantics( button: true, inMutuallyExclusiveGroup: true, selected: widget.groupValue.Equals(currentKey), child: child )*/ ) ) ); _backgroundColors.Add(getBackgroundColor(index, currentKey)); _gestureChildren.Add(child); index += 1; } Widget box = new _SegmentedControlRenderWidget( children: _gestureChildren, selectedIndex: selectedIndex, pressedIndex: pressedIndex, backgroundColors: _backgroundColors, borderColor: _borderColor ); return new Padding( padding: widget.padding ?? CupertinoSegmentedControlsUtils._kHorizontalItemPadding, child: new UnconstrainedBox( constrainedAxis: Axis.horizontal, child: box ) ); } } public class _SegmentedControlRenderWidget : MultiChildRenderObjectWidget { public _SegmentedControlRenderWidget( Key key = null, List children = null, int? selectedIndex = null, int? pressedIndex = null, List backgroundColors = null, Color borderColor = null ) : base( key: key, children: children ?? new List() ) { this.selectedIndex = selectedIndex; this.pressedIndex = pressedIndex; this.backgroundColors = backgroundColors; this.borderColor = borderColor; } public readonly int? selectedIndex; public readonly int? pressedIndex; public readonly List backgroundColors; public readonly Color borderColor; public override RenderObject createRenderObject(BuildContext context) { return new _RenderSegmentedControl( textDirection: Directionality.of(context), selectedIndex: selectedIndex, pressedIndex: pressedIndex, backgroundColors: backgroundColors, borderColor: borderColor ); } public override void updateRenderObject(BuildContext context,RenderObject renderObject) { renderObject = (_RenderSegmentedControl) renderObject; ((_RenderSegmentedControl) renderObject).textDirection = Directionality.of(context); ((_RenderSegmentedControl) renderObject).selectedIndex = selectedIndex; ((_RenderSegmentedControl) renderObject).pressedIndex = pressedIndex; ((_RenderSegmentedControl) renderObject).backgroundColors = backgroundColors; ((_RenderSegmentedControl) renderObject).borderColor = borderColor; } } class _SegmentedControlContainerBoxParentData : ContainerBoxParentData { public RRect surroundingRect; } public delegate RenderBox _NextChild(RenderBox child); class _RenderSegmentedControl : RenderBoxContainerDefaultsMixinContainerRenderObjectMixinRenderBox> { public _RenderSegmentedControl( int? selectedIndex = null, int? pressedIndex = null, TextDirection? textDirection = null, List backgroundColors = null, Color borderColor = null ) { D.assert(textDirection != null); _textDirection = textDirection; _selectedIndex = selectedIndex; _pressedIndex = pressedIndex; _backgroundColors = backgroundColors; _borderColor = borderColor; } public int? selectedIndex { get {return _selectedIndex; } set { if (_selectedIndex == value) { return; } _selectedIndex = value; markNeedsPaint(); } } int? _selectedIndex; public int? pressedIndex { get { return _pressedIndex; } set { if (_pressedIndex == value) { return; } _pressedIndex = value; markNeedsPaint(); } } int? _pressedIndex; public TextDirection? textDirection { get { return _textDirection; } set { if (_textDirection == value) { return; } _textDirection = value; markNeedsLayout(); } } TextDirection? _textDirection; public List backgroundColors { get { return _backgroundColors; } set { if (_backgroundColors == value) { return; } _backgroundColors = value; markNeedsPaint(); } } List _backgroundColors; public Color borderColor { get { return _borderColor; } set { if (_borderColor == value) { return; } _borderColor = value; markNeedsPaint(); } } Color _borderColor; protected internal override float computeMinIntrinsicWidth(float height) { RenderBox child = firstChild; float minWidth = 0.0f; while (child != null) { _SegmentedControlContainerBoxParentData childParentData = child.parentData as _SegmentedControlContainerBoxParentData; float childWidth = child.getMinIntrinsicWidth(height); minWidth = Mathf.Max(minWidth, childWidth); child = childParentData.nextSibling; } return minWidth * childCount; } protected internal override float computeMaxIntrinsicWidth(float height) { RenderBox child = firstChild; float maxWidth = 0.0f; while (child != null) { _SegmentedControlContainerBoxParentData childParentData = child.parentData as _SegmentedControlContainerBoxParentData; float childWidth = child.getMaxIntrinsicWidth(height); maxWidth = Mathf.Max(maxWidth, childWidth); child = childParentData.nextSibling; } return maxWidth * childCount; } protected internal override float computeMinIntrinsicHeight(float width) { RenderBox child = firstChild; float minHeight = 0.0f; while (child != null) { _SegmentedControlContainerBoxParentData childParentData = child.parentData as _SegmentedControlContainerBoxParentData; float childHeight = child.getMinIntrinsicHeight(width); minHeight = Mathf.Max(minHeight, childHeight); child = childParentData.nextSibling; } return minHeight; } protected internal override float computeMaxIntrinsicHeight(float width) { RenderBox child = firstChild; float maxHeight = 0.0f; while (child != null) { _SegmentedControlContainerBoxParentData childParentData = child.parentData as _SegmentedControlContainerBoxParentData; float childHeight = child.getMaxIntrinsicHeight(width); maxHeight = Mathf.Max(maxHeight, childHeight); child = childParentData.nextSibling; } return maxHeight; } public override float? computeDistanceToActualBaseline(TextBaseline baseline) { return defaultComputeDistanceToHighestActualBaseline(baseline); } public override void setupParentData(RenderObject child) { child = (RenderBox) child; if (!(child.parentData is _SegmentedControlContainerBoxParentData)) { child.parentData = new _SegmentedControlContainerBoxParentData(); } } void _layoutRects(_NextChild nextChild, RenderBox leftChild, RenderBox rightChild) { RenderBox child = leftChild; float start = 0.0f; while (child != null) { _SegmentedControlContainerBoxParentData childParentData = child.parentData as _SegmentedControlContainerBoxParentData; Offset childOffset = new Offset(start, 0.0f); childParentData.offset = childOffset; Rect childRect = Rect.fromLTWH(start, 0.0f, child.size.width, child.size.height); RRect rChildRect = null; if (child == leftChild) { rChildRect = RRect.fromRectAndCorners(childRect, topLeft: Radius.circular(3.0f), bottomLeft: Radius.circular(3.0f)); } else if (child == rightChild) { rChildRect = RRect.fromRectAndCorners(childRect, topRight: Radius.circular(3.0f), bottomRight: Radius.circular(3.0f)); } else { rChildRect = RRect.fromRectAndCorners(childRect,topRight:Radius.zero); } childParentData.surroundingRect = rChildRect; start += child.size.width; child = nextChild(child); } } protected override void performLayout() { BoxConstraints constraints = this.constraints; float maxHeight = CupertinoSegmentedControlsUtils._kMinSegmentedControlHeight; float childWidth = constraints.minWidth / childCount; foreach ( RenderBox child in getChildrenAsList()) { childWidth = Mathf.Max(childWidth, child.getMaxIntrinsicWidth(float.PositiveInfinity)); } childWidth = Mathf.Min(childWidth, constraints.maxWidth / childCount); RenderBox child1 = firstChild; while (child1 != null) { float boxHeight = child1.getMaxIntrinsicHeight(childWidth); maxHeight = Mathf.Max(maxHeight, boxHeight); child1 = childAfter(child1); } constraints.constrainHeight(maxHeight); BoxConstraints childConstraints = BoxConstraints.tightFor( width: childWidth, height: maxHeight ); child1 = firstChild; while (child1 != null) { child1.layout(childConstraints, parentUsesSize: true); child1 = childAfter(child1); } switch (textDirection) { case TextDirection.rtl: _layoutRects( childBefore, lastChild, firstChild ); break; case TextDirection.ltr: _layoutRects( childAfter, firstChild, lastChild ); break; } size = constraints.constrain(new Size(childWidth * childCount, maxHeight)); } public override void paint(PaintingContext context, Offset offset) { RenderBox child = firstChild; int index = 0; while (child != null) { _paintChild(context, offset, child, index); child = childAfter(child); index += 1; } } void _paintChild(PaintingContext context, Offset offset, RenderBox child, int childIndex) { D.assert(child != null); _SegmentedControlContainerBoxParentData childParentData = child.parentData as _SegmentedControlContainerBoxParentData; context.canvas.drawRRect( childParentData.surroundingRect.shift(offset), new Paint(){ color = backgroundColors[childIndex], style = PaintingStyle.fill} ); context.canvas.drawRRect( childParentData.surroundingRect.shift(offset), new Paint(){ color = borderColor, strokeWidth = 1.0f, style = PaintingStyle.stroke} ); context.paintChild(child, childParentData.offset + offset); } protected override bool hitTestChildren(BoxHitTestResult result, Offset position = null ) { D.assert(position != null); RenderBox child = lastChild; while (child != null) { _SegmentedControlContainerBoxParentData childParentData = child.parentData as _SegmentedControlContainerBoxParentData; if (childParentData.surroundingRect.contains(position)) { Offset center = (Offset.zero & child.size).center; return result.addWithRawTransform( transform: MatrixUtils.forceToPoint(center), position: center, hitTest: (BoxHitTestResult result1, Offset position1) =>{ D.assert(position1 == center); return child.hitTest(result1, position: center); } ); } child = childParentData.previousSibling; } return false; } } }