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
                        {
                            //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 };
            }
        }
    }
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 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);
                }
            }
        }
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 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 元素


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM