スクロールに合わせてページが拡大(縮小)するPageViewです。 Exprollableはexpandableとscrollableを組み合わせた造語です。このパッケージはiOS版Apple BooksアプリのセミモーダルUIを再現しようという試みです。
このWidgetを使えばこのようなUIを簡単に作成することができます:
安定版のバージョン1.0.0がリリースされました。詳しくは マイグレーションガイドをご覧ください。
バージョン1.0.0-rc.1がリリースされました 🎉!このバージョンはいくつかの破壊的な変更を含んでいます。もしベータ版(1.0.0-beta.xx)をすでに使用中の場合、コードの修正が必要になるかもしれません。詳しくは マイグレーションガイドをご覧ください。
サンプルアプリでこのパッケージが提供する全ての機能を試すことができます。
git clone git@github.com:fujidaiti/exprollable_page_view.git
cd example
flutter pub get
flutter run
またExprollablePageView
とGoogle Maps APIを統合するサンプルもあります。詳しくは maps_example/READMEをご覧ください。
pub.dev上で公開されているので、pub
コマンドからインストールできます。
flutter pub add exprollable_page_view
特定の使用方法をお探しの場合はHow-toセクションをご覧下さい。
ExprollablePageView
は通常のPageView
と同じように使うことができます。各ページにはScrollController
を取り付けることができるスクロール可能なWidget(例えばListView
)を配置してください。ただし、必ずPageContentScrollController.of
経由で取得したScrollController
を使用してください。
import 'package:exprollable_page_view/exprollable_page_view.dart';
@override
Widget build(BuildContext context) {
return Scaffold(
body: ExprollablePageView(
itemCount: 5,
itemBuilder: (context, page) {
return ListView.builder(
// これを忘れずに
controller: PageContentScrollController.of(context),
itemBuilder: (context, index) {
return ListTile(title: Text('Item#$index'));
},
);
},
),
);
}
ExprollablePageView
のコンストラクタはPageView.builder
コンストラクタとほとんど同じシグネチャを持ちます。各パラメータの詳細はPageViewの公式ドキュメントをご覧ください。
const ExprollablePageView({
Key? key,
IndexedWidgetBuilder itemBuilder,
int? itemCount,
ExprollablePageController? controller,
bool reverse = false,
ScrollPhysics? physics,
DragStartBehavior dragStartBehavior = DragStartBehavior.start,
bool allowImplicitScrolling = false,
String? restorationId,
Clip clipBehavior = Clip.hardEdge,
ScrollBehavior? scrollBehavior,
bool padEnds = true,
void Function(PageViewportMetrics metrics)? onViewportChanged,
void Function(int page)? onPageChanged,
});
PageView
のPageController
と同様に、ExprollablePageController
はExprollablePageView
の表示ページを制御するオブジェクトです。またviewportの振る舞いを制御するためにも使用されます。
final controller = ExprollablePageController(
initialPage: 0,
viewportConfiguration: ViewportConfiguration(
minFraction: 0.9,
),
);
ViewportConfiguration
のパラメータを調整することでviewportの振る舞いを細かく決めることができます。
factory ViewportConfiguration({
bool overshootEffect = false,
double minFraction = 0.9,
double maxFraction = 1.0,
ViewportInset shrunkInset = ViewportInset.shrunk,
ViewportInset? initialInset,
List<ViewportInset> extraSnapInsets = const [],
});
minFraction
:viewportに対する各ページの最小縮小率initialInset
:viewport insetの初期値shrunkInset
: ページが完全に縮小されるときのviewport insetextraSnapInsets
: ページをスナップさせるinsetのリスト(詳しくは[ページビューをボトムシートのようにする](#make-the-page-view like-a-bottomsheet) を参照)overshootEffect
:overshoot effectが有効かどうか(詳しくはオーバーシュート効果セクションを参照)
viewportの状態はfractionとinsetの2つによって表されます。fractionは各ページのサイズがviewportに対してどのくらいの割合であるかを示し、またinsetはviewportの上部から現在のページの上部までの距離を表します。これらの状態はViewport
クラスが管理しており、ExprollablePageController
を通じて参照することができます。詳しくはviewportの状態を監視するをご覧ください。
ViewportInset
はinset値を表現するためのクラスです。事前定義されたinsetは3種類あり、それぞれ定数として定義されています。
ViewportInset.expanded
: ページが完全に拡大されるデフォルトのinsetViewportInset.shrunk
: ページが完全に縮小されるデフォルトのinsetViewportInset.overshoot
: overshoot effectを有効にする際に使用されるinset
ユーザー定義のinsetはViewportInset.fixed
やViewportInset.fractional
から作成できます。またViewportInset
を継承したクラスを作成することでより複雑な計算を実行することができます。これらのクラスの使用例はmake the PageView like a BottomSheet, observe the state of the viewportで見ることができます。
overshoot effectはページが拡大される際の視覚効果です。これが有効になっている場合、ページがフルスクリーンになる時、ページの上部がviewportの上部を少しだけはみ出すような挙動をとります。より正確に言えば、fractionが1になる時insetが負の値をとるということです。これにより図のようなダイナミックな視覚効果が得られます(左:overshoot effectあり、真ん中:なし)。Apple BooksアプリのUIでもこのような挙動が実現されています(右図)。
ViewportConfiguration
はviewportの動作をカスタマイズするための柔軟な方法を提供します。ほとんどのケースではViewportConfiguration
の無名コンストラクタで十分だと思いますが、より細かい制御が必要な場合は、ViewportConfiguration.raw
を使用してください。viewport fractionとinsetの範囲、またページが完全に拡大(縮小)する位置を指定することができます。以下のコードはページを4つの状態にスナップさせるExprollablePageController
の例です。
- Collapsed: ページがほとんど隠れた状態
- Shrunk: bottom sheetのような状態
- Expanded: Shrunkと似ているが、ページが拡大されている状態
- Fullscreen: 全画面状態
const fullscreenInset = ViewportInset.fixed(0);
const expandedInset = ViewportInset.fractional(0.2);
const shrunkInset = ViewportInset.fractional(0.5);
const collapsedkInset = ViewportInset.fractional(0.9);
final controller = ExprollablePageController(
viewportConfiguration: ViewportConfiguration.raw(
minInset: fullscreenInset,
expandedInset: expandedInset,
shrunkInset: shrunkInset,
maxInset: collapsedInset,
initialInset: collapsedInset,
snapInsets: [
fullscreenInset,
expandedInset,
shrunkInset,
collapsedInset,
],
),
);
![](https://private-user-images.githubusercontent.com/68946713/238885677-1e848ad0-5111-4d5a-8663-ee0b07d813c6.gif?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MjU5NzMzNDEsIm5iZiI6MTcyNTk3MzA0MSwicGF0aCI6Ii82ODk0NjcxMy8yMzg4ODU2NzctMWU4NDhhZDAtNTExMS00ZDVhLTg2NjMtZWUwYjA3ZDgxM2M2LmdpZj9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDA5MTAlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQwOTEwVDEyNTcyMVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTkwYTRlMjU1MTBhMzFmODU2MWNhYmQ1OThkN2YxOTQ2YWExM2Q0N2I5NWRkYjUxN2U3MTFmMDNjMDA5MjQ0MjAmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JmFjdG9yX2lkPTAma2V5X2lkPTAmcmVwb19pZD0wIn0.Nehs3L-H0EIS_ldA4LiyIwjaURij_Ax4_f6Zkl4nKUM)
ModalExprollable
を使用すれば、半透明の背景と「下にスワイプして閉じる」アクションを持つモーダルダイアログ形式のExprollablePageView
を簡単に作成することができます。showModalExprollable
関数は、ExprollablePageView
をModalExprollable
でラップし、ダイアログとして表示する便利な方法です。ダイアログの表示/非表示時の動作をカスタマイズする場合は、独自のPageRoute
を作成しModalExprollable
その中でModalExprollable
を使用してください。
showModalExprollable(
context,
builder: (context) {
return ExprollablePageView(...);
},
);
より詳しい例は modal_dialog_example.dart をご覧ください。
![](https://private-user-images.githubusercontent.com/68946713/241362148-ec375a0d-0844-481c-a8ae-47cb5bb24b19.gif?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MjU5NzMzNDEsIm5iZiI6MTcyNTk3MzA0MSwicGF0aCI6Ii82ODk0NjcxMy8yNDEzNjIxNDgtZWMzNzVhMGQtMDg0NC00ODFjLWE4YWUtNDdjYjViYjI0YjE5LmdpZj9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDA5MTAlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQwOTEwVDEyNTcyMVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWFmNjZlZjdlNGVkZDY1NjA2NGMxODk3YjZlZDQ4NzczMzQwOGZhODdjZTdhYjBiOTNmM2VlYmUxMTMyZDYzZjcmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JmFjdG9yX2lkPTAma2V5X2lkPTAmcmVwb19pZD0wIn0.P0drnv_A23vzAupo3QFe-eaFW1rs8DOJf-iZw2IA0Ao)
組み込みのPageView
に対するExprollablePageView
の利点の一つは、flutter_slidableなどの水平方向のスライドアクションを持つウィジットをページ内で使用できることです。より詳しい例はexample/lib/src/complex_example/album_details.dart
で見ることができます。
ExprollablePageController.currentPage
から取得できます。
final page = pageController.currentPage.value;
currentPage
はValueListenable<int>
型なので、リスナーを作成して変更を検知することもできます。
pageController.currentPage.addListener(() {
final page = pageController.currentPage.value;
});
別の方法として、ExprollablePageView.onPageChanged
コールバックを指定することでページの変更を監視することができます。これは内部的に上記のコードと等価なことをしています。
ExprollablePageView(
onPageChanged: (page) { ... },
);
ExprollablePageController
とViewportConfiguration
を使用してください。以下は、3つの状態にスナップするためのコントローラの例である:
- ページが完全に展開されている (
Viewport.fraction == 1.0
) - ページがビューポートより少し小さい (
Viewport.fraction == 0.9
) -
Viewport.fraction == 0.9
で、ページビューがボトムシートのように画面の半分だけをカバーする。
コードの詳細は、custom_snap_offsets_example.dartを参照してください。
controller = ExprollablePageController(
viewportConfiguration: ViewportConfiguration(
minFraction: 0.9,
extraSnapInsets: [
ViewportInset.fractional(0.5),
],
),
);
viewportの状態を監視する方法は3つあります。
ExprollablePageController.viewport
は ValueListenable<ViewportMetrics>
で、 ViewportMetrics
にはビューポートの現在の状態が含まれます。したがって、ビューポートの状態に依存するアクションを実行するために、ビューポートをリッスンして使用することができます。
controller.viewport.addListener(() {
final ViewportMetrics vp = controller.viewport.value;
final bool isShrunk = vp.isPageShrunk;
final bool isExpanded = vp.isPageExpanded;
});
ExprollablePageView
は、状態が変化するたびに ViewportUpdateNotification
をディスパッチし、その中に ViewportMetrics
が含まれます。この通知は NotificationListener
ウィジェットを使って聞くことができます。ウィジェットツリーで NotificationListener
が ExprollablePageView
の祖先であることを確認してください。このメソッドは、状態依存のアクションを実行したいが、コントローラがない場合に便利です。
NotificationListener<ViewportUpdateNotification>(
onNotification: (notification) {
final ViewportMetrics vp = notification.metrics;
return false;
},
child: ...,
ExprollablePageView
のコンストラクタは、ビューポートの状態が変化するたびに呼び出されるコールバックを受け取ります。
ExprollablePageView(
onPageViewChanged: (ViewportMetrics vp) {...},
);
各ページをPageGutter
で囲ってください。
ExprollablePageView(
itemBuilder: (context, page) {
return PageGutter(
gutterWidth: 12,
child: ListView(...),
);
},
);
AdaptivePagePadding
を使用します。このウィジェットは、現在のビューポートのオフセットに従って、子ウィジェットに適切なパディングを追加します。コードの例は adaptive_padding_example.dart にあります。
Container(
color: Colors.lightBlue,
child: AdaptivePagePadding(
child: SizedBox(
height: height,
child: const Placeholder(),
),
),
);
ExprollablePageController.animateViewportInsetTo
を使用します。以下のコードは1秒かけてスクロールアニメーションと共にページを縮小する例です。
// Shrunk the current page with scroll animation in 1 second.
controller.animateViewportInsetTo(
ViewportInset.shrunk,
curve: Curves.easeInOutSine,
duration: Duration(seconds: 1),
);
より具体的な例はanimation_example.dartで見ることができます。
![](https://private-user-images.githubusercontent.com/68946713/239451208-63b2a875-3b54-4031-9817-a808bce2b3f8.gif?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MjU5NzMzNDEsIm5iZiI6MTcyNTk3MzA0MSwicGF0aCI6Ii82ODk0NjcxMy8yMzk0NTEyMDgtNjNiMmE4NzUtM2I1NC00MDMxLTk4MTctYTgwOGJjZTJiM2Y4LmdpZj9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDA5MTAlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQwOTEwVDEyNTcyMVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTdhNTM4MWM2OWMxOGVkYzBjY2YyN2FjOTI2MjZiZGFkZjNmM2IxMmM1MmJlYTlmNzIzMDFhNWRlY2M1N2I4OTQmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JmFjdG9yX2lkPTAma2V5X2lkPTAmcmVwb19pZD0wIn0.tZ_M8gshrQ3w3HnnHrbsNRs8MAbRChGSHWREL4OS_JI)
バージョン1.0.0以前では、overshoot effectは以下の条件を満たした場合のみ正しく機能しました。
MediaQuery.padding.bottom
> 0NavigationBar
やBottomAppBar
のようなwidgetがExprollablePageView
の下部を覆い隠している
バージョン1.0.0からは上記の制限がなくなり、Scaffold.bottomNavigationBar
やScaffold.extendBody
の有無に関わらずovershoot effectを有効にすることができるようになりました。
controller = ExprollablePageController(
viewportConfiguration: ViewportConfiguration(
overshootEffect: true,
),
);
Widget build(BuildContext context) {
return Scaffold(
// 以下の2行はバージョン1.0.0以降では不要になりました
// extendBody: true,
// bottomNavigationBar: BottomNavigationBar(...),
body: ExprollablePageView(
controller: controller,
itemBuilder: (context, page) { ... },
),
);
}
1.0.0-rc.1のリリースに伴い、いくつかの破壊的変更があります。
PageViewportMetrics
mixinは ViewportMetrics
mixinに統合され、削除されました。またそれに伴い一部のプロパティ名も変更されています。以下に従ってコード内のシンボル名を置き換えてください:
PageViewportMetrics
👉ViewportMetrics
PageViewportMetrics.isShrunk
👉ViewportMetrics.isPageShrunk
PageViewportMetrics.isExpanded
👉ViewportMetrics.isPageExpanded
PageViewportMetrics.xxxOffset
👉ViewportMetrics.xxxInset
(接尾辞Offset
はInset
で置換)
またPageViewportMetrics.overshootEffect
は削除されました。
ViewportController
クラスは PageViewport
に名前が変更されました。またPageViewport
はViewportMetrics
を実装していません(削除されたため)。
For ViewportOffset
and its inherited classes, the suffix Offset
was replaced with the new suffix Inset
, and 2 new inherited classes were introduced (see Viewport fraction and inset).
-
ViewportOffset
👉ViewportInset
-
ExpandedViewportOffset
👉DefaultExpandedViewportinset
-
ShrunkViewportOffset
👉DefaultShrunkViewportInset
ViewportConfiguration
の導入に伴い、ExprollablePageController
のコンストラクタのシグネチャも変更されました。
Before:
final controller = ExprollablePageController(
initialPage: 0,
minViewportFraction: 0.9,
overshootEffect: true,
initialViewportOffset: ViewportOffset.shrunk,
);
After:
final controller = ExprollablePageController(
initialPage: 0,
viewportConfiguration: ViewportConfiguration(
minFraction: 0.9,
overshootEffect: true,
initialInset: ViewportInset.shrunk,
),
);
加えてExprollablePageController.withAdditionalSnapOffsets
も削除されたため、代わりにViewportConfiguration.extraSnapInsets
を使用してください。詳しくはExprollablePageControllerをご覧ください。
Before:
final controller = ExprollablePageController.withAdditionalSnapOffsets([
ViewportOffset.fractional(0.5),
]);
After:
final controller = ExprollablePageController(
viewportConfiguration: ViewportConfiguration(
extraSnapOffset: [ViewportInset.fractional(0.5)],
),
);
StaticPageViewportMetrics
👉StaticViewportMetrics
PageViewportUpdateNotification
👉ViewportUpdateNotification
PageViewport
👉Viewport