UWP Composition API - PullToRefresh

背景:

之前用ScrollViewer 来做过 PullToRefresh的控件,在项目一些特殊的条件下总有一些问题,比如ScrollViewer不会及时到达指定位置。
于是便有了使用Composition API来重新实现PullToRefresh控件。本控件的难点不是实现,而是对Composition API的一些探索。

本文的一些观点或者说结论不一定是全对的,都是通过实验得到的,Composition API 可用的资料实在是太少了。

成品效果图:

资料:

Composition API 资料

1.官方Sample

2. 原作者 Nick Waggoner 供职于微软 native Windows UI platform 链接是对文章的翻译 VALID VOID

3.比较旧的资料

在网上查阅了些资料,看到网上有大神已经实现过了。了解了大概的实现过程,因为自己想做的效果还是跟大神的有差距的,所以

还是自己动手封装成控件。

实现原理:

这里引用 VALID VOID里面的话

输入驱动动画

自大约五年前触摸渐成主流起,创造低延迟体验成为了一种普遍需求。使用手指或笔在屏幕上操作,使得人眼获得了更直观的参照点来辨识操作的延迟和流畅性。为使操作流畅,主流操作系统公司均将更多的操作移交至系统和 GPU (如 ChromeIE)执行。在 Windows 上,这由 DirectManipulation 这一或多或少是针对于触摸构建的动画引擎实现的。它解决了关键的延迟挑战,也就是如何自然地以展示从输入驱动到事件驱动过渡的动效。但另一方面,它也几乎没有提供对定制惯性观感的支持,就像福特 T 型车那样——“只要车是黑色的,你可以把它涂成任意你喜欢的颜色”。2

ElementCompositionPreview.GetScrollViewerManipulationPropertySet 是让你能够把玩输入驱动动效的第一步。虽然它仍然没给你任何对内容滚动时观感进行控制的额外能力,但它确实允许你对次级内容应用表达式动画。例如,我们终于能完成我们的基础视差滚动代码:

// 创建驱动视差滚动的表达式动画。
ExpressionAnimation parallaxAnimation = compositor.CreateExpressionAnimation("MyForeground.Translation.Y / MyParallaxRatio");

// 设置对前景对象的引用。
CompositionPropertySet MyPropertySet = ElementCompositionPreview.GetScrollViewerManipulationPropertySet(MyScrollViewer);
parallaxAnimation.SetReferenceParameter("MyForeground", MyPropertySet);

// 设置背景对象视差滚动的速度。
parallaxAnimation.SetScalarParameter("MyParallaxRatio", 0.5f);

// 对背景对象开始视差动画。
backgroundVisual.StartAnimation("Offset.Y", parallaxAnimation);

使用这一技巧,你能够实现多种优秀的效果:视差滚动、粘性表头、自定义滚动条等等。唯一缺失的就是定制操作本身的观感……

我讲一下我的理解:使用过ScrollViewer 和Manipulation相关事件的童鞋都知道,想要得到一些ScrollViewer 触摸的详情太难了,

DirectManipulationStarted和DirectManipulationCompleted得到信息太少了,而Manipulation其他的事件又需要设置ManipulationMode,这样全部的情况都要你自己来处理。当看到

ElementCompositionPreview.GetScrollViewerManipulationPropertySet(MyScrollViewer); 

的时候,你是不是感觉有点亲切。看上去你拿到了ScrollViewer 的一些Manipulation 的信息。。话说这里是最最坑爹了,

MyForeground 其实是GetScrollViewerManipulationPropertySet返回的东东,

但MyForeground.Translation.Y这是什么鬼东西。。Manipulation 的Translation??? 后来我在网上搜索了一下,

但我查了下CompositionPropertySet 并没发现有相关Translation的属性啊? 

难道跟Manipulation事件里面的参数里面的Translation 是一样的吗??这是我的推测。

网上都是这样用的,但是没有文档。。除了Translation不知道还有其他属性能使用不。

暂时没有寻找到答案,希望知道的童鞋可以留言,万分感激。。。

上面这段的代码的意思就是说把Manipulation.Translation.Y 映射到backgroundVisual的Offset.Y上面。

也就是说你现在已经可以找到ScrollViewer滚动的时候一些有用的数值了。。值得说的是,用鼠标滚动的时候 映射依然能生效。

这种映射是实时的,是会有惯性效果的。

实现过程:

因为要用到ScrollViewer,所以我做了2种,一种是刷新内容第一元素是ScrollViewer的,一种不是ScrollViewer的。

