43.【AppleMusicクローン】セクション2 のレイアウトをListView を使って実装する
今回はセクション2の部分のレイアウトを作っていきます。
前回の記事はこちらになります。
enum でいえば、 Relax
に当たる箇所です。
enum Section { Top, Relax, ActivityMood, ShortMovie, Daily, Update, Attention, New, Now, Others, Favorite, Must, BestInterview, ComingSoon }
デザインでいえば
この部分になります。
特に手元にある iPhone から Apple Music を触ってみてもそんな特殊なレイアウトではないようなので、
ListView の Axis.horizontal
を使えば実現できますね。
ListView の使い方で不安な方は過去記事に簡単な使い方を説明していますので復習してみてください。
設計について
今回のアプリは API を使わずに静的なレイアウトで組みます。
ヘッダー部分とコンテンツ部分にレイアウトを分けられそうなので分けました。
- second_sectoin_header_item (ヘッダー部分)
- second_sectoin_item (コンテンツ部分)
使用する画像リソースのインポート
今回は仮置きとして画像ファイルを使っています。
当然ですがこちらは仮で画像を当てはめているだけです。 実務で静的なデータをこんな感じでプロジェクトに置いていませんのでご注意ください。
これらの画像を assets/images/
のディレクトリに配置させます。
画像をインポートした後は pubspec.yaml
ファイルに画像を使うための宣言を行います。
pubspec.yaml
flutter: # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class. uses-material-design: true # To add assets to your application, add an assets section, like this: assets: - assets/images/image1.png - assets/images/image2.png - assets/images/image3.png
これでこれらの画像をソースコードで使えるようになりました。
ソースコードについて
それでは実際のソースコードを書いていきます。 まずは今回新しく作成した2つのファイルについてです。
second_section_header_item.dart
import 'package:flutter/material.dart'; class SecondSectionHeaderItem extends StatelessWidget { @override Widget build(BuildContext context) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[ Text( 'くつろぎのひと時を', style: TextStyle( fontSize: 17, fontWeight: FontWeight.bold), ), FlatButton( onPressed: () {}, child: Text( 'すべて見る', style: TextStyle(fontSize: 10, color: Colors.pink), ), ) ], ); } }
Row
ウィジェットを使って横並びしました。
乗せているウィジェットはそれっぽいデザインを実現させているだけなので設計としてはいいのかは分かりません汗。
次にコンテンツ部分のコードです。
second_section_item.dart
import 'package:flutter/material.dart'; import 'package:apple_music_clone/utils/constants.dart'; class SecondSectionItem extends StatelessWidget { @override Widget build(BuildContext context) { return Container( height: 220, child: ListView( scrollDirection: Axis.horizontal, children: <Widget>[ _contentItem('assets/images/image2.png'), _contentItem('assets/images/image3.png'), _contentItem('assets/images/image2.png'), _contentItem('assets/images/image3.png'), _contentItem('assets/images/image2.png'), _contentItem('assets/images/image3.png'), ], ), ); } Widget _contentItem(String imageString) { return Padding( padding: const EdgeInsets.only(right: 10.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Container( width: Constants().thumbnailImageSize, height: Constants().thumbnailImageSize, child: Image.asset(imageString), ), Text('イージー ヒッツ'), Text('Apple Music') ], ), ); } }
ここまで長くなると解説するだけでもとても大変です。 上級者からは見てのおわかりだと思いますが、命名規則は全然考慮していません。 レイアウト部分とデザイン部分を別々のファイルで管理したほうがいいのか、 同じファイルで管理したほうがいいのか、これらはまだ理解していない状態で組んでいます。
そして、最後に柱になる ContentSliverList
のソースコードです。
content_sliver_list.dart
import 'package:apple_music_clone/utils/hex_color.dart'; import 'package:apple_music_clone/widgets/second_section_header_item.dart'; import 'package:apple_music_clone/widgets/second_section_item.dart'; import 'package:apple_music_clone/widgets/top_section_column_item.dart'; import 'package:flutter/material.dart'; enum Section { Top, Relax, ActivityMood, ShortMovie, Daily, Update, Attention, New, Now, Others, Favorite, Must, BestInterview, ComingSoon } class ContentSliverList extends StatelessWidget { @override Widget build(BuildContext context) { return SliverList( delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { if (index == Section.Top.index) { return _buildTopSectionPageView(context, 4); } else if (index == Section.Relax.index + 1) { return Padding( padding: const EdgeInsets.only(left: 5.0, right: 5.0), child: Column( mainAxisAlignment: MainAxisAlignment.start, children: <Widget>[ SecondSectionHeaderItem(), SecondSectionItem(), ], ), ); } else if (index % 2 == 0) { /// サンプル用のウィジェット return _buildSamplePageWidget(context, 4); } else { /// セパレーター return Padding( padding: const EdgeInsets.fromLTRB(20, 0, 20, 0), child: Divider( color: Colors.black, ), ); } }, childCount: 20, ), ); } Widget _buildTopSectionPageView(BuildContext context, int itemCount) { return Column( mainAxisSize: MainAxisSize.min, children: <Widget>[ SizedBox( height: 300.0, child: PageView.builder( itemCount: itemCount, controller: PageController(viewportFraction: 0.9), itemBuilder: (BuildContext context, int itemIndex) { return _buildTopSectionItem(); }, ), ) ], ); } Widget _buildTopSectionItem() { final verticalPadding = const EdgeInsets.symmetric(vertical: 1.0); final horizontalPadding = const EdgeInsets.symmetric(horizontal: 5.0); return Padding( padding: horizontalPadding, child: TopSectionColumnItem(verticalPadding), ); } } /// サンプル用ウィジェット Widget _buildSamplePageWidget(BuildContext context, int itemCount) { return Column( mainAxisSize: MainAxisSize.min, children: <Widget>[ SizedBox( height: 300.0, child: PageView.builder( itemCount: itemCount, controller: PageController(viewportFraction: 0.9), itemBuilder: (BuildContext context, int itemIndex) { return _buildSampleColumn(context, itemCount, itemIndex); }, ), ) ], ); } Widget _buildSampleColumn( BuildContext context, int carouselIndex, int itemIndex) { final padding = const EdgeInsets.only(top: 1.0, bottom: 1.0); return Padding( padding: EdgeInsets.symmetric(horizontal: 5.0), child: Column( children: <Widget>[ Align( alignment: Alignment.centerLeft, child: Padding( padding: padding, child: Text( 'ニューアルバム', style: TextStyle( color: HexColor('#C24B65'), fontSize: 10, fontWeight: FontWeight.w700), ), ), ), Align( alignment: Alignment.centerLeft, child: Padding( padding: padding, child: Text('Sparkle', style: TextStyle( color: HexColor('#030303'), fontSize: 15, fontWeight: FontWeight.w500)), ), ), Align( alignment: Alignment.centerLeft, child: Padding( padding: padding, child: Text('iri', style: TextStyle( color: HexColor('#89898B'), fontSize: 15, fontWeight: FontWeight.w500)), ), ), Container( decoration: BoxDecoration( borderRadius: BorderRadius.all(Radius.circular(5.0)), ), child: Image.asset('assets/images/image1.png'), ) ], ), ); }
とても長くなりました。
Apple Music アプリぐらいになるとソースコードで複雑さが分かります。 ただ Flutter の場合は適切なウィジェットは揃っている感じなので使うウィジェットさえ間違えなければ 作りたい UI / UX は実現できそうです。
これをビルドすると次のような画面が表示されます。
これでセクション2つ目のデザインが仕上がりました。 今のところは iOS と違って delegate やプロトコル的なものを使わずにやりたいことができています。
そろそろ Swift でいうところの Delegate や Notification 的な機能を使ってみたいですね。 本日はコードが長くなりましたのでこれで終わりにします。