【Win10】實現 ListViewBase 平滑滾動

首先解釋下標題的 ListViewBase 是什麼鬼。ListViewBase 咱們能夠查閱 MSDN 文檔:https://msdn.microsoft.com/zh-cn/library/windows.ui.xaml.controls.listviewbase.aspx 得知,ListViewBase 是 ListView 和 GridView 的基類(ListView 和 GridView 則爲經常使用的數據展現控件之一)。而本文的主要目的就是實現 ListView 和 GridView 的平滑滾動,所以我將標題寫成「實現 ListViewBase 平滑滾動」而不是「實現 ListView 和 GridView 平滑滾動」(實際上本文適用於任何繼承自 ListViewBase 的控件)。windows

 

首先咱們先複習一下怎麼滾動到 ListViewBase 的某一個 item。異步

在 ListViewBase 類中,有一個方法叫作 ScrollIntoView。這個方法有兩個重載,咱們看複雜一點,有兩個參數的這個:動畫

//
// 摘要:
//     滾動列表,以將指定數據項移入具備指定對齊方式的視圖中。
//
// 參數:
//   item:
//     要在視圖中顯示的數據項。
//
//   alignment:
//     指定項是使用 Default 仍是 Leading 對齊方式的枚舉值。
[Overload("ScrollIntoViewWithAlignment")]
 public void ScrollIntoView(System.Object item, ScrollIntoViewAlignment alignment);

第一個參數就是咱們須要滾動到當前可視區域的 item,而第二個參數,Default 是指讓其滾動到當前可視區域便可,Leading 則是指讓其滾動到當前可視區域的頂部。ui

 

可是比較遺憾的是,這個方法一執行(?)立馬滾動到目標 item 了,徹底不帶一丁點動畫效果(後文你會了解到內部執行仍需不多一段時間,儘管咱們肉眼察覺不到)。在這個時代,沒有一個好的 UI,怎麼能吸引用戶呢?所以咱們就來研究並實現怎樣能讓 ListViewBase 平滑滾動到某個 item。this

 

提及滾動的話,咱們必定會想到 ScrollBar、ScrollViewer 這類的控件的。而幸運的是,ScrollViewer 有一個方法,叫 ChangeView 是帶動畫效果的(也能夠選擇不使用動畫效果)。而且 ListView、GridView 內部都是有一個 ScrollViewer 的。那麼咱們天然而然就想到,是否是能夠操做 ListViewBase 內部的這個 ScrollViewer 來實現平滑滾動。spa

 

先開始編寫代碼吧:code

public static class ListViewBaseExtensions
{
    public static void ScrollIntoViewSmoothly(this ListViewBase listViewBase, object item, ScrollIntoViewAlignment alignment)
    {
        if (listViewBase == null)
        {
            throw new ArgumentNullException(nameof(listViewBase));
        }

        // GetFirstDescendantOfType 是 WinRTXamlToolkit 中的擴展方法,
        // 尋找該控件在可視樹上第一個符合類型的子元素。
        ScrollViewer scrollViewer = listViewBase.GetFirstDescendantOfType<ScrollViewer>();

        // 因爲 ScrollViewer 確定有,所以不作 null 檢查判斷了。

        scrollViewer.ChangeView(targetHorizontalOffset, targetVerticalOffset, null);
    }
}

然而問題來了,targetHorizontalOffset 和 targetVerticalOffset 咱們是不知道的,也就是說,咱們不知道目標 item 所在的位置。blog

儘管咱們不知道,可是,ListViewBase 自身的 ScrollIntoView 方法它是知道的,那咱們乾脆就讓它當個跑腿,先執行一次,而後就能夠獲取目標位置了。繼承

public static class ListViewBaseExtensions
{
    public static void ScrollIntoViewSmoothly(this ListViewBase listViewBase, object item, ScrollIntoViewAlignment alignment)
    {
        if (listViewBase == null)
        {
            throw new ArgumentNullException(nameof(listViewBase));
        }

        // GetFirstDescendantOfType 是 WinRTXamlToolkit 中的擴展方法,
        // 尋找該控件在可視樹上第一個符合類型的子元素。
        ScrollViewer scrollViewer = listViewBase.GetFirstDescendantOfType<ScrollViewer>();

        // 因爲 ScrollViewer 確定有,所以不作 null 檢查判斷了。

        // 記錄初始位置,用於 ScrollIntoView 檢測目標位置後復原。
        double originHorizontalOffset = scrollViewer.HorizontalOffset;
        double originVerticalOffset = scrollViewer.VerticalOffset;

        // 跑腿。
        listViewBase.ScrollIntoView(item, alignment);

        // 獲取目標位置。
        double targetHorizontalOffset = scrollViewer.HorizontalOffset;
        double targetVerticalOffset = scrollViewer.VerticalOffset;

        // scrollViewer.ChangeView(targetHorizontalOffset, targetVerticalOffset, null);
    }
}

