Share via


引っ張って更新

引っ張って更新を使うと、タッチ操作でデータの一覧を引き下げることで、より多くのデータを取得できます。 引っ張って更新は、タッチ スクリーンを備えたデバイスで広く使用されます。 ここに表示されている API を使用して、アプリに引っ張って更新を実装できます。

引っ張って更新 gif

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

ユーザーが定期的に更新するデータのリストやグリッドがあり、アプリがタッチ操作主体のデバイスで実行されることが多いときは、引っ張って更新を使います。

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

更新コントロール

引っ張って更新は、2 つのコントロールで有効になっています。

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

メインのコントロールは、ユーザーが更新をトリガするためにプルするコンテンツ全体のラッパーとして配置する RefreshContainer です。 RefreshContainer はタッチでのみ機能するため、タッチ インターフェイスを持たないユーザーが使用できる更新ボタンを設定することをお勧めします。 更新ボタンは、コマンド バーまたは更新されるサーフェスに近い場所など、アプリの適切な場所に配置できます。

更新の視覚エフェクト

既定の更新の視覚エフェクトは、更新が発生したときに通信するために使用される循環進行スピンと、更新が開始された後の更新の進行状況です。 更新ビジュアライザーには、5 つの状態があります。

ユーザーが更新を開始するために一覧でプル ダウンする必要がある距離をしきい値といいます。 ビジュアライザー状態 は、このしきい値に関連するプル状態によって決まります。 使用可能な値は、RefreshVisualizerState 列挙に含まれています。

アイドル

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

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

操作中

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

  • この状態でユーザーがコントロールを離すと、コントロールはアイドルに戻ります。

    引っ張って更新の事前しきい値

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

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

    しきい値にある引っ張って更新

    視覚的に、アイコンが 100% の不透明度に切り替わり、切り替えの過程でサイズは最大 150% になり、その後 100% に戻ります。

Pending

ユーザーがしきい値を超えてリストをプルした場合、ビジュアライザーは保留中状態になります。

  • ユーザーがリストを解放せずにしきい値を超えて戻すと、操作中状態に戻ります。
  • ユーザーがリストを解放すると、更新要求が開始され、更新中状態に切り替わります。

引っ張って更新の事後しきい値

図で表すと、アイコンはサイズと不透明度のどちらも 100% になります。 この状態では、アイコンはスクロール操作で下に移動し続けますが、回転することはありません。

更新しています

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

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

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

図で表すと、アイコンはしきい値の位置に戻り、更新の期間中回転します。 この回転は、更新の進行状況を表示するために使用され、受信したコンテンツのアニメーションによって置き換えられます。

ピーク

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

  • この状態でユーザーがコントロールを離すと、コントロールはアイドルに戻ります。

プルの方向

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

PullDirection プロパティでは、これらの RefreshPullDirection 値のいずれかを取得します: BottomToTopTopToBottomRightToLeft、または LeftToRight

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

UWP と WinUI 2

重要

この記事の情報と例は、Windows アプリ SDKWinUI 3 を使用するアプリ向けに最適化されていますが、一般に WinUI 2 を使用する UWP アプリに適用されます。 プラットフォーム固有の情報と例については、UWP API リファレンスを参照してください。

このセクションには、UWP または WinUI 2 アプリでコントロールを使用するために必要な情報が含まれています。

UWP アプリの更新コントロールは、Windows UI ライブラリ 2 の一部として含まれています。 インストール手順などについて詳しくは、「Windows UI Library (Windows UI ライブラリ)」をご覧ください。 このコントロールの API は、 Windows.UI.Xaml.Controls (UWP) 名前空間と 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 を呼び出すと、ビジュアライザーの状態は直接アイドル状態から更新中に切り替わります。

更新要求の処理

必要に応じて新しいコンテンツを取得するには、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; }
}

サンプル コードの入手