プルして更新する

引っ張って更新を使うと、ユーザーは、タッチ操作でデータのリストを下に引っ張ることで、より多くのデータを取得できるようになります。 引っ張って更新は、タッチ スクリーン機能があるデバイスで広く使用されています。 ここで示す API を使用すると、アプリで引っ張って更新を実装できます。

引っ張って更新 gif

これは適切なコントロールですか?

ユーザーが定期的に更新したいデータのリストやグリッドがあり、そのアプリがタッチ優先のデバイス上で動作する可能性がある場合、プル・トゥ・リフレッシュ機能を利用します。

また、RefreshVisualizer を使用して、更新ボタンなどの他の方法で呼び出される一貫した更新エクスペリエンスを作成することもできます。

コントロールの更新

プルダウンして再読み込みは、2つのコントロールで有効化されます。

  • RefreshContainer - プルして更新するエクスペリエンスのラッパーを提供する ContentControl。 タッチ操作を処理し、内部更新ビジュアライザーの状態を管理します。
  • RefreshVisualizer - 次のセクションで説明する更新の視覚化をカプセル化します。

メイン コントロールは RefreshContainer であり、ユーザーが更新をトリガーするために引っ張るコンテンツのラッパーとして配置します。 RefreshContainer はタッチでのみ機能するため、タッチ インターフェイスを持たないユーザーも更新ボタンを使用できるようにすることをお勧めします。 更新ボタンは、アプリ内の適切な位置、コマンド バー、または更新する表面に近い場所に配置できます。

ビジュアライゼーションを更新

既定の更新視覚化は、更新がいつ行われるか、および更新が開始された後の更新の進行状況を伝えるために使用される循環型進行状況インジケータです。 更新ビジュアライザーには 5 つの状態があります。

更新を開始するためにユーザーがリストにプル ダウンする必要がある距離をしきい値と呼びます。 ビジュアライザーの状態は、このしきい値に関連するプル状態によって決まります。 指定できる値は、RefreshVisualizerState 列挙体に含まれています。

待機中

ビジュアライザーの既定の状態は [アイドル] です。 ユーザーがタッチを介して RefreshContainer と対話していないので、進行中の更新はありません。

視覚的には、更新ビジュアライザーの証拠はありません。

インタラクション

ユーザーが PullDirection プロパティで指定された方向にリストをプルし、しきい値に達する前に、ビジュアライザーが [対話] 状態になります。

  • ユーザーがこの状態の間にコントロールを解放すると、コントロールは [アイドル] 状態に戻ります。

    プル・トゥ・リフレッシュの事前しきい値

    視覚的には、アイコンは無効 (60% の不透明度) として表示されます。 さらに、アイコンはスクロール アクションで 1 回転します。

  • ユーザーがしきい値を超えてリストをプルすると、ビジュアライザーは [対話] から [保留中] に切り替わります。

    しきい値でのプルして更新

    視覚的には、アイコンは 100% の不透明度に切り替え、150% までのサイズでパルスを切り替え、遷移中に 100% のサイズに戻ります。

保留中

ユーザーがしきい値を超えてリストを引っ張ると、ビジュアライザーは、[保留中] 状態になります。

  • ユーザーがリストを解放せずにしきい値を超えてリストを戻すと、[対話] 状態に戻ります。
  • ユーザーがリストを解放すると、更新要求が開始され、[更新] 状態に遷移します。

プルして更新の事前しきい値

視覚的には、アイコンはサイズと不透明度の両方で 100% です。 この状態では、アイコンはスクロール アクションと共に下に移動し続けますが、回転しなくなります。

再読み込み中

ユーザーがしきい値を超えてビジュアライザーを解放すると、[更新] 状態になります。

この状態が入力されると、RefreshRequested イベントが発生します。 これは、アプリのコンテンツの更新を開始するための合図です。 イベント引数 (RefreshRequestedEventArgs) には Deferral オブジェクトが含まれています。このオブジェクトは、イベント ハンドラーでハンドルを受け取る必要があります。 その後、更新を実行するコードが完了したら、遅延を完了としてマークする必要があります。

更新が完了すると、ビジュアライザーは [アイドル] 状態に戻ります。

視覚的には、アイコンはしきい値の場所に戻り、更新中に回転します。 この回転は、更新の進行状況を示すために使用され、受信コンテンツのアニメーションに置き換えられます。

ピーク

ユーザーが更新が許可されていない開始位置から更新方向にプルすると、ビジュアライザーは [ピーク] 状態になります。 これは通常、ユーザーがプルを開始したときに ScrollViewer が 0 の位置にない場合に発生します。

  • ユーザーがこの状態の間にコントロールを解放すると、コントロールは [アイドル] 状態に戻ります。

プル方向

既定では、ユーザーはリストを上から下にプルして更新を開始します。 向きが異なるリストまたはグリッドがある場合は、更新コンテナーのプル方向を一致するように変更する必要があります。

PullDirection プロパティは、RefreshPullDirection 値であるBottomToTop、TopToBottom、RightToLeft またはLeftToRight のひとつを受け取ります。

プル方向を変更すると、ビジュアライザーの進行状況編集インジケーターの開始位置が自動的に回転し、矢印がプル方向の適切な位置から開始されます。 必要に応じて、RefreshVisualizer.Orientation プロパティを変更すると自動動作をオーバーライドできます。 ほとんどの場合、既定値の [自動] のままにしておくことをお勧めします。

プル・トゥー・リフレッシュを実装する

WinUI 3 ギャラリー アイコン WinUI 3 ギャラリー アプリには、WinUI コントロールと機能の対話型の例が含まれています。 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 を呼び出すと、ビジュアライザーの状態は [アイドル] 状態から [更新] 状態に直接戻ります。

更新要求を処理する

必要に応じた新しいコンテンツを取得するには、RefreshRequested イベントを処理します。 イベント ハンドラーでは、新しいコンテンツを取得するためにアプリに固有のコードが必要です。

イベント引数 (RefreshRequestedEventArgs) には 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 などのスクロール可能なコントロールである必要があります。コンテンツを Grid などのコントロールに設定すると、未定義の動作が発生します。

この例では、スクロール ビューアーで引っ張って更新を使用する方法を示します。

<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; }
}