UWP Composition API - GroupListView(一)

2023-04-27,,

需求:

光看标题大家肯定不知道是什么东西,先上效果图:

这不就是ListView的Group效果吗?? 看上去是的。但是请听完需求.
1.Group中的集合需要支持增量加载ISupportIncrementalLoading

2.支持UI Virtualization

oh,no。ListView 自带的Group都不支持这2个需求。好吧,只有靠自己撸Code了。。

实现前思考:

仔细想了下,其实要解决的主要问题有2个
数据源的处理 和 GroupHeader的UI的处理

1.数据源的处理 

因为之前在写 UWP VirtualizedVariableSizedGridView 支持可虚拟化可变大小Item的View的时候已经做过这种处理源的工作了,所以方案出来的比较快。

不管有几个group,其实当第1个hasMore等false的时候,我们就可以加载第2个group里面的集合。

我为此写了一个类GroupObservableCollection<T> 它是继承 ObservableCollection<T>, IGroupCollection

        public class GroupObservableCollection<T> : ObservableCollection<T>, IGroupCollection
{
private List<IList<T>> souresList; private List<int> firstIndexInEachGroup = new List<int>();
private List<IGroupHeader> groupHeaders; bool _isLoadingMoreItems = false; public GroupObservableCollection(List<IList<T>> souresList, List<IGroupHeader> groupHeaders)
{
this.souresList = souresList;
this.groupHeaders = groupHeaders;
} public bool HasMoreItems
{
get
{
if (CurrentGroupIndex < souresList.Count)
{
var source = souresList[currentGroupIndex];
if (source is ISupportIncrementalLoading)
{
if (!(source as ISupportIncrementalLoading).HasMoreItems)
{
if (!_isLoadingMoreItems)
{
if (this.Count < GetSourceListTotoalCount())
{
int count = ;
int preCount = this.Count;
foreach (var item in souresList)
{
foreach (var item1 in item)
{
if (count >= preCount)
{
this.Add(item1);
if (item == source && groupHeaders[currentGroupIndex].FirstIndex==-)
{
groupHeaders[currentGroupIndex].FirstIndex = this.Count - ;
}
}
count++;
}
}
} groupHeaders[currentGroupIndex].LastIndex = this.Count - ; return false;
}
else
{
return true;
}
}
else
{
return true;
}
}
else
{
if (CurrentGroupIndex == source.Count - )
{
if (this.Count < GetSourceListTotoalCount())
{
int count = ;
int preCount = this.Count;
foreach (var item in souresList)
{
foreach (var item1 in item)
{
if (count >= preCount)
{
this.Add(item1);
if (item == source && groupHeaders[currentGroupIndex].FirstIndex == -)
{
groupHeaders[currentGroupIndex].FirstIndex = this.Count - ;
}
}
count++;
}
}
}
groupHeaders[currentGroupIndex].LastIndex = this.Count - ;
return false;
}
else
{
return true;
}
}
}
else
{
return false;
}
}
} int GetSourceListTotoalCount()
{
int i = ;
foreach (var item in souresList)
{
i += item.Count;
}
return i;
} public List<int> FirstIndexInEachGroup
{
get
{
return firstIndexInEachGroup;
} set
{
firstIndexInEachGroup = value;
}
} public List<IGroupHeader> GroupHeaders
{
get
{
return groupHeaders;
} set
{
groupHeaders = value;
}
} public IAsyncOperation<LoadMoreItemsResult> LoadMoreItemsAsync(uint count)
{
return FetchItems(count).AsAsyncOperation();
} private int currentGroupIndex;
public int CurrentGroupIndex
{
get
{
int count = ; for (int i = ; i < souresList.Count; i++)
{
var source = souresList[i];
count += source.Count;
if (count > this.Count)
{
currentGroupIndex = i;
return currentGroupIndex;
}
else if (count == this.Count)
{
currentGroupIndex = i;
if ((source is ISupportIncrementalLoading))
{
if (!(source as ISupportIncrementalLoading).HasMoreItems)
{
if (!_isLoadingMoreItems)
{
groupHeaders[i].LastIndex = this.Count - ;
if (currentGroupIndex + < souresList.Count)
{
currentGroupIndex = i + ;
}
}
}
}
else
{
//next
if (currentGroupIndex + < souresList.Count)
{
currentGroupIndex = i + ;
}
} return currentGroupIndex;
}
else
{
continue;
}
}
currentGroupIndex = ;
return currentGroupIndex;
}
} private async Task<LoadMoreItemsResult> FetchItems(uint count)
{
var source = souresList[CurrentGroupIndex]; if (source is ISupportIncrementalLoading)
{
int firstIndex = ;
if (groupHeaders[currentGroupIndex].FirstIndex != -)
{
firstIndex = source.Count;
}
_isLoadingMoreItems = true;
var result = await (source as ISupportIncrementalLoading).LoadMoreItemsAsync(count); for (int i = firstIndex; i < source.Count; i++)
{
this.Add(source[i]);
if (i == )
{
groupHeaders[currentGroupIndex].FirstIndex = this.Count - ;
}
}
_isLoadingMoreItems = false;
return result;
}
else
{
int firstIndex = ;
if (groupHeaders[currentGroupIndex].FirstIndex != -)
{
firstIndex = source.Count;
}
for (int i = firstIndex; i < source.Count; i++)
{
this.Add(source[i]);
if (i == )
{
groupHeaders[currentGroupIndex].FirstIndex = this.Count - ;
}
}
groupHeaders[currentGroupIndex].LastIndex = this.Count - ; return new LoadMoreItemsResult() { Count = (uint)source.Count };
}
}
}

