话不多说,先上效果



这里使用了一个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. 「玩转Python」突破封锁继续爬取百万妹子图

    前言 从零学 Python 案例,自从提交第一个妹子图版本引来了不少小伙伴的兴趣.最近,很多小伙伴发来私信说,妹子图不能爬了!? 趁着周末试了一把,果然爬不动了,爬下来的都是些 0kb 的假图片,然后 ...

  2. NET多线程之进程间同步锁Mutex

    Mutex类似于lock.Monitor,都是为了解决多线程环境下,资源竞争导致的访问顺序问题.常见资源竞争有以下情况: 1.单例,如何确保单例: 2.IO文件操作,如果同时又多个线程访问同一个文件会 ...

  3. 算法导论--最小生成树(Kruskal和Prim算法)

    转载出处:勿在浮沙筑高台http://blog.csdn.net/luoshixian099/article/details/51908175 关于图的几个概念定义: 连通图:在无向图中,若任意两个顶 ...

  4. ASP.NET 前端数据绑定---<%#%>及Eval()的使用

    ASP.NET 前端html代码中会经常出现的<%%>的代码,里面的文本其实就是不能直接输出到客户端浏览器的文本,是需要服务器解释的. 在ASP中,<%%>里面的文本是vbsc ...

  5. Sublime Text 3 实现C语言代码的编译和运行

    Sublime Text 3 是一款优秀的代码编辑软件.界面简洁,轻巧快速,很受大家的欢迎. 最近开始用他来编辑数据结构的C语言代码,这就需要在新建编译系统.具体方法如下: 首先: 接下来是关键的一步 ...

  6. spring mvc 拦截器的使用

    Spring MVC 拦截器的使用 拦截器简介 Spring MVC 中的拦截器(Interceptor)类似于 Servler 中的过滤器(Filter).用于对处理器进行预处理和后处理.常用于日志 ...

  7. error: 'commit' is not possible because you have unmerged files.

    解决方案: 1.把修改的文件add下,如:git add bidder_mod/src/common/dragon_bidder_data.cc2.git commit

  8. 从草图绘制到实施交付:优秀API设计完整流程

    设计好的API是一项繁复的工作,但是优秀的设计是可以通过人为规划实现的,在本文中,我们将研究什么是好的设计以及如何在开发过程中实现它,还将介绍API设计的三个重要阶段:草图绘制,原型设计和交付实施,最 ...

  9. 基于Ajax的前后端分离

    这种开发模式可以称为SPA (Single Page Application 单页面应用)时代. 这种模式下,前后端的分工非常清晰,前后端的关键协作点是 Ajax 接口.看起来是如此美妙,但回过头来看 ...

  10. 爬虫之解析库pyquery

    初始化 安装: pip install pyquery 字符串的形式初始化 html = """ <html lang="en"> < ...