mirror of
https://gitee.com/chinware/atomui.git
synced 2024-12-03 12:28:27 +08:00
完成动画渲染
This commit is contained in:
parent
b75f96a901
commit
c0d0953b9c
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
using System.Reactive.Disposables;
|
||||
using System.Reflection;
|
||||
using AtomUI.Controls.Primitives;
|
||||
using AtomUI.Media;
|
||||
using AtomUI.Platform.Windows;
|
||||
using Avalonia;
|
||||
|
190
src/AtomUI.Controls/Primitives/MotionGhostControl.cs
Normal file
190
src/AtomUI.Controls/Primitives/MotionGhostControl.cs
Normal 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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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 -->
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -11,4 +11,6 @@ public interface IMotion
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public IList<AvaloniaProperty> GetActivatedProperties();
|
||||
public IList<MotionConfig> GetMotionConfigs();
|
||||
public IObservable<bool>? CompletedObservable { get; }
|
||||
}
|
@ -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; }
|
||||
|
@ -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}");
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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);
|
||||
|
@ -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)
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user