话不多说,先上效果



这里使用了一个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. CDQZ集训DAY7 日记

    并没有考试然而心情比考试还糟糕…… 上午讲的基本就听不懂,讲课人迷之停顿.根本让人跟不上趟,声音好奇怪的说……好不容易讲到反演,Hzoi集体上线,等待装逼时刻的到来.然而,讲课人再次迷之停顿,讲一个p ...

  2. 使用WebService发布soap接口,并实现客户端的https验证

    什么是https HTTPS其实是有两部分组成:HTTP + SSL / TLS, 也就是在HTTP上又加了一层处理加密信息的模块,并且会进行身份的验证. 如何进行身份验证? 首先我们要明白什么是对称 ...

  3. CAD2014学习笔记-文字编辑与尺寸标注

    基于 虎课网huke88.com CAD教程 文字与表格 输入文字:TEXT.MTEXT 插入表格:table 新建表格样式 尺寸标注 测量工具:Di.DLI 开启标注:打开工具-工具栏-标注 对齐/ ...

  4. sizeof()和lstrlen()和strlen()区别

    strlen()是返回字符串的字节长度,   lstrlen()是返回字符串的字符长度.   也就是说第二个函数可能和第一个函数结果一样,如果字符串中字符单位都是单字节的话.       一般来说主要 ...

  5. VirtualBox下安装Linux系统

    Linux 开源的类Unix操作系统,拥有图形界面文字界面,旗下发行版不胜其数(Ubuntu.Deepin.CentOS...)在互联网公司中Linux服务器大多是用的CentOS系统,其他Linux ...

  6. 基础篇-1.2Java世界的规章制度(下)

    1 Java运算符 Java世界中的运算其实就是数学运算,而运算符就是其中的媒介. 算术运算符 操作符 描述 + 加法,对符号两边的数值相加 - 减法,符号左边的数减去右边的数 * 乘法,符号两边的数 ...

  7. PhpCms V9中的{date('Y-m-d',$r[inputtime])}问题解决方法

    不少朋友会碰到这个问题:在PhpCms V9中的首页或者文章内容页调用发布时间{date('Y-m-d',$r[inputtime])}调用显示1970-01-01,然后尝试用截断的方法也没有成功,应 ...

  8. HashSet源码解读

    一:先看其实现了哪些接口和继承了哪些类 1.实现了Serializable接口,表明它支持序列化. 2.实现了Cloneable接口,表明它支持克隆,可以调用超类的clone()方法进行浅拷贝. 3. ...

  9. VMWare虚拟机:三台虚拟机互通且连网

    虚拟机:三台虚拟机互通且连网 目录 一.虚拟机 相关软件 虚拟机安装 Linux系统安装 1) 使用三个Linux虚拟机 多台虚拟机互通且上网 1) 多台配置注意事项 2) 虚拟机软件的配置 3) W ...

  10. kubernetes二进制高可用部署实战

    环境: 192.168.30.20 VIP(虚拟) 192.168.30.21 master1 192.168.30.22 master2 192.168.30.23 node1 192.168.30 ...