而IGroupCollection是个接口。

    public interface IGroupCollection: ISupportIncrementalLoading
{
List<IGroupHeader> GroupHeaders { get; set; }
int CurrentGroupIndex { get; }
} public interface IGroupHeader
{
string Name { get; set; }
int FirstIndex { get; set; }
int LastIndex { get; set; }
double Height { get; set; }
} public class DefaultGroupHeader : IGroupHeader
{
public string Name { get; set; }
public int FirstIndex { get; set; }
public int LastIndex { get; set; }
public double Height { get; set; }
public DefaultGroupHeader()
{
FirstIndex = -;
LastIndex = -;
}
}

IGroupHeader 是用来描述Group header的,你可以继承它,添加一些绑定GroupHeader的属性(注意请给FirstIndex和LastIndex赋值-1的初始值)

比如:在效果图中,如果只有全部评论,没有精彩评论,那么后面的导航的按钮是应该不现实的,所以我加了GoToButtonVisibility属性来控制。

    public class MyGroupHeader : IGroupHeader, INotifyPropertyChanged
{
public string Name { get; set; }
public int FirstIndex { get; set; }
public int LastIndex { get; set; }
public double Height { get; set; }
public string GoTo { get; set; }
private Visibility _goToButtonVisibility = Visibility.Collapsed; public Visibility GoToButtonVisibility
{
get { return _goToButtonVisibility; }
set
{
_goToButtonVisibility = value;
OnPropertyChanged("GoToButtonVisibility");
}
} public MyGroupHeader()
{
FirstIndex = -;
LastIndex = -;
} public event PropertyChangedEventHandler PropertyChanged; void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}

数据源的处理还是比较简单的。

2.GroupHeader的UI的处理

首先我想到的是加一个Grid,然后这些GroupHeader放在里面,通过ScrollViewer的ViewChanged来处理它们。

比较了下ListView的Group效果,Scrollbar是会挡住GroupHeader的,所以我把这个Grid放进了ScrollViewer的模板里面。

GroupListView的模板,这里大家可以看到我加入了个ProgressRing,这个是后面做导航功能需要的,后面再讲。

 <ControlTemplate TargetType="local:GroupListView">
