Add AbstractMotion

This commit is contained in:
polarboy 2024-10-07 12:53:32 +08:00
parent 2ef0690a96
commit e7bc16ad4e
4 changed files with 290 additions and 320 deletions

View File

@ -1,188 +1,72 @@
using System.Diagnostics;
using System.Reactive.Linq;
using AtomUI.Media;
using Avalonia;
using Avalonia;
using Avalonia.Animation;
using Avalonia.Animation.Easings;
using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Media.Transformation;
using Avalonia.Threading;
namespace AtomUI.MotionScene;
public enum TransitionKind
internal class AbstractMotion : IMotion
{
Double,
TransformOperations
}
public bool IsRunning { get; }
public record class MotionConfig
{
public AvaloniaProperty Property { get; set; }
public object? StartValue { get; set; }
public object? EndValue { get; set; }
public Easing MotionEasing { get; set; } = new LinearEasing();
public TimeSpan MotionDuration { get; set; } = TimeSpan.FromMilliseconds(300);
public TransitionKind TransitionKind { get; set; }
public RelativePoint RenderTransformOrigin { get; protected set; }
public IList<Animation> Animations { get; }
public TimeSpan Duration { get; }
public Easing Easing { get; }
public FillMode PropertyValueFillMode { get; }
public MotionConfig(AvaloniaProperty targetProperty, object? startValue = null, object? endValue = null)
public AbstractMotion(TimeSpan duration, Easing? easing = null, FillMode fillMode = FillMode.None)
{
Property = targetProperty;
StartValue = startValue;
EndValue = endValue;
}
}
public abstract class AbstractMotion : AvaloniaObject, IMotion
{
public bool IsRunning { get; } = false;
private readonly Dictionary<AvaloniaProperty, MotionConfig> _motionConfigs;
private readonly List<ITransition> _transitions;
public IObservable<bool>? CompletedObservable { get; private set; }
// 定义我们目前支持的动效属性
public static readonly StyledProperty<double> MotionOpacityProperty =
Visual.OpacityProperty.AddOwner<AbstractMotion>();
public static readonly StyledProperty<double> MotionWidthProperty =
Layoutable.WidthProperty.AddOwner<AbstractMotion>();
public static readonly StyledProperty<double> MotionHeightProperty =
Layoutable.HeightProperty.AddOwner<AbstractMotion>();
public static readonly StyledProperty<RelativePoint> MotionRenderTransformOriginProperty =
Visual.RenderTransformOriginProperty.AddOwner<AbstractMotion>();
public static readonly StyledProperty<ITransform?> MotionRenderTransformProperty =
Visual.RenderTransformProperty.AddOwner<AbstractMotion>();
protected double MotionOpacity
{
get => GetValue(MotionOpacityProperty);
set => SetValue(MotionOpacityProperty, value);
Animations = new List<Animation>();
Duration = duration;
Easing = easing ?? new LinearEasing();
PropertyValueFillMode = fillMode;
}
protected double MotionWidth
public async Task RunAsync(MotionActorControl actor,
Action? aboutToStart = null,
Action? completedAction = null,
CancellationToken cancellationToken = default)
{
get => GetValue(MotionWidthProperty);
set => SetValue(MotionWidthProperty, value);
}
protected double MotionHeight
{
get => GetValue(MotionHeightProperty);
set => SetValue(MotionHeightProperty, value);
}
internal RelativePoint MotionRenderTransformOrigin
{
get => GetValue(MotionRenderTransformOriginProperty);
set => SetValue(MotionRenderTransformOriginProperty, value);
}
protected ITransform? MotionRenderTransform
{
get => GetValue(MotionRenderTransformProperty);
set => SetValue(MotionRenderTransformProperty, value);
}
public AbstractMotion()
{
_motionConfigs = new Dictionary<AvaloniaProperty, MotionConfig>();
_transitions = new List<ITransition>();
}
/// <summary>
/// 创建动效动画对象
/// </summary>
/// <param name="motionTarget"></param>
/// <returns></returns>
internal List<ITransition> BuildTransitions(Control motionTarget)
{
foreach (var entry in _motionConfigs)
Configure();
await Dispatcher.UIThread.InvokeAsync(async () =>
{
var config = entry.Value;
NotifyPreBuildTransition(config, motionTarget);
var transition = NotifyBuildTransition(config);
_transitions.Add(transition);
}
var completedObservables = new IObservable<bool>[_transitions.Count];
for (var i = 0; i < _transitions.Count; ++i)
{
var transition = _transitions[i];
if (transition is INotifyTransitionCompleted notifyTransitionCompleted)
using var originRestore = new RenderTransformOriginRestore(actor);
actor.RenderTransformOrigin = RenderTransformOrigin;
if (aboutToStart is not null)
{
completedObservables[i] = notifyTransitionCompleted.CompletedObservable;
aboutToStart();
}
}
CompletedObservable =
Observable.CombineLatest(completedObservables).Select(list => { return list.All(v => v); });
return _transitions;
NotifyPreStart();
foreach (var animation in Animations)
{
await animation.RunAsync(actor, cancellationToken);
}
if (completedAction is not null)
{
completedAction();
}
NotifyCompleted();
});
}
// 生命周期接口
internal virtual void NotifyPreStart()
protected virtual void Configure()
{
}
internal virtual void NotifyStarted()
protected virtual void NotifyPreStart()
{
}
internal virtual void NotifyCompleted()
protected virtual void NotifyCompleted()
{
}
internal virtual void NotifyConfigMotionTarget(Control motionTarget)
{
}
internal virtual void NotifyRestoreMotionTarget(Control motionTarget)
{
}
protected virtual void NotifyPreBuildTransition(MotionConfig config, Control motionTarget)
{
}
protected virtual ITransition NotifyBuildTransition(MotionConfig config)
{
TransitionBase transition = default!;
if (config.TransitionKind == TransitionKind.Double)
{
transition = new NotifiableDoubleTransition();
}
else if (config.TransitionKind == TransitionKind.TransformOperations)
{
transition = new NotifiableTransformOperationsTransition();
}
transition.Property = config.Property;
transition.Duration = config.MotionDuration;
transition.Easing = config.MotionEasing;
return transition;
}
protected MotionConfig? GetMotionConfig(AvaloniaProperty property)
{
if (_motionConfigs.TryGetValue(property, out var motionConfig))
{
return motionConfig;
}
return null;
}
protected void AddMotionConfig(MotionConfig config)
{
Debug.Assert(!_motionConfigs.ContainsKey(config.Property));
_motionConfigs.Add(config.Property, config);
}
/// <summary>
/// 计算顶层动画渲染层的大小
/// </summary>
@ -208,42 +92,52 @@ public abstract class AbstractMotion : AvaloniaObject, IMotion
return motionTargetPosition;
}
public IList<AvaloniaProperty> GetActivatedProperties()
{
return _motionConfigs.Keys.ToList();
}
public IList<MotionConfig> GetMotionConfigs()
{
return _motionConfigs.Values.ToList();
}
protected TransformOperations BuildScaleTransform(double scaleX, double scaleY)
protected static TransformOperations BuildScaleTransform(double scaleX, double scaleY)
{
var builder = new TransformOperations.Builder(1);
builder.AppendScale(scaleX, scaleY);
return builder.Build();
}
protected TransformOperations BuildScaleTransform(double scale)
protected static TransformOperations BuildScaleTransform(double scale)
{
return BuildScaleTransform(scale, scale);
}
protected TransformOperations BuildScaleXTransform(double scale)
protected static TransformOperations BuildScaleXTransform(double scale)
{
return BuildScaleTransform(scale, 1.0);
}
protected TransformOperations BuildScaleYTransform(double scale)
protected static TransformOperations BuildScaleYTransform(double scale)
{
return BuildScaleTransform(1.0, scale);
}
protected TransformOperations BuildTranslateTransform(double offsetX, double offsetY)
protected static TransformOperations BuildTranslateTransform(double offsetX, double offsetY)
{
var builder = new TransformOperations.Builder(1);
builder.AppendTranslate(offsetX, offsetY);
return builder.Build();
}
protected static TransformOperations BuildTranslateScaleAndTransform(
double scaleX, double scaleY, double offsetX, double offsetY)
{
var builder = new TransformOperations.Builder(2);
builder.AppendScale(scaleX, scaleY);
builder.AppendTranslate(offsetX, offsetY);
return builder.Build();
}
protected Animation CreateAnimation()
{
var animation = new Animation
{
Duration = Duration,
Easing = Easing,
FillMode = PropertyValueFillMode
};
return animation;
}
}

