リバース・エンジニアリング

Flultterとテックブログと時々iOS

43.【AppleMusicクローン】セクション2 のレイアウトをListView を使って実装する

今回はセクション2の部分のレイアウトを作っていきます。

前回の記事はこちらになります。

blog.tamappe.com

enum でいえば、 Relax に当たる箇所です。

enum Section {
  Top,
  Relax,
  ActivityMood,
  ShortMovie,
  Daily,
  Update,
  Attention,
  New,
  Now,
  Others,
  Favorite,
  Must,
  BestInterview,
  ComingSoon
}

デザインでいえば

f:id:qed805:20200408230553p:plain
セクション2 のデザイン

この部分になります。 特に手元にある iPhone から Apple Music を触ってみてもそんな特殊なレイアウトではないようなので、 ListView の Axis.horizontal を使えば実現できますね。

ListView の使い方で不安な方は過去記事に簡単な使い方を説明していますので復習してみてください。

blog.tamappe.com

blog.tamappe.com

設計について

今回のアプリは API を使わずに静的なレイアウトで組みます。

ヘッダー部分とコンテンツ部分にレイアウトを分けられそうなので分けました。

f:id:qed805:20200408231328p:plain
second_section_header_item と second_section_item

  • second_sectoin_header_item (ヘッダー部分)
  • second_sectoin_item (コンテンツ部分)

使用する画像リソースのインポート

今回は仮置きとして画像ファイルを使っています。

f:id:qed805:20200408231942p:plain
image1.png

f:id:qed805:20200408232007p:plain
image2.png

f:id:qed805:20200408232018p:plain
image3.png

当然ですがこちらは仮で画像を当てはめているだけです。 実務で静的なデータをこんな感じでプロジェクトに置いていませんのでご注意ください。

これらの画像を assets/images/ディレクトリに配置させます。

f:id:qed805:20200408232222p:plain

画像をインポートした後は 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 は実現できそうです。

これをビルドすると次のような画面が表示されます。

f:id:qed805:20200408233940p:plain
スクリーンショット

これでセクション2つ目のデザインが仕上がりました。 今のところは iOS と違って delegateプロトコル的なものを使わずにやりたいことができています。

そろそろ Swift でいうところの Delegate や Notification 的な機能を使ってみたいですね。 本日はコードが長くなりましたのでこれで終わりにします。