<Grid BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}">
<ScrollViewer x:Name="ScrollViewer" Style="{StaticResource GroupListViewScrollViewer}" 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}">
<ItemsPresenter FooterTransitions="{TemplateBinding FooterTransitions}" FooterTemplate="{TemplateBinding FooterTemplate}" Footer="{TemplateBinding Footer}" HeaderTemplate="{TemplateBinding HeaderTemplate}" Header="{TemplateBinding Header}" HeaderTransitions="{TemplateBinding HeaderTransitions}" Padding="{TemplateBinding Padding}"/>
</ScrollViewer>
<ProgressRing x:Name="ProgressRing" Visibility="Collapsed" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Grid>
</ControlTemplate>

ScrollViewer的模板

                       <Grid Background="{TemplateBinding Background}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ScrollContentPresenter x:Name="ScrollContentPresenter" Grid.ColumnSpan="" ContentTemplate="{TemplateBinding ContentTemplate}" Margin="{TemplateBinding Padding}" Grid.RowSpan=""/>
<Grid x:Name="GroupHeadersCanvas" Grid.RowSpan="" Grid.ColumnSpan="" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
<ContentControl x:Name="TopGroupHeader" Grid.RowSpan="" Grid.ColumnSpan="" VerticalAlignment="Top" HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch"/>
<ScrollBar x:Name="VerticalScrollBar" Grid.Column="" HorizontalAlignment="Right" IsTabStop="False" Maximum="{TemplateBinding ScrollableHeight}" Orientation="Vertical" Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}" Value="{TemplateBinding VerticalOffset}" ViewportSize="{TemplateBinding ViewportHeight}"/>
<ScrollBar x:Name="HorizontalScrollBar" IsTabStop="False" Maximum="{TemplateBinding ScrollableWidth}" Orientation="Horizontal" Grid.Row="" Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}" Value="{TemplateBinding HorizontalOffset}" ViewportSize="{TemplateBinding ViewportWidth}"/>
<Border x:Name="ScrollBarSeparator" Background="{ThemeResource SystemControlPageBackgroundChromeLowBrush}" Grid.Column="" Grid.Row=""/>
</Grid>

下面就是实现对GroupHeader显示的控制了。

很快代码写好了。。运行起来效果还可以。。但是童鞋们说。。你这个跟Composition API 一毛钱关系都没有啊。。

大家别急。。听我说。。模拟器里面运行还行,拿实体机器上运行的时候,当我快速向上或者向下滑动的时候,GroupHeader会出现顿一顿的感觉,卡一下,不会有惯性的感觉。

看到这个,我立马明白了。。不管是ViewChanging或者ViewChanged事件,它们跟Manipulation都不是同步的。

看了上一盘 UWP Composition API - PullToRefresh的童鞋会说,好吧,隐藏的真深。

那我们还是用Composition API来建立GroupHeader和ScrollViewer之间的关系。

1.首先我想的是,当进入Viewport再用Composition API来建立关系,但是很快被我否决了。还是因为ViewChanged这个事件是有惯性的原因,这样没法让创建GroupHeader和ScrollViewer之间的关系的初始数据完全准确。

就是说GroupHeader因为初始数据不正确的情况会造成没放在我想要的位置,只有当惯性停止的时候获取的位置信息才是准确的。

