提取以重新整理

「拖動以重新整理」讓使用者以觸控方式向下拖動資料清單來擷取更多資料。 「拖動以重新整理」已在有觸控式螢幕的裝置上廣泛使用。 您可以使用這裡顯示的 API,實作您的應用程式中的「拖動以重新整理」。

pull-to-refresh gif

這是正確的控制項嗎?

當您提供使用者可能需要定期重新整理的資料清單或方格,且您的應用程式可能要在以觸控為主的裝置上執行時,請使用「拖動以重新整理」。

您也可以使用 RefreshVisualizer,建立以其他方式 (例如透過 [重新整理] 按鈕) 叫用的一致重新整理體驗。

重新整理控制項

「拖動以重新整理」由 2 個控制項啟用。

  • RefreshContainer:提供「拖動以重新整理」體驗包裝函式的 ContentControl。 這會處理觸控互動,並管理其內部重新整理視覺化檢視的狀態。
  • RefreshVisualizer:封裝重新整理視覺效果 (下一節會說明這點)。

主要的控制項是 RefreshContainer,您要當做使用者所拖動觸發重新整理之內容的包裝函式來放置。 RefreshContainer 只對觸控有作用,因此建議您也應該為沒有觸控介面的使用者提供 [重新整理] 按鈕。 您可以將 [重新整理] 按鈕放置在應用程式中的適當位置,而這若不是在命令列上,就是在靠近要重新整理之介面的位置。

重新整理視覺效果

預設重新整理視覺效果是圓形進度環,用來傳達有關何時會進行重新整理,以及重新整理起始後進度的資訊。 重新整理視覺化檢視有 5 個狀態。

使用者必須向下拖動清單才能起始重新整理的距離,稱為閾值。 視覺化檢視狀態取決於拖動狀態,因為此狀態與這個閾值有關。 可能的值包含在 RefreshVisualizerState 列舉中。

閒置

視覺化檢視的預設狀態是 Idle。 使用者未透過觸控與 RefreshContainer 互動,而且沒有重新整理在進行中。

在視覺上,沒有任何重新整理視覺化檢視的跡象。

Interacting

當使用者依 PullDirection 屬性所指定的方向拖動清單時,在到達閾值之前,視覺化檢視所處在的狀態即是 Interacting

  • 如果使用者在這種狀態時放開控制項,控制項會回復到 Idle

    pull-to-refresh pre-threshold

    在視覺上,圖示會顯示為已停用 (60% 不透明度)。 此外,圖示還會隨著捲動動作旋轉完整一圈。

  • 如果使用者拖動清單超過閾值,視覺化檢視就會從 Interacting 轉換到 Pending

    pull-to-refresh at threshold

    在視覺上,圖示會切換為 100% 不透明度,而大小則在轉換期間跳上 150% 後又回到 100%。

待定

當使用者有拖動清單超過閾值時,視覺化檢視處於 Pending 狀態。

  • 如果使用者將清單移回閾值以內但不放開,則會回到 Interacting 狀態。
  • 如果使用者放開清單,則會起始重新整理要求,並轉換到 Refreshing 狀態。

pull-to-refresh post-threshold

在視覺上,圖示的大小和不透明度都是 100%。 在這種狀態下,圖示會繼續隨著捲動動作向下移動,但不再旋轉。

正在重新整理

當使用者在超過閾值後放開視覺化檢視,則會處於 Refreshing 狀態。

進入此狀態後,會引發 RefreshRequested 事件。 這是開始進行應用程式內容重新整理的訊號。 事件引數 (RefreshRequestedEventArgs) 包含 Deferral 物件,您應該在事件處理常式中取得其控制代碼。 然後在完成了執行重新整理的程式碼時,您應該將此延遲 (Deferral) 標示為已完成。

重新整理完成時,視覺化檢視會回到 Idle 狀態。

在視覺上,圖示會重新回到閾值位置,並在重新整理期間旋轉。 這個旋轉動作是用來顯示重新整理進度,並且會由傳入內容的動畫取代。

Peeking

當使用者依重新整理方向從不允許重新整理的起始位置拖動時,視覺化檢視會進入 Peeking 狀態。 當 ScrollViewer 不在使用者開始拖動時的位置 0 時,通常會發生這種情況。

  • 如果使用者在這種狀態時放開控制項,控制項會回復到 Idle

拖動方向

使用預設會由上而下拖動清單來起始重新整理。 如果您的清單或方格使用不同的方向,您應該變更重新整理容器的拖動方向來配合。

PullDirection 屬性會採用下列其中一種 RefreshPullDirection 值:BottomToTopTopToBottomRightToLeftLeftToRight