如果刷新内容第一元素是ScrollViewer,模板如下:

  <ControlTemplate TargetType="local:PullToRefreshGrid1">
                    <Grid>
                        <ContentControl x:Name="Header" Opacity="0" VerticalAlignment="Top" ContentTemplate="{TemplateBinding HeaderTemplate}" HorizontalContentAlignment="Center" VerticalContentAlignment="Bottom" />
                        <ContentPresenter x:Name="Content" ContentTemplate="{TemplateBinding ContentTemplate}" ContentTransitions="{TemplateBinding ContentTransitions}" Content="{TemplateBinding Content}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
                    </Grid>
                </ControlTemplate>

如果刷新内容第一元素不是ScrollViewer,那么我为它添加了一个ScrollViewer:

  <ControlTemplate TargetType="local:PullToRefreshGrid">
                    <Grid>
                        <ContentControl x:Name="Header" Opacity="0" VerticalAlignment="Top" ContentTemplate="{TemplateBinding HeaderTemplate}" HorizontalContentAlignment="Center" VerticalContentAlignment="Bottom" />
                        <ScrollViewer x:Name="ScrollViewer" VerticalSnapPointsType="MandatorySingle"  VerticalSnapPointsAlignment="Near"
                          VerticalScrollMode="Enabled" VerticalScrollBarVisibility="Hidden" VerticalContentAlignment="Stretch" VerticalAlignment="Stretch">
                            <ContentPresenter x:Name="Content" ContentTemplate="{TemplateBinding ContentTemplate}" ContentTransitions="{TemplateBinding ContentTransitions}" Content="{TemplateBinding Content}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
                        </ScrollViewer>
                    </Grid>
                </ControlTemplate>

当然,其实第2种也是统用的,我只是想减少控件的Child,如果你不知道刷新内容里面有没有ScrollViewer,那么用第2种就好了。有人会说为啥控件的名字这么奇怪。。那是因为我之前也写过PullToRefresh控件(PullToRefreshControl,PullToRefreshPanel),我把名字都想完了。。实在想不出更好的了。。大家体谅下。。( ╯□╰ )

好了,重点就是拿到这个ScrollViewer 然后跟Header,产生某种关系(你懂的)。。。

            if (RefreshThreshold == 0.0)
            {
                RefreshThreshold = headerHeight;
            }
            ratio = RefreshThreshold / headerHeight;

            _offsetAnimation = _compositor.CreateExpressionAnimation("(min(max(0, ScrollManipulation.Translation.Y * ratio) / Divider, 1)) * MaxOffsetY");
            _offsetAnimation.SetScalarParameter("Divider", (float)RefreshThreshold);
            _offsetAnimation.SetScalarParameter("MaxOffsetY", (float)RefreshThreshold * 5 / 4);
            _offsetAnimation.SetScalarParameter("ratio", (float)ratio);
            _offsetAnimation.SetReferenceParameter("ScrollManipulation", _scrollerViewerManipulation);

            _opacityAnimation = _compositor.CreateExpressionAnimation("min((max(0, ScrollManipulation.Translation.Y * ratio) / Divider), 1)");
            _opacityAnimation.SetScalarParameter("Divider", (float)headerHeight);
            _opacityAnimation.SetScalarParameter("ratio", (float)1);
            _opacityAnimation.SetReferenceParameter("ScrollManipulation", _scrollerViewerManipulation);

            _headerVisual = ElementCompositionPreview.GetElementVisual(_header);

            _contentVisual = ElementCompositionPreview.GetElementVisual(_scrollViewerBorder);

            _headerVisual.StartAnimation("Offset.Y", _offsetAnimation);
            _headerVisual.StartAnimation("Opacity", _opacityAnimation);
            _contentVisual.StartAnimation("Offset.Y", _offsetAnimation);

RefreshThreshold 是到达Release to refresh的一个点。。可以由用户设定,默认是header的高度。

MaxOffsetY 是当到达RefreshThreshold之后我还能拖动的最大值,这里设置为RefreshThreshold 的5/4。

(min(max(0, ScrollManipulation.Translation.Y * ratio) / Divider, 1)) * MaxOffsetY

我再来讲讲这个表达式的意思,ScrollManipulation.Translation.Y 大家都已经知道了。是ScrollViewer进行Manipulation的Y值,向下是正值,向上时负值,初始为0.