然而經過斷點檢查後,發現 targetHorizontalOffset 和 targetVerticalOffset 並無發生變化。可是執行事後,ListViewBase 確實發生了滾動,所以咱們質疑,是否是 ScrollIntoView 方法在控件內部是以一個異步的形式執行。事件

這個時候,咱們仍是想起近乎萬能的 LayoutUpdated 事件吧。改寫下代碼。

public static class ListViewBaseExtensions
{
    public static void ScrollIntoViewSmoothly(this ListViewBase listViewBase, object item, ScrollIntoViewAlignment alignment)
    {
        if (listViewBase == null)
        {
            throw new ArgumentNullException(nameof(listViewBase));
        }

        // GetFirstDescendantOfType 是 WinRTXamlToolkit 中的擴展方法,
        // 尋找該控件在可視樹上第一個符合類型的子元素。
        ScrollViewer scrollViewer = listViewBase.GetFirstDescendantOfType<ScrollViewer>();

        // 因爲 ScrollViewer 確定有,所以不作 null 檢查判斷了。

        // 記錄初始位置,用於 ScrollIntoView 檢測目標位置後復原。
        double originHorizontalOffset = scrollViewer.HorizontalOffset;
        double originVerticalOffset = scrollViewer.VerticalOffset;

        EventHandler<object> layoutUpdatedHandler = null;
        layoutUpdatedHandler = delegate
        {
            listViewBase.LayoutUpdated -= layoutUpdatedHandler;

            // 獲取目標位置。
            double targetHorizontalOffset = scrollViewer.HorizontalOffset;
            double targetVerticalOffset = scrollViewer.VerticalOffset;

            // scrollViewer.ChangeView(targetHorizontalOffset, targetVerticalOffset, null);
        };
        listViewBase.LayoutUpdated += layoutUpdatedHandler;

        // 跑腿。
        listViewBase.ScrollIntoView(item, alignment);
    }
}

此次咱們再斷點後,發現可以獲取目標位置了!!(因此我上面說「內部執行仍需不多一段時間,儘管咱們肉眼察覺不到」)

 

接下來,因爲跑腿是已經滾動目標位置了,所以咱們須要復原到原來的位置,再滾動到目標位置以實現平滑滾動的動畫效果。

public static void ScrollIntoViewSmoothly(this ListViewBase listViewBase, object item, ScrollIntoViewAlignment alignment)
    {
        if (listViewBase == null)
        {
            throw new ArgumentNullException(nameof(listViewBase));
        }

        // GetFirstDescendantOfType 是 WinRTXamlToolkit 中的擴展方法,
        // 尋找該控件在可視樹上第一個符合類型的子元素。
        ScrollViewer scrollViewer = listViewBase.GetFirstDescendantOfType<ScrollViewer>();

        // 因爲 ScrollViewer 確定有,所以不作 null 檢查判斷了。

        // 記錄初始位置,用於 ScrollIntoView 檢測目標位置後復原。
        double originHorizontalOffset = scrollViewer.HorizontalOffset;
        double originVerticalOffset = scrollViewer.VerticalOffset;

        EventHandler<object> layoutUpdatedHandler = null;
        layoutUpdatedHandler = delegate
        {
            listViewBase.LayoutUpdated -= layoutUpdatedHandler;

            // 獲取目標位置。
            double targetHorizontalOffset = scrollViewer.HorizontalOffset;
            double targetVerticalOffset = scrollViewer.VerticalOffset;

            // 復原位置,且不須要使用動畫效果。
            scrollViewer.ChangeView(originHorizontalOffset, originVerticalOffset, null, true);

            // 最終目的,帶平滑滾動效果滾動到 item。
            scrollViewer.ChangeView(targetHorizontalOffset, targetVerticalOffset, null);
        };
        listViewBase.LayoutUpdated += layoutUpdatedHandler;

        // 跑腿。
        listViewBase.ScrollIntoView(item, alignment);
    }
}

執行以後,然而咱們發現仍是直接滾動到目標,不帶一丁點動畫效果哭泣的臉。可是,有了上面 ScrollIntoView 的經驗後,咱們天然而然也能夠質疑 ChangeView 方法是否是像 ScrollIntoView 同樣,內部也是異步執行的。再改寫下:

