需求:

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

这不就是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{//nextif (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 };}}}

View Code

而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;}}

View Code

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

        internal void UpdateGroupHeaders(bool isIntermediate = true){var firstVisibleItemIndex = this.GetFirstVisibleIndex();foreach (var item in groupDic){//top headerif (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 headerelse{HandleGroupHeader(isIntermediate, item);}}}

View Code

这里我简单说下几种状态:
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 frameexpression.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 topvar 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 元素

转载于:https://www.cnblogs.com/FaDeKongJian/p/5629715.html

UWP Composition API - GroupListView(一)相关推荐

  1. UWP Composition API - GroupListView(二)

    还是先上效果图: 看完了上一篇UWP Composition API - GroupListView(一)的童鞋会问,这不是跟上一篇一样的吗??? 骗点击的?? No,No,其实相对上一个有更简单粗暴 ...

  2. UWP Composition API - 锁定列的FlexGrid

    原文:UWP Composition API - 锁定列的FlexGrid 需求是第一列锁定,那么怎么让锁定列不跟着滚动条向做移动呢? 其实很简单,让锁定列跟scrollviewer的滚动做反方向移动 ...

  3. UWP Composition API - PullToRefresh

    原文:UWP Composition API - PullToRefresh 背景: 之前用ScrollViewer 来做过 PullToRefresh的控件,在项目一些特殊的条件下总有一些问题,比如 ...

  4. UWP Composition API - RadialMenu

    原文:UWP Composition API - RadialMenu 用Windows 8.1的童鞋应该知道OneNote里面有一个RadialMenu.如下图,下图是WIn10应用Drawboar ...

  5. WPF 使用 Composition API 做高性能渲染

    在 WPF 中很多小伙伴都会遇到渲染性能的问题,虽然 WPF 的渲染可以甩浏览器渲染几条街,但是还是支持不了游戏级的渲染.在 WPF 使用的 DX 只是优化等级为 9 和 DX 9 差不多的性能,微软 ...

  6. vue3学习笔记 Composition API setup

    一.Composition API优势 相对于vue2的option API Vue3的Composition API设计更有优势 Composition(组合式)Api 功能分组 Compositi ...

  7. 敲黑板!vue3重点!一文了解Composition API新特性:ref、toRef、toRefs

    一文了解Composition API新特性:ref.toRef.toRefs 一.

  8. Vue3 Composition API(三)——生命周期钩子、Provide函数 和 Inject函数、封装Hook案例、setup顶层编写方式

    一.生命周期钩子 我们前面说过 setup 可以用来替代 data . methods . computed .watch 等等这些选项,也可以替代 生命周期钩子. 那么setup中如何使用生命周期函 ...

  9. Vue3 Composition API(二)——computed、watchEffect、setup中使用ref

    一.computed 在前面我们讲解过计算属性computed:当我们的某些属性是依赖其他状态时,我们可以使用计算属性来处理 在前面的Options API中,我们是使用computed选项来完成的: ...

最新文章

  1. python 栈实现 加减乘除_数据结构与算法(六):基于栈实现简单的四则运算
  2. 北航算法作业一 约瑟夫环问题
  3. ML/DL:关于机器学习、深度学习算法模型的选择
  4. 本人开源项目 Lu-Rpc
  5. 人名和成绩一起排序_#excel中怎么让人名和他的成绩一起排序#excle排序 怎么弄同名次...
  6. 命名空间_python基础 13 类命名空间于对象、实例的命名空间,组合方法
  7. 刀片 显卡 排行_AMD发布RadeonRX6000系列游戏显卡 4599元起
  8. Python自动化之列表
  9. php怎样连接上数据库服务器,php怎样连接数据库
  10. linux路由内核实现分析(二)---FIB相关数据结构(4)
  11. 汇编语言中OUT和IN的用法
  12. 阿特拉斯开发协议--与ATLAS 扭力控制器交互
  13. 做运营活动的几点心得
  14. pidgin安装_如何在Ubuntu中禁用Pidgin通知
  15. 模拟电子技术基础-什么是放大?
  16. java怎么做界面设计_11-Java 界面设计
  17. python求鸡兔同笼 鸡兔总数鸡兔腿_编写一个程序解鸡兔同笼问题:已知鸡兔总数为a,鸡兔腿总数为b,计算鸡兔各有多少只?...
  18. 服务器主板安装win10系统,装win10主板怎么设置_装win10主板设置教程 - 系统家园...
  19. 利用opencv识别文本
  20. php脾组词,脾组词_脾字组词_脾的组词意思及拼音

热门文章

  1. #转载:杨辉三角形实现过程详解-c语言基础
  2. 计算机网络基础学测,《计算机网络技术基础》第二1章单元学习测习题-20210622072616.docx-原创力文档...
  3. 翻译 | ORB: An efficient alternative to SIFT or SURF(ORB:对SIFT或SURF的一种有效选择)
  4. 光流 | 视频中物体检测的研究现状
  5. 我为什么鼓励工程师写blog
  6. python的原则_python 类的使用原则
  7. html js更改title,如何使用js改变HTML中title里面固定的文字
  8. 程序员法律考试笔记(2)-依法治国
  9. 【深度学习】PyTorch常用代码段合集
  10. 【Python】全网最新最全Pyecharts可视化教程(三):制作多个子图