如果之前看了 UWP Jenkins + NuGet + MSBuild 手把手教你做自动UWP Build 和 App store包
这篇的童鞋,针对VS2017,需要对应更新一下配置,需要的童鞋点击查看一下,在文章最后。
之前写过一篇 锁定列的FlexGrid,没看过的童鞋可以去先看一下那一篇。
先放上效果图
制作新控件的背景是SDK升级到了14393,Composition API 有了相应的改变。
对我们有较大的影响就是:
10586:
在 10586 版 SDK 中, ElementCompositionPreview.GetElementVisual
方法返回的 Visual 仅由调用者控制。通过对应的 Visual 对一个 UIElement
进行的操作纯粹只会对 XAML 施加增量影响。这是因为返回的 Visual
(在底层)是 UIElement
的 Visual 作为根 Visual 的子项。
14393:
在 14332 版及后续版本中, ElementCompositionPreview.GetElementVisual
方法返回的 Visual与 XAML 布局操作的 Visual 相同。这意味着不同于 11 月更新,现在通过对应的 Visual 对一个 UIElement
元素进行的操作会绝对地改变 XAML 布局。
因为调用者和 XAML 都在操作同一个 Visual
,XAML 有可能会覆盖调用者的赋值。以下是 XAML 可能设置的属性:
- Offset
- Size
- Opacity
- TransformMatrix
- Clip
- CompositeMode
XAML 对一个互操作 Visual 属性进行更新的规则如下:
-
XAML 在布局过程中会覆写互操作 Visual 的属性值。
-
XAML 不会读回由程序代码直接对互操作 Visual 的属性赋的值。
-
XAML 只在新值不等于上次赋的旧值时,才会对互操作 Visual 的属性赋值。亦即如果 XAML 一侧的属性值没有发生变化,则 XAML 不会去更改 Visual 一侧的属性值。
-
XAML 一侧的上次赋值的值默认与互操作 Visual 属性的默认值一致。也就是说如果 XAML 一侧的属性值保持在默认值不变,则 XAML 不会去更新 Visual 一侧的属性值(例如 XAML 布局中的 offset,对应 Visual 中的 Visual.Offset,默认值为 [0,0])。
-
Visual 一侧的属性值不会覆写到 XAML 一侧。
-
UI 元素最终呈现的效果取决于最后生效的值(Visual 一侧的取值或 XAML 覆写 Visual 的值)。
总的来讲就是以前Visual 是绝对由我来控制,而现在XAML也会共同影响Visual 的最终值。
这样一搞,宝宝就不开心了,直接把以前的项目升级到14393,FlexGrid各种问题。
秉着吐槽不如自己动手的心情,让我们自己创建New FlexGird。
首先,我们来看一下整个New FlexGird的构成,整个控件是一个ListView,头(Column Header 和 锁定的行)都放在ListView的ScrollViewer的TopHeader里面。
下面是整个New FlexGird的模板。
<ControlTemplate TargetType="local:NewFlexGrid"> <Border x:Name="RootBorder" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}"> <ScrollViewer x:Name="ScrollViewer" Style="{StaticResource FlexGridScrollViewerStyle}" AutomationProperties.AccessibilityView="Raw" BringIntoViewOnFocusChange="{TemplateBinding ScrollViewer.BringIntoViewOnFocusChange}" HorizontalScrollMode="{TemplateBinding ScrollViewer.HorizontalScrollMode}" HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}" IsHorizontalRailEnabled="{TemplateBinding ScrollViewer.IsHorizontalRailEnabled}" IsHorizontalScrollChainingEnabled="{TemplateBinding ScrollViewer.IsHorizontalScrollChainingEnabled}" IsVerticalScrollChainingEnabled="{TemplateBinding ScrollViewer.IsVerticalScrollChainingEnabled}" IsVerticalRailEnabled="{TemplateBinding ScrollViewer.IsVerticalRailEnabled}" IsDeferredScrollingEnabled="{TemplateBinding ScrollViewer.IsDeferredScrollingEnabled}" TabNavigation="{TemplateBinding TabNavigation}" VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}" VerticalScrollMode="{TemplateBinding ScrollViewer.VerticalScrollMode}" ZoomMode="{TemplateBinding ScrollViewer.ZoomMode}"> <ScrollViewer.TopHeader> <StackPanel Orientation="Vertical" Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> <local:NewFlexGridColumnHeader x:Name="ColumnHeader" FrozenCount="{TemplateBinding ColumnHeaderFrozenCount}" SelectionMode="None" IsItemClickEnabled="True" Style="{StaticResource NoScrollViewerListViewStyle}" ItemsSource="{TemplateBinding ColumnsHeaderItemsSource}" ItemTemplate="{TemplateBinding ColumnsHeaderItemTemplate}"> <local:NewFlexGridColumnHeader.ItemsPanel> <ItemsPanelTemplate> <StackPanel Orientation="Horizontal"/> </ItemsPanelTemplate> </local:NewFlexGridColumnHeader.ItemsPanel> </local:NewFlexGridColumnHeader> <local:NewFlexGridFrozenRows x:Name="FrozenRows" ItemTemplate="{TemplateBinding ItemTemplate}" ItemsSource="{TemplateBinding FrozenRowsItemsSource}" IsItemClickEnabled="True" SelectionMode="None" Style="{StaticResource NoScrollViewerListViewStyle}" ItemContainerStyle="{TemplateBinding ItemContainerStyle}"> <local:NewFlexGridFrozenRows.ItemsPanel> <ItemsPanelTemplate> <StackPanel Orientation="Vertical"/> </ItemsPanelTemplate> </local:NewFlexGridFrozenRows.ItemsPanel> </local:NewFlexGridFrozenRows> </StackPanel> </ScrollViewer.TopHeader> <ItemsPresenter HorizontalAlignment="Left" VerticalAlignment="Top" FooterTransitions="{TemplateBinding FooterTransitions}" FooterTemplate="{TemplateBinding FooterTemplate}" Footer="{TemplateBinding Footer}" Padding="{TemplateBinding Padding}"/> <!--HeaderTemplate="{TemplateBinding HeaderTemplate}" Header="{TemplateBinding Header}" HeaderTransitions="{TemplateBinding HeaderTransitions}"--> </ScrollViewer> </Border> </ControlTemplate>
在获取到ScrollViewer元素以及New FlexGird Loaded的事件当中,我们需要准备Composition 的元素
private void PrepareCompositionAnimation() { if (_scrollViewer != null) { if (_scrollerViewerManipulation == null) { _scrollerViewerManipulation = ElementCompositionPreview.GetScrollViewerManipulationPropertySet(_scrollViewer); } if (_offsetXAnimation == null) { _offsetXAnimation = _scrollerViewerManipulation.Compositor.CreateExpressionAnimation("-min(0,ScrollManipulation.Translation.X)"); _offsetXAnimation.SetReferenceParameter("ScrollManipulation", _scrollerViewerManipulation); _columnsHeader._offsetXAnimation = _offsetXAnimation; _frozenRows._offsetXAnimation = _offsetXAnimation; } } }
看过之前几遍的Composition 相关文章的童鞋应该知道这是在做什么,不知道的童鞋请先看一下UWP Composition API - PullToRefresh
NewFlexGridColumnHeader 是一个横向的ListView,它没有ScrollViewer,通过New FlexGird中的ColumnHeaderFrozenCount/ColumnsHeaderItemsSource/ColumnsHeaderItemTemplate属性进行关联。
在NewFlexGridColumnHeader 的PrepareContainerForItemOverride方法中,我们使用前面准备好的_offsetXAnimation,让符合条件的(具体就是第几个Column header)执行动画。
protected override void PrepareContainerForItemOverride(DependencyObject element, object item) { base.PrepareContainerForItemOverride(element, item); int index = this.IndexFromContainer(element); if (index > -1 && index < FrozenCount && _offsetXAnimation != null) { Canvas.SetZIndex((element as UIElement), 10); var _frozenContentVisual = ElementCompositionPreview.GetElementVisual(element as UIElement); _frozenContentVisual.StartAnimation("Offset.X", _offsetXAnimation); } }
NewFlexGridFrozenRows 是一个竖向的ListView,它也是没有ScrollViewer,用于存放锁定的行,通过New FlexGird中的FrozenRowsItemsSource/ItemTemplate/ItemContainerStyle等属性关联。
在NewFlexGridFrozenRows 的PrepareContainerForItemOverride方法中,我们主要做的是注册NewFlexGridFrozenRows_Loaded 事件,
protected override void PrepareContainerForItemOverride(DependencyObject element, object item) { base.PrepareContainerForItemOverride(element, item); var flexGridItem = element as ListViewItem; flexGridItem.RightTapped -= FlexGridItem_RightTapped; flexGridItem.Holding -= FlexGridItem_Holding; flexGridItem.RightTapped += FlexGridItem_RightTapped; flexGridItem.Holding += FlexGridItem_Holding; flexGridItem.Loaded += NewFlexGridFrozenRows_Loaded; }
当Item Loaded的时候,我们将之前准备好的_offsetXAnimation,通过我们定义一个附件属性来得知是哪个元素需要做Frozen的动画。
private void NewFlexGridFrozenRows_Loaded(object sender, RoutedEventArgs e) { (sender as ListViewItem).Loaded -= NewFlexGridFrozenRows_Loaded; var templateRoot = (sender as ListViewItem).ContentTemplateRoot; var child = templateRoot.GetAllChildren(); var _frozenContent = child.Where(x => FlexGridItemFrozenContent.GetIsFrozenContent(x)); if (_frozenContent != null && _offsetXAnimation != null) { foreach (var item in _frozenContent) { var _frozenContentVisual = ElementCompositionPreview.GetElementVisual(item); _frozenContentVisual.StartAnimation("Offset.X", _offsetXAnimation); } } }
在我们New FlexGird 也跟NewFlexGridFrozenRows 当中一样的操作。
在Unloaded事件中我们要释放掉一些资源防止内存泄漏,并且在合适的时机去释放掉全部的Composition 资源(见Dispose 方法)
private void NewFlexGrid_Unloaded(object sender, RoutedEventArgs e) { if (_offsetXAnimation != null) { _offsetXAnimation.Dispose(); _offsetXAnimation = null; } //don't dispose at this moment,some page NavigationCacheMode is required //you must dispose it at page back. //if (_scrollerViewerManipulation != null) //{ // _scrollerViewerManipulation.Dispose(); // _scrollerViewerManipulation = null; //} }
public void Dispose() { if (_offsetXAnimation != null) { _offsetXAnimation.Dispose(); _offsetXAnimation = null; } if (_scrollerViewerManipulation != null) { _scrollerViewerManipulation.Dispose(); _scrollerViewerManipulation = null; } }
在我们的New FlexGird的ItemTemplate里面定义好锁定的列(蓝色部分)
<DataTemplate x:Key="WideScreenItemTemplate"> <Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" > <Grid.ColumnDefinitions> <ColumnDefinition Width="110"/> <ColumnDefinition Width="110" /> <ColumnDefinition Width="110" /> <ColumnDefinition Width="110" /> <ColumnDefinition Width="110" /> <ColumnDefinition Width="110" /> <ColumnDefinition Width="110" /> </Grid.ColumnDefinitions> <Grid Background="Green" Width="110" flexgrid:FlexGridItemFrozenContent.IsFrozenContent="True"> <TextBlock Text="{Binding Age}" /> </Grid> <TextBlock Text="{Binding Name}" Grid.Column="1"/> <TextBlock Text="{Binding IsMale}" Grid.Column="2"/> <Grid Background="Yellow" Width="110" Grid.Column="3" > <TextBlock Text="{Binding Age}" /> </Grid> <TextBlock Text="{Binding Name}" Grid.Column="4"/> <TextBlock Text="{Binding IsMale}" Grid.Column="5"/> <TextBlock Text="{Binding Name}" Grid.Column="6"/> </Grid> </DataTemplate>
ok,运行起来就实现了锁定行列。
在使用当中,可能有童鞋发现,还有一些其他问题。
1.锁定的列或者行,由于是透明背景,没法盖住移动的部分,解决办法是 给你要锁定的列元素加上 Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
2.Pointer over的样式,由于1里面加了不透明的色,这部分会挡住ListViewitem PointerOver的颜色,解决办法是
重写ListviewItem的样式,由微软文档可知道,ListViewItem有2种模板
我们这里把第2种模板重写一下,将模板里面的PointerOverBorder元素上移动到ContentBorder之上(就是Xaml里面把它移动到ContentBorder后面),具体的模板
请查看NewFlexGridItemStyle
3.当Pointer press下去的时候,冻结的列的前端会显示出来它后面挡住的内容,解决办法是
<Border HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" flexgrid:FlexGridItemFrozenContent.IsFrozenContent="True"> <StackPanel Margin="-15,0,0,0" Padding="15,4,0,4" Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" VerticalAlignment="Center"> <TextBlock Text="{x:Bind Name, Mode=OneWay}"}" TextTrimming="CharacterEllipsis"/> </StackPanel> </Border>
将用于挡住后面内容的色块,这里是StackPanel,加上一个 负X(-15)的Margin。那大家可能会问为什么不加在Border上面,根据14393 SDK的更新中,如果作为Visual 的内容具有初始的位置属性的话,这种会影响到Visual 的最终值。大家可以试试给加了flexgrid:FlexGridItemFrozenContent.IsFrozenContent="True" 的元素加上一些影响位置的属性设置,都会导致最终动画的无效。
不知道Creators Update里面会不会其他变化,好像有了新的API,等我研究好了,再发给大家看看。
最后开源有益:New FlexGird,大家拿去用吧。。