UWP下控件开发07 - 虚拟化与瀑布流

本篇文章的代码示例均为 C++/CX,很容易转换为 C#。

在上一部分( UWP下控件开发06 - 完善虚拟化方案 )中我们实现了一个能够直接调用两个方法就能实例化与虚拟化子项,并且能够对子项容器进行回收利用的基类面板 VirtualizingPanel。

本部分将会着重描述将虚拟化过程与测量布局过程紧密结合,完成我们瀑布流面板的创建。

瀑布流面板模型

瀑布流说起来并不陌生,在之前的文章 UWP下控件开发02 - 进阶瀑布流布局 中我们实现了一个不带虚拟化的瀑布流面板,在面板中加入虚拟化最难的莫非在于如何将虚拟化抽象于面板的布局。

虚拟化的难题

通过简单的思考,我们不难想到下面这些关于虚拟化面板的难题:

01.由于我们面板中的实际项目会少于 ItemSource 中提供的项目,那么布局中我们如何能够快速的确定哪些项目需要被实例化?

02.由于容器是被反复利用,在列表前端被虚拟化后的容器再被实例化后会加入 Children 的最后,布局与测量也不再是直接遍历 Children,所以可视化数中的子项容器的布局顺序又是如何确定的?

03.由于列表的滚动是频繁触发的,我们在面板中求可视化项目的时候能否有更高效的方法?

04.当子项不仅仅是被添加在末尾,更是添加到了列表中间的时候,亦或是子项被移出面板,面板又该如何处理?

05.当整个面板被 Resize 的时候,该怎么办,被虚拟化的项目该如何 Resize?

WaterfallFlowLayout 抽象的布局过程

为了解决以上问题,我们决定,将布局过程在面板中抽象出来,用一个类来专门负责瀑布流的布局,那么瀑布流需要哪些元素呢?

using namespace Windows::Foundation;

namespace Marduk  
{
    namespace Controls
    {
        ref class WaterfallFlowLayout sealed
        {
        public:
            WaterfallFlowLayout(double spacing, double width);

            void AddItem(Platform::Object^ item, double height);
            WaterfallFlowWindow GetVisableItemsIndex(WaterfallFlowWindow window, int* firstIndex, int* lastIndex);
            Rect GetLayoutRect(int index);
            bool FillWindow(WaterfallFlowWindow window, Platform::Object^ item, double height);
        private:
        };
    }
}

为了简单起见,我们这次的面板就只做纵向布局,并且只有两列的瀑布流。

对于瀑布流列表来说,面板的宽度是需要知道的,而且子项之间的间距也需要知道,这些都是布局的必备值,所以在构造函数里面我们需要传入这两个参数。

AddItem 方法能够在布局中加入一个新的项目,由于容器是不固定的,我们采用原始的子项对象与测量的高度,来加入到布局中。

GetVisableItemsIndex 方法能够获取当前在可是窗口中的第一个可视项与最后一个可视化项的索引,并返回实际的可视化窗口大小。

GetLayoutRect 能够返回某个子项的布局大小与位置,交给面板进行布局。

FillWindow 能够加入一个项目,并返回一个窗口是否已经被子项填满,这个方法主要用于判断可视窗口是否填满,主要发生在滚动条在最后,此时可视窗口并未填满,需要继续实例化对象才行,亦或是立即新加入的子项。

WaterfallFlowWindow 窗口结构

WaterfallFlowWindow 是一个窗口的结构,这个结构将滚动行为抽象为一个滑动窗口在一根长棍上滑动,记录了窗口在长棍上的偏移,与窗口的长度。

namespace Marduk  
{
    namespace Controls
    {
        value class WaterfallFlowWindow sealed
        {
        public:
            double Offset = 0;
            double Length = 0;


        internal:
            WaterfallFlowWindow(double offset, double length);
            RegisterReadOnlyProperty(double, Offset + Length, EndOffset);
            bool Contain(WaterfallFlowWindow window);
            bool GetIntersection(WaterfallFlowWindow window, WaterfallFlowWindow* intersectionWindow);
            bool GetUnion(WaterfallFlowWindow window, WaterfallFlowWindow* unionWindow);
            bool IsEmpty();
        };
    }
}

