Jetpack Composeで画像をズームする(5) ライブラリの紹介
これまで4回にわたって、Jetpack Composeで画像をズームする方法を説明してきましたが、ソースコードの量もそれなりに多くなりました。(その1・その2・その3・その4)
そこで、少ないコードで画像ズームを実現できるように、これまでに紹介した処理をさらにブラッシュアップして、「Zoomable」というライブラリとして公開しています。今回はそのライブラリの使い方の紹介です。
Zoomable
Zoomableは、Jetpack Composeで画像などのコンテンツのズームを簡単に実現するための小さなライブラリです。ピンチジェスチャーと、ダブルタップとドラッグを組み合わせた1本指ジェスチャーの2つに対応しています。

ピンチジェスチャー

1本指ジェスチャー
一番簡単な実装は、以下のようにModifier.zoomable()を1行追加するだけです。
Image( painter = painterResource(id = R.drawable.penguin), contentDescription = null, modifier = Modifier.zoomable(rememberZoomState()),)Zoomableは、Imageはもちろん、Textなどいろいろなコンポーザブルコンポーネントに使えます。CoilのAsyncImageなど、非同期で画像を読み込むコンポーネントでも使えます。
また、AccompanistのHorizontalPagerとVerticalPagerや、Compose 1.4で追加されたAndroidxのHorizontalPagerとVerticalPagerと組み合わせて使うこともできます。
準備
ZoomableはMaven Centralからダウンロードできます。
settings.gradleにmavenCentral()が書かれていることを確認します。(Android Studioを使って通常の手順でプロジェクトを作成した場合は、初めから含まれていると思います。)
pluginManagement { repositories { mavenCentral() }}アプリのbuild.gradleに依存関係を追加します。
dependencies { implementation "net.engawapg.lib:zoomable:$version"}最新バージョンは下記を参照してください。
https://github.com/usuiat/Zoomable/releases
使い方
Zoomableは、コンポーネントをズーム可能にするModifier.zoomable()関数と、ズーム状態を管理するZoomStateを提供しています。ZoomStateはrememberZoomable()関数で取得します。
もっともシンプルな実装だと、下記のようにModifier.zoomable()を1行追加するだけでOKです。
Image( painter = painterResource(id = R.drawable.penguin), contentDescription = null, modifier = Modifier.zoomable(rememberZoomState()))
画面にフィットさせる
先ほどの例だと、ズームしてドラッグした時に画像が画面外まで移動してしまいます。これを防ぐには、contentSizeに画像のサイズを指定します。
val painter = painterResource(id = R.drawable.penguin)val zoomState = rememberZoomState(contentSize = painter.intrinsicSize)Image( painter = painter, contentDescription = null, modifier = Modifier.zoomable(zoomState))
これで、画像の端がImageコンポーネントの境界より内側に移動しないようになります。
非同期読み込みの画像で使う
Coilなどの非同期画像読み込みライブラリと組み合わせて使う場合は、画像読み込み完了時にsetContentSize()で画像のサイズを設定することができます。CoilのAsyncImageの場合は、onSuccessのAsyncImagePainter.State.Successから画像サイズを取得できます。
val zoomState = rememberZoomState()AsyncImage( model = "https://github.com/usuiat.png", contentDescription = null, onSuccess = { state -> zoomState.setContentSize(state.painter.intrinsicSize) }, modifier = Modifier.zoomable(zoomState))
Pagerレイアウトと組み合わせて使う
AccompanistのHorizontalPagerやVerticalPagerと組み合わせて使うこともできます。Compose 1.4で追加された本家JetpackのHorizontalPagerとVerticalPagerでも使えます。ここでは、いずれスタンダードになるであろう本家JetpackのHorizontalPagerと組み合わせて使う方法を紹介します。
val resources = listOf(R.drawable.penguin, R.drawable.bird)val pagerState = rememberPagerState()HorizontalPager( pageCount = resources.size, state = pagerState,) { page -> val painter = painterResource(id = resources[page]) val zoomState = rememberZoomState(contentSize = painter.intrinsicSize) Image( painter = painter, contentDescription = null, modifier = Modifier .fillMaxSize() .zoomable(zoomState) ) val isVisible = page == pagerState.settledPage LaunchedEffect(isVisible) { if (!isVisible) { zoomState.reset() } }}
Modifier.zoomable()を指定する部分は、Image単独で使う場合と変わりません。Zoomableは、画像がズームされていない場合や、ズームされた画像が端までスクロールしている場合に、Pagerにスクロールイベントを伝えます。これによって、ズームされた画像のスクロールと、Pagerのページ送りを両立させています。
サンプルコードの後半は、ページが画面外に移動した時にズーム状態をリセットする処理を記述しています。PagerState.settledPageを使ってページが画面内に見えているかどうかを判定し、画面外に移動した時にZoomState.reset()を呼び出しています。settledPageはAccompanistのPagerには存在しなかったプロパティですが、「PagerのcurrentPage, settledPage, targetPageの挙動」で紹介していますので、詳しく知りたい方はご覧ください。
AccompanistのPagerを使ったサンプルは、GitHubのZoomableのサンプルにありますので、そちらをご覧ください。
1本指ジェスチャーについて
Zoomableは、2本の指を使ったピンチジェスチャーのほかに、ダブルタップに続けて縦方向にドラッグして拡大・縮小する1本指ジェスチャーにも対応しています。1本指ジェスチャーはデフォルトで有効になっていますが、無効にしたい場合はzoomable()のenableOneFingerZoomにfalseを指定してください。

Image( painter = painterResource(id = R.drawable.penguin), contentDescription = null, modifier = Modifier.zoomable( zoomState = rememberZoomState(), // enableOneFingerZoom = false, ))