這是 Avalonia 的已知問題,我已經報告給官方,詳細請看 https://github.com/AvaloniaUI/Avalonia/pull/17370
我嘗試修復了此問題,請看 https://github.com/AvaloniaUI/Avalonia/pull/17370
復現的步驟如下:
- 在介面放入一個 UI 控制元件,如 Border 控制元件
- 透過
ElementComposition.GetElementVisual
方法獲取 CompositionVisual 物件,再使用此物件建立和播放一個 Vector3DKeyFrameAnimation 動畫 - 重複執行步驟 2
此時你可以看到重複執行步驟 2 時,原本正在播放的動畫已經停止播放了
以下是我的 XAML 介面程式碼
<Grid>
<Border x:Name="ScanBorder" ZIndex="101" IsVisible="True" HorizontalAlignment="Center" VerticalAlignment="Top"
Height="220" Width="600">
<Border.Background>
<LinearGradientBrush StartPoint="0%,0%" EndPoint="0%,100%">
<GradientStop Color="#0033CEFF" Offset="0" />
<GradientStop Color="#CC3592FF" Offset="1" />
</LinearGradientBrush>
</Border.Background>
<Border.RenderTransform>
<TranslateTransform />
</Border.RenderTransform>
</Border>
<Button x:Name="ControlButton" Content="Click" Click="ControlButton_OnClick"></Button>
</Grid>
以下是我的 C# 程式碼
private Vector3DKeyFrameAnimation? _vector3DKeyFrameAnimation;
private CompositionVisual? _scanBorderCompositionVisual;
private void ControlButton_OnClick(object? sender, RoutedEventArgs e)
{
_scanBorderCompositionVisual = ElementComposition.GetElementVisual(ScanBorder)!;
var compositor = _scanBorderCompositionVisual.Compositor;
_vector3DKeyFrameAnimation = compositor.CreateVector3DKeyFrameAnimation();
_vector3DKeyFrameAnimation.InsertKeyFrame(0f, _scanBorderCompositionVisual.Offset with { Y = 0 });
_vector3DKeyFrameAnimation.InsertKeyFrame(1f, _scanBorderCompositionVisual.Offset with { Y = this.Bounds.Height - ScanBorder.Height });
_vector3DKeyFrameAnimation.Duration = TimeSpan.FromSeconds(2);
_vector3DKeyFrameAnimation.IterationBehavior = AnimationIterationBehavior.Count;
_vector3DKeyFrameAnimation.IterationCount = 30;
_scanBorderCompositionVisual.StartAnimation("Offset", _vector3DKeyFrameAnimation);
}
我將最簡復現步驟的例子專案上傳到 github 和 gitee 上,可以使用如下命令列拉取程式碼。我整個程式碼倉庫比較龐大,使用以下命令列可以進行部分拉取,拉取速度比較快
先建立一個空資料夾,接著使用命令列 cd 命令進入此空資料夾,在命令列裡面輸入以下程式碼,即可獲取到本文的程式碼
git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin f82af28bab6f5cdfbd13c48c19b4f0a21a50ae06
以上使用的是國內的 gitee 的源,如果 gitee 不能訪問,請替換為 github 的源。請在命令列繼續輸入以下程式碼,將 gitee 源換成 github 源進行拉取程式碼。如果依然拉取不到程式碼,可以發郵件向我要程式碼
git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin f82af28bab6f5cdfbd13c48c19b4f0a21a50ae06
獲取程式碼之後,進入 AvaloniaIDemo/JallkeleejurCihayaiqalker 資料夾,即可獲取到原始碼
預期的行為是能夠控制 Composition 的 Animation 動畫的停止以及開啟新的動畫
根據我的分析問題原因是在更基礎的 InlineDictionary 在處理單項重新賦值時的不正確行為,讓動畫模組第二次進入時不能符合預期工作
根據閱讀 Avalonia 的程式碼可以看到 InlineDictionary 在只有單項的行為是透過 Set 方法呼叫進入時,將會忽略 overwrite 引數,從而導致 InlineDictionary 只有一項時,再次呼叫 Set 時的效果將會和呼叫 Add 方法相同。此行為將導致 composition animation 動畫播放行為不符合預期,將導致第二次的 composition animation 無法播放。為什麼第二次的 composition animation 無法播放?原因是第二次準備播放的 composition animation 無法將第一次的 composition animation 替換掉,而是將第二次的 composition animation 加入到第一次的 composition animation 後面,從而導致第二次設定的 composition animation 無法被執行
核心程式碼如下
internal struct InlineDictionary<TKey, TValue> : IEnumerable<KeyValuePair<TKey, TValue>> where TKey : class
{
public void Set(TKey key, TValue value) => SetCore(key, value, true);
public TValue this[TKey key]
{
get
{
if (TryGetValue(key, out var rv))
return rv;
throw new KeyNotFoundException();
}
set => Set(key, value);
}
void SetCore(TKey key, TValue value, bool overwrite)
{
if (key == null)
throw new ArgumentNullException();
if (_data == null)
{
_data = key;
_value = value;
}
else if (_data is KeyValuePair[] arr)
{
...
}
else if (_data is Dictionary<TKey, TValue?> dic)
{
...
}
else
{
// We have a single element, upgrade to array.
arr = new KeyValuePair[6];
arr[0] = new KeyValuePair((TKey)_data, _value);
arr[1] = new KeyValuePair(key, value);
_data = arr;
_value = default;
}
}
}
透過以上程式碼分析可以看到,在 InlineDictionary 首次加入時,將會進入 if (_data == null)
分支,使用如下程式碼分別給 _data
和 _value
賦值
但是第二次進來的時候,將會進入 else
分支,在這個分支裡面啥都判斷,沒有判斷 overwrite
和 key
的值,直接就建立為 KeyValuePair 陣列。這就意味著第二次進入的時候,將讓 Set 方法和 Add 方法相同,都是做新增而不是替換
這就導致了在 Composition 的 Animation 動畫裡面第二次設定動畫的時候,停止播放動畫
如以下的 ServerObjectAnimations 程式碼,可以看到在加入動畫的時候,先獲取舊的程式碼,將其呼叫 Deactivate 停下,再將其賦值為新的動畫
class ServerObjectAnimations
{
... // 忽略其他程式碼
private InlineDictionary<CompositionProperty, ServerObjectAnimationInstance> _animations;
public void OnSetAnimatedValue<T>(CompositionProperty<T> prop, ref T field, TimeSpan committedAt, IAnimationInstance animation) where T : struct
{
if (_owner.IsActive && _animations.TryGetValue(prop, out var oldAnimation))
oldAnimation.Animation.Deactivate();
_animations[prop] = new ServerObjectAnimationInstance<T>(this, animation, prop);
animation.Initialize(committedAt, ExpressionVariant.Create(field), prop);
if(_owner.IsActive)
animation.Activate();
OnSetDirectValue(prop);
}
}
由於 InlineDictionary 存在問題,只有一項的時候,賦值進入第二項,做的是新增第二項但不刪除第一項。這就導致第二次加入動畫時候,第一個動畫被停止,但是第一個動畫還在字典裡面,後續獲取將會返回第一個動畫。第二個動畫將不會被返回。這就是為什麼第二次的動畫無法播放的原因