由于值类型是不能有公共字段之外的公共成员,所以需要用到的其他成员都是内部的。

Offset 字段记录了窗口的偏移。

Length 字段记录了窗口的长度。

EndOffset 属性是窗口的长度加偏移,也就是窗口结束的偏移。

Contain 方法用于判断,当前窗口与一个窗口是包含关系。

GetIntersection 方法用于求两个窗口的交集。

GetUnion 方法用于求两个窗口的并集。

IsEmpty 方法判断窗口是否为空。

WaterfallFlowUnit 布局单元

WaterfallFlowLayout 的需要记录每个子项的长度,并为其定位,如果全部由 WaterfallFlowLayout 来完成,WaterfallFlowLayout 里面的东西就会很复杂,所以我们抽象了一个 WaterfallFlowUnit 来表示每个需要布局的子项,里面记录了布局所需要的所有东西。

using namespace Windows::Foundation;

namespace Marduk  
{
    namespace Controls
    {
        ref class WaterfallFlowUnit sealed
        {
        public:
            WaterfallFlowUnit(Platform::Object^ item, Size desiredSize);

            RegisterProperty(Platform::Object^, _item, Item);
            RegisterProperty(int, _stackIndex, StackIndex);
            RegisterProperty(double, _offset, Offset);
            RegisterProperty(Size, _desiredSize, DesiredSize);

        private:
            Platform::Object^ _item;
            int _stackIndex = -1;
            double _offset = -1;
            Size _desiredSize = Size::Empty;
        };
    }
}

Item 属性是该布局单元关联的子项。

StackIndex 是该布局单元所处的瀑布流栈的索引,用于之后拓展多列的瀑布了。

Offset 是该布局单元在面板中的索引。

DesiredSize 是该布局单元的大小。

WaterfallFlowLayout 完成布局过程

需要的对象都抽象出来了,接下来就是完成布局的过程了。为了布局的需要我们又添加了一些必要的字段,下面是 WaterfallFlowLayout.h 的完整代码。

#pragma once
#include "WaterfallFlowWindow.h"
#include "WaterfallFlowUnit.h"

using namespace Windows::Foundation;

namespace Marduk  
{
    namespace Controls
    {
        ref class WaterfallFlowLayout sealed
        {
        public:
            RegisterReadOnlyProperty(double, _spacing, Spacing);
            RegisterReadOnlyProperty(double, _width, Width);
            RegisterReadOnlyProperty(int, _units->size(), UnitCount);
            RegisterReadOnlyProperty(double, max(_leftStack, _rightStack), LayoutLength);

            WaterfallFlowLayout(double spacing, double width);

            void AddItem(Platform::Object^ item, double height);
            WaterfallFlowWindow GetVisableItemsIndex(WaterfallFlowWindow window, int* firstIndex, int* lastIndex);
            Rect GetLayoutRect(int index);
            bool FillWindow(WaterfallFlowWindow window, Platform::Object^ item, double height);
        private:
            ~WaterfallFlowLayout();
            std::vector<WaterfallFlowUnit^>* _units;
            double _spacing;
            double _width;
            double _leftStack = 0;
            double _rightStack = 0;
        };
    }
}

WaterfallFlowLayout 具体实现

构造与析构函数,构造里面初始化需要的字段,析构里面释放原生的 C++ 对象。

WaterfallFlowLayout::WaterfallFlowLayout(double spacing, double width)  
{
    _spacing = spacing;
    _width = width;
    _units = new std::vector<WaterfallFlowUnit^>();
}

WaterfallFlowLayout::~WaterfallFlowLayout()  
{
    delete(_units);
}

添加子项,通过对比两个布局栈的长度,挑出最短的布局栈,加入新元素,这就是瀑布流的中心思想。