在PrepareContainerForItemOverride中判断是否GroupHeader 的那个Item已经准备添加到ItemsPanel里面。

         protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
{
base.PrepareContainerForItemOverride(element, item);
ListViewItem listViewItem = element as ListViewItem;
listViewItem.SizeChanged -= ListViewItem_SizeChanged;
if (listViewItem.Tag == null)
{
defaultListViewItemMargin = listViewItem.Margin;
} if (groupCollection != null)
{
var index = IndexFromContainer(element);
var group = groupCollection.GroupHeaders.FirstOrDefault(x => x.FirstIndex == index || x.LastIndex == index);
if (group != null)
{
if (!groupDic.ContainsKey(group))
{
ContentControl groupheader = CreateGroupHeader(group);
ContentControl tempGroupheader = CreateGroupHeader(group); ExpressionAnimationItem expressionAnimationItem = new ExpressionAnimationItem();
expressionAnimationItem.VisualElement = groupheader;
expressionAnimationItem.TempElement = tempGroupheader; groupDic[group] = expressionAnimationItem; var temp = new Dictionary<IGroupHeader, ExpressionAnimationItem>();
foreach (var keyValue in groupDic.OrderBy(x => x.Key.FirstIndex))
{
temp[keyValue.Key] = keyValue.Value;
}
groupDic = temp;
if (groupHeadersCanvas != null)
{
groupHeadersCanvas.Children.Add(groupheader);
groupHeadersCanvas.Children.Add(tempGroupheader); groupheader.Measure(new Windows.Foundation.Size(this.ActualWidth, this.ActualHeight)); group.Height = groupheader.DesiredSize.Height; groupheader.Height = tempGroupheader.Height = group.Height;
groupheader.Width = tempGroupheader.Width = this.ActualWidth; if (group.FirstIndex == index)
{
listViewItem.Tag = listViewItem.Margin;
listViewItem.Margin = GetItemMarginBaseOnDeafult(groupheader.DesiredSize.Height);
listViewItem.SizeChanged += ListViewItem_SizeChanged;
} groupheader.Visibility = Visibility.Collapsed;
tempGroupheader.Visibility = Visibility.Collapsed;
UpdateGroupHeaders();
} }
else
{
if (group.FirstIndex == index)
{
listViewItem.Tag = listViewItem.Margin;
listViewItem.Margin = GetItemMarginBaseOnDeafult(group.Height);
listViewItem.SizeChanged += ListViewItem_SizeChanged;
}
else
{
listViewItem.Margin = defaultListViewItemMargin;
}
} }
else
{
listViewItem.Margin = defaultListViewItemMargin;
}
}
else
{
listViewItem.Margin = defaultListViewItemMargin;
}
}

在UpdateGroupHeader方法里面去设置Header的状态

        internal void UpdateGroupHeaders(bool isIntermediate = true)
{
var firstVisibleItemIndex = this.GetFirstVisibleIndex();
foreach (var item in groupDic)
{
//top header
if (item.Key.FirstIndex <= firstVisibleItemIndex && (firstVisibleItemIndex <= item.Key.LastIndex || item.Key.LastIndex == -))
{
currentTopGroupHeader.Visibility = Visibility.Visible;
currentTopGroupHeader.Margin = new Thickness();
currentTopGroupHeader.Clip = null;
currentTopGroupHeader.DataContext = item.Key; if (item.Key.FirstIndex == firstVisibleItemIndex)
{
if (item.Value.ScrollViewer == null)
{
item.Value.ScrollViewer = scrollViewer;
} var isActive = item.Value.IsActive; item.Value.StopAnimation();
item.Value.VisualElement.Clip = null;
item.Value.VisualElement.Visibility = Visibility.Collapsed; if (!isActive)
{
if (!isIntermediate)
{
item.Value.VisualElement.Margin = new Thickness();
item.Value.StartAnimation(true);
}
}
else
{
item.Value.StartAnimation(false);
} }
ClearTempElement(item);
}
//moving header
else
{
HandleGroupHeader(isIntermediate, item);
}
}
}

这里我简单说下几种状态:
1. 在ItemsPanel里面

1)全部在Viewport里面

动画开启,Clip设置为Null

2)部分在Viewport里面

动画开启,并且设置Clip
3)没有在viewport里面

动画开启,Visible 设置为Collapsed
2. 没有在ItemsPanel里面

动画停止。