如果變更拖動方向,視覺化檢視進度環的開始位置會自動旋轉,使箭頭開始指到拖動方向的適當位置。 如有需要,您可以變更 RefreshVisualizer.Orientation 屬性來覆寫此自動行為。 在大部分情況下,我們建議您保留 Auto 的預設值。

UWP 和 WinUI 2

重要

本文中的資訊和範例針對使用 Windows App SDKWinUI 3 的應用程式進行了最佳化,但通常適用於使用 WinUI 2 的 UWP 應用程式。 如需平台特定資訊和範例,請參閱 UWP API 參考。

本節包含您在 UWP 或 WinUI 2 應用程式中使用控制項所需的資訊。

UWP 應用程式的重新整理控制項包含在 Windows UI 程式庫 2 當中。 如需詳細資訊 (包括安裝指示),請參閱 Windows UI 程式庫。 此控制項的 API 同時位於 Windows.UI.Xaml.Controls (WinUI) 和 Microsoft.UI.Xaml.Controls (WinUI) 命名空間中。

我們建議使用最新的 WinUI 2 來取得所有控制項的最新樣式、範本和功能。

若要在 WinUI 2 中使用本文中的程式碼,請在 XAML 中使用別名 (我們使用 muxc) 來表示專案中包含的 Windows UI 程式庫 API。 如需詳細資訊,請參閱開始使用 WinUI 2

xmlns:muxc="using:Microsoft.UI.Xaml.Controls"

<muxc:RefreshContainer />

實作提取到重新整理

WinUI 3 資源庫應用程式包含大多數 WinUI 3 控制項和功能的互動式範例。 從 Microsoft Store 取得應用程式,或在 GitHub 上取得原始程式碼

若要將「拖動以重新整理」功能新增至清單,只需要幾個步驟。

  1. 將您的清單包裝在 RefreshContainer 控制項中。
  2. 處理 RefreshRequested 事件以重新整理您的內容。
  3. 或者,呼叫 RequestRefresh (例如,從按鈕按一下動作中) 以起始重新整理。

注意

您可以讓 RefreshVisualizer 獨自具現化。 不過,我們建議您將內容包裝在 RefreshContainer 中,並使用 RefreshContainer.Visualizer 屬性所提供的 RefreshVisualizer,即使對非觸控案例,也是這樣。 在本文中,我們假設視覺化檢視一律是從重新整理容器中取得。

此外,為了方便,還要使用重新整理容器的 RequestRefresh 和 RefreshRequested 成員。 refreshContainer.RequestRefresh() 相當於 refreshContainer.Visualizer.RequestRefresh(),而任何一個都會引發 RefreshContainer.RefreshRequested 事件和 RefreshVisualizer.RefreshRequested 事件。

要求重新整理

重新整理容器會處理觸控互動,讓使用者透過觸控重新整理內容。 我們建議您為非觸控介面提供其他能供性,例如 [重新整理] 按鈕或語音控制。

若要起始重新整理,請呼叫 RequestRefresh 方法。

// See the Examples section for the full code.
private void RefreshButtonClick(object sender, RoutedEventArgs e)
{
    RefreshContainer.RequestRefresh();
}

當您呼叫 RequestRefresh 時,視覺化檢視狀態會直接從 Idle 進入 Refreshing

處理重新整理要求

若要在需要時取得重新整理內容,請處理 RefreshRequested 事件。 在事件處理常式中,您將需要應用程式的專屬程式碼來取得重新整理內容。

事件引數 (RefreshRequestedEventArgs) 包含 Deferral 物件。 在事件處理常式中取得此延遲 (Deferral) 的控制代碼。 然後在完成了執行重新整理的程式碼時,將此延遲 (Deferral) 標示為已完成。

// See the Examples section for the full code.
private async void RefreshContainer_RefreshRequested(RefreshContainer sender, RefreshRequestedEventArgs args)
{
    // Respond to a request by performing a refresh and using the deferral object.
    using (var RefreshCompletionDeferral = args.GetDeferral())
    {
        // Do some async operation to refresh the content

         await FetchAndInsertItemsAsync(3);

        // The 'using' statement ensures the deferral is marked as complete.
        // Otherwise, you'd call
        // RefreshCompletionDeferral.Complete();
        // RefreshCompletionDeferral.Dispose();
    }
}

回應狀態變更

您可以視需要回應視覺化檢視狀態的變更。 例如,若要防止多個重新整理要求,您可以在視覺化檢視正在重新整理時停用 [重新整理] 按鈕。

// See the Examples section for the full code.
private void Visualizer_RefreshStateChanged(RefreshVisualizer sender, RefreshStateChangedEventArgs args)
{
    // Respond to visualizer state changes.
    // Disable the refresh button if the visualizer is refreshing.
    if (args.NewState == RefreshVisualizerState.Refreshing)
    {
        RefreshButton.IsEnabled = false;
    }
    else
    {
        RefreshButton.IsEnabled = true;
    }
}