void WaterfallFlowLayout::AddItem(Platform::Object^ item, double heigth)  
{
    Size size = Size((Width - Spacing) / 2, heigth);
    auto unit = ref new WaterfallFlowUnit(item, size);
    if (_leftStack <= _rightStack)
    {
        unit->StackIndex = 0;

        if (_leftStack == 0)
        {
            unit->Offset = _leftStack;
            _leftStack += size.Height;
        }
        else
        {
            unit->Offset = _leftStack + Spacing;
            _leftStack += size.Height + Spacing;
        }
    }
    else
    {
        unit->StackIndex = 1;

        if (_rightStack == 0)
        {
            unit->Offset = _rightStack;
            _rightStack += size.Height;
        }
        else
        {
            unit->Offset = _rightStack + Spacing;
            _rightStack += size.Height + Spacing;
        }
    }
    _units->push_back(unit);
}

获取可视窗口中的可视化项目索引,在这里我做了一些优化,如果传进来的 firstIndex 与 lastIndex 是负数,将会遍历来计算,但是如果传入之前可视化的项目的索引,就可以直接根据上一次的 firstIndex 与 lastIndex 来推测这次的,可以避免遍历整个列表。

WaterfallFlowWindow WaterfallFlowLayout::GetVisableItemsIndex(WaterfallFlowWindow window, int* firstIndex, int* lastIndex)  
{
    WaterfallFlowWindow result = WaterfallFlowWindow();

    if (*firstIndex < 0)
    {
        for (int i = 0; i < _units->size(); i++)
        {
            if (_units->at(i)->Offset >= window.Offset)
            {
                *firstIndex = i - 1;
                break;
            }
        }
    }
    else
    {
        if (_units->at(0)->Offset + _units->at(0)->DesiredSize.Height > window.Offset)
        {
            *firstIndex = 0;
        }
        else
        {
            if (_units->at(*firstIndex)->Offset > window.Offset)
            {
                for (int i = *firstIndex; i >= 0; i--)
                {
                    if (_units->at(i)->Offset + _units->at(i)->DesiredSize.Height < window.Offset)
                    {
                        *firstIndex = i + 1;
                        break;
                    }
                }
            }
            else
            {
                for (int i = *firstIndex; i < _units->size(); i++)
                {
                    if (_units->at(i)->Offset + _units->at(i)->DesiredSize.Height >= window.Offset)
                    {
                        *firstIndex = i;
                        break;
                    }
                }
            }
        }
    }

    if (*firstIndex < 0)
    {
        *firstIndex = 0;
    }

    if (*lastIndex < 0)
    {
        for (int i = *firstIndex; i < _units->size(); i++)
        {
            if (_units->at(i)->Offset >= window.EndOffset)
            {
                *lastIndex = i - 1;
                break;
            }
        }
    }
    else
    {
        if (_units->at(_units->size() - 1)->Offset < window.EndOffset)
        {
            *lastIndex = _units->size() - 1;
        }
        else
        {
            if (_units->at(*lastIndex)->Offset > window.EndOffset)
            {
                for (int i = *lastIndex; i >= 0; i--)
                {
                    if (_units->at(i)->Offset < window.EndOffset)
                    {
                        *lastIndex = i;
                        break;
                    }
                }
            }
            else
            {
                for (int i = *lastIndex; i < _units->size(); i++)
                {
                    if (_units->at(i)->Offset >= window.EndOffset)
                    {
                        *lastIndex = i - 1;
                        break;
                    }
                }
            }
        }
    }

    if (*lastIndex < 0)
    {
        *lastIndex = _units->size() - 1;
    }

    result.Offset = _units->at(*firstIndex)->Offset;
    result.Length = _units->at(*lastIndex)->Offset + _units->at(*lastIndex)->DesiredSize.Height - result.Offset;

    return result;
}

获取布局的大小和位置,这些信息都储存在单元里面,所以我们可以很快的计算出来。

Rect WaterfallFlowLayout::GetLayoutRect(int index)  
{
    Rect result = Rect();
    double unitWidth = (Width - Spacing) / 2;

    auto unit = _units->at(index);

    result.Height = unit->DesiredSize.Height;
    result.Width = unit->DesiredSize.Width;
    result.X = unit->StackIndex * (unitWidth + Spacing);
    result.Y = unit->Offset;

    return result;
}

瀑布流面板的声明

接下来就可以声明一个瀑布流面板,考虑我们需要重写的成员,与需要的信息。

#pragma once
#include "VirtualizingPanel.h"
#include "WaterfallFlowWindow.h"
#include "WaterfallFlowLayout.h"

using namespace Windows::Foundation;  
using namespace Windows::UI::Xaml;

namespace Marduk  
{
    namespace Controls
    {
        // Waterfall flow layout panel without resizeable virtualization
        public ref class WaterfallFlowPanel sealed :
            public VirtualizingPanel
        {
            RegisterDependencyProperty(double, _spacingProperty, SpacingProperty, Spacing);

        public:
            WaterfallFlowPanel();

        protected:
            virtual void RegisterDependencyProperties() override;
            virtual Size MeasureOverride(Size availableSize) override;
            virtual Size ArrangeOverride(Size finalSize) override;
            virtual void OnItemsChanged(IObservableVector<Platform::Object^>^ source,IVectorChangedEventArgs^ e) override;

        private:
            WinCon::ScrollViewer^ _parentScrollView;
            int _viewIndex = -1;
            int _firstRealizationItemIndex = -1;
            int _lastRealizationItemIndex = -1;

            WaterfallFlowWindow _requestWindow = WaterfallFlowWindow(0,0);
            WaterfallFlowLayout^ _layout;

            void OnViewChanging(Platform::Object^ sender, WinCon::ScrollViewerViewChangingEventArgs ^ e);

            static void OnSpacingChangedStatic(DependencyObject^ sender, Windows::UI::Xaml::DependencyPropertyChangedEventArgs^ e);
        };
    }
}

Spacing 依赖属性是子项间的间距。 RegisterDependencyProperties 重写该方法来注册新的依赖属性。

MeasureOverride 是重新测量过程。

ArrangeOverride 是重新布局过程。

OnItemsChanged 是子项发生改变的处理逻辑。

_parentScrollView 是包含面板的 ScrollViewer,监听其滑动事件来更新面板内容。

_viewIndex 是 ScrollViewer 的位移记录,该字段的用途在后面将会说明。

_firstRealizationItemIndex 是第一个可视项的索引。

_lastRealizationItemIndex 是最后一个可视项的索引。

_requestWindow 是需要的可视窗口。

_layout 完成布局的类。

瀑布流面板的具体实现

MeasureOverride 重写测量过程

测量过程会稍微复杂一些:

01.首先是一些字段的初始化,第一是 _parentScrollView,然后是 _layout。

02.然后计算要求的可视窗口大小,我在这里的处理是 3 倍的 ScrollViewer 可视高度,上下都留一个可视高度用于缓冲。

03.然后先填充该窗口。

04.获取可视项目的索引,与当前的索引做比较,直接虚拟化不需要的项目,与实例化需要呈现的项目。

05.更新可视项目的所以,并返回布局的高度。

