diff --git a/src/AtomUI.Controls/Flyouts/Flyout.cs b/src/AtomUI.Controls/Flyouts/Flyout.cs index 55d29a2..2dd38ce 100644 --- a/src/AtomUI.Controls/Flyouts/Flyout.cs +++ b/src/AtomUI.Controls/Flyouts/Flyout.cs @@ -269,30 +269,37 @@ public class Flyout : PopupFlyoutBase } CalculateShowArrowEffective(); var result = base.ShowAtCore(placementTarget, showAtPointer); - PlayShowUpMotion(); + PlayShowUpMotion(placementTarget); return result; } - private void PlayShowUpMotion() + private void PlayShowUpMotion(Control placementTarget) { if (Popup.Host is PopupRoot popupRoot) { - var director = Director.Instance; - var motion = new ZoomBigInMotion(); - motion.ConfigureOpacity(_motionDuration); - motion.ConfigureRenderTransform(_motionDuration); - BoxShadows boxShadows = default; - if (Popup is Popup shadowAwarePopup) { - boxShadows = shadowAwarePopup.MaskShadows; + if (popupRoot.Content is Control content) { + Popup.Opacity = 0; + var director = Director.Instance; + var motion = new ZoomBigInMotion(); + motion.ConfigureOpacity(_motionDuration); + motion.ConfigureRenderTransform(_motionDuration); + BoxShadows boxShadows = default; + if (Popup is Popup shadowAwarePopup) { + boxShadows = shadowAwarePopup.MaskShadows; + } + + var topLevel = TopLevel.GetTopLevel(placementTarget); + var motionActor = new PopupMotionActor(boxShadows, popupRoot, content, motion); + motionActor.DispatchInSceneLayer = true; + motionActor.SceneParent = topLevel; + motionActor.Completed += (sender, args) => + { + _animating = false; + // Popup.Opacity = 1; + }; + + director?.Schedule(motionActor); + _animating = true; } - var motionActor = new PopupMotionActor(boxShadows.Thickness(), popupRoot, motion); - motionActor.DispatchInSceneLayer = true; - motionActor.Completed += (sender, args) => - { - _animating = false; - }; - - director?.Schedule(motionActor); - _animating = true; } } } \ No newline at end of file diff --git a/src/AtomUI.Controls/Popup/Popup.cs b/src/AtomUI.Controls/Popup/Popup.cs index 6b19f2c..abc6d6c 100644 --- a/src/AtomUI.Controls/Popup/Popup.cs +++ b/src/AtomUI.Controls/Popup/Popup.cs @@ -95,6 +95,8 @@ public class Popup : AbstractPopup _compositeDisposable = new CompositeDisposable(); _shadowLayer = new PopupShadowLayer(toplevel); _compositeDisposable?.Add(BindUtils.RelayBind(this, MaskShadowsProperty, _shadowLayer!)); + _compositeDisposable?.Add(BindUtils.RelayBind(this, OpacityProperty, _shadowLayer!)); + _compositeDisposable?.Add(BindUtils.RelayBind(this, OpacityProperty, (popupHost as Control)!)); _shadowLayer.AttachToTarget(this); } } diff --git a/src/AtomUI.Controls/Popup/PopupMotionActor.cs b/src/AtomUI.Controls/Popup/PopupMotionActor.cs index c51fb42..7a80940 100644 --- a/src/AtomUI.Controls/Popup/PopupMotionActor.cs +++ b/src/AtomUI.Controls/Popup/PopupMotionActor.cs @@ -1,24 +1,44 @@ -using AtomUI.MotionScene; +using AtomUI.Controls.Primitives; +using AtomUI.Media; +using AtomUI.MotionScene; using Avalonia; +using Avalonia.Controls; using Avalonia.Controls.Primitives; +using Avalonia.Media; namespace AtomUI.Controls; public class PopupMotionActor : MotionActor { - private Thickness _boxShadowsThickness; + private BoxShadows _boxShadows; + private PopupRoot _popupRoot; - public PopupMotionActor(Thickness boxShadowsThickness, PopupRoot entity, AbstractMotion motion) - : base(entity, motion) + public PopupMotionActor(BoxShadows boxShadows, + PopupRoot popupRoot, + Control motionTarget, + AbstractMotion motion) + : base(motionTarget, motion) { - _boxShadowsThickness = boxShadowsThickness; + _popupRoot = popupRoot; + _boxShadows = boxShadows; } protected override Point CalculateTopLevelGhostPosition() { - var popup = (MotionTarget as PopupRoot)!; - var winPos = popup.PlatformImpl!.Position; // TODO 可能需要乘以 scaling - var scaledThickness = _boxShadowsThickness * popup.DesktopScaling; + var boxShadowsThickness = _boxShadows.Thickness(); + var winPos = _popupRoot.PlatformImpl!.Position; // TODO 可能需要乘以 scaling + var scaledThickness = boxShadowsThickness * _popupRoot.DesktopScaling; return new Point(winPos.X - scaledThickness.Left, winPos.Y - scaledThickness.Top); } + + protected override void BuildGhost() + { + if (_ghost is null) { + _ghost = new MotionGhostControl(MotionTarget) + { + Shadows = _boxShadows, + MaskCornerRadius = new CornerRadius(6) + }; + } + } } \ No newline at end of file diff --git a/src/AtomUI.Controls/Popup/PopupShadowLayer.cs b/src/AtomUI.Controls/Popup/PopupShadowLayer.cs index 84e44bb..bc45112 100644 --- a/src/AtomUI.Controls/Popup/PopupShadowLayer.cs +++ b/src/AtomUI.Controls/Popup/PopupShadowLayer.cs @@ -1,5 +1,6 @@ using System.Reactive.Disposables; using System.Reflection; +using AtomUI.Controls.Primitives; using AtomUI.Media; using AtomUI.Platform.Windows; using Avalonia; diff --git a/src/AtomUI.Controls/Primitives/MotionGhostControl.cs b/src/AtomUI.Controls/Primitives/MotionGhostControl.cs new file mode 100644 index 0000000..a3e2c20 --- /dev/null +++ b/src/AtomUI.Controls/Primitives/MotionGhostControl.cs @@ -0,0 +1,190 @@ +using System.Reactive.Disposables; +using AtomUI.Media; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Layout; +using Avalonia.LogicalTree; +using Avalonia.Media; + +namespace AtomUI.Controls.Primitives; + +internal class MotionGhostControl : Control +{ + public static readonly StyledProperty ShadowsProperty = + Border.BoxShadowProperty.AddOwner(); + + public static readonly StyledProperty MaskCornerRadiusProperty = + Border.CornerRadiusProperty.AddOwner(); + + /// + /// 渲染的阴影值 + /// + public BoxShadows Shadows + { + get => GetValue(ShadowsProperty); + set => SetValue(ShadowsProperty, value); + } + + /// + /// mask 的圆角大小 + /// + public CornerRadius MaskCornerRadius + { + get => GetValue(MaskCornerRadiusProperty); + set => SetValue(MaskCornerRadiusProperty, value); + } + + protected Border? _maskRenderer; + protected Border? _contentRenderer; + protected bool _initialized = false; + protected Canvas? _layout; + private Control _motionTarget; + + private CompositeDisposable? _compositeDisposable; + + static MotionGhostControl() + { + AffectsMeasure(ShadowsProperty); + AffectsRender(MaskCornerRadiusProperty); + } + + public MotionGhostControl(Control motionTarget) + { + _motionTarget = motionTarget; + } + + protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) + { + base.OnAttachedToLogicalTree(e); + if (!_initialized) { + _compositeDisposable = new CompositeDisposable(); + HorizontalAlignment = HorizontalAlignment.Stretch; + VerticalAlignment = VerticalAlignment.Stretch; + IsHitTestVisible = false; + _layout = new Canvas(); + VisualChildren.Add(_layout); + ((ISetLogicalParent)_layout).SetParent(this); + _maskRenderer = CreateMaskRenderer(); + _contentRenderer = CreateContentRenderer(); + SetupMaskRenderer(_maskRenderer); + SetupContentRenderer(_maskRenderer, _contentRenderer); + _layout.Children.Add(_maskRenderer); + _layout.Children.Add(_contentRenderer); + _initialized = true; + } + } + + protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) + { + base.OnDetachedFromLogicalTree(e); + _compositeDisposable?.Dispose(); + } + + protected override Size MeasureOverride(Size availableSize) + { + Size motionTargetSize = default; + if (_motionTarget.DesiredSize == default) { + // Popup may not have been shown yet. Measure content + motionTargetSize = LayoutHelper.MeasureChild(_motionTarget, availableSize, new Thickness()); + } else { + motionTargetSize = _motionTarget.DesiredSize; + } + + var shadowThickness = Shadows.Thickness(); + return motionTargetSize.Inflate(shadowThickness); + } + + private Border CreateContentRenderer() + { + var contentRenderer = new Border + { + BorderThickness = new Thickness(0), + HorizontalAlignment = HorizontalAlignment.Stretch, + VerticalAlignment = VerticalAlignment.Stretch, + Background = new VisualBrush + { + Visual = _motionTarget, + Stretch = Stretch.None, + AlignmentX = AlignmentX.Left, + } + }; + + return contentRenderer; + } + + private Border CreateMaskRenderer() + { + var maskContent = new Border + { + BorderThickness = new Thickness(0), + HorizontalAlignment = HorizontalAlignment.Stretch, + VerticalAlignment = VerticalAlignment.Stretch, + Background = new VisualBrush + { + Visual = _motionTarget, + Stretch = Stretch.None, + AlignmentX = AlignmentX.Left, + } + }; + + return maskContent; + } + + private void SetupMaskRenderer(Border maskRenderer) + { + + CornerRadius cornerRadius = default; + BoxShadows shadows = default; + + if (Shadows != default) { + shadows = Shadows; + } + + if (MaskCornerRadius != default) { + cornerRadius = MaskCornerRadius; + } + var shadowThickness = shadows.Thickness(); + var offsetX = shadowThickness.Left; + var offsetY = shadowThickness.Top; + if (_motionTarget is IShadowMaskInfoProvider shadowMaskInfoProvider) { + var maskCornerRadius = shadowMaskInfoProvider.GetMaskCornerRadius(); + var maskBounds = shadowMaskInfoProvider.GetMaskBounds(); + if (cornerRadius == default) { + cornerRadius = maskCornerRadius; + } + + offsetY += maskBounds.Y; + offsetX += maskBounds.X; + maskRenderer.Width = maskBounds.Width; + maskRenderer.Height = maskBounds.Height; + } else if (_motionTarget is BorderedStyleControl bordered) { + if (cornerRadius == default) { + cornerRadius = bordered.CornerRadius; + } + maskRenderer.Width = _motionTarget.DesiredSize.Width; + maskRenderer.Height = _motionTarget.DesiredSize.Height; + } else if (_motionTarget is TemplatedControl templatedControl) { + if (cornerRadius == default) { + cornerRadius = templatedControl.CornerRadius; + } + maskRenderer.Width = _motionTarget.DesiredSize.Width; + maskRenderer.Height = _motionTarget.DesiredSize.Height; + } + + maskRenderer.BoxShadow = shadows; + maskRenderer.CornerRadius = cornerRadius; + + Canvas.SetLeft(maskRenderer, offsetX); + Canvas.SetTop(maskRenderer, offsetY); + } + + private void SetupContentRenderer(Border maskRenderer, Border contentRenderer) + { + contentRenderer.Width = _motionTarget.DesiredSize.Width; + contentRenderer.Height = _motionTarget.DesiredSize.Height; + var shadowThickness = maskRenderer.BoxShadow.Thickness(); + Canvas.SetLeft(contentRenderer, shadowThickness.Left); + Canvas.SetTop(contentRenderer, shadowThickness.Top); + } +} \ No newline at end of file diff --git a/src/AtomUI.Controls/Popup/ShadowRenderer.cs b/src/AtomUI.Controls/Primitives/ShadowRenderer.cs similarity index 81% rename from src/AtomUI.Controls/Popup/ShadowRenderer.cs rename to src/AtomUI.Controls/Primitives/ShadowRenderer.cs index f08dd6f..a6396f4 100644 --- a/src/AtomUI.Controls/Popup/ShadowRenderer.cs +++ b/src/AtomUI.Controls/Primitives/ShadowRenderer.cs @@ -6,7 +6,7 @@ using Avalonia.Layout; using Avalonia.LogicalTree; using Avalonia.Media; -namespace AtomUI.Controls; +namespace AtomUI.Controls.Primitives; internal class ShadowRenderer : Control { @@ -15,6 +15,9 @@ internal class ShadowRenderer : Control public static readonly StyledProperty MaskCornerRadiusProperty = Border.CornerRadiusProperty.AddOwner(); + + public static readonly StyledProperty MaskContentBackgroundProperty = + Border.BackgroundProperty.AddOwner(); /// /// 渲染的阴影值 @@ -34,13 +37,20 @@ internal class ShadowRenderer : Control set => SetValue(MaskCornerRadiusProperty, value); } - private Border? _maskContent; - private bool _initialized = false; - private Canvas? _layout; + public IBrush? MaskContentBackground + { + get => GetValue(MaskContentBackgroundProperty); + set => SetValue(MaskContentBackgroundProperty, value); + } + + protected Border? _maskContent; + protected bool _initialized = false; + protected Canvas? _layout; static ShadowRenderer() { AffectsMeasure(ShadowsProperty); + MaskContentBackgroundProperty.OverrideDefaultValue(new SolidColorBrush(Colors.White)); } protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) @@ -66,13 +76,13 @@ internal class ShadowRenderer : Control var maskContent = new Border { BorderThickness = new Thickness(0), - Background = new SolidColorBrush(Colors.White), HorizontalAlignment = HorizontalAlignment.Stretch, VerticalAlignment = VerticalAlignment.Stretch }; // TODO 这个是否需要资源管理起来 BindUtils.RelayBind(this, ShadowsProperty, maskContent, ShadowsProperty); BindUtils.RelayBind(this, MaskCornerRadiusProperty, maskContent, Border.CornerRadiusProperty); + BindUtils.RelayBind(this, MaskContentBackgroundProperty, maskContent, Border.BackgroundProperty); return maskContent; } diff --git a/src/AtomUI/AtomUI.csproj b/src/AtomUI/AtomUI.csproj index af9b8ba..4cfcb26 100644 --- a/src/AtomUI/AtomUI.csproj +++ b/src/AtomUI/AtomUI.csproj @@ -32,6 +32,7 @@ + @@ -42,10 +43,6 @@ - - - - diff --git a/src/AtomUI/Interceptors/TransitionInterceptors.cs b/src/AtomUI/Interceptors/TransitionInterceptors.cs index f03da71..b2ab9e7 100644 --- a/src/AtomUI/Interceptors/TransitionInterceptors.cs +++ b/src/AtomUI/Interceptors/TransitionInterceptors.cs @@ -1,4 +1,5 @@ -using System.Reflection; +using System.Reactive.Subjects; +using System.Reflection; using AtomUI.Media; using Avalonia.Animation; using Avalonia.Media; @@ -6,7 +7,7 @@ using HarmonyLib; namespace AtomUI.Interceptors; -internal static class TransitionInterceptor +internal static class TransitionInterceptor where TTransition : TransitionBase { private static Dictionary _disposables; @@ -16,9 +17,10 @@ internal static class TransitionInterceptor _disposables = new Dictionary(); } - public static bool DoTransitionPrefix(TTransition __instance, IObservable progress) + public static bool DoTransitionPrefix(TTransition __instance, ref IObservable progress) { if (!_disposables.ContainsKey(__instance) && __instance is INotifyTransitionCompleted notifier) { + progress = CreateRelayObservable(progress); var disposable = progress.Subscribe(onNext: d => { }, onCompleted: () => { HandleCompleted(notifier, true); }, onError: exception => { HandleCompleted(notifier, false); }); _disposables.Add(notifier, disposable); @@ -27,6 +29,16 @@ internal static class TransitionInterceptor return true; } + // TODO review 不知道是否有内存泄露 + private static IObservable CreateRelayObservable(IObservable progress) + { + var subject = new Subject(); + progress.Subscribe(onNext: value => subject.OnNext(value), + onError: exception => subject.OnError(exception), + onCompleted: () => subject.OnCompleted()); + return subject; + } + private static void HandleCompleted(INotifyTransitionCompleted notifier, bool succeed) { notifier.NotifyTransitionCompleted(succeed); @@ -48,8 +60,8 @@ internal static class TransitionInterceptorsRegister var origin = typeof(TransformOperationsTransition).GetMethod("DoTransition", BindingFlags.Instance | BindingFlags.NonPublic); - var prefixInterceptor = typeof(TransitionInterceptor<,>) - .MakeGenericType(typeof(TransformOperationsTransition), typeof(ITransform)) + var prefixInterceptor = typeof(TransitionInterceptor<>) + .MakeGenericType(typeof(TransformOperationsTransition)) .GetMethod("DoTransitionPrefix", BindingFlags.Static | BindingFlags.Public); harmony.Patch(origin, prefix: new HarmonyMethod(prefixInterceptor)); } @@ -57,8 +69,8 @@ internal static class TransitionInterceptorsRegister private static void RegisterDoubleTransition(Harmony harmony) { var origin = typeof(DoubleTransition).GetMethod("DoTransition", BindingFlags.Instance | BindingFlags.NonPublic); - var prefixInterceptor = typeof(TransitionInterceptor<,>) - .MakeGenericType(typeof(DoubleTransition), typeof(double)) + var prefixInterceptor = typeof(TransitionInterceptor<>) + .MakeGenericType(typeof(DoubleTransition)) .GetMethod("DoTransitionPrefix", BindingFlags.Static | BindingFlags.Public); harmony.Patch(origin, prefix: new HarmonyMethod(prefixInterceptor)); } diff --git a/src/AtomUI/Media/INotifyTransitionCompleted.cs b/src/AtomUI/Media/INotifyTransitionCompleted.cs index d692b3a..763721d 100644 --- a/src/AtomUI/Media/INotifyTransitionCompleted.cs +++ b/src/AtomUI/Media/INotifyTransitionCompleted.cs @@ -12,6 +12,7 @@ public class TransitionCompletedEventArgs : EventArgs internal interface INotifyTransitionCompleted { - internal event EventHandler? TransitionCompleted; - internal void NotifyTransitionCompleted(bool status); + public IObservable CompletedObservable { get; } + public event EventHandler? TransitionCompleted; + public void NotifyTransitionCompleted(bool status); } \ No newline at end of file diff --git a/src/AtomUI/Media/NotifiableDoubleTransition.cs b/src/AtomUI/Media/NotifiableDoubleTransition.cs index 60b8cbc..cf220fd 100644 --- a/src/AtomUI/Media/NotifiableDoubleTransition.cs +++ b/src/AtomUI/Media/NotifiableDoubleTransition.cs @@ -3,7 +3,7 @@ using Avalonia.Animation; namespace AtomUI.Media; -public class NotifiableDoubleTransition : DoubleTransition +public class NotifiableDoubleTransition : DoubleTransition, INotifyTransitionCompleted { public event EventHandler? TransitionCompleted; private Subject _subject; @@ -13,15 +13,12 @@ public class NotifiableDoubleTransition : DoubleTransition _subject = new Subject(); } - internal protected void NotifyTransitionCompleted(bool status) + void INotifyTransitionCompleted.NotifyTransitionCompleted(bool status) { _subject.OnNext(status); _subject.OnCompleted(); TransitionCompleted?.Invoke(this, new TransitionCompletedEventArgs(status)); } - - internal IObservable GetCompletedObservable() - { - return _subject; - } + + IObservable INotifyTransitionCompleted.CompletedObservable => _subject; } \ No newline at end of file diff --git a/src/AtomUI/Media/NotifiableTransformOperationsTransition.cs b/src/AtomUI/Media/NotifiableTransformOperationsTransition.cs index 00577c0..c7e9a16 100644 --- a/src/AtomUI/Media/NotifiableTransformOperationsTransition.cs +++ b/src/AtomUI/Media/NotifiableTransformOperationsTransition.cs @@ -3,7 +3,7 @@ using Avalonia.Animation; namespace AtomUI.Media; -public class NotifiableTransformOperationsTransition : TransformOperationsTransition +public class NotifiableTransformOperationsTransition : TransformOperationsTransition, INotifyTransitionCompleted { public event EventHandler? TransitionCompleted; private Subject _subject; @@ -13,15 +13,12 @@ public class NotifiableTransformOperationsTransition : TransformOperationsTransi _subject = new Subject(); } - internal protected void NotifyTransitionCompleted(bool status) + void INotifyTransitionCompleted.NotifyTransitionCompleted(bool status) { _subject.OnNext(status); _subject.OnCompleted(); TransitionCompleted?.Invoke(this, new TransitionCompletedEventArgs(status)); } - - internal IObservable GetCompletedObservable() - { - return _subject; - } + + IObservable INotifyTransitionCompleted.CompletedObservable => _subject; } \ No newline at end of file diff --git a/src/AtomUI/MotionScene/AbstractMotion.cs b/src/AtomUI/MotionScene/AbstractMotion.cs index 0e2dc0f..72b4cf7 100644 --- a/src/AtomUI/MotionScene/AbstractMotion.cs +++ b/src/AtomUI/MotionScene/AbstractMotion.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Reactive.Linq; using AtomUI.Media; using Avalonia; using Avalonia.Animation; @@ -38,6 +39,7 @@ public abstract class AbstractMotion : AvaloniaObject, IMotion private Dictionary _motionConfigs; private List _transitions; + public IObservable? CompletedObservable { get; private set; } // 定义我们目前支持的动效属性 public static readonly StyledProperty MotionOpacityProperty = @@ -103,14 +105,27 @@ public abstract class AbstractMotion : AvaloniaObject, IMotion NotifyPreBuildTransition(config, motionTarget); var transition = NotifyBuildTransition(config); _transitions.Add(transition); + } + var completedObservables = new IObservable[_transitions.Count]; + for (int i = 0; i < _transitions.Count; ++i) { + var transition = _transitions[i]; + if (transition is INotifyTransitionCompleted notifyTransitionCompleted) { + completedObservables[i] = (notifyTransitionCompleted.CompletedObservable); + } + } + + CompletedObservable = Observable.CombineLatest(completedObservables).Select(list => + { + return list.All(v=> v); + }); return _transitions; } // 生命周期接口 internal virtual void NotifyPreStart() {} internal virtual void NotifyStarted() {} - internal virtual void NotifyStopped() {} + internal virtual void NotifyCompleted() {} internal virtual void NotifyConfigMotionTarget(Control motionTarget) {} internal virtual void NotifyRestoreMotionTarget(Control motionTarget) {} @@ -174,4 +189,9 @@ public abstract class AbstractMotion : AvaloniaObject, IMotion { return _motionConfigs.Keys.ToList(); } + + public IList GetMotionConfigs() + { + return _motionConfigs.Values.ToList(); + } } \ No newline at end of file diff --git a/src/AtomUI/MotionScene/Director.cs b/src/AtomUI/MotionScene/Director.cs index 88a7505..2669f05 100644 --- a/src/AtomUI/MotionScene/Director.cs +++ b/src/AtomUI/MotionScene/Director.cs @@ -14,7 +14,8 @@ public class Director : IDirector public static IDirector? Instance => AvaloniaLocator.Current.GetService(); private Dictionary _states; - + private CompositeDisposable? _compositeDisposable; + public Director() { _states = new Dictionary(); @@ -34,38 +35,54 @@ public class Director : IDirector if (actor.DispatchInSceneLayer) { sceneLayer = PrepareSceneLayer(actor); } - var cleanupPopup = Disposable.Create((sceneLayer), state => + _compositeDisposable = new CompositeDisposable(); + _compositeDisposable.Add(Disposable.Create((sceneLayer), state => { - if (sceneLayer is not null) { - sceneLayer.Hide(); - sceneLayer.Dispose(); - } - }); - var state = new MotionActorState(actor, cleanupPopup); + // if (sceneLayer is not null) { + // sceneLayer.Hide(); + // sceneLayer.Dispose(); + // } + })); + var state = new MotionActorState(actor, _compositeDisposable); _states.Add(actor, state); if (actor.DispatchInSceneLayer) { - var ghost = actor.BuildGhost(); + var ghost = actor.GetAnimatableGhost(); sceneLayer!.SetMotionTarget(ghost); actor.NotifyMotionTargetAddedToScene(ghost); ghost.IsVisible = false; // 默认是不显示的 - sceneLayer.Show(); + sceneLayer!.Show(); } HandleMotionPreStart(actor); + ExecuteMotionAction(actor); HandleMotionStarted(actor); } private SceneLayer PrepareSceneLayer(MotionActor actor) { - var motionTarget = actor.MotionTarget; - var topLevel = (TopLevel.GetTopLevel(motionTarget) as PopupRoot)!; + if (actor.SceneParent is null) { + throw new ArgumentException("When the DispatchInSceneLayer property is true, the SceneParent property cannot be null."); + } // TODO 这里除了 Popup 这种顶层元素以外,还会不会有其他的顶层元素种类 // 暂时先处理 Popup 这种情况 - var sceneLayer = new SceneLayer(topLevel, topLevel.PlatformImpl!); + var sceneLayer = new SceneLayer(actor.SceneParent, actor.SceneParent.PlatformImpl!.CreatePopup()!); actor.NotifySceneLayerCreated(sceneLayer); return sceneLayer; } + private void ExecuteMotionAction(MotionActor actor) + { + // 根据 Motion 配置的对 Actor 对象的属性赋值 + actor.EnableMotion(); + var ghost = actor.GetAnimatableGhost(); + ghost.IsVisible = true; + foreach (var motionConfig in actor.Motion.GetMotionConfigs()) { + var property = motionConfig.Property; + var endValue = motionConfig.EndValue; + actor.SetValue(property, endValue); + } + } + private class MotionActorState : IDisposable { private readonly IDisposable _cleanup; @@ -85,22 +102,28 @@ public class Director : IDirector } } - protected void HandleMotionActionCompleted(MotionActor actor) - { - if (_states.TryGetValue(actor, out var state)) { - state.Dispose(); - } - - _states.Remove(actor); - } - private void HandleMotionPreStart(MotionActor actor) { actor.NotifyMotionPreStart(); MotionPreStart?.Invoke(this, new MotionEventArgs(actor)); - + if (actor.Motion.CompletedObservable is null) { + throw new InvalidOperationException("The CompletedObservable property of the Motion is empty."); + } // 设置相关的完成检测 + _compositeDisposable?.Add(actor.Motion.CompletedObservable.Subscribe(status => + { + actor.CompletedStatus = status; + }, onCompleted: () => + { + HandleMotionCompleted(actor); + })); + // 设置动画对象初始值 + foreach (var motionConfig in actor.Motion.GetMotionConfigs()) { + var property = motionConfig.Property; + var startValue = motionConfig.StartValue; + actor.SetValue(property, startValue); + } } private void HandleMotionStarted(MotionActor actor) @@ -113,6 +136,12 @@ public class Director : IDirector { actor.NotifyMotionCompleted(); MotionCompleted?.Invoke(this, new MotionEventArgs(actor)); + + if (_states.TryGetValue(actor, out var state)) { + state.Dispose(); + } + + _states.Remove(actor); } } diff --git a/src/AtomUI/MotionScene/IMotion.cs b/src/AtomUI/MotionScene/IMotion.cs index 400556a..bdddcf3 100644 --- a/src/AtomUI/MotionScene/IMotion.cs +++ b/src/AtomUI/MotionScene/IMotion.cs @@ -11,4 +11,6 @@ public interface IMotion /// /// public IList GetActivatedProperties(); + public IList GetMotionConfigs(); + public IObservable? CompletedObservable { get; } } \ No newline at end of file diff --git a/src/AtomUI/MotionScene/IMotionActor.cs b/src/AtomUI/MotionScene/IMotionActor.cs index 31faa25..6996e09 100644 --- a/src/AtomUI/MotionScene/IMotionActor.cs +++ b/src/AtomUI/MotionScene/IMotionActor.cs @@ -8,6 +8,8 @@ internal interface IMotionActor public event EventHandler? Started; public event EventHandler? Completed; + public bool CompletedStatus { get; } + public Control MotionTarget { get; set; } public IMotion Motion { get; } public bool DispatchInSceneLayer { get; set; } diff --git a/src/AtomUI/MotionScene/MotionActor.cs b/src/AtomUI/MotionScene/MotionActor.cs index 086af33..5d1eefe 100644 --- a/src/AtomUI/MotionScene/MotionActor.cs +++ b/src/AtomUI/MotionScene/MotionActor.cs @@ -11,6 +11,7 @@ namespace AtomUI.MotionScene; /// /// 动效配置类,只要给 Director 提供动效相关信息 +/// 动效驱动 Actor 的属性,然后由 Actor 驱动动画控件,防止污染动画控件的 Transitions 配置 /// public class MotionActor : Animatable, IMotionActor { @@ -32,6 +33,8 @@ public class MotionActor : Animatable, IMotionActor private static readonly MethodInfo EnableTransitionsMethodInfo; private static readonly MethodInfo DisableTransitionsMethodInfo; + + public bool CompletedStatus { get; internal set; } = true; protected double MotionOpacity { @@ -61,12 +64,17 @@ public class MotionActor : Animatable, IMotionActor /// 动画实体 /// public Control MotionTarget { get; set; } + + /// + /// 当 DispatchInSceneLayer 为 true 的时候,必须指定一个动画 SceneLayer 的父窗口,最好不要是 Popup + /// + public TopLevel? SceneParent { get; set; } public IMotion Motion => _motion; public bool DispatchInSceneLayer { get; set; } = true; - private Control? _ghost; - private AbstractMotion _motion; + protected Control? _ghost; + protected AbstractMotion _motion; static MotionActor() { @@ -92,10 +100,11 @@ public class MotionActor : Animatable, IMotionActor return false; } - public virtual Control BuildGhost() + protected virtual void BuildGhost() {} + + public Control GetAnimatableGhost() { - _ghost = MotionTarget; - return MotionTarget; + return _ghost ?? MotionTarget; } /// @@ -136,7 +145,8 @@ public class MotionActor : Animatable, IMotionActor return; } - var ghost = BuildGhost(); + var ghost = GetAnimatableGhost(); + Size motionTargetSize; // Popup.Child can't be null here, it was set in ShowAtCore. if (ghost.DesiredSize == default) { @@ -154,7 +164,10 @@ public class MotionActor : Animatable, IMotionActor public virtual void NotifyPostedToDirector() { DisableMotion(); - BuildGhost(); + if (DispatchInSceneLayer) { + BuildGhost(); + } + RelayMotionProperties(); var transitions = new Transitions(); foreach (var transition in _motion.BuildTransitions(_ghost!)) { @@ -169,9 +182,15 @@ public class MotionActor : Animatable, IMotionActor return; } // TODO 这个看是否需要管理起来 + var motionProperties = Motion.GetActivatedProperties(); foreach (var property in motionProperties) { - BindUtils.RelayBind(this, property, _ghost, property); + if (property == MotionRenderTransformProperty) { + BindUtils.RelayBind(this, property, _ghost, Visual.RenderTransformProperty); + } else { + BindUtils.RelayBind(this, property, _ghost, property); + } + } } @@ -194,8 +213,31 @@ public class MotionActor : Animatable, IMotionActor { DisableTransitionsMethodInfo.Invoke(this, new object[]{}); } + + internal virtual void NotifyMotionPreStart() + { + _motion.NotifyPreStart(); + _motion.NotifyConfigMotionTarget(_ghost!); + PreStart?.Invoke(this, EventArgs.Empty); + } + + internal virtual void NotifyMotionStarted() + { + _motion.NotifyStarted(); + Started?.Invoke(this, EventArgs.Empty); + } + + internal virtual void NotifyMotionCompleted() + { + _motion.NotifyCompleted(); + _motion.NotifyRestoreMotionTarget(_ghost!); + Completed?.Invoke(this, EventArgs.Empty); + + } - internal virtual void NotifyMotionPreStart() {} - internal virtual void NotifyMotionStarted() {} - internal virtual void NotifyMotionCompleted() {} + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + Console.WriteLine($"{change.Property.Name}-{change.NewValue}"); + } } \ No newline at end of file diff --git a/src/AtomUI/MotionScene/SceneLayer.cs b/src/AtomUI/MotionScene/SceneLayer.cs index 29b0186..4c9cf63 100644 --- a/src/AtomUI/MotionScene/SceneLayer.cs +++ b/src/AtomUI/MotionScene/SceneLayer.cs @@ -1,4 +1,5 @@ using System.Reflection; +using AtomUI.Platform.Windows; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives.PopupPositioning; @@ -19,6 +20,7 @@ public class SceneLayer : WindowBase, IHostedVisualTreeRoot, IDisposable BackgroundProperty.OverrideDefaultValue(typeof(SceneLayer), Brushes.Transparent); ManagedPopupPositionerPopupInfo = typeof(ManagedPopupPositioner).GetField("_popup", BindingFlags.Instance | BindingFlags.NonPublic)!; + } public SceneLayer(TopLevel parent, IPopupImpl impl) @@ -35,6 +37,9 @@ public class SceneLayer : WindowBase, IHostedVisualTreeRoot, IDisposable { ParentTopLevel = parent; impl.SetWindowManagerAddShadowHint(false); + if (this is WindowBase window) { + //window.SetTransparentForMouseEvents(true); + } if (PlatformImpl?.PopupPositioner is ManagedPopupPositioner managedPopupPositioner) { _managedPopupPositionerPopup = @@ -43,6 +48,12 @@ public class SceneLayer : WindowBase, IHostedVisualTreeRoot, IDisposable _layout = new Canvas(); Content = _layout; +#if DEBUG + if (this is TopLevel topLevel) { + topLevel.AttachDevTools(); + } +#endif + Focusable = true; } /// diff --git a/src/AtomUI/MotionScene/ZoomMotion.cs b/src/AtomUI/MotionScene/ZoomMotion.cs index 3fb0ba9..9fa2ea0 100644 --- a/src/AtomUI/MotionScene/ZoomMotion.cs +++ b/src/AtomUI/MotionScene/ZoomMotion.cs @@ -86,7 +86,7 @@ public class ZoomBigInMotion : AbstractMotion TransitionKind = TransitionKind.Double, StartValue = 0d, EndValue = 1d, - MotionDuration = duration, + MotionDuration = TimeSpan.FromMilliseconds(2000), MotionEasing = easing }; AddMotionConfig(config); @@ -98,9 +98,9 @@ public class ZoomBigInMotion : AbstractMotion var config = new MotionConfig(MotionRenderTransformProperty) { TransitionKind = TransitionKind.TransformOperations, - StartValue = new ScaleTransform(0.8, 0.8), + StartValue = new ScaleTransform(0.2, 0.2), EndValue = new ScaleTransform(1, 1), - MotionDuration = duration, + MotionDuration = TimeSpan.FromMilliseconds(3000), MotionEasing = easing }; AddMotionConfig(config); diff --git a/src/AtomUI/Platform/Windows/WindowExt.cs b/src/AtomUI/Platform/Windows/WindowExt.cs index f5426d0..533d270 100644 --- a/src/AtomUI/Platform/Windows/WindowExt.cs +++ b/src/AtomUI/Platform/Windows/WindowExt.cs @@ -32,6 +32,20 @@ internal static class WindowExt } SetExtendedStyle(impl, currentStyles, false); } + + + public static void SetTransparentForMouseEvents(this Window window, bool flag) + { + var impl = window.PlatformImpl!; + var currentStyles = GetExtendedStyle(impl); + // 不是确定这样处理是否合适 + if (flag) { + currentStyles |= WS_EX_TRANSPARENT | WS_EX_LAYERED; + } else { + currentStyles &= ~(WS_EX_TRANSPARENT | WS_EX_LAYERED); + } + SetExtendedStyle(impl, currentStyles, false); + } private static uint GetExtendedStyle(object instance) {