关于GroupHeader初始状态的设置,这里是最坑的,遇到很多问题。

        public void StartAnimation(bool update = false)
{ if (update || expression == null || visual == null)
{
visual = ElementCompositionPreview.GetElementVisual(VisualElement);
//if (0 <= VisualElement.Margin.Top && VisualElement.Margin.Top <= ScrollViewer.ActualHeight)
//{
// min = (float)-VisualElement.Margin.Top;
// max = (float)ScrollViewer.ActualHeight + min;
//}
//else if (VisualElement.Margin.Top < 0)
//{ //}
//else if (VisualElement.Margin.Top > ScrollViewer.ActualHeight)
//{ //}
if (scrollViewerManipProps == null)
{
scrollViewerManipProps = ElementCompositionPreview.GetScrollViewerManipulationPropertySet(ScrollViewer);
}
Compositor compositor = scrollViewerManipProps.Compositor; // Create the expression
//expression = compositor.CreateExpressionAnimation("min(max((ScrollViewerManipProps.Translation.Y + VerticalOffset), MinValue), MaxValue)");
////Expression = compositor.CreateExpressionAnimation("ScrollViewerManipProps.Translation.Y +VerticalOffset"); //expression.SetScalarParameter("MinValue", min);
//expression.SetScalarParameter("MaxValue", max);
//expression.SetScalarParameter("VerticalOffset", (float)ScrollViewer.VerticalOffset); expression = compositor.CreateExpressionAnimation("ScrollViewerManipProps.Translation.Y + VerticalOffset");
////Expression = compositor.CreateExpressionAnimation("ScrollViewerManipProps.Translation.Y +VerticalOffset"); //expression.SetScalarParameter("MinValue", min);
//expression.SetScalarParameter("MaxValue", max);
VerticalOffset = ScrollViewer.VerticalOffset;
expression.SetScalarParameter("VerticalOffset", (float)ScrollViewer.VerticalOffset); // set "dynamic" reference parameter that will be used to evaluate the current position of the scrollbar every frame
expression.SetReferenceParameter("ScrollViewerManipProps", scrollViewerManipProps); } visual.StartAnimation("Offset.Y", expression); IsActive = true;
//Windows.UI.Xaml.Media.CompositionTarget.Rendering -= OnCompositionTargetRendering; //Windows.UI.Xaml.Media.CompositionTarget.Rendering += OnCompositionTargetRendering;
}

注释掉了的代码是处理:

当GroupHeader进入Viewport的时候才启动动画,离开之后就关闭动画,表达式就是一个限制,这个就不讲了。

expression = compositor.CreateExpressionAnimation("ScrollViewerManipProps.Translation.Y + VerticalOffset");

可以看到我给表达式加了一个VericalOffset。。嗯。其实Visual的Offset是表示 Visual 相对于其父 Visual 的位置偏移量。

举2个例子,整个Viewport的高度是500,现在滚动条的VericalOffset是100。

1.如果我想把Header(header高度为50)放到Viewport的最下面(Header刚好全部进入Viewport),那么初始的参数应该是哪些呢?

Header.Margin = new Thickness(450);

Header.Clip=null;

expression = compositor.CreateExpressionAnimation("ScrollViewerManipProps.Translation.Y +100");

这样向上滚ScrollViewerManipProps.Translation.Y(-450),Header 就会滚Viewport的顶部。

2.如果我想把Header(header高度为50)放到Viewport的最下面(Header刚好一半全部进入Viewport),那么初始的参数应该是哪些呢?

Header.Margin = new Thickness(475);

Header.Clip=new RectangleGeometry() { Rect = new Rect(0, 0, this.ActualWidth, 25) };

expression = compositor.CreateExpressionAnimation("ScrollViewerManipProps.Translation.Y +100");

当向上或者向下滚动的时候,记得更新Clip值就可以了。

说到为什么要加Clip,因为如果你的控件不是整个Page大小的时候,这个Header会显示到控件外部去,大家应该都是懂得。

这里说下这个里面碰到一个问题。当GroupHeader Viewport之外的时候(在Grid之外的,Margin大于Grid的高度)创建动画,会发现你怎么修改Header属性都是没有效果的。

最终结果的是不会在屏幕上显示任何东西。

实验了下用Canvas发现就可以了,但是Grid却不行,是不是可以认为Visual在创建的时候如果对象不在它父容器的Size范围之内,创建出来都是看不见的??

这个希望懂得童鞋能留言告诉一下。

把ScrollViewer模板里面的Grid换成Canvas就好了。。

剩下的都是一些计算,计算位置,计算大小变化。

最后就是GoToGroup方法,当跳转的Group没有load出来的时候(也就是FirstIndex还没有值得时候),我们就Load,Load,Load,直到

