using System.Collections.Generic; using Unity.UIWidgets.animation; using Unity.UIWidgets.foundation; using Unity.UIWidgets.painting; using Unity.UIWidgets.rendering; using Unity.UIWidgets.ui; using Unity.UIWidgets.utils; using Unity.UIWidgets.widgets; namespace Unity.UIWidgets.material { public abstract class MergeableMaterialItem { public MergeableMaterialItem( LocalKey key) { D.assert(key != null); this.key = key; } public readonly LocalKey key; } public class MaterialSlice : MergeableMaterialItem { public MaterialSlice( LocalKey key = null, Widget child = null) : base(key: key) { D.assert(key != null); D.assert(child != null); this.child = child; } public readonly Widget child; public override string ToString() { return "MergeableSlice(key: " + this.key + ", child: " + this.child + ")"; } } public class MaterialGap : MergeableMaterialItem { public MaterialGap( LocalKey key = null, double size = 16.0) : base(key: key) { D.assert(key != null); this.size = size; } public readonly double size; public override string ToString() { return "MaterialGap(key: " + this.key + ", child: " + this.size + ")"; } } public class MergeableMaterial : StatefulWidget { public MergeableMaterial( Key key = null, Axis mainAxis = Axis.vertical, int elevation = 2, bool hasDividers = false, List children = null) : base(key: key) { this.mainAxis = mainAxis; this.elevation = elevation; this.hasDividers = hasDividers; this.children = children ?? new List(); } public readonly List children; public readonly Axis mainAxis; public readonly int elevation; public readonly bool hasDividers; public override void debugFillProperties(DiagnosticPropertiesBuilder properties) { base.debugFillProperties(properties); properties.add(new EnumProperty("mainAxis", this.mainAxis)); properties.add(new DoubleProperty("elevation", this.elevation)); } public override State createState() { return new _MergeableMaterialState(); } } public class _AnimationTuple { public _AnimationTuple( AnimationController controller = null, CurvedAnimation startAnimation = null, CurvedAnimation endAnimation = null, CurvedAnimation gapAnimation = null, double gapStart = 0.0) { this.controller = controller; this.startAnimation = startAnimation; this.endAnimation = endAnimation; this.gapAnimation = gapAnimation; this.gapStart = gapStart; } public readonly AnimationController controller; public readonly CurvedAnimation startAnimation; public readonly CurvedAnimation endAnimation; public readonly CurvedAnimation gapAnimation; public double gapStart; } public class _MergeableMaterialState : TickerProviderStateMixin { List _children; public readonly Dictionary _animationTuples = new Dictionary(); public override void initState() { base.initState(); this._children = new List(); this._children.AddRange(this.widget.children); for (int i = 0; i < this._children.Count; i++) { if (this._children[i] is MaterialGap) { this._initGap((MaterialGap) this._children[i]); this._animationTuples[this._children[i].key].controller.setValue(1.0); } } D.assert(this._debugGapsAreValid(this._children)); } void _initGap(MaterialGap gap) { AnimationController controller = new AnimationController( duration: ThemeUtils.kThemeAnimationDuration, vsync: this); CurvedAnimation startAnimation = new CurvedAnimation( parent: controller, curve: Curves.fastOutSlowIn); CurvedAnimation endAnimation = new CurvedAnimation( parent: controller, curve: Curves.fastOutSlowIn); CurvedAnimation gapAnimation = new CurvedAnimation( parent: controller, curve: Curves.fastOutSlowIn); controller.addListener(this._handleTick); this._animationTuples[gap.key] = new _AnimationTuple( controller: controller, startAnimation: startAnimation, endAnimation: endAnimation, gapAnimation: gapAnimation); } public override void dispose() { foreach (MergeableMaterialItem child in this._children) { if (child is MaterialGap) { this._animationTuples[child.key].controller.dispose(); } } base.dispose(); } void _handleTick() { this.setState(() => { }); } bool _debugHasConsecutiveGaps(List children) { for (int i = 0; i < this.widget.children.Count - 1; i++) { if (this.widget.children[i] is MaterialGap && this.widget.children[i + 1] is MaterialGap) { return true; } } return false; } bool _debugGapsAreValid(List children) { if (this._debugHasConsecutiveGaps(children)) { return false; } if (children.isNotEmpty()) { if (children.first() is MaterialGap || children.last() is MaterialGap) { return false; } } return true; } void _insertChild(int index, MergeableMaterialItem child) { this._children.Insert(index, child); if (child is MaterialGap) { this._initGap((MaterialGap) child); } } void _removeChild(int index) { MergeableMaterialItem child = this._children[index]; this._children.RemoveAt(index); if (child is MaterialGap) { this._animationTuples[child.key] = null; } } bool _isClosingGap(int index) { if (index < this._children.Count - 1 && this._children[index] is MaterialGap) { return this._animationTuples[this._children[index].key].controller.status == AnimationStatus.reverse; } return false; } void _removeEmptyGaps() { int j = 0; while (j < this._children.Count) { if (this._children[j] is MaterialGap && this._animationTuples[this._children[j].key].controller.status == AnimationStatus.dismissed) { this._removeChild(j); } else { j++; } } } public override void didUpdateWidget(StatefulWidget oldWidget) { base.didUpdateWidget(oldWidget); MergeableMaterial _oldWidget = (MergeableMaterial) oldWidget; HashSet oldKeys = new HashSet(); foreach (MergeableMaterialItem child in _oldWidget.children) { oldKeys.Add(child.key); } HashSet newKeys = new HashSet(); foreach (MergeableMaterialItem child in this.widget.children) { newKeys.Add(child.key); } HashSet newOnly = new HashSet(); foreach (var key in newKeys) { if (!oldKeys.Contains(key)) { newOnly.Add(key); } } HashSet oldOnly = new HashSet(); foreach (var key in oldKeys) { if (!newKeys.Contains(key)) { oldOnly.Add(key); } } List newChildren = this.widget.children; int i = 0; int j = 0; D.assert(this._debugGapsAreValid(newChildren)); this._removeEmptyGaps(); while (i < newChildren.Count && j < this._children.Count) { if (newOnly.Contains(newChildren[i].key) || oldOnly.Contains(this._children[j].key)) { int startNew = i; int startOld = j; while (newOnly.Contains(newChildren[i].key)) { i++; } while (oldOnly.Contains(this._children[j].key) || this._isClosingGap(j)) { j++; } int newLength = i - startNew; int oldLength = j - startOld; if (newLength > 0) { if (oldLength > 1 || oldLength == 1 && this._children[startOld] is MaterialSlice) { if (newLength == 1 && newChildren[startNew] is MaterialGap) { double gapSizeSum = 0.0; while (startOld < j) { if (this._children[startOld] is MaterialGap) { MaterialGap gap = (MaterialGap) this._children[startOld]; gapSizeSum += gap.size; } this._removeChild(startOld); j--; } this._insertChild(startOld, newChildren[startNew]); this._animationTuples[newChildren[startNew].key].gapStart = gapSizeSum; this._animationTuples[newChildren[startNew].key].controller.forward(); j++; } else { for (int k = 0; k < oldLength; k++) { this._removeChild(startOld); } for (int k = 0; k < newLength; k++) { this._insertChild(startOld + k, newChildren[startNew + k]); } j += (newLength - oldLength); } } else if (oldLength == 1) { if (newLength == 1 && newChildren[startNew] is MaterialGap && this._children[startOld].key == newChildren[startNew].key) { this._animationTuples[newChildren[startNew].key].controller.forward(); } else { double gapSize = this._getGapSize(startOld); this._removeChild(startOld); for (int k = 0; k < newLength; k++) { this._insertChild(startOld + k, newChildren[startNew + k]); } j += (newLength - 1); double gapSizeSum = 0.0; for (int k = startNew; k < i; k++) { if (newChildren[k] is MaterialGap) { MaterialGap gap = (MaterialGap) newChildren[k]; gapSizeSum += gap.size; } } for (int k = startNew; k < i; k++) { if (newChildren[k] is MaterialGap) { MaterialGap gap = (MaterialGap) newChildren[k]; this._animationTuples[gap.key].gapStart = gapSize * gap.size / gapSizeSum; this._animationTuples[gap.key].controller.setValue(0.0); this._animationTuples[gap.key].controller.forward(); } } } } else { for (int k = 0; k < newLength; k++) { this._insertChild(startOld + k, newChildren[startNew + k]); if (newChildren[startNew + k] is MaterialGap) { MaterialGap gap = (MaterialGap) newChildren[startNew + k]; this._animationTuples[gap.key].controller.forward(); } } j += newLength; } } else { if (oldLength > 1 || oldLength == 1 && this._children[startOld] is MaterialSlice) { double gapSizeSum = 0.0; while (startOld < j) { if (this._children[startOld] is MaterialGap) { MaterialGap gap = (MaterialGap) this._children[startOld]; gapSizeSum += gap.size; } this._removeChild(startOld); j--; } if (gapSizeSum != 0.0) { MaterialGap gap = new MaterialGap(key: new UniqueKey(), size: gapSizeSum); this._insertChild(startOld, gap); this._animationTuples[gap.key].gapStart = 0.0; this._animationTuples[gap.key].controller.setValue(1.0); this._animationTuples[gap.key].controller.reverse(); j++; } } else if (oldLength == 1) { MaterialGap gap = (MaterialGap) this._children[startOld]; this._animationTuples[gap.key].gapStart = 0.0; this._animationTuples[gap.key].controller.reverse(); } } } else { if ((this._children[j] is MaterialGap) == (newChildren[i] is MaterialGap)) { this._children[j] = newChildren[i]; i++; j++; } else { D.assert(this._children[j] is MaterialGap); j++; } } } while (j < this._children.Count) { this._removeChild(j); } while (i < newChildren.Count) { this._insertChild(j, newChildren[i]); i++; j++; } } BorderRadius _borderRadius(int index, bool start, bool end) { D.assert(MaterialConstantsUtils.kMaterialEdges[MaterialType.card].topLeft == MaterialConstantsUtils.kMaterialEdges[MaterialType.card].topRight); D.assert(MaterialConstantsUtils.kMaterialEdges[MaterialType.card].topLeft == MaterialConstantsUtils.kMaterialEdges[MaterialType.card].bottomLeft); D.assert(MaterialConstantsUtils.kMaterialEdges[MaterialType.card].topLeft == MaterialConstantsUtils.kMaterialEdges[MaterialType.card].bottomRight); Radius cardRadius = MaterialConstantsUtils.kMaterialEdges[MaterialType.card].topLeft; Radius startRadius = Radius.zero; Radius endRadius = Radius.zero; if (index > 0 && this._children[index - 1] is MaterialGap) { startRadius = Radius.lerp( Radius.zero, cardRadius, this._animationTuples[this._children[index - 1].key].startAnimation.value); } if (index < this._children.Count - 2 && this._children[index + 1] is MaterialGap) { endRadius = Radius.lerp( Radius.zero, cardRadius, this._animationTuples[this._children[index + 1].key].endAnimation.value); } if (this.widget.mainAxis == Axis.vertical) { return BorderRadius.vertical( top: start ? cardRadius : startRadius, bottom: end ? cardRadius : endRadius); } else { return BorderRadius.horizontal( left: start ? cardRadius : startRadius, right: end ? cardRadius : endRadius); } } double _getGapSize(int index) { MaterialGap gap = (MaterialGap) this._children[index]; return MathUtils.lerpDouble(this._animationTuples[gap.key].gapStart, gap.size, this._animationTuples[gap.key].gapAnimation.value); } bool _willNeedDivider(int index) { if (index < 0) { return false; } if (index >= this._children.Count) { return false; } return this._children[index] is MaterialSlice || this._isClosingGap(index); } public override Widget build(BuildContext context) { this._removeEmptyGaps(); List widgets = new List(); List slices = new List(); int i; for (i = 0; i < this._children.Count; i++) { if (this._children[i] is MaterialGap) { D.assert(slices.isNotEmpty()); widgets.Add( new Container( decoration: new BoxDecoration( color: Theme.of(context).cardColor, borderRadius: this._borderRadius(i - 1, widgets.isEmpty(), false), shape: BoxShape.rectangle), child: new ListBody( mainAxis: this.widget.mainAxis, children: slices) ) ); slices = new List(); widgets.Add( new SizedBox( width: this.widget.mainAxis == Axis.horizontal ? this._getGapSize(i) : (double?) null, height: this.widget.mainAxis == Axis.vertical ? this._getGapSize(i) : (double?) null) ); } else { MaterialSlice slice = (MaterialSlice) this._children[i]; Widget child = slice.child; if (this.widget.hasDividers) { bool hasTopDivider = this._willNeedDivider(i - 1); bool hasBottomDivider = this._willNeedDivider(i + 1); Border border; BorderSide divider = Divider.createBorderSide( context, width: 0.5 ); if (i == 0) { border = new Border( bottom: hasBottomDivider ? divider : BorderSide.none); } else if (i == this._children.Count - 1) { border = new Border( top: hasTopDivider ? divider : BorderSide.none); } else { border = new Border( top: hasTopDivider ? divider : BorderSide.none, bottom: hasBottomDivider ? divider : BorderSide.none ); } D.assert(border != null); child = new AnimatedContainer( key: new _MergeableMaterialSliceKey(this._children[i].key), decoration: new BoxDecoration(border: border), duration: ThemeUtils.kThemeAnimationDuration, curve: Curves.fastOutSlowIn, child: child ); } slices.Add( new Material( type: MaterialType.transparency, child: child ) ); } } if (slices.isNotEmpty()) { widgets.Add( new Container( decoration: new BoxDecoration( color: Theme.of(context).cardColor, borderRadius: this._borderRadius(i - 1, widgets.isEmpty(), true), shape: BoxShape.rectangle ), child: new ListBody( mainAxis: this.widget.mainAxis, children: slices ) ) ); slices = new List(); } return new _MergeableMaterialListBody( mainAxis: this.widget.mainAxis, boxShadows: ShadowConstants.kElevationToShadow[this.widget.elevation], items: this._children, children: widgets ); } } class _MergeableMaterialSliceKey : GlobalKey { public _MergeableMaterialSliceKey(LocalKey value) : base() { this.value = value; } public readonly LocalKey value; public bool Equals(_MergeableMaterialSliceKey other) { if (ReferenceEquals(null, other)) { return false; } if (ReferenceEquals(this, other)) { return true; } return other.value == this.value; } public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) { return false; } if (ReferenceEquals(this, obj)) { return true; } if (obj.GetType() != this.GetType()) { return false; } return this.Equals((_MergeableMaterialSliceKey) obj); } public static bool operator ==(_MergeableMaterialSliceKey left, _MergeableMaterialSliceKey right) { return Equals(left, right); } public static bool operator !=(_MergeableMaterialSliceKey left, _MergeableMaterialSliceKey right) { return !Equals(left, right); } public override int GetHashCode() { unchecked { var hashCode = this.value.GetHashCode(); return hashCode; } } public override string ToString() { return "_MergeableMaterialSliceKey(" + this.value + ")"; } } class _MergeableMaterialListBody : ListBody { public _MergeableMaterialListBody( List children = null, Axis mainAxis = Axis.vertical, List items = null, List boxShadows = null ) : base(children: children, mainAxis: mainAxis) { this.items = items; this.boxShadows = boxShadows; } public readonly List items; public readonly List boxShadows; AxisDirection _getDirection(BuildContext context) { return AxisDirectionUtils.getAxisDirectionFromAxisReverseAndDirectionality(context, this.mainAxis, false) ?? AxisDirection.right; } public override RenderObject createRenderObject(BuildContext context) { return new _RenderMergeableMaterialListBody( axisDirection: this._getDirection(context), boxShadows: this.boxShadows ); } public override void updateRenderObject(BuildContext context, RenderObject renderObject) { _RenderMergeableMaterialListBody materialRenderListBody = (_RenderMergeableMaterialListBody) renderObject; materialRenderListBody.axisDirection = this._getDirection(context); materialRenderListBody.boxShadows = this.boxShadows; } } class _RenderMergeableMaterialListBody : RenderListBody { public _RenderMergeableMaterialListBody( List children = null, AxisDirection axisDirection = AxisDirection.down, List boxShadows = null ) : base(children: children, axisDirection: axisDirection) { this.boxShadows = boxShadows; } public List boxShadows; void _paintShadows(Canvas canvas, Rect rect) { foreach (BoxShadow boxShadow in this.boxShadows) { Paint paint = boxShadow.toPaint(); canvas.drawRRect( MaterialConstantsUtils.kMaterialEdges[MaterialType.card].toRRect(rect), paint); } } public override void paint(PaintingContext context, Offset offset) { RenderBox child = this.firstChild; int i = 0; while (child != null) { ListBodyParentData childParentData = (ListBodyParentData) child.parentData; Rect rect = (childParentData.offset + offset) & child.size; if (i % 2 == 0) { this._paintShadows(context.canvas, rect); } child = childParentData.nextSibling; i++; } this.defaultPaint(context, offset); } } }