kamulog

xamarin.formsのネタなど

Xamarin.Forms CarouselView を試してみる

この記事は Xamarin Advent Calendar 2019 の 5日目の記事です。

CarouselViewの公式版が試験版で実装されていたので試してみました。

docs.microsoft.com

結果

www.youtube.com

終盤のAndroidのボタンクリックの動きは想定外です笑

下準備

試験版なので AppDelegate.cs / MainActivity.cs で Forms.Initの前に以下のコードが必要です。

Xamarin.Forms.Forms.SetFlags("CarouselView_Experimental");

XAML

XAMLとしては以下な感じで使います。

<CarouselView Grid.Row="0" ItemsSource="{Binding Books}" 
        ItemSizingStrategy="MeasureFirstItem"
        HorizontalScrollBarVisibility="Never"
        PeekAreaInsets="{OnPlatform Android=150,iOS=50}"
        NumberOfSideItems="0"
        Position="{Binding Position.Value}"
        RemainingItemsThreshold="1"
        RemainingItemsThresholdReachedCommand="{Binding BindingContext.MoreCommand,Source={x:Reference me}}"
        VerticalOptions="Center" HorizontalOptions="Center" HeightRequest="350">
    <CarouselView.ItemsLayout>
        <LinearItemsLayout Orientation="Horizontal"
                            ItemSpacing="10"
                            SnapPointsType="MandatorySingle"
                            SnapPointsAlignment="Center" />
    </CarouselView.ItemsLayout>
    <CarouselView.ItemTemplate>
        <DataTemplate x:DataType="model:WebBook">
            <Frame CornerRadius="16" BorderColor="Silver" HasShadow="False" IsClippedToBounds="True">
                <Image Source="{Binding Thumbnail}" Aspect="AspectFill" Margin="-20" />
            </Frame>
        </DataTemplate>
    </CarouselView.ItemTemplate>
</CarouselView>

プロパティ

  • HorizontalScrollBarVisibility
    • スクロールバーの表示有無。Neverで非表示に。
  • PeekAreaInsets
    • 両隣でチラ見せするThickness値。これを設定することで現在のアイテムの両隣のアイテムの一部を表示させることが可能。
    • これはかなり嬉しい機能です。ただAndroidの単位が論理ピクセルじゃなくて物理ピクセルサイズな気がしてならない。
    • なのでAndroidだけ3倍の値にして誤魔化しました。
  • NumberOfSideItems
    • 隣に表示するアイテムの数。1にすると現在のアイテムの両隣に1個ずつの計3つのアイテムが並ぶことになります。
    • PeekAreaInsetsと組み合わせ可能で、組み合わせると計3つと両端に一部を表示させるといった感じになります。
  • Position
    • 現在のアイテムのIndexを参照したり設定したりします。
    • 現時点では初期位置を指定しても動かなかったり、指定位置に動かそうとしても動かなかったり微妙です。
  • RemainingItemsThreshold
    • 残り何個の段階でRemainingItemsThresholdReachedCommandまたは同様の機能のイベントを呼ぶかの閾値
    • 0だと最後が表示された段階で、1だと1つ前のアイテムが表示された段階でCommand/Eventを発火する。
  • RemainingItemsThresholdReachedCommand
    • 最後まで(最後からRemainingItemsThreshold番目まで)表示したら発火するCommand。
    • データの追加読み込みなどで使う。
  • SnapPointsType
    • スクロールした時の動き。
    • MandatorySingleで一回のスワイプで1項目分スクロールして自然にとまる感じの動きになる。
    • これが一番カルーセルっぽいと思います。
  • SnapPointsAlignment
    • スクロールが止まった時の配置
    • Centerがカルーセルとして自然な動きかと思います。

WidthRequestに関して

アイテムのWidthRequestの値はCarouselの特性上無視されるようです。 なのでアスペクト比を考慮する場合はデバイスの幅とかを取得して計算して各プロパティの値を決めていく必要があると思います。

RemainingItemsThresholdReachedCommand の注意事項

AsyncReactiveCommand などの二度押し防止機能付きなら問題ないのですが、そうでない場合は、閾値に達してからスクロールを続ける度にCommandが連続で発火してしまいます。 なので

if (isLoading)
{
    return;
}
isLoading = true;
// 読み込み処理
isLoading = false;

みたいな処理が多分必要になると思います。

ViewModel例

Prism + ReactiveProperty 使ってます。

public class MainPageViewModel:BindableBase,INavigatedAware
{
    public ObservableCollection<WebBook> Books { get; set; }
    public ReactivePropertySlim<int> Position { get; } = new ReactivePropertySlim<int>(0);
    public ReactivePropertySlim<WebBook> CurrentItem { get; } = new ReactivePropertySlim<WebBook>();
    public AsyncReactiveCommand MoreCommand { get; } = new AsyncReactiveCommand();
    public ReactiveCommand PrevCommand { get; } = new ReactiveCommand();
    public ReactiveCommand NextCommand { get; } = new ReactiveCommand();

    int _nextPage = 0;
    IWebApi _webApi;
    public MainPageViewModel(IWebApi webApi)
    {
        _webApi = webApi;

        MoreCommand.Subscribe(async _ =>
        {
            foreach(var book in await GetData())
            {
                Books.Add(book);
            }
        });

        PrevCommand.Subscribe(_ =>
        {
            if(Position.Value > 0)
            {
                Position.Value--;
            }
        });

        NextCommand.Subscribe(_ =>
        {
            if (Position.Value < Books.Count - 1)
            {
                Position.Value++;
            }
        });
    }

    public void OnNavigatedTo(INavigationParameters parameters)
    {
        _ = InitializeData();
    }

    public async Task InitializeData()
    {
        Books = new ObservableCollection<WebBook>(await GetData());
        RaisePropertyChanged(nameof(Books));
        Position.Value = 1;
    }

    public async Task<List<WebBook>> GetData()
    {
        var books = await _webApi.GetByKeyword("Xamarin", 6, _nextPage);
        if(books.Count() < 6)
        {
            _nextPage = 0;
        }
        else
        {
            _nextPage += 6;
        }

        return books.ToList();
    }
}

初期化の段階で ObservableCollection に foreach で Addしていくと、エラーになったり6件あるはずが2件しか表示されなかったりと、うまくいかなかったので、new でListを渡してインスタンスを生成するようにしてます。 MoreCommandの段階では逐次追加でも問題なかったのでforeachの方法で。

Position.Value = 1; で初期位置を2番目にしてるつもりなんですが、動きませんでした笑

GetDataでは最後まで読み込んだら0からにする処理を入れて一応無限スクロールっぽくしています。

まとめ

現時点では間違いなく試験版でした。実践投入はまだ早いかなといったところです。 特にPosition周りとスワイプの挙動が怪しいです。 ただ表示するだけなら現時点でも問題はなさそうです。 プラグイン版には無かった現在アイテム以外のチラ見せとかにも対応しているので、安定したら頼もしいコントロールになると思います。 期待して待ちましょう!

リポジトリ

github.com