完成动画渲染

This commit is contained in:
polarboy 2024-07-04 22:22:28 +08:00
parent b75f96a901
commit c0d0953b9c
19 changed files with 450 additions and 96 deletions

View File

@ -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;
}
}
}

View File

@ -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);
}
}

View File

@ -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)
};
}
}
}

View File

@ -1,5 +1,6 @@
using System.Reactive.Disposables;
using System.Reflection;
using AtomUI.Controls.Primitives;
using AtomUI.Media;
using AtomUI.Platform.Windows;
using Avalonia;

View File

@ -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<BoxShadows> ShadowsProperty =
Border.BoxShadowProperty.AddOwner<MotionGhostControl>();
public static readonly StyledProperty<CornerRadius> MaskCornerRadiusProperty =
Border.CornerRadiusProperty.AddOwner<MotionGhostControl>();
/// <summary>
/// 渲染的阴影值
/// </summary>
public BoxShadows Shadows
{
get => GetValue(ShadowsProperty);
set => SetValue(ShadowsProperty, value);
}
/// <summary>
/// mask 的圆角大小
/// </summary>
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<ShadowRenderer>(ShadowsProperty);
AffectsRender<ShadowRenderer>(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);
}
}

View File

@ -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<CornerRadius> MaskCornerRadiusProperty =
Border.CornerRadiusProperty.AddOwner<ShadowRenderer>();
public static readonly StyledProperty<IBrush?> MaskContentBackgroundProperty =
Border.BackgroundProperty.AddOwner<ShadowRenderer>();
/// <summary>
/// 渲染的阴影值
@ -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<ShadowRenderer>(ShadowsProperty);
MaskContentBackgroundProperty.OverrideDefaultValue<ShadowRenderer>(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;
}

View File

@ -32,6 +32,7 @@
<ItemGroup>
<PackageReference Include="Lib.Harmony.Thin" />
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" />
</ItemGroup>
<ItemGroup>
@ -42,10 +43,6 @@
<Compile Include="Platform\Windows\*.cs"/>
<PackageReference Include="Avalonia.Win32"/>
</ItemGroup>
<ItemGroup>
<Folder Include="Transitions\" />
</ItemGroup>
<PropertyGroup>
<!-- enable private APIs -->

View File

@ -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<TTransition, TValue>
internal static class TransitionInterceptor<TTransition>
where TTransition : TransitionBase
{
private static Dictionary<object, IDisposable> _disposables;
@ -16,9 +17,10 @@ internal static class TransitionInterceptor<TTransition, TValue>
_disposables = new Dictionary<object, IDisposable>();
}
public static bool DoTransitionPrefix(TTransition __instance, IObservable<TValue> progress)
public static bool DoTransitionPrefix(TTransition __instance, ref IObservable<double> 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<TTransition, TValue>
return true;
}
// TODO review 不知道是否有内存泄露
private static IObservable<double> CreateRelayObservable(IObservable<double> progress)
{
var subject = new Subject<double>();
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));
}

View File

@ -12,6 +12,7 @@ public class TransitionCompletedEventArgs : EventArgs
internal interface INotifyTransitionCompleted
{
internal event EventHandler<TransitionCompletedEventArgs>? TransitionCompleted;
internal void NotifyTransitionCompleted(bool status);
public IObservable<bool> CompletedObservable { get; }
public event EventHandler<TransitionCompletedEventArgs>? TransitionCompleted;
public void NotifyTransitionCompleted(bool status);
}

View File

@ -3,7 +3,7 @@ using Avalonia.Animation;
namespace AtomUI.Media;
public class NotifiableDoubleTransition : DoubleTransition
public class NotifiableDoubleTransition : DoubleTransition, INotifyTransitionCompleted
{
public event EventHandler<TransitionCompletedEventArgs>? TransitionCompleted;
private Subject<bool> _subject;
@ -13,15 +13,12 @@ public class NotifiableDoubleTransition : DoubleTransition
_subject = new Subject<bool>();
}
internal protected void NotifyTransitionCompleted(bool status)
void INotifyTransitionCompleted.NotifyTransitionCompleted(bool status)
{
_subject.OnNext(status);
_subject.OnCompleted();
TransitionCompleted?.Invoke(this, new TransitionCompletedEventArgs(status));
}
internal IObservable<bool> GetCompletedObservable()
{
return _subject;
}
IObservable<bool> INotifyTransitionCompleted.CompletedObservable => _subject;
}

View File

@ -3,7 +3,7 @@ using Avalonia.Animation;
namespace AtomUI.Media;
public class NotifiableTransformOperationsTransition : TransformOperationsTransition
public class NotifiableTransformOperationsTransition : TransformOperationsTransition, INotifyTransitionCompleted
{
public event EventHandler<TransitionCompletedEventArgs>? TransitionCompleted;
private Subject<bool> _subject;
@ -13,15 +13,12 @@ public class NotifiableTransformOperationsTransition : TransformOperationsTransi
_subject = new Subject<bool>();
}
internal protected void NotifyTransitionCompleted(bool status)
void INotifyTransitionCompleted.NotifyTransitionCompleted(bool status)
{
_subject.OnNext(status);
_subject.OnCompleted();
TransitionCompleted?.Invoke(this, new TransitionCompletedEventArgs(status));
}
internal IObservable<bool> GetCompletedObservable()
{
return _subject;
}
IObservable<bool> INotifyTransitionCompleted.CompletedObservable => _subject;
}

View File

@ -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<AvaloniaProperty, MotionConfig> _motionConfigs;
private List<ITransition> _transitions;
public IObservable<bool>? CompletedObservable { get; private set; }
// 定义我们目前支持的动效属性
public static readonly StyledProperty<double> 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<bool>[_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<MotionConfig> GetMotionConfigs()
{
return _motionConfigs.Values.ToList();
}
}

View File

@ -14,7 +14,8 @@ public class Director : IDirector
public static IDirector? Instance => AvaloniaLocator.Current.GetService<IDirector>();
private Dictionary<IMotionActor, MotionActorState> _states;
private CompositeDisposable? _compositeDisposable;
public Director()
{
_states = new Dictionary<IMotionActor, MotionActorState>();
@ -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);
}
}

View File

@ -11,4 +11,6 @@ public interface IMotion
/// </summary>
/// <returns></returns>
public IList<AvaloniaProperty> GetActivatedProperties();
public IList<MotionConfig> GetMotionConfigs();
public IObservable<bool>? CompletedObservable { get; }
}

View File

@ -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; }

View File

@ -11,6 +11,7 @@ namespace AtomUI.MotionScene;
/// <summary>
/// 动效配置类,只要给 Director 提供动效相关信息
/// 动效驱动 Actor 的属性,然后由 Actor 驱动动画控件,防止污染动画控件的 Transitions 配置
/// </summary>
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
/// 动画实体
/// </summary>
public Control MotionTarget { get; set; }
/// <summary>
/// 当 DispatchInSceneLayer 为 true 的时候,必须指定一个动画 SceneLayer 的父窗口,最好不要是 Popup
/// </summary>
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;
}
/// <summary>
@ -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}");
}
}

View File

@ -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;
}
/// <summary>

View File

@ -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);

View File

@ -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)
{