UWP实现吸顶的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
我的博客: 超威蓝火
UWP实现吸顶的Pivot的更多相关文章
- UWP中使用Composition API实现吸顶(1)
前几天需要在UWP中实现吸顶,就在网上找了一些文章: 吸顶大法 -- UWP中的工具栏吸顶的实现方式之一 在UWP中页面滑动导航栏置顶 发现前人的实现方式大多是控制ListViewBase的Heade ...
- UWP中使用Composition API实现吸顶(2)
在上一篇中我们讨论了不涉及Pivot的吸顶操作,但是一般来说,吸顶的部分都是Pivot的Header,所以在此我们将讨论关于Pivot多个Item关联同一个Header的情况. 老样子,先做一个简单的 ...
- 吸顶大法 -- UWP中的工具栏吸顶的实现方式之一
如果一个页面中有很长的列表/内容,很多应用都会在用户向下滚动时隐藏页面的头,给用户留出更多的阅读空间,同时提供一个方便的吸顶工具栏,比如淘宝中的店铺页面. 下面是一个比较简单的实现,如果有同学有更好的 ...
- status bar、navigationBar、tableView吸顶view设置
1. 隐藏navigationBar self.navigationController.navigationBar.hidden = YES; 2. status bar设置 -(void)view ...
- collectionview cell吸顶效果
p.p1 { margin: 0.0px 0.0px 0.0px 0.0px; font: 14.0px "Hiragino Sans GB"; color: #cf8724 } ...
- 原生js实现吸顶导航和回到顶部特效
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
- ECSTORE导航吸顶功能
ecstore导航吸顶功能,在导航父元素中加入id,如: <div id="mainNav1"></div> 在footer.html中添加以下js代码: ...
- React制作吸顶功能总结
总结一下最近用react写项目时,遇到的一些坑,恩,真的还蛮坑的,主要是设置状态的时候特别不好控制,下面我们一起来看下,这里自己做了几个demo,分别看下, 主页面代码如下: class Head e ...
- 自定义tab吸顶效果一(原理)
PS:问题:什么是吸顶,吸顶有什么作用,吸顶怎么使用? 在很多app商城中,介绍软件的时候就会使用吸顶效果, 吸顶有很多作用,一个最简单粗暴的作用就是,让用户知道此刻在浏览哪个模块,并可以选择另外的模 ...
随机推荐
- cogs426血帆海盗(网络流打法)
这道题基本上来就能看的出来是网络流但难点在于它的结果貌似与最大流,最小割都没啥关系,我猜不少萌新像我一样想暴力枚举边(要是考试我就真这么做了),但很明显,出题人没打算让你这么水过,100000的数据不 ...
- Web前端_微信小程序实战开发
微信小程序开发实战教程 一.微信小程序 它是一种混合开发的方式. 是安装在微信中的程序(一个程序最多2M空间). 1.1 注册 1 2 点击立即注册:进入下方页面 3 4 点击小程序进入表单填写页 ...
- Mysql两张表的关联字段不一致
工作中遇到了一个问题,邮件系统群发失败,后来经过排查查找到了原因 原来是因为mysql中的两张表的关联字段竟然不一致, 表A mysql> desc rm_user_router;+------ ...
- NetworkError: Failed to execute 'send' on 'XMLHttpRequest': Failed to load xxxx错误解决方法
在开发项目的过程中,和后端对接,我们使用是一个成熟的集成很全面的架构JHipster.后端为java spring-boot 前端ts+react,需求是有一个需要在页面里嵌套iframe的需求.然后 ...
- wussUI v1.0.0小程序UI组件库 第一期开发已完成
经过了两个月不到的开发时间,我们phonycode团队顺利的发布了小程序的UI组件库 wuss-ui 的第一个版本.目前大体预览如下 介绍 wussUI 现在有大概27个组件左右, 目前基础组件都有了 ...
- Java编程思想:标准I/O
import com.sun.xml.internal.ws.policy.privateutil.PolicyUtils; import java.io.*; public class Test { ...
- Android - ScrollView 使用小计 里面嵌套的View 如何设置全屏
设置ScrollView的属性android:fillViewport="true" 即可 <?xml version="1.0" encoding=&q ...
- git,github,gitlab,码云的区别
git 是版本控制工具. github https://github.com/和gitlab https://about.gitlab.com/都是基于git仓库的web开发流程代码托管平台.两者的区 ...
- c语言的strcpy函数
strcpy是用于复制字符串的函数 上面这个程序输出的结果为 为什么输出字符串%s时s是abABC,而输出字符%c时s是abABCg呢 因为strcpy函数本身的性质:复制字符串直到’\0’结束符为止 ...
- 消费端如何保证消息队列MQ的有序消费
消息无序产生的原因 消息队列,既然是队列就能保证消息在进入队列,以及出队列的时候保证消息的有序性,显然这是在消息的生产端(Producer),但是往往在生产环境中有多个消息的消费端(Consumer) ...