public static class ListViewBaseExtensions
{
    public static void ScrollIntoViewSmoothly(this ListViewBase listViewBase, object item, ScrollIntoViewAlignment alignment)
    {
        if (listViewBase == null)
        {
            throw new ArgumentNullException(nameof(listViewBase));
        }

        // GetFirstDescendantOfType 是 WinRTXamlToolkit 中的擴展方法,
        // 尋找該控件在可視樹上第一個符合類型的子元素。
        ScrollViewer scrollViewer = listViewBase.GetFirstDescendantOfType<ScrollViewer>();

        // 因爲 ScrollViewer 確定有,所以不作 null 檢查判斷了。

        // 記錄初始位置,用於 ScrollIntoView 檢測目標位置後復原。
        double originHorizontalOffset = scrollViewer.HorizontalOffset;
        double originVerticalOffset = scrollViewer.VerticalOffset;

        EventHandler<object> layoutUpdatedHandler = null;
        layoutUpdatedHandler = delegate
        {
            listViewBase.LayoutUpdated -= layoutUpdatedHandler;

        // 獲取目標位置。
        double targetHorizontalOffset = scrollViewer.HorizontalOffset;
            double targetVerticalOffset = scrollViewer.VerticalOffset;

            EventHandler<ScrollViewerViewChangedEventArgs> scrollHandler = null;
            scrollHandler = delegate
            {
                scrollViewer.ViewChanged -= scrollHandler;

            // 最終目的,帶平滑滾動效果滾動到 item。
            scrollViewer.ChangeView(targetHorizontalOffset, targetVerticalOffset, null);
            };
            scrollViewer.ViewChanged += scrollHandler;

        // 復原位置,且不須要使用動畫效果。
        scrollViewer.ChangeView(originHorizontalOffset, originVerticalOffset, null, true);

        };
        listViewBase.LayoutUpdated += layoutUpdatedHandler;

        // 跑腿。
        listViewBase.ScrollIntoView(item, alignment);
    }
}

此次咱們終於成功了!!!

 

效果:

ListViewBaseScrollIntoViewSmoothly

最後咱們像 ListViewBase 的 ScrollIntoView 方法,加多個只有一個參數的重載吧。

最終代碼:

public static class ListViewBaseExtensions
{
    public static void ScrollIntoViewSmoothly(this ListViewBase listViewBase, object item)
    {
        ScrollIntoViewSmoothly(listViewBase, item, ScrollIntoViewAlignment.Default);
    }

    public static void ScrollIntoViewSmoothly(this ListViewBase listViewBase, object item, ScrollIntoViewAlignment alignment)
    {
        if (listViewBase == null)
        {
            throw new ArgumentNullException(nameof(listViewBase));
        }

        // GetFirstDescendantOfType 是 WinRTXamlToolkit 中的擴展方法,
        // 尋找該控件在可視樹上第一個符合類型的子元素。
        ScrollViewer scrollViewer = listViewBase.GetFirstDescendantOfType<ScrollViewer>();

        // 因爲 ScrollViewer 確定有,所以不作 null 檢查判斷了。

        // 記錄初始位置,用於 ScrollIntoView 檢測目標位置後復原。
        double originHorizontalOffset = scrollViewer.HorizontalOffset;
        double originVerticalOffset = scrollViewer.VerticalOffset;

        EventHandler<object> layoutUpdatedHandler = null;
        layoutUpdatedHandler = delegate
        {
            listViewBase.LayoutUpdated -= layoutUpdatedHandler;

            // 獲取目標位置。
            double targetHorizontalOffset = scrollViewer.HorizontalOffset;
            double targetVerticalOffset = scrollViewer.VerticalOffset;

            EventHandler<ScrollViewerViewChangedEventArgs> scrollHandler = null;
            scrollHandler = delegate
            {
                scrollViewer.ViewChanged -= scrollHandler;

                // 最終目的,帶平滑滾動效果滾動到 item。
                scrollViewer.ChangeView(targetHorizontalOffset, targetVerticalOffset, null);
            };
            scrollViewer.ViewChanged += scrollHandler;

            // 復原位置,且不須要使用動畫效果。
            scrollViewer.ChangeView(originHorizontalOffset, originVerticalOffset, null, true);

        };
        listViewBase.LayoutUpdated += layoutUpdatedHandler;

        // 跑腿。
        listViewBase.ScrollIntoView(item, alignment);
    }
}

最後再附送上 Demo:ListViewBaseScrollSmoothlyDemo.zip


最後的最後,冰天雪地裸體 360 度跪求一份 UWP/WP8.1 相關的工做QQ圖片20150925222632。(長期有效)

相關文章
相關標籤/搜索