UWP Composition API - GroupListView(一)

时间:2023-03-08 20:23:22

需求:

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

UWP Composition API - GroupListView(一)

这不就是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 元素