View File

@ -1,139 +0,0 @@
using Avalonia.Animation.Easings;
using Avalonia.Controls;
using Avalonia.Layout;
namespace AtomUI.MotionScene;
public class CollapseMotion : AbstractMotion
{
public MotionConfig? OpacityConfig => GetMotionConfig(MotionOpacityProperty);
public MotionConfig? HeightConfig => GetMotionConfig(MotionHeightProperty);
/// <summary>
/// 收起的方向,垂直还是水平方向
/// </summary>
public Orientation Orientation { get; set; } = Orientation.Vertical;
public void ConfigureHeight(TimeSpan duration, Easing? easing = null)
{
easing ??= new CubicEaseInOut();
var config = new MotionConfig(Orientation == Orientation.Vertical
? MotionHeightProperty
: MotionWidthProperty)
{
TransitionKind = TransitionKind.Double,
EndValue = 0d,
MotionDuration = duration,
MotionEasing = easing
};
AddMotionConfig(config);
}
public void ConfigureOpacity(TimeSpan duration, Easing? easing = null)
{
easing ??= new CubicEaseInOut();
var config = new MotionConfig(MotionOpacityProperty)
{
TransitionKind = TransitionKind.Double,
StartValue = 1d,
EndValue = 0d,
MotionDuration = duration,
MotionEasing = easing
};
AddMotionConfig(config);
}
protected override void NotifyPreBuildTransition(MotionConfig config, Control motionTarget)
{
base.NotifyPreBuildTransition(config, motionTarget);
if (config.Property == MotionHeightProperty)
{
if (!double.IsNaN(motionTarget.Height))
{
config.StartValue = Math.Ceiling(motionTarget.Height);
}
else
{
config.StartValue = Math.Ceiling(motionTarget.DesiredSize.Height);
}
}
else if (config.Property == MotionWidthProperty)
{
if (!double.IsNaN(motionTarget.Width))
{
config.StartValue = Math.Ceiling(motionTarget.Width);
}
else
{
config.StartValue = Math.Ceiling(motionTarget.DesiredSize.Width);
}
}
}
}
public class ExpandMotion : AbstractMotion
{
public MotionConfig? OpacityConfig => GetMotionConfig(MotionOpacityProperty);
public MotionConfig? HeightConfig => GetMotionConfig(MotionHeightProperty);
/// <summary>
/// 展开的方向,垂直还是水平方向
/// </summary>
public Orientation Orientation { get; set; } = Orientation.Vertical;
public void ConfigureHeight(TimeSpan duration, Easing? easing = null)
{
easing ??= new CubicEaseInOut();
var config = new MotionConfig(Orientation == Orientation.Vertical
? MotionHeightProperty
: MotionWidthProperty)
{
TransitionKind = TransitionKind.Double,
StartValue = 0,
MotionDuration = duration,
MotionEasing = easing
};
AddMotionConfig(config);
}
public void ConfigureOpacity(TimeSpan duration, Easing? easing = null)
{
easing ??= new CubicEaseInOut();
var config = new MotionConfig(MotionOpacityProperty)
{
TransitionKind = TransitionKind.Double,
StartValue = 0d,
EndValue = 1d,
MotionDuration = duration,
MotionEasing = easing
};
AddMotionConfig(config);
}
protected override void NotifyPreBuildTransition(MotionConfig config, Control motionTarget)
{
base.NotifyPreBuildTransition(config, motionTarget);
if (config.Property == MotionHeightProperty)
{
if (!double.IsNaN(motionTarget.Height))
{
config.EndValue = Math.Ceiling(motionTarget.Height);
}
else
{
config.EndValue = Math.Ceiling(motionTarget.DesiredSize.Height);
}
}
else if (config.Property == MotionWidthProperty)
{
if (!double.IsNaN(motionTarget.Width))
{
config.EndValue = Math.Ceiling(motionTarget.Width);
}
else
{
config.EndValue = Math.Ceiling(motionTarget.DesiredSize.Width);
}
}
}
}

