diff --git a/src/AtomUI.Controls/Primitives/MotionActorControl.cs b/src/AtomUI.Controls/Primitives/MotionActorControl.cs new file mode 100644 index 0000000..66f7bcd --- /dev/null +++ b/src/AtomUI.Controls/Primitives/MotionActorControl.cs @@ -0,0 +1,372 @@ +using System.Reactive.Linq; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Media; + +namespace AtomUI.Controls.Primitives; + +public class MotionActorControl : Decorator +{ + #region 公共属性定义 + + public static readonly StyledProperty MotionTransformProperty = + AvaloniaProperty.Register(nameof(MotionTransform)); + + public ITransform? MotionTransform + { + get => GetValue(MotionTransformProperty); + set => SetValue(MotionTransformProperty, value); + } + + public Control? MotionTransformRoot => Child; + + #endregion + + /// + /// RenderTransform/MatrixTransform applied to MotionTransformRoot. + /// + private readonly MatrixTransform _matrixTransform = new(); + + /// + /// Transformation matrix corresponding to _matrixTransform. + /// + private Matrix _transformation = Matrix.Identity; + + private IDisposable? _transformChangedEvent; + + /// + /// Acceptable difference between two doubles. + /// + private const double AcceptableDelta = 0.0001; + + /// + /// Number of decimals to round the Matrix to. + /// + private const int DecimalsAfterRound = 4; + + /// + /// Actual DesiredSize of Child element (the value it returned from its MeasureOverride method). + /// + private Size _childActualSize; + + static MotionActorControl() + { + ClipToBoundsProperty.OverrideDefaultValue(true); + + MotionTransformProperty.Changed + .AddClassHandler((x, e) => x.HandleLayoutTransformChanged(e)); + + ChildProperty.Changed + .AddClassHandler((x, _) => x.HandleChildChanged()); + } + + private void HandleLayoutTransformChanged(AvaloniaPropertyChangedEventArgs e) + { + var newTransform = e.NewValue as Transform; + + _transformChangedEvent?.Dispose(); + _transformChangedEvent = null; + + if (newTransform != null) + { + _transformChangedEvent = Observable.FromEventPattern( + v => newTransform.Changed += v, v => newTransform.Changed -= v) + .Subscribe(_ => ApplyMotionTransform()); + } + + ApplyMotionTransform(); + } + + private void HandleChildChanged() + { + if (null != MotionTransformRoot) + { + // 这里我们会过滤掉 Scale 缩放 + MotionTransformRoot.RenderTransform = _matrixTransform; + MotionTransformRoot.RenderTransformOrigin = new RelativePoint(0, 0, RelativeUnit.Absolute); + } + + ApplyMotionTransform(); + } + + /// + /// Applies the layout transform on the LayoutTransformerControl content. + /// + /// + /// Only used in advanced scenarios (like animating the LayoutTransform). + /// Should be used to notify the LayoutTransformer control that some aspect + /// of its Transform property has changed. + /// + private void ApplyMotionTransform() + { + // Get the transform matrix and apply it + var matrix = MotionTransform is null ? Matrix.Identity : RoundMatrix(MotionTransform.Value, DecimalsAfterRound); + + if (_transformation == matrix) + { + return; + } + + _transformation = matrix; + _matrixTransform.Matrix = matrix; + // New transform means re-layout is necessary + InvalidateMeasure(); + } + + /// + /// Rounds the non-offset elements of a Matrix to avoid issues due to floating point imprecision. + /// + /// Matrix to round. + /// Number of decimal places to round to. + /// Rounded Matrix. + private static Matrix RoundMatrix(Matrix matrix, int decimals) + { + return new Matrix( + Math.Round(matrix.M11, decimals), + Math.Round(matrix.M12, decimals), + Math.Round(matrix.M21, decimals), + Math.Round(matrix.M22, decimals), + matrix.M31, + matrix.M32); + } + + protected override Size ArrangeOverride(Size finalSize) + { + if (MotionTransformRoot == null || MotionTransform == null) + { + // TODO 这里可能会引起混淆,因为我们不会对 Target 实施 Scale 转换 + SetCurrentValue(MotionTransformProperty, RenderTransform); + return base.ArrangeOverride(finalSize); + } + + // Determine the largest available size after the transformation + Size finalSizeTransformed = ComputeLargestTransformedSize(finalSize); + if (IsSizeSmaller(finalSizeTransformed, MotionTransformRoot.DesiredSize)) + { + // Some elements do not like being given less space than they asked for (ex: TextBlock) + // Bump the working size up to do the right thing by them + finalSizeTransformed = MotionTransformRoot.DesiredSize; + } + + // Transform the working size to find its width/height + Rect transformedRect = + new Rect(0, 0, finalSizeTransformed.Width, finalSizeTransformed.Height).TransformToAABB(_transformation); + // Create the Arrange rect to center the transformed content + Rect finalRect = new Rect( + -transformedRect.X + ((finalSize.Width - transformedRect.Width) / 2), + -transformedRect.Y + ((finalSize.Height - transformedRect.Height) / 2), + finalSizeTransformed.Width, + finalSizeTransformed.Height); + + // Perform an Arrange on MotionTransformRoot (containing Child) + Size arrangedsize; + MotionTransformRoot.Arrange(finalRect); + arrangedsize = MotionTransformRoot.Bounds.Size; + + // This is the first opportunity under Silverlight to find out the Child's true DesiredSize + if (IsSizeSmaller(finalSizeTransformed, arrangedsize) && _childActualSize == default) + { + //// Unfortunately, all the work so far is invalid because the wrong DesiredSize was used + //// Make a note of the actual DesiredSize + //_childActualSize = arrangedsize; + //// Force a new measure/arrange pass + //InvalidateMeasure(); + } + else + { + // Clear the "need to measure/arrange again" flag + _childActualSize = default; + } + + // Return result to perform the transformation + return finalSize; + } + + protected override Size MeasureOverride(Size availableSize) + { + Console.WriteLine(MotionTransform); + if (MotionTransformRoot == null || MotionTransform == null) + { + return base.MeasureOverride(availableSize); + } + + Size measureSize; + if (_childActualSize == default) + { + // Determine the largest size after the transformation + measureSize = ComputeLargestTransformedSize(availableSize); + } + else + { + // Previous measure/arrange pass determined that Child.DesiredSize was larger than believed + measureSize = _childActualSize; + } + + // Perform a measure on the MotionTransformRoot (containing Child) + MotionTransformRoot.Measure(measureSize); + + var desiredSize = MotionTransformRoot.DesiredSize; + + // Transform DesiredSize to find its width/height + Rect transformedDesiredRect = + new Rect(0, 0, desiredSize.Width, desiredSize.Height).TransformToAABB(_transformation); + Size transformedDesiredSize = new Size(transformedDesiredRect.Width, transformedDesiredRect.Height); + + // Return result to allocate enough space for the transformation + return transformedDesiredSize; + } + + /// + /// Compute the largest usable size (greatest area) after applying the transformation to the specified bounds. + /// + /// Arrange bounds. + /// Largest Size possible. + private Size ComputeLargestTransformedSize(Size arrangeBounds) + { + // Computed largest transformed size + Size computedSize = default; + + // Detect infinite bounds and constrain the scenario + bool infiniteWidth = double.IsInfinity(arrangeBounds.Width); + if (infiniteWidth) + { + // arrangeBounds.Width = arrangeBounds.Height; + arrangeBounds = arrangeBounds.WithWidth(arrangeBounds.Height); + } + + bool infiniteHeight = double.IsInfinity(arrangeBounds.Height); + if (infiniteHeight) + { + //arrangeBounds.Height = arrangeBounds.Width; + arrangeBounds = arrangeBounds.WithHeight(arrangeBounds.Width); + } + + // Capture the matrix parameters + double a = _transformation.M11; + double b = _transformation.M12; + double c = _transformation.M21; + double d = _transformation.M22; + + // Compute maximum possible transformed width/height based on starting width/height + // These constraints define two lines in the positive x/y quadrant + double maxWidthFromWidth = Math.Abs(arrangeBounds.Width / a); + double maxHeightFromWidth = Math.Abs(arrangeBounds.Width / c); + double maxWidthFromHeight = Math.Abs(arrangeBounds.Height / b); + double maxHeightFromHeight = Math.Abs(arrangeBounds.Height / d); + + // The transformed width/height that maximize the area under each segment is its midpoint + // At most one of the two midpoints will satisfy both constraints + double idealWidthFromWidth = maxWidthFromWidth / 2; + double idealHeightFromWidth = maxHeightFromWidth / 2; + double idealWidthFromHeight = maxWidthFromHeight / 2; + double idealHeightFromHeight = maxHeightFromHeight / 2; + + // Compute slope of both constraint lines + double slopeFromWidth = -(maxHeightFromWidth / maxWidthFromWidth); + double slopeFromHeight = -(maxHeightFromHeight / maxWidthFromHeight); + + if ((0 == arrangeBounds.Width) || (0 == arrangeBounds.Height)) + { + // Check for empty bounds + computedSize = new Size(arrangeBounds.Width, arrangeBounds.Height); + } + else if (infiniteWidth && infiniteHeight) + { + // Check for completely unbound scenario + computedSize = new Size(double.PositiveInfinity, double.PositiveInfinity); + } + else if (!_transformation.HasInverse) + { + // Check for singular matrix + computedSize = new Size(0, 0); + } + else if ((0 == b) || (0 == c)) + { + // Check for 0/180 degree special cases + double maxHeight = (infiniteHeight ? double.PositiveInfinity : maxHeightFromHeight); + double maxWidth = (infiniteWidth ? double.PositiveInfinity : maxWidthFromWidth); + if ((0 == b) && (0 == c)) + { + // No constraints + computedSize = new Size(maxWidth, maxHeight); + } + else if (0 == b) + { + // Constrained by width + double computedHeight = Math.Min(idealHeightFromWidth, maxHeight); + computedSize = new Size( + maxWidth - Math.Abs((c * computedHeight) / a), + computedHeight); + } + else if (0 == c) + { + // Constrained by height + double computedWidth = Math.Min(idealWidthFromHeight, maxWidth); + computedSize = new Size( + computedWidth, + maxHeight - Math.Abs((b * computedWidth) / d)); + } + } + else if ((0 == a) || (0 == d)) + { + // Check for 90/270 degree special cases + double maxWidth = (infiniteHeight ? double.PositiveInfinity : maxWidthFromHeight); + double maxHeight = (infiniteWidth ? double.PositiveInfinity : maxHeightFromWidth); + if ((0 == a) && (0 == d)) + { + // No constraints + computedSize = new Size(maxWidth, maxHeight); + } + else if (0 == a) + { + // Constrained by width + double computedHeight = Math.Min(idealHeightFromHeight, maxHeight); + computedSize = new Size( + maxWidth - Math.Abs((d * computedHeight) / b), + computedHeight); + } + else if (0 == d) + { + // Constrained by height + double computedWidth = Math.Min(idealWidthFromWidth, maxWidth); + computedSize = new Size( + computedWidth, + maxHeight - Math.Abs((a * computedWidth) / c)); + } + } + else if (idealHeightFromWidth <= ((slopeFromHeight * idealWidthFromWidth) + maxHeightFromHeight)) + { + // Check the width midpoint for viability (by being below the height constraint line) + computedSize = new Size(idealWidthFromWidth, idealHeightFromWidth); + } + else if (idealHeightFromHeight <= ((slopeFromWidth * idealWidthFromHeight) + maxHeightFromWidth)) + { + // Check the height midpoint for viability (by being below the width constraint line) + computedSize = new Size(idealWidthFromHeight, idealHeightFromHeight); + } + else + { + // Neither midpoint is viable; use the intersection of the two constraint lines instead + // Compute width by setting heights equal (m1*x+c1=m2*x+c2) + double computedWidth = (maxHeightFromHeight - maxHeightFromWidth) / (slopeFromWidth - slopeFromHeight); + // Compute height from width constraint line (y=m*x+c; using height would give same result) + computedSize = new Size( + computedWidth, + (slopeFromWidth * computedWidth) + maxHeightFromWidth); + } + + // Return result + return computedSize; + } + + /// + /// Returns true if Size a is smaller than Size b in either dimension. + /// + /// Second Size. + /// First Size. + /// True if Size a is smaller than Size b in either dimension. + private static bool IsSizeSmaller(Size a, Size b) + { + return (a.Width + AcceptableDelta < b.Width) || (a.Height + AcceptableDelta < b.Height); + } +} \ No newline at end of file diff --git a/src/AtomUI.Controls/Primitives/MotionAppliedControl.cs b/src/AtomUI.Controls/Primitives/MotionAppliedControl.cs deleted file mode 100644 index e0df6ee..0000000 --- a/src/AtomUI.Controls/Primitives/MotionAppliedControl.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace AtomUI.Controls.Primitives; - -public class MotionAppliedControl -{ - - -} \ No newline at end of file