话不多说,先上效果



这里使用了一个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

我的博客: 超威蓝火

UWP实现吸顶的Pivot的更多相关文章

  1. UWP中使用Composition API实现吸顶(1)

    前几天需要在UWP中实现吸顶,就在网上找了一些文章: 吸顶大法 -- UWP中的工具栏吸顶的实现方式之一 在UWP中页面滑动导航栏置顶 发现前人的实现方式大多是控制ListViewBase的Heade ...

  2. UWP中使用Composition API实现吸顶(2)

    在上一篇中我们讨论了不涉及Pivot的吸顶操作,但是一般来说,吸顶的部分都是Pivot的Header,所以在此我们将讨论关于Pivot多个Item关联同一个Header的情况. 老样子,先做一个简单的 ...

  3. 吸顶大法 -- UWP中的工具栏吸顶的实现方式之一

    如果一个页面中有很长的列表/内容,很多应用都会在用户向下滚动时隐藏页面的头,给用户留出更多的阅读空间,同时提供一个方便的吸顶工具栏,比如淘宝中的店铺页面. 下面是一个比较简单的实现,如果有同学有更好的 ...

  4. status bar、navigationBar、tableView吸顶view设置

    1. 隐藏navigationBar self.navigationController.navigationBar.hidden = YES; 2. status bar设置 -(void)view ...

  5. collectionview cell吸顶效果

    p.p1 { margin: 0.0px 0.0px 0.0px 0.0px; font: 14.0px "Hiragino Sans GB"; color: #cf8724 } ...

  6. 原生js实现吸顶导航和回到顶部特效

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  7. ECSTORE导航吸顶功能

    ecstore导航吸顶功能,在导航父元素中加入id,如: <div id="mainNav1"></div> 在footer.html中添加以下js代码: ...

  8. React制作吸顶功能总结

    总结一下最近用react写项目时,遇到的一些坑,恩,真的还蛮坑的,主要是设置状态的时候特别不好控制,下面我们一起来看下,这里自己做了几个demo,分别看下, 主页面代码如下: class Head e ...

  9. 自定义tab吸顶效果一(原理)

    PS:问题:什么是吸顶,吸顶有什么作用,吸顶怎么使用? 在很多app商城中,介绍软件的时候就会使用吸顶效果, 吸顶有很多作用,一个最简单粗暴的作用就是,让用户知道此刻在浏览哪个模块,并可以选择另外的模 ...

随机推荐

  1. HiLoGenerator生成id

    using System.Linq; namespace Product.Host { public class HiLoGenerator { ; ; ; private object Sequen ...

  2. something good

    CF292A CF304B CF383A CF409D CF409F CF632A CF652B CF656A CF656B CF656D CF659A CF678A CF697A CF735D CF ...

  3. 微信小程序踩坑日记1——调用微信授权窗口

    0. 引言 微信小程序为了优化用户体验,取消了在进入小程序时立马出现授权窗口.需要用户主动点击按钮,触发授权窗口. 那么,在我实践过程中,出现了以下问题. . 无法弹出授权窗口 . 希望在用户已经授权 ...

  4. python函数基础-参数-返回值-注释-01

    什么是函数 函数就是有特定功能的工具 # python中有内置函数(python解释器预先封装好的)与自定义函数(用户自定义封装的)之分 为什么要用函数 # 可以减少代码冗余,增加代码复用性 # 使代 ...

  5. 解决Windows10下安装Ubuntu16.04双系统后开机没有Ubuntu引导

    转载 https://blog.csdn.net/qq_27838307/article/details/79149791 1.按照网上教程在磁盘中压缩硬盘并且不需要给他新建卷标,就让他显示空闲就好了 ...

  6. Modify column Vs change column

    引言 I know, we can not rename a column using modify column syntax,but can change column syntax. My qu ...

  7. idea使用技巧总结

    1.idea代码自动补全 https://jingyan.baidu.com/article/36d6ed1f62e9821bcf4883af.html 2.优化_生成main方法_sysout方法 ...

  8. PHP与ECMAScript_1_变量与常量

    PHP ECMAScript 变量命名规则 (相同点) 变量包含:字母.数字.下划线字符 变量只能以字母或下划线开头 变量不能以数字开头 变量名是区分大小写 变量包含:字母.数字.下划线字符 变量只能 ...

  9. Divide and Conquer

    1 2 218 The Skyline Problem     最大堆   遍历节点 public List<int[]> getSkyline(int[][] buildings) { ...

  10. Unity3D热更新之LuaFramework篇[09]--资源热更新与代码热更新的具体实现

    前言 在上一篇文章 Unity3D热更新之LuaFramework篇[08]--热更新原理及热更服务器搭建 中,我介绍了热更新的基本原理,并且着手搭建一台服务器. 本篇就做一个实战练习,真正的来实现热 ...