使用 RefreshContainer 中的 ScrollViewer

注意

RefreshContainer 的內容必須是可滾動的控制項,例如 ScrollViewer、GridView、ListView 等。將內容設定為網格這類控制項會導致未定義的行為。

此範例示範如何搭配捲動檢視器使用「拖動以重新整理」。

<RefreshContainer>
    <ScrollViewer VerticalScrollMode="Enabled"
                  VerticalScrollBarVisibility="Auto"
                  HorizontalScrollBarVisibility="Auto">
 
        <!-- Scrollviewer content -->

    </ScrollViewer>
</RefreshContainer>

將「拖動以重新整理」新增至 ListView

此範例示範如何搭配清單檢視使用「拖動以重新整理」。

<StackPanel Margin="0,40" Width="280">
    <CommandBar OverflowButtonVisibility="Collapsed">
        <AppBarButton x:Name="RefreshButton" Click="RefreshButtonClick"
                      Icon="Refresh" Label="Refresh"/>
        <CommandBar.Content>
            <TextBlock Text="List of items" 
                       Style="{StaticResource TitleTextBlockStyle}"
                       Margin="12,8"/>
        </CommandBar.Content>
    </CommandBar>

    <RefreshContainer x:Name="RefreshContainer">
        <ListView x:Name="ListView1" Height="400">
            <ListView.ItemTemplate>
                <DataTemplate x:DataType="local:ListItemData">
                    <Grid Height="80">
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="*" />
                        </Grid.RowDefinitions>
                        <TextBlock Text="{x:Bind Path=Header}"
                                   Style="{StaticResource SubtitleTextBlockStyle}"
                                   Grid.Row="0"/>
                        <TextBlock Text="{x:Bind Path=Date}"
                                   Style="{StaticResource CaptionTextBlockStyle}"
                                   Grid.Row="1"/>
                        <TextBlock Text="{x:Bind Path=Body}"
                                   Style="{StaticResource BodyTextBlockStyle}"
                                   Grid.Row="2"
                                   Margin="0,4,0,0" />
                    </Grid>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </RefreshContainer>
</StackPanel>
public sealed partial class MainPage : Page
{
    public ObservableCollection<ListItemData> Items { get; set; } 
        = new ObservableCollection<ListItemData>();

    public MainPage()
    {
        this.InitializeComponent();

        Loaded += MainPage_Loaded;
        ListView1.ItemsSource = Items;
    }

    private async void MainPage_Loaded(object sender, RoutedEventArgs e)
    {
        Loaded -= MainPage_Loaded;
        RefreshContainer.RefreshRequested += RefreshContainer_RefreshRequested;
        RefreshContainer.Visualizer.RefreshStateChanged += Visualizer_RefreshStateChanged;

        // Add some initial content to the list.
        await FetchAndInsertItemsAsync(2);
    }

    private void RefreshButtonClick(object sender, RoutedEventArgs e)
    {
        RefreshContainer.RequestRefresh();
    }

    private async void RefreshContainer_RefreshRequested(RefreshContainer sender, RefreshRequestedEventArgs args)
    {
        // Respond to a request by performing a refresh and using the deferral object.
        using (var RefreshCompletionDeferral = args.GetDeferral())
        {
            // Do some async operation to refresh the content

            await FetchAndInsertItemsAsync(3);

            // The 'using' statement ensures the deferral is marked as complete.
            // Otherwise, you'd call
            // RefreshCompletionDeferral.Complete();
            // RefreshCompletionDeferral.Dispose();
        }
    }

    private void Visualizer_RefreshStateChanged(RefreshVisualizer sender, RefreshStateChangedEventArgs args)
    {
        // Respond to visualizer state changes.
        // Disable the refresh button if the visualizer is refreshing.
        if (args.NewState == RefreshVisualizerState.Refreshing)
        {
            RefreshButton.IsEnabled = false;
        }
        else
        {
            RefreshButton.IsEnabled = true;
        }
    }

    // App specific code to get fresh data.
    private async Task FetchAndInsertItemsAsync(int updateCount)
    {
        for (int i = 0; i < updateCount; ++i)
        {
            // Simulate delay while we go fetch new items.
            await Task.Delay(1000);
            Items.Insert(0, GetNextItem());
        }
    }

    private ListItemData GetNextItem()
    {
        return new ListItemData()
        {
            Header = "Header " + DateTime.Now.Second.ToString(),
            Date = DateTime.Now.ToLongDateString(),
            Body = DateTime.Now.ToLongTimeString()
        };
    }
}

public class ListItemData
{
    public string Header { get; set; }
    public string Date { get; set; }
    public string Body { get; set; }
}

取得範例程式碼