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 元素
UWP Composition API - GroupListView(一)的更多相关文章
- UWP Composition API - GroupListView(二)
还是先上效果图: 看完了上一篇UWP Composition API - GroupListView(一)的童鞋会问,这不是跟上一篇一样的吗??? 骗点击的?? No,No,其实相对上一个有更简单粗暴 ...
- UWP Composition API - 锁定列的FlexGrid
需求是第一列锁定,那么怎么让锁定列不跟着滚动条向做移动呢? 其实很简单,让锁定列跟scrollviewer的滚动做反方向移动. 先看一下这个控件的模板,嗯,其实很简单,就是ListView的模板,不同 ...
- UWP Composition API - New FlexGrid 锁定行列
如果之前看了 UWP Jenkins + NuGet + MSBuild 手把手教你做自动UWP Build 和 App store包 这篇的童鞋,针对VS2017,需要对应更新一下配置,需要的童鞋点 ...
- UWP Composition API - RadialMenu
用Windows 8.1的童鞋应该知道OneNote里面有一个RadialMenu.如下图,下图是WIn10应用Drawboard PDF的RadialMenu,Win8.1的机器不好找了.哈哈,由于 ...
- UWP Composition API - PullToRefresh
背景: 之前用ScrollViewer 来做过 PullToRefresh的控件,在项目一些特殊的条件下总有一些问题,比如ScrollViewer不会及时到达指定位置.于是便有了使用Compositi ...
- UWP中使用Composition API实现吸顶(1)
前几天需要在UWP中实现吸顶,就在网上找了一些文章: 吸顶大法 -- UWP中的工具栏吸顶的实现方式之一 在UWP中页面滑动导航栏置顶 发现前人的实现方式大多是控制ListViewBase的Heade ...
- win10 UWP 等级控件Building a UWP Rating Control using XAML and the Composition API | XAML Brewer, by Diederik Krols
原文:Building a UWP Rating Control using XAML and the Composition API | XAML Brewer, by Diederik Krols ...
- [UWP小白日记-12]使用新的Composition API来实现控件的阴影
前言 看了好久官方的Windows UI Dev Labs示例好久才有点心得,真是头大.(其实是英语幼儿园水平(⊙﹏⊙)b) 真的网上关于这个API的资料可以说几乎没有. 正文 首先用这东西的添加WI ...
- Windows Composition API 指南 - 认识 Composition API
微软在 Windows 10中 面向通用 Windows 应用 (Universal Windows Apps, UWA) 新引入了一套用于用户界面合成的 API:Composition API.Co ...
随机推荐
- Opera 浏览器各版本下载地址
新版本下载地址: 正式分支: http://get.opera.com/ftp/pub/opera/desktop/ beta分支:http://get.opera.com/ftp/pub/opera ...
- websocket
websocket是一个协议,在单个TCP连接上提供全双工通信. websocket被设计并被实现在 web浏览器和 web 服务器上,但是它可以被用于任何c/s 架构的应用程序中. websock ...
- Redis Cluster
使用 Redis Cluster Redis 3.0 在2015年出了Stable版本,3.0版本相对于2.8版本带来的主要新特性包括: 实现了Redis Cluster,从而做到了对集群的支持: 引 ...
- Excel—分离中英文字符
1.如下图: 2.提取中文字符为: 3.提取应为字符为: 4.说明: 该方法的原理利用了LENB和LEN计算方法的不同,LEN计算字符数,中英文都算作一个字符:LENB计算字节数,中文算两个字节,英文 ...
- 【Win10】SplitView控件
SplitView是Win10中的新控件. 用于呈现两部分视图. 一个视图是主要内容,另一个视图是用于导航.(也就是通常说的汉堡菜单.) 主要结构: <SplitView> <Spl ...
- frame和bounds
- frame 是一个以**父视图**为坐标系的位置- bounds 是一个以**自身**为坐标系的位置- 如果改变了bounds 那么会影响子控件的显示位置
- 基于ZK构建统一配置中心的方案和实践
背景: 近期使用Zk实现了一个简单的配置管理的小东西,在此开源出来,有兴趣的希望提出您的宝贵意见.如果恰巧您也使用或者接触过类似的东西, 也希望您可以分享下您觉得现在这个项目可以优化和改进的地方. 项 ...
- Java 压缩/ 解压 .Z 文件
1.问题描述 公司项目有需要用 JAVA 解压 .z文件. .z 是 unix 系统常见的压缩文件. 2.源码 import com.chilkatsoft.CkUnixCompress; impor ...
- ubuntu
mongoChef: http://3t.io/mongochef/download/core/platform/#tab-id-3 背景色改成豆沙绿: /usr/share/themes/Ambia ...
- C# 调用webservice 几种办法(转载)
原文地址: http://www.cnblogs.com/eagle1986/archive/2012/09/03/2669699.html //=========================== ...