Size WaterfallFlowPanel::MeasureOverride(Size availableSize)  
{

    if (_parentScrollView == nullptr)
    {
        _parentScrollView = dynamic_cast<WinCon::ScrollViewer^>(this->Parent);
    }

    if (_parentScrollView == nullptr)
    {
        return Size(availableSize.Width, 0);
    }
    else
    {
        _parentScrollView->ViewChanging += ref new Windows::Foundation::EventHandler<WinCon::ScrollViewerViewChangingEventArgs ^>(this, &WaterfallFlowPanel::OnViewChanging);
    }

    if (_parentScrollView->ViewportHeight == 0)
    {
        return Size(availableSize.Width, 0);
    }


    if (Items->Size == 0)
    {
        return Size(availableSize.Width, 0);
    }

    double spacing = Spacing;
    double unitSize = (availableSize.Width - spacing) / 2;


    if (_layout == nullptr)
    {
        _layout = ref new WaterfallFlowLayout(spacing, availableSize.Width);
    }

    _requestWindow = WaterfallFlowWindow(max(_parentScrollView->VerticalOffset - _parentScrollView->ViewportHeight, 0), _parentScrollView->ViewportHeight * 3);
    Size itemAvailableSize = Size((availableSize.Width - spacing) / 2, availableSize.Height);

    for (int i = _layout->UnitCount; i < Items->Size; i++)
    {
        if (_layout->FillWindow(_requestWindow, nullptr, 0))
        {
            break;
        }
        auto container = RealizeItem(Items->GetAt(i));
        container->Measure(itemAvailableSize);
        _layout->AddItem(Items->GetAt(i), container->DesiredSize.Height);
    }

    int requestFirstRealizationItemIndex = _firstRealizationItemIndex;
    int requestLastRealizationItemIndex = _lastRealizationItemIndex;

    _layout->GetVisableItemsIndex(_requestWindow, &requestFirstRealizationItemIndex, &requestLastRealizationItemIndex);

    if (_firstRealizationItemIndex < 0 || _lastRealizationItemIndex < 0)
    {
        for (int i = requestFirstRealizationItemIndex; i < requestLastRealizationItemIndex; i++)
        {
            auto container = RealizeItem(Items->GetAt(i));
            container->Measure(itemAvailableSize);
        }
    }
    else
    {
        if (requestFirstRealizationItemIndex > _firstRealizationItemIndex)
        {
            for (int i = _firstRealizationItemIndex; i < requestFirstRealizationItemIndex; i++)
            {
                RecycleItem(Items->GetAt(i));
            }
        }
        else
        {
            for (int i = requestFirstRealizationItemIndex; i < _firstRealizationItemIndex; i++)
            {
                auto container = RealizeItem(Items->GetAt(i));
                container->Measure(itemAvailableSize);
            }
        }

        if (requestLastRealizationItemIndex > _lastRealizationItemIndex)
        {
            for (int i = _lastRealizationItemIndex; i <= requestLastRealizationItemIndex; i++)
            {
                auto container = RealizeItem(Items->GetAt(i));
                container->Measure(itemAvailableSize);
            }
        }
        else
        {
            for (int i = requestLastRealizationItemIndex + 1; i <= _lastRealizationItemIndex; i++)
            {
                RecycleItem(Items->GetAt(i));
            }
        }
    }
    _firstRealizationItemIndex = requestFirstRealizationItemIndex;
    _lastRealizationItemIndex = requestLastRealizationItemIndex;

    return Size(availableSize.Width, _layout->LayoutLength);
}

ArrangeOverride 重写布局过程

这个过程就很简单了,因为我们在 WaterfallFlowLayout 中可以直接过去指定项目的位置与大小,直接获取所有的实例化的项目,然后按照 WaterfallFlowLayout 的 GetLayoutRect 返回值来布局它即可。

Size WaterfallFlowPanel::ArrangeOverride(Size finalSize)  
{
    if (_layout == nullptr)
    {
        return finalSize;
    }

    for (int i = _firstRealizationItemIndex; i <= _lastRealizationItemIndex; i++)
    {
        auto rect = _layout->GetLayoutRect(i);
        auto container = GetContainerFormIndex(i);
        container->Arrange(rect);
    }
    return finalSize;
}

OnViewChanging ScrollViewer 滚动的逻辑

在这里我做了一个优化,使用 viewIndex 来表示当前的位置,以半个 ScrollViewer 可视高度为一个阙值,如果滚动的范围小于这个,我们就不回去更新面板,这样能避免在滚动的时候频繁测量与布局,以优化面板的性能。

void WaterfallFlowPanel::OnViewChanging(Platform::Object^ sender, WinCon::ScrollViewerViewChangingEventArgs ^ e)  
{
    int viewIndex = floor(e->NextView->VerticalOffset / (_parentScrollView->ViewportHeight / 2)) + 1;
    if (viewIndex != _viewIndex)
    {
        _viewIndex = viewIndex;
        InvalidateMeasure();
        InvalidateArrange();
    }
}

OnItemsChanged 子项列表变更的处理

由于这次我们简单起见,所以没有实现子项插入,删除的操作,仅实现了在尾部加入元素的操作。在尾部加入元素是,如果当前的可视窗口没有被填满就会重新测量与布局。

