対象者
- Flutterでパララックス効果をしたい人
はじめに
Flutterでパララックス効果を使ってみたいと思っていました。
公式のサンプルを見ました。難しい。ちょー長い。諦めました。Flowとか頑張りたくない。
日本語でわかりやすそうな記事がありましたが、FlutterHooksを使っている。FlutterHooks、使わない。そのせいか、理解ができない。ただ、なんとなく実装の方向性は分かったので、参考にしてsetState版を作りました。
パララックス効果とは
背景と前景の速度や方向が異なることによって、奥行きのある視覚的効果を生み出すことです。例えば、移動する車の窓から外の景色を見ると、車の速度に応じて景色が流れるように見えます。
この効果は、Webデザインやモバイルアプリ、ビデオゲームなどのデジタルコンテンツにおいてよく使用されています。パララックス効果を使うことで、ユーザーはよりインタラクティブな体験をすることができ、より深い視覚的なインパクトを受けることができます。
前提条件
公式のパララックスのサンプルの画像を使います。これは、ほぼ正方形の画像です(一つ目を確認すると、1200×1224でした)。これを16:9の横長の窓から見て、画像の表示する場所をスクロールと一緒にずらしていきます。
説明
どのように画像をずらすのか
Image.networkの引数alignmentでずらす方法を定義してます。設定すると、以下のように画像が移動されます。
- -1であれば画像の一番上が窓の一番上
- 1であれば画像の一番下が窓の一番下
Image.network(
key: _keyList[index],
urls[index],
fit: BoxFit.fitWidth,
alignment: Alignment(0, gap),
)
画像の画面上の位置を取得するため、keyでGlobalKeyを割り当ててます。
横長に表示するため、BoxFit.fitWidthを使ってます。
alignmentでgapを渡して、窓の中で動的に画像がずれるようにしてます。
ずらす距離を計算する
画像の画面のY座標を取得し、画面の高さで割って、画像が画面の一番上なら1,一番下なら-1になるように以下のように調整します。
final box = _keyList[index].currentContext?.findRenderObject();
final gap = box is RenderBox
? 2 * box.localToGlobal(Offset.zero).dy / screenHeight - 1
: 0.0;
画面を更新する
スクロールしたら、画面が更新するように NotificationListener
NotificationListener<ScrollNotification>(
onNotification: (notification) {
setState(() {});
return false;
},
まとめ
以上でパララックス効果の説明をしました。割とシンプルに収まったので、別のところでも十分流用できるソースになっているかと思います。公式のサンプルは本気すぎて、流用できそうもない、、、
参考
-
Create a scrolling parallax effect
Flutter公式のパララックス効果の実装方法。パララックス以前にFlowの理解が大変。 -
【Flutter】5分でParallax Animationを実装する👅【Flutter Hooks】
Flutter Hooksを使えば、5分で実装できるのだろうか。alignmentでずれを設定する箇所がヒントになりました!
全ソース
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
),
home: const MyHomePage(title: 'Flutter Parallax Demo Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final _keyList = List.generate(urls.length, (index) => GlobalKey());
@override
Widget build(BuildContext context) {
final screenHeight = MediaQuery.of(context).size.height;
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: NotificationListener<ScrollNotification>(
onNotification: (notification) {
setState(() {});
return false;
},
child: ListView.builder(
itemCount: urls.length,
itemBuilder: (context, index) {
final box = _keyList[index].currentContext?.findRenderObject();
if (box is RenderBox) {
print('Image $index: ${box.localToGlobal(Offset.zero).dy}');
}
final gap = box is RenderBox
? 2 * box.localToGlobal(Offset.zero).dy / screenHeight - 1
: 0.0;
return Padding(
padding: const EdgeInsets.all(16.0),
child: AspectRatio(
aspectRatio: 16 / 9,
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Image.network(
key: _keyList[index],
urls[index],
fit: BoxFit.fitWidth,
alignment: Alignment(0, gap),
),
),
),
);
}),
),
),
);
}
}
const urlPrefix =
'https://docs.flutter.dev/cookbook/img-files/effects/parallax';
final urls = [
'$urlPrefix/01-mount-rushmore.jpg',
'$urlPrefix/02-singapore.jpg',
'$urlPrefix/03-machu-picchu.jpg',
'$urlPrefix/04-vitznau.jpg',
'$urlPrefix/05-bali.jpg',
'$urlPrefix/06-mexico-city.jpg',
'$urlPrefix/07-cairo.jpg',
];