話不多說,先上效果 這裡使用了一個 "ScrollProgressProvider.cs" ,我們這篇文章先解析一下整體的動畫思路,以後再詳細解釋這個Provider的實現方式。 結構 整個頁面大致結構是 這個Header是修改的ListBox,當然也可以用ListView代替。 隱藏Pivot預設 ...
話不多說,先上效果
這裡使用了一個ScrollProgressProvider.cs,我們這篇文章先解析一下整體的動畫思路,以後再詳細解釋這個Provider的實現方式。
結構
整個頁面大致結構是
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid x:Name="Target">
<TextBlock />
<Header />
</Grid>
<Pivot.ItemTemplate Grid.RowSpan="2">
<Pivot.ItemTemplate>
<DataTemplate>
<ScrollViewer x:Name="sv">
<StackPanel>
<Border Margin="0,250,0,0" />
</StackPanel>
</ScrollViewer>
</DataTemplate>
</DataTemplate>
</Pivot.ItemTemplate>
</Grid>
這個Header是修改的ListBox,當然也可以用ListView代替。
隱藏Pivot預設Header的方式是在Pivot的樣式中找到如下行。
<PivotPanel x:Name="Panel" VerticalAlignment="Stretch">
<Grid x:Name="PivotLayoutElement">
<Grid.RowDefinitions>
<RowDefinition Height="0" /><!--修改這行為0-->
<RowDefinition Height="*" />
</Grid.RowDefinitions>
...
動畫過程大致就是在Pivot頁面切換時,查找到當頁的ScrollViewer,綁定動畫。
查找
大家在爬視圖樹時,應該經常遇到元素還未載入的情況,這裡為瞭解決這種狀況,封裝了一個WaitForLoaded方法。
private async Task<T> WaitForLoaded<T>(FrameworkElement element, Func<T> func, Predicate<T> pre, CancellationToken cancellationToken)
{
TaskCompletionSource<T> tcs = null;
try
{
tcs = new TaskCompletionSource<T>();
cancellationToken.ThrowIfCancellationRequested();
var result = func.Invoke();
if (pre(result)) return result;
element.Loaded += Element_Loaded;
return await tcs.Task;
}
catch
{
element.Loaded -= Element_Loaded;
var result = func.Invoke();
if (pre(result)) return result;
}
return default;
void Element_Loaded(object sender, RoutedEventArgs e)
{
if (tcs == null) return;
try
{
cancellationToken.ThrowIfCancellationRequested();
element.Loaded -= Element_Loaded;
var _result = func.Invoke();
if (pre(_result)) tcs.SetResult(_result);
else tcs.SetCanceled();
}
catch
{
System.Diagnostics.Debug.WriteLine("canceled");
}
}
}
使用起來是這樣的
CancellationTokenSource cts;
private async void EventChanged(object sender, EventArgs e)
{
if (cts != null) cts.Cancel();
cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
var child = await WaitForLoaded(element, () => find_element_method(), c => judge_find_success_method(), cts.Token);
}
我們在Pivot的SelectionChanged事件里,修改ScrollProgressProvider托管的ScrollViewer,provider就會自動將ScrollViewer設置到正確的位置。
接下來在Page的Loaded事件中綁定動畫,這裡有兩種選擇。provider提供了ProgressChanged事件和GetProgressPropertySet方法。可以在ProgressChanged事件中直接設置元素的值來實現動畫,不過由於ScrollViewer的限制,ProgressChanged事件觸發頻率不是很高,所以更推薦使用GetProgressPropertySet獲取到CompositionPropertySet,通過Composition Api實現動畫。
var providerProp = provider.GetProgressPropertySet();
var gv = ElementCompositionPreview.GetElementVisual(Target); // 容器Visual
var tv = ElementCompositionPreview.GetElementVisual(HeaderText); //文本Visual
ScrollProgressProvider生成的PropertySet內有progress和threshold兩個欄位可以用作動畫。
Composition Api提供了Lerp(start, end, progress)方法,用在此處剛好合適。
我們需要定義容器平移,文本平移和文本縮放三個動畫。
容器平移向上移動閾值的高度
var gvOffsetExp = Window.Current.Compositor.CreateExpressionAnimation("Vector3(0f, -provider.threshold * provider.progress, 0f)");
gvOffsetExp.SetReferenceParameter("provider", providerProp);
gv.StartAnimation("Offset", gvOffsetExp);
文本平移動畫從容器中心平移到左下角
var startOffset = "Vector3((host.Size.X - this.Target.Size.X) / 2, (host.Size.Y - 50 - this.Target.Size.Y) / 2, 1f)";
var endOffset = $"Vector3(0f, provider.threshold, 1f)";
var offsetExp = Window.Current.Compositor.CreateExpressionAnimation($"lerp({startOffset}, {endOffset}, provider.progress)");
offsetExp.SetReferenceParameter("host", gv);
offsetExp.SetReferenceParameter("provider", providerProp);
tv.StartAnimation("Offset", offsetExp);
文本縮放
var scale = "(50f / this.Target.Size.Y)";
var startScale = "Vector3(1f, 1f, 1f)";
var endScale = $"Vector3({scale}, {scale}, 1f)";
var scaleExp = Window.Current.Compositor.CreateExpressionAnimation($"lerp({startScale}, {endScale}, provider.progress)");
scaleExp.SetReferenceParameter("host", gv);
scaleExp.SetReferenceParameter("provider", providerProp);
tv.StartAnimation("Scale", scaleExp);
觸摸
觸摸比起滑鼠點擊要更複雜一些。
Pivot應該是UWP內置控制項里比較玄學的一個了。
對於滑鼠操作,Pivot會先觸發SelectionChanged事件,再觸發PivotItemLoaded事件,並且播放動畫。
而對於觸摸事件,整個順序是相反的。手指開始滑動界面時,可以被看到的Item會開始載入,並且觸發PivotItemLoaded事件,鬆手之後才開始計算是否應該導航到其他頁,並且決定是否觸發SelectionChanged事件。這樣就會有一個問題,我們在SelectionChanged中修改ScrollViewer偏移之前,我們已經能看到他了,這時的高度是不正確的。我們需要抽象出一個可以在滑鼠和觸摸觸發事件時將下一個Item的ScrollViewer設置為正確偏移的方法。
我的想法很簡單,將所有已載入的頁內的ScrollViewer緩存下來,隨著Progress的改變而改變,做法也很簡單。
private HashSet<ScrollViewer> scrolls = new HashSet<ScrollViewer>();
private async void Pivot_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
...
scrolls.Remove(provider.ScrollViewer);
}
private void Pivot_PivotItemLoaded(Pivot sender, PivotItemEventArgs args)
{
var sv = (args.Item.ContentTemplateRoot as FrameworkElement).FindName("sv") as ScrollViewer;
if (sv != provider.ScrollViewer)
{
sv.ChangeView(null, provider.Progress * provider.Threshold, null, true);
scrolls.Add(sv);
}
}
private void Pivot_PivotItemUnloading(Pivot sender, PivotItemEventArgs args)
{
var sv = (args.Item.ContentTemplateRoot as FrameworkElement).FindName("sv") as ScrollViewer;
if (sv != null)
{
scrolls.Remove(sv);
}
}
private void Provider_ProgressChanged(object sender, double args)
{
foreach (var sv in scrolls)
{
sv.ChangeView(null, provider.Progress * provider.Threshold, null, true);
}
}
需要註意的是,我們要在載入完成事件中獲取ScrollViewer,而在卸載開始事件中移除ScrollViewer。
GitHub: https://github.com/cnbluefire/ShyHeaderPivot
ExpressionAnimation:
https://docs.microsoft.com/en-us/uwp/api/Windows.UI.Composition.ExpressionAnimation
CompositionAnimation: https://docs.microsoft.com/zh-cn/windows/uwp/composition/composition-animation
我的博客: 超威藍火