void WaterfallFlowPanel::OnItemsChanged(IObservableVector<Platform::Object^>^ source, IVectorChangedEventArgs^ e)  
{
    if (_layout == nullptr)
    {
        InvalidateMeasure();
        InvalidateArrange();
        return;
    }

    switch (e->CollectionChange)
    {
    case CollectionChange::Reset:
        InvalidateMeasure();
        InvalidateArrange();
        break;
    case CollectionChange::ItemInserted:
        if (e->Index != Items->Size - 1)
        {
            throw Exception::CreateException(-1, "Unexpected collection operation.");
            break;
        }

        if (_layout->FillWindow(_requestWindow, nullptr, 0))
        {
            break;
        }

        InvalidateMeasure();
        InvalidateArrange();
        break;
    case CollectionChange::ItemRemoved:
        throw Exception::CreateException(-1, "Unexpected collection operation.");
        break;
    case CollectionChange::ItemChanged:
        throw Exception::CreateException(-1, "Unexpected collection operation.");
        break;

    default:
        throw Exception::CreateException(-1, "Unexpected collection operation.");
        break;
    }
}

完整的 WaterfallFlowPanel.cpp 文件

下面是完整的 WaterfallFlowPanel.cpp 文件

#include "pch.h"
#include "WaterfallFlowPanel.h"

using namespace Marduk::Controls;

DependencyProperty^ WaterfallFlowPanel::_spacingProperty = nullptr;

WaterfallFlowPanel::WaterfallFlowPanel()  
{
    RegisterDependencyProperties();
}

void WaterfallFlowPanel::RegisterDependencyProperties()  
{
    if (_spacingProperty == nullptr)
    {
        _spacingProperty = DependencyProperty::Register(
            nameof(Spacing),
            typeof(double),
            typeof(VirtualizingPanel),
            ref new PropertyMetadata(5.0,
                ref new PropertyChangedCallback(
                    &WaterfallFlowPanel::OnSpacingChangedStatic)));
    }
    VirtualizingPanel::RegisterDependencyProperties();
}

Size WaterfallFlowPanel::MeasureOverride(Size availableSize)  
{

    if (_parentScrollView == nullptr)
    {
        _parentScrollView = dynamic_cast<WinCon::ScrollViewer^>(this->Parent);
    }

    if (_parentScrollView == nullptr)
    {
        return Size(availableSize.Width, 0);
    }
    else
    {
        _parentScrollView->ViewChanging += ref new Windows::Foundation::EventHandler<WinCon::ScrollViewerViewChangingEventArgs ^>(this, &WaterfallFlowPanel::OnViewChanging);
    }

    if (_parentScrollView->ViewportHeight == 0)
    {
        return Size(availableSize.Width, 0);
    }


    if (Items->Size == 0)
    {
        return Size(availableSize.Width, 0);
    }

    double spacing = Spacing;
    double unitSize = (availableSize.Width - spacing) / 2;


    if (_layout == nullptr)
    {
        _layout = ref new WaterfallFlowLayout(spacing, availableSize.Width);
    }

    //if (_leftWindow.IsEmpty() || _rightWindow.IsEmpty())
    //{
    //    RecycleAllItem();
    //    _parentScrollView->ChangeView(0.0, 0.0, 1.0f, true);
    //    _viewIndex = floor(_parentScrollView->VerticalOffset / (_parentScrollView->ViewportHeight / 2)) + 1;
    //}
    _requestWindow = WaterfallFlowWindow(max(_parentScrollView->VerticalOffset - _parentScrollView->ViewportHeight, 0), _parentScrollView->ViewportHeight * 3);
    Size itemAvailableSize = Size((availableSize.Width - spacing) / 2, availableSize.Height);

    for (int i = _layout->UnitCount; i < Items->Size; i++)
    {
        if (_layout->FillWindow(_requestWindow, nullptr, 0))
        {
            break;
        }
        auto container = RealizeItem(Items->GetAt(i));
        container->Measure(itemAvailableSize);
        _layout->AddItem(Items->GetAt(i), container->DesiredSize.Height);
    }

    int requestFirstRealizationItemIndex = _firstRealizationItemIndex;
    int requestLastRealizationItemIndex = _lastRealizationItemIndex;

    _layout->GetVisableItemsIndex(_requestWindow, &requestFirstRealizationItemIndex, &requestLastRealizationItemIndex);

    if (_firstRealizationItemIndex < 0 || _lastRealizationItemIndex < 0)
    {
        for (int i = requestFirstRealizationItemIndex; i < requestLastRealizationItemIndex; i++)
        {
            auto container = RealizeItem(Items->GetAt(i));
            container->Measure(itemAvailableSize);
        }
    }
    else
    {
        if (requestFirstRealizationItemIndex > _firstRealizationItemIndex)
        {
            for (int i = _firstRealizationItemIndex; i < requestFirstRealizationItemIndex; i++)
            {
                RecycleItem(Items->GetAt(i));
            }
        }
        else
        {
            for (int i = requestFirstRealizationItemIndex; i < _firstRealizationItemIndex; i++)
            {
                auto container = RealizeItem(Items->GetAt(i));
                container->Measure(itemAvailableSize);
            }
        }

        if (requestLastRealizationItemIndex > _lastRealizationItemIndex)
        {
            for (int i = _lastRealizationItemIndex; i <= requestLastRealizationItemIndex; i++)
            {
                auto container = RealizeItem(Items->GetAt(i));
                container->Measure(itemAvailableSize);
            }
        }
        else
        {
            for (int i = requestLastRealizationItemIndex + 1; i <= _lastRealizationItemIndex; i++)
            {
                RecycleItem(Items->GetAt(i));
            }
        }
    }
    _firstRealizationItemIndex = requestFirstRealizationItemIndex;
    _lastRealizationItemIndex = requestLastRealizationItemIndex;

    return Size(availableSize.Width, _layout->LayoutLength);
}



