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 = 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(一)相关推荐
- UWP Composition API - GroupListView(二)
还是先上效果图: 看完了上一篇UWP Composition API - GroupListView(一)的童鞋会问,这不是跟上一篇一样的吗??? 骗点击的?? No,No,其实相对上一个有更简单粗暴 ...
- UWP Composition API - 锁定列的FlexGrid
原文:UWP Composition API - 锁定列的FlexGrid 需求是第一列锁定,那么怎么让锁定列不跟着滚动条向做移动呢? 其实很简单,让锁定列跟scrollviewer的滚动做反方向移动 ...
- UWP Composition API - PullToRefresh
原文:UWP Composition API - PullToRefresh 背景: 之前用ScrollViewer 来做过 PullToRefresh的控件,在项目一些特殊的条件下总有一些问题,比如 ...
- UWP Composition API - RadialMenu
原文:UWP Composition API - RadialMenu 用Windows 8.1的童鞋应该知道OneNote里面有一个RadialMenu.如下图,下图是WIn10应用Drawboar ...
- WPF 使用 Composition API 做高性能渲染
在 WPF 中很多小伙伴都会遇到渲染性能的问题,虽然 WPF 的渲染可以甩浏览器渲染几条街,但是还是支持不了游戏级的渲染.在 WPF 使用的 DX 只是优化等级为 9 和 DX 9 差不多的性能,微软 ...
- vue3学习笔记 Composition API setup
一.Composition API优势 相对于vue2的option API Vue3的Composition API设计更有优势 Composition(组合式)Api 功能分组 Compositi ...
- 敲黑板!vue3重点!一文了解Composition API新特性:ref、toRef、toRefs
一文了解Composition API新特性:ref.toRef.toRefs 一.
- Vue3 Composition API(三)——生命周期钩子、Provide函数 和 Inject函数、封装Hook案例、setup顶层编写方式
一.生命周期钩子 我们前面说过 setup 可以用来替代 data . methods . computed .watch 等等这些选项,也可以替代 生命周期钩子. 那么setup中如何使用生命周期函 ...
- Vue3 Composition API(二)——computed、watchEffect、setup中使用ref
一.computed 在前面我们讲解过计算属性computed:当我们的某些属性是依赖其他状态时,我们可以使用计算属性来处理 在前面的Options API中,我们是使用computed选项来完成的: ...
最新文章
- python 栈实现 加减乘除_数据结构与算法(六):基于栈实现简单的四则运算
- 北航算法作业一 约瑟夫环问题
- ML/DL:关于机器学习、深度学习算法模型的选择
- 本人开源项目 Lu-Rpc
- 人名和成绩一起排序_#excel中怎么让人名和他的成绩一起排序#excle排序 怎么弄同名次...
- 命名空间_python基础 13 类命名空间于对象、实例的命名空间,组合方法
- 刀片 显卡 排行_AMD发布RadeonRX6000系列游戏显卡 4599元起
- Python自动化之列表
- php怎样连接上数据库服务器,php怎样连接数据库
- linux路由内核实现分析(二)---FIB相关数据结构(4)
- 汇编语言中OUT和IN的用法
- 阿特拉斯开发协议--与ATLAS 扭力控制器交互
- 做运营活动的几点心得
- pidgin安装_如何在Ubuntu中禁用Pidgin通知
- 模拟电子技术基础-什么是放大?
- java怎么做界面设计_11-Java 界面设计
- python求鸡兔同笼 鸡兔总数鸡兔腿_编写一个程序解鸡兔同笼问题:已知鸡兔总数为a,鸡兔腿总数为b,计算鸡兔各有多少只?...
- 服务器主板安装win10系统,装win10主板怎么设置_装win10主板设置教程 - 系统家园...
- 利用opencv识别文本
- php脾组词,脾组词_脾字组词_脾的组词意思及拼音
热门文章
- #转载:杨辉三角形实现过程详解-c语言基础
- 计算机网络基础学测,《计算机网络技术基础》第二1章单元学习测习题-20210622072616.docx-原创力文档...
- 翻译 | ORB: An efficient alternative to SIFT or SURF(ORB:对SIFT或SURF的一种有效选择)
- 光流 | 视频中物体检测的研究现状
- 我为什么鼓励工程师写blog
- python的原则_python 类的使用原则
- html js更改title,如何使用js改变HTML中title里面固定的文字
- 程序员法律考试笔记(2)-依法治国
- 【深度学习】PyTorch常用代码段合集
- 【Python】全网最新最全Pyecharts可视化教程(三):制作多个子图