它有值,这个可能是个长的时间过程,所以加了ProgressRing,找到Index,最后用ListView的API来跳转就好了。

        public async Task GoToGroupAsync(int groupIndex, ScrollIntoViewAlignment scrollIntoViewAlignment = ScrollIntoViewAlignment.Leading)
{
if (groupCollection != null)
{
var gc = groupCollection;
if (groupIndex < gc.GroupHeaders.Count && groupIndex >= && !isGotoGrouping)
{
isGotoGrouping = true;
//load more so that ScrollIntoViewAlignment.Leading can go to top
var loadcount = this.GetVisibleItemsCount() + ; progressRing.IsActive = true;
progressRing.Visibility = Visibility.Visible;
//make sure user don't do any other thing at the time.
this.IsHitTestVisible = false;
//await Task.Delay(3000);
while (gc.GroupHeaders[groupIndex].FirstIndex == -)
{
if (gc.HasMoreItems)
{
await gc.LoadMoreItemsAsync(loadcount);
}
else
{
break;
}
} if (gc.GroupHeaders[groupIndex].FirstIndex != -)
{
//make sure there are enought items to go ScrollIntoViewAlignment.Leading
//this.count > (firstIndex + loadcount)
if (scrollIntoViewAlignment == ScrollIntoViewAlignment.Leading)
{
var more = this.Items.Count - (gc.GroupHeaders[groupIndex].FirstIndex + loadcount);
if (gc.HasMoreItems && more < )
{
await gc.LoadMoreItemsAsync((uint)Math.Abs(more));
}
}
progressRing.IsActive = false;
progressRing.Visibility = Visibility.Collapsed;
var groupFirstIndex = gc.GroupHeaders[groupIndex].FirstIndex;
ScrollIntoView(this.Items[groupFirstIndex], scrollIntoViewAlignment);
//already in viewport, maybe it will not change view
if (groupDic.ContainsKey(gc.GroupHeaders[groupIndex]) && groupDic[gc.GroupHeaders[groupIndex]].Visibility == Visibility.Visible)
{
this.IsHitTestVisible = true;
isGotoGrouping = false;
}
}
else
{
this.IsHitTestVisible = true;
isGotoGrouping = false;
progressRing.IsActive = false;
progressRing.Visibility = Visibility.Collapsed;
} }
}
}

 总结:

这个控件做下来,基本上都是在计算计算计算。。当然也知道了一些Composition API的东西。

其实Vistual的属性还有很多,在做这个控件的时候没有用到,以后用到了会继续分享的。 开源有益,源码GitHub地址。

UWP Composition API - GroupListView(二)

Visual 元素有些基本的呈现相关属性,这些属性都能使用 Composition API 的动画 API 来演示动画。

Opacity 
表示 Visual 的透明度。

Offset 
表示 Visual 相对于其父 Visual 的位置偏移量。

Clip 
表示 Visual 裁剪区域。

CenterPoint 
表示 Visual 的中心点。

TransformMatrix 
表示 Visual 的变换矩阵。

Size 
表示 Visual 的尺寸大小。

Scale 
表示 Visual 的缩放大小。

RotationAxis 
表示 Visual 的旋转轴。

RotationAngle 
表示 Visual 的旋转角度。

有 4 个类派生自 Visual,他们分别对应了不同种类的 Visual,分别是:

ContainerVisual 
表示容器 Visual,可能有子节点的 Visual,大部分的 XAML 可视元素基本都是该 Visual,其他的 Visual 都也是派生自该类。

EffectVisual 
表示通过特效来呈现内容的 Visual,可以通过配合 Win2D 的支持 Composition 的 Effects 来呈现丰富多彩的内容。

ImageVisual 
表示通过图片来呈现内容的 Visual,可以用于呈现图片。

SolidColorVisual
表示一个纯色矩形的 Visual 元素

UWP Composition API - GroupListView(一)的相关教程结束。

《UWP Composition API - GroupListView(一).doc》

下载本文的Word格式文档,以方便收藏与打印。