综合起来就是最大值为MaxOffsetY最小值为0.。这个速率是根据RefreshThreshold/headerHeight来的,因为你会发现,你向下drag scrollViewer的时候是有一定最大值的,当RefreshThreshold比较大的时候,你很难ScrollManipulation.Translation.Y值达到RefreshThreshold。

min((max(0, ScrollManipulation.Translation.Y * ratio) / Divider), 1)

这个也比较简单,是给header的Opaicty做了一个动画。

            _headerVisual.StartAnimation("Offset.Y", _offsetAnimation);
            _headerVisual.StartAnimation("Opacity", _opacityAnimation);
            _contentVisual.StartAnimation("Offset.Y", _offsetAnimation);

完成之后我们将动画开始就好了。这样在向下拖scrollviewer的时候,scrollviewer和header就都会向下移动。

接下来我们需要监听 offset。

        private void ScrollViewer_DirectManipulationStarted(object sender, object e)
        {
            Windows.UI.Xaml.Media.CompositionTarget.Rendering += OnCompositionTargetRendering;
            _refresh = false;
            _header.Opacity = 1;
        }

在开始manipulat的时候,注册CompositionTarget.Rendering事件。在这个事件里面我们就可以实时获得offset的变化了。

         private void OnCompositionTargetRendering(object sender, object e)
        {
            _headerVisual.StopAnimation("Offset.Y");

            var offsetY = _headerVisual.Offset.Y;
            IsReachThreshold = offsetY >= RefreshThreshold;
            _scrollViewerBorder.Clip = new RectangleGeometry() { Rect = new Rect(0, 0, _content.Width, _content.Height - offsetY) };
            Debug.WriteLine(IsReachThreshold + "," + _headerVisual.Offset.Y + "," + RefreshThreshold);
            _headerVisual.StartAnimation("Offset.Y", _offsetAnimation);

            if (!_refresh)
            {
                _refresh = IsReachThreshold;
            }

            if (_refresh)
            {
                _pulledDownTime = DateTime.Now;
            }

            if (_refresh && offsetY <= 1)
            {
                _releaseTime = DateTime.Now;
            }

        }

这里有点坑爹的是,发现如果不StopAnimation,那么Offset.Y永远都是0.。。很囧啊。。

最后我们在ScrollViewer_DirectManipulationCompleted事件里面处理是否要 触发PullToRefresh事件就ok了。

         private void ScrollViewer_DirectManipulationCompleted(object sender, object e)
        {
            Windows.UI.Xaml.Media.CompositionTarget.Rendering -= OnCompositionTargetRendering;

            var cancelled = (_releaseTime - _pulledDownTime) > TimeSpan.FromMilliseconds(250);

            if (_refresh)
            {
                _refresh = false;
                if (cancelled)
                {
                    Debug.WriteLine("Refresh cancelled...");
                }
                else
                {
                    Debug.WriteLine("Refresh now!!!");
                    if (PullToRefresh != null)
                    {
                        _headerVisual.StopAnimation("Offset.Y");
                        LastRefreshTime = DateTime.Now;
                        _headerVisual.StartAnimation("Offset.Y", _offsetAnimation);
                        PullToRefresh(this, null);
                    }
                }
            }
        }

最后说下Header模板(HeaderTemplate)是可以定义的。。它的DataContext是绑定到这个控件上的,有用的属性有(LastRefreshTime,IsReachThreshold等),你可以用它们创造你属于你喜欢的Header样式。

通过本控件,初步了解Composition API 的一些用法。下一篇,我会讲讲一些更多的探索。。

开源有益,源码GitHub地址

问题:

1.Visual 是继承IDisposable,我们需要在什么时候Dispose 掉它呢? 还是它自己管理的?

我试过在unload的时候去dispose它。但是会出了Win32的异常。。官方sample里面对这个也讲的不清楚。

希望知道的童鞋能留言告知,万分感谢。另外希望有对这个比较了解的童鞋能提供一些sample,资料,再次感谢。

时间: 2024-10-07 18:07:36

UWP Composition API - PullToRefresh的相关文章

UWP Composition API - GroupListView(一)

需求: 光看标题大家肯定不知道是什么东西,先上效果图: 这不就是ListView的Group效果吗?? 看上去是的.但是请听完需求.1.Group中的集合需要支持增量加载ISupportIncrementalLoading 2.支持UI Virtualization oh,no.ListView 自带的Group都不支持这2个需求.好吧,只有靠自己撸Code了.. 实现前思考: 仔细想了下,其实要解决的主要问题有2个数据源的处理 和 GroupHeader的UI的处理 1.数据源的处理  因为之