Size WaterfallFlowPanel::ArrangeOverride(Size finalSize)  
{
    if (_layout == nullptr)
    {
        return finalSize;
    }

    for (int i = _firstRealizationItemIndex; i <= _lastRealizationItemIndex; i++)
    {
        auto rect = _layout->GetLayoutRect(i);
        auto container = GetContainerFormIndex(i);
        container->Arrange(rect);
    }
    return finalSize;
}

void WaterfallFlowPanel::OnViewChanging(Platform::Object^ sender, WinCon::ScrollViewerViewChangingEventArgs ^ e)  
{
    int viewIndex = floor(e->NextView->VerticalOffset / (_parentScrollView->ViewportHeight / 2)) + 1;
    if (viewIndex != _viewIndex)
    {
        _viewIndex = viewIndex;
        InvalidateMeasure();
        InvalidateArrange();
    }
}

void WaterfallFlowPanel::OnItemsChanged(IObservableVector<Platform::Object^>^ source, IVectorChangedEventArgs^ e)  
{
    if (_layout == nullptr)
    {
        InvalidateMeasure();
        InvalidateArrange();
        return;
    }

    switch (e->CollectionChange)
    {
    case CollectionChange::Reset:
        InvalidateMeasure();
        InvalidateArrange();
        break;
    case CollectionChange::ItemInserted:
        if (e->Index != Items->Size - 1)
        {
            throw Exception::CreateException(-1, "Unexpected collection operation.");
            break;
        }

        if (_layout->FillWindow(_requestWindow, nullptr, 0))
        {
            break;
        }

        InvalidateMeasure();
        InvalidateArrange();
        break;
    case CollectionChange::ItemRemoved:
        throw Exception::CreateException(-1, "Unexpected collection operation.");
        break;
    case CollectionChange::ItemChanged:
        throw Exception::CreateException(-1, "Unexpected collection operation.");
        break;

    default:
        throw Exception::CreateException(-1, "Unexpected collection operation.");
        break;
    }
}

void WaterfallFlowPanel::OnSpacingChangedStatic(DependencyObject^ sender, Windows::UI::Xaml::DependencyPropertyChangedEventArgs^ e)  
{

}

其他的思考

到这里我们就完成了能够进行虚拟化的瀑布流面板的创建。

但是面板并没有完美,我们还需要能够插入和删除项目,呈现相关的属性变更,改变横向与纵向布局,Resize 等一系列复杂的操作,这些动作在虚拟化中会打破我们所用的布局缓存,导致完全重新布局或部分重新布局。

如何在完全重新布局与部分重新布局中,充分的优化面板,让其性能不会太过于糟糕呢?

这些操作在虚拟化中又是一段需要详细讲述的内容,那么下一个部分我们就会一一实现这些功能。敬请期待!

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章