View File

@ -0,0 +1,212 @@
using Avalonia;
using Avalonia.Animation;
using Avalonia.Animation.Easings;
using Avalonia.Styling;
namespace AtomUI.MotionScene;
internal class CollapseMotion : AbstractMotion
{
public Direction Direction { get; }
public CollapseMotion(Direction direction,
TimeSpan duration,
Easing? easing = null,
FillMode fillMode = FillMode.None)
: base(duration, easing ?? new CubicEaseOut(), fillMode)
{
Direction = direction;
}
protected override void Configure()
{
var isHorizontal = Direction == Direction.Left || Direction == Direction.Right;
var animation = CreateAnimation();
var startFrame = new KeyFrame
{
Cue = new Cue(0.0)
};
{
var opacitySetter = new Setter
{
Property = Visual.OpacityProperty,
Value = 1.0
};
startFrame.Setters.Add(opacitySetter);
if (isHorizontal)
{
var scaleXSetter = new Setter
{
Property = MotionActorControl.MotionTransformProperty,
Value = BuildScaleXTransform(1.0)
};
startFrame.Setters.Add(scaleXSetter);
}
else
{
var scaleYSetter = new Setter
{
Property = MotionActorControl.MotionTransformProperty,
Value = BuildScaleYTransform(1.0)
};
startFrame.Setters.Add(scaleYSetter);
}
}
animation.Children.Add(startFrame);
var endFrame = new KeyFrame
{
Cue = new Cue(1.0)
};
{
var opacitySetter = new Setter
{
Property = Visual.OpacityProperty,
Value = 0.0
};
endFrame.Setters.Add(opacitySetter);
if (isHorizontal)
{
var scaleXSetter = new Setter
{
Property = MotionActorControl.MotionTransformProperty,
Value = BuildScaleXTransform(0.0)
};
endFrame.Setters.Add(scaleXSetter);
}
else
{
var scaleYSetter = new Setter
{
Property = MotionActorControl.MotionTransformProperty,
Value = BuildScaleYTransform(0.0)
};
endFrame.Setters.Add(scaleYSetter);
}
}
animation.Children.Add(endFrame);
if (Direction == Direction.Left)
{
RenderTransformOrigin = new RelativePoint(1, 0.5, RelativeUnit.Relative);
}
else if (Direction == Direction.Right)
{
RenderTransformOrigin = new RelativePoint(0, 0.5, RelativeUnit.Relative);
}
else if (Direction == Direction.Top)
{
RenderTransformOrigin = new RelativePoint(0.5, 1.0, RelativeUnit.Relative);
}
else
{
RenderTransformOrigin = new RelativePoint(0.5, 0.0, RelativeUnit.Relative);
}
Animations.Add(animation);
}
}
internal class ExpandMotion : AbstractMotion
{
public Direction Direction { get; }
public ExpandMotion(Direction direction,
TimeSpan duration,
Easing? easing = null,
FillMode fillMode = FillMode.None)
: base(duration, easing ?? new CubicEaseIn(), fillMode)
{
Direction = direction;
}
protected override void Configure()
{
var isHorizontal = Direction == Direction.Left || Direction == Direction.Right;
var animation = CreateAnimation();
var startFrame = new KeyFrame
{
Cue = new Cue(0.0)
};
{
var opacitySetter = new Setter
{
Property = Visual.OpacityProperty,
Value = 0.0
};
startFrame.Setters.Add(opacitySetter);
if (isHorizontal)
{
var scaleXSetter = new Setter
{
Property = MotionActorControl.MotionTransformProperty,
Value = BuildScaleXTransform(0.01)
};
startFrame.Setters.Add(scaleXSetter);
}
else
{
var scaleYSetter = new Setter
{
Property = MotionActorControl.MotionTransformProperty,
Value = BuildScaleYTransform(0.01)
};
startFrame.Setters.Add(scaleYSetter);
}
}
animation.Children.Add(startFrame);
var endFrame = new KeyFrame
{
Cue = new Cue(1.0)
};
{
var opacitySetter = new Setter
{
Property = Visual.OpacityProperty,
Value = 1.0
};
endFrame.Setters.Add(opacitySetter);
if (isHorizontal)
{
var scaleXSetter = new Setter
{
Property = MotionActorControl.MotionTransformProperty,
Value = BuildScaleXTransform(1.0)
};
endFrame.Setters.Add(scaleXSetter);
}
else
{
var scaleYSetter = new Setter
{
Property = MotionActorControl.MotionTransformProperty,
Value = BuildScaleYTransform(1.0)
};
endFrame.Setters.Add(scaleYSetter);
}
}
animation.Children.Add(endFrame);
if (Direction == Direction.Left)
{
RenderTransformOrigin = new RelativePoint(1.0, 0.5, RelativeUnit.Relative);
}
else if (Direction == Direction.Right)
{
RenderTransformOrigin = new RelativePoint(0.0, 0.5, RelativeUnit.Relative);
}
else if (Direction == Direction.Top)
{
RenderTransformOrigin = new RelativePoint(0.5, 1.0, RelativeUnit.Relative);
}
else
{
RenderTransformOrigin = new RelativePoint(0.5, 1.0, RelativeUnit.Relative);
}
Animations.Add(animation);
}
}

View File

@ -1,17 +1,20 @@
using Avalonia;
using Avalonia.Animation;
using Avalonia.Animation.Easings;
namespace AtomUI.MotionScene;
public interface IMotion
internal interface IMotion
{
public bool IsRunning { get; }
public RelativePoint RenderTransformOrigin { get; }
public IList<Animation> Animations { get; }
public TimeSpan Duration { get; }
public Easing Easing { get; }
public FillMode PropertyValueFillMode { get; }
/// <summary>
/// 获取当前动效激活的动画属性列表
/// </summary>
/// <returns></returns>
public IList<AvaloniaProperty> GetActivatedProperties();
public IList<MotionConfig> GetMotionConfigs();
public IObservable<bool>? CompletedObservable { get; }
public Task RunAsync(MotionActorControl actor,
Action? aboutToStart = null,
Action? completedAction = null,
CancellationToken cancellationToken = default);
}