UWP Composition API - New FlexGrid 锁定行列

原文:UWP Composition API - New FlexGrid 锁定行列 如果之前看了 UWP Jenkins + NuGet + MSBuild 手把手教你做自动UWP Build 和 App store包 这篇的童鞋,针对VS2017,需要对应更新一下配置,需要的童鞋点击查看一下,在文章最后. 之前写过一篇 锁定列的FlexGrid,没看过的童鞋可以去先看一下那一篇. 先放上效果图 制作新控件的背景是SDK升级到了14393,Composition API 有了相应的改变. 对我

UWP Composition API - GroupListView(二)

还是先上效果图: 看完了上一篇UWP Composition API - GroupListView(一)的童鞋会问,这不是跟上一篇一样的吗??? 骗点击的?? No,No,其实相对上一个有更简单粗暴的方案,因为上篇是为了研究Composition API,所以含着泪都要做完(有没有被骗的赶脚)..( ╯□╰ ) 那是有没有简单点的方法呢?? 嗯,看到这篇,那答案肯定是Yes. 我再啰嗦下需求: 1.Group中的集合需要支持增量加载ISupportIncrementalLoading 2.支持

UWP Composition API - RadialMenu

原文:UWP Composition API - RadialMenu 用Windows 8.1的童鞋应该知道OneNote里面有一个RadialMenu.如下图,下图是WIn10应用Drawboard PDF的RadialMenu,Win8.1的机器不好找了.哈哈,由于整个文章比较长,大家可以放<给我一首歌的时间> 边听边看.<滑稽> 从设计到开发包括修复一些bug,大概用了不连续的2个月,想看源代码的童鞋可以先到 RadialMenu 查看效果和代码. 先放上项目里面的最终效果

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

原文:UWP中使用Composition API实现吸顶(2) 在上一篇中我们讨论了不涉及Pivot的吸顶操作,但是一般来说,吸顶的部分都是Pivot的Header,所以在此我们将讨论关于Pivot多个Item关联同一个Header的情况. 老样子,先做一个简单的页面,页面有一个Grid当Header,一个去掉了头部的Pivot,Pivot内有三个ListView,ListView设置了和页面Header高度一致的空白Header. <Page x:Class="TestListViewH

Vue3.0基于Proxy 实现的数据更变检测 支持Composition API和Options API,以及typings

Composition API 纯函数式 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"></div> <script src="vue

【Win 10 应用开发】UI Composition 札记(四):绘制图形

使用 Win 2D 组件,就可以很轻松地绘制各种图形,哪怕你没有 D2D 相关基础,也不必写很复杂的 C++ 代码. 先来说说如何获取 Win 2D 组件.很简单,创建 UWP 应用项目后,你打开“解决方案资源管理器”窗口,然后在[引用]节点上右击,从快捷菜单中选择[管理 Nuget 程序包]命令,在打开的窗口中搜索“Win 2D”,然后安装带有 uwp 标识的那个就可以了. 顺便说一下,nuget 的包缓存在你的用户文件夹下,就是系统盘下的 \users\xxx,xxx是你登录系统的用户名,在

【Win 10 应用开发】UI Composition 札记(一):视图框架的实现

在开始今天的内容之前,老周先说一个问题,这个问题记得以前有人提过的. 设置 Windows.ApplicationModel.Core.CoreApplicationView.TitleBar.ExtendViewIntoTitleBar 属性可以让应用窗口中的内容扩展到标题栏.简单地说,就是你的UI区域可以扩大,并填充到标题栏,这在开发自定义标题栏或弄个什么毛玻璃效果时很有用. 不过,这个 ExtendViewIntoTitleBar 属性有个“八阿哥”,一旦你设置之后,系统会对其进行记录,很

【Win 10 应用开发】UI Composition 札记(六):动画

动画在 XAML 中也有,而且基本上与 WPF 中的用法一样.不过,在 UWP 中,动画还有一种表现方式—— 通过 UI Composition 来创建. 基于 UI Composition 的动画,相对于 XAML 动画,有以下优点: 1.不使用 UI 线程,XAML 动画是共享 UI 线程的,而 Composition 中的动画是使用辅助线程的. 2.Composition 动画支持表达式(计算公式)来产生动画,相对灵活. 老周的建议是:两者都用,因为基于 XAML 和基于 Composit