需求:
光看标题大家肯定不知道是什么东西,先上效果图:
这不就是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 = 0;
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==-1)
{
groupHeaders[currentGroupIndex].FirstIndex = this.Count - 1;
}
}
count++;
}
}
}
groupHeaders[currentGroupIndex].LastIndex = this.Count - 1;
return false;
}
else
{
return true;
}
}
else
{
return true;
}
}
else
{
if (CurrentGroupIndex == source.Count - 1)
{
if (this.Count < GetSourceListTotoalCount())
{
int count = 0;
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 == -1)
{
groupHeaders[currentGroupIndex].FirstIndex = this.Count - 1;
}
}
count++;
}
}
}
groupHeaders[currentGroupIndex].LastIndex = this.Count - 1;
return false;
}
else
{
return true;
}
}
}
else
{
return false;
}
}
}
int GetSourceListTotoalCount()
{
int i = 0;
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 = 0;
for (int i = 0; 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 - 1;
if (currentGroupIndex + 1 < souresList.Count)
{
currentGroupIndex = i + 1;
}
}
}
}
else
{
//next
if (currentGroupIndex + 1 < souresList.Count)
{
currentGroupIndex = i + 1;
}
}
return currentGroupIndex;
}
else
{
continue;
}
}
currentGroupIndex = 0;
return currentGroupIndex;
}
}
private async Task<LoadMoreItemsResult> FetchItems(uint count)
{
var source = souresList[CurrentGroupIndex];
if (source is ISupportIncrementalLoading)
{
int firstIndex = 0;
if (groupHeaders[currentGroupIndex].FirstIndex != -1)
{
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 == 0)
{
groupHeaders[currentGroupIndex].FirstIndex = this.Count - 1;
}
}
_isLoadingMoreItems = false;
return result;
}
else
{
int firstIndex = 0;
if (groupHeaders[currentGroupIndex].FirstIndex != -1)
{
firstIndex = source.Count;
}
for (int i = firstIndex; i < source.Count; i++)
{
this.Add(source[i]);
if (i == 0)
{
groupHeaders[currentGroupIndex].FirstIndex = this.Count - 1;
}
}
groupHeaders[currentGroupIndex].LastIndex = this.Count - 1;
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 = -1;
LastIndex = -1;
}
}
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 = -1;
LastIndex = -1;
}
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="2" ContentTemplate="{TemplateBinding ContentTemplate}" Margin="{TemplateBinding Padding}" Grid.RowSpan="2"/>
<Grid x:Name="GroupHeadersCanvas" Grid.RowSpan="2" Grid.ColumnSpan="2" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
<ContentControl x:Name="TopGroupHeader" Grid.RowSpan="2" Grid.ColumnSpan="2" VerticalAlignment="Top" HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch"/>
<ScrollBar x:Name="VerticalScrollBar" Grid.Column="1" 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="1" Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}" Value="{TemplateBinding HorizontalOffset}" ViewportSize="{TemplateBinding ViewportWidth}"/>
<Border x:Name="ScrollBarSeparator" Background="{ThemeResource SystemControlPageBackgroundChromeLowBrush}" Grid.Column="1" Grid.Row="1"/>
</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 == -1))
{
currentTopGroupHeader.Visibility = Visibility.Visible;
currentTopGroupHeader.Margin = new Thickness(0);
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(0);
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 >= 0 && !isGotoGrouping)
{
isGotoGrouping = true;
//load more so that ScrollIntoViewAlignment.Leading can go to top
var loadcount = this.GetVisibleItemsCount() + 1;
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 == -1)
{
if (gc.HasMoreItems)
{
await gc.LoadMoreItemsAsync(loadcount);
}
else
{
break;
}
}
if (gc.GroupHeaders[groupIndex].FirstIndex != -1)
{
//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 < 0)
{
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 元素
来源:oschina
链接:https://my.oschina.net/u/4316924/blog/4051092