web-dev-qa-db-ja.com

フラッターでタップでカードを拡張する方法は?

タップでマテリアルデザインカードの動作を実現したいと考えています。タップすると全画面に展開し、追加のコンテンツ/新しいページが表示されます。どうすれば達成できますか?

https://material.io/design/components/cards.html#behavior

私はNavigator.of(context).Push()を使用して新しいページを表示し、ヒーローアニメーションで遊んでカードの背景を新しい足場に移動しましたが、新しいページがカードから表示されていないため、移動する方法ではないようですそれ自体、または私はそれを作ることができません。上記のmaterial.ioと同じ動作を実現しようとしています。どういうわけか私を案内していただけませんか?

ありがとうございました

7
Matt

しばらく前にその正確なページ/トランジションを複製してみましたが、完全に同じようには見えませんでしたが、かなり近づきました。これはすぐにまとめられたものであり、実際にはベストプラクティスなどに準拠していません。

重要な部分はヒーローウィジェット、特にそれに付随するタグです。これらが一致しない場合は機能しません。

import 'package:flutter/material.Dart';

void main() => runApp(MyApp());

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          backgroundColor: Colors.deepPurple,
        ),
        body: ListView.builder(
          itemBuilder: (context, index) {
            return TileItem(num: index);
          },
        ),
      ),
    );
  }
}

class TileItem extends StatelessWidget {
  final int num;

  const TileItem({Key key, this.num}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Hero(
      tag: "card$num",
      child: Card(
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.all(
            Radius.circular(8.0),
          ),
        ),
        clipBehavior: Clip.antiAliasWithSaveLayer,
        child: Stack(
          children: <Widget>[
            Column(
              children: <Widget>[
                AspectRatio(
                  aspectRatio: 485.0 / 384.0,
                  child: Image.network("https://picsum.photos/485/384?image=$num"),
                ),
                Material(
                  child: ListTile(
                    title: Text("Item $num"),
                    subtitle: Text("This is item #$num"),
                  ),
                )
              ],
            ),
            Positioned(
              left: 0.0,
              top: 0.0,
              bottom: 0.0,
              right: 0.0,
              child: Material(
                type: MaterialType.transparency,
                child: InkWell(
                  onTap: () async {
                    await Future.delayed(Duration(milliseconds: 200));
                    Navigator.Push(
                      context,
                      MaterialPageRoute(
                        builder: (context) {
                          return new PageItem(num: num);
                        },
                        fullscreenDialog: true,
                      ),
                    );
                  },
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class PageItem extends StatelessWidget {
  final int num;

  const PageItem({Key key, this.num}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    AppBar appBar = new AppBar(
      primary: false,
      leading: IconTheme(data: IconThemeData(color: Colors.white), child: CloseButton()),
      flexibleSpace: Container(
        decoration: BoxDecoration(
          gradient: LinearGradient(
            begin: Alignment.topCenter,
            end: Alignment.bottomCenter,
            colors: [
              Colors.black.withOpacity(0.4),
              Colors.black.withOpacity(0.1),
            ],
          ),
        ),
      ),
      backgroundColor: Colors.transparent,
    );
    final MediaQueryData mediaQuery = MediaQuery.of(context);

    return Stack(children: <Widget>[
      Hero(
        tag: "card$num",
        child: Material(
          child: Column(
            children: <Widget>[
              AspectRatio(
                aspectRatio: 485.0 / 384.0,
                child: Image.network("https://picsum.photos/485/384?image=$num"),
              ),
              Material(
                child: ListTile(
                  title: Text("Item $num"),
                  subtitle: Text("This is item #$num"),
                ),
              ),
              Expanded(
                child: Center(child: Text("Some more content goes here!")),
              )
            ],
          ),
        ),
      ),
      Column(
        children: <Widget>[
          Container(
            height: mediaQuery.padding.top,
          ),
          ConstrainedBox(
            constraints: BoxConstraints(maxHeight: appBar.preferredSize.height),
            child: appBar,
          )
        ],
      ),
    ]);
  }
}

編集:コメントへの応答として、私はヒーローがどのように機能するか(または少なくとも私がそれがどのように機能するか= D)の説明を書きます。

基本的に、ページ間のトランジションが開始されると、トランジションを実行する基本的なメカニズム(ナビゲーターの一部)が、現在のページと新しいページで「ヒーロー」ウィジェットを探します。ヒーローが見つかった場合、そのサイズと位置はページごとに計算されます。

ページ間の移行が実行されると、新しいページのヒーローが古いヒーローと同じ場所のオーバーレイに移動し、そのサイズと位置が新しいページの最終的なサイズと位置に向かってアニメーション化されます。 (少しの作業で必要に応じて変更できることに注意してください。詳細は このブログ を参照してください)。

これは、OPが達成しようとしていたことです。

カードをタップすると、その背景色が拡大し、AppbarのあるScaffoldの背景色になります。

これを行う最も簡単な方法は、足場自体をヒーローに配置することです。ヒーロートランジションの実行中はオーバーレイにあるため、トランジション中はAppBarが不明瞭になります。以下のコードを参照してください。遷移が遅くなるようにクラスに追加したので、何が起こっているのかを確認できます。通常の速度で遷移を確認するには、SlowMaterialPageRouteをMaterialPageRouteにプッシュする部分を変更します。

That looks something like this:
import 'Dart:math';

import 'package:flutter/cupertino.Dart';
import 'package:flutter/material.Dart';

void main() => runApp(MyApp());

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          backgroundColor: Colors.deepPurple,
        ),
        body: ListView.builder(
          itemBuilder: (context, index) {
            return TileItem(num: index);
          },
        ),
      ),
    );
  }
}

Color colorFromNum(int num) {
  var random = Random(num);
  var r = random.nextInt(256);
  var g = random.nextInt(256);
  var b = random.nextInt(256);
  return Color.fromARGB(255, r, g, b);
}

class TileItem extends StatelessWidget {
  final int num;

  const TileItem({Key key, this.num}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Hero(
      tag: "card$num",
      child: Card(
        color: colorFromNum(num),
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.all(
            Radius.circular(8.0),
          ),
        ),
        clipBehavior: Clip.antiAliasWithSaveLayer,
        child: Stack(
          children: <Widget>[
            Column(
              children: <Widget>[
                AspectRatio(
                  aspectRatio: 485.0 / 384.0,
                  child: Image.network("https://picsum.photos/485/384?image=$num"),
                ),
                Material(
                  type: MaterialType.transparency,
                  child: ListTile(
                    title: Text("Item $num"),
                    subtitle: Text("This is item #$num"),
                  ),
                )
              ],
            ),
            Positioned(
              left: 0.0,
              top: 0.0,
              bottom: 0.0,
              right: 0.0,
              child: Material(
                type: MaterialType.transparency,
                child: InkWell(
                  onTap: () async {
                    await Future.delayed(Duration(milliseconds: 200));
                    Navigator.Push(
                      context,
                      SlowMaterialPageRoute(
                        builder: (context) {
                          return new PageItem(num: num);
                        },
                        fullscreenDialog: true,
                      ),
                    );
                  },
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class PageItem extends StatelessWidget {
  final int num;

  const PageItem({Key key, this.num}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Hero(
      tag: "card$num",
      child: Scaffold(
        backgroundColor: colorFromNum(num),
        appBar: AppBar(
          backgroundColor: Colors.white.withOpacity(0.2),
        ),
      ),
    );
  }
}

class SlowMaterialPageRoute<T> extends MaterialPageRoute<T> {
  SlowMaterialPageRoute({
    WidgetBuilder builder,
    RouteSettings settings,
    bool maintainState = true,
    bool fullscreenDialog = false,
  }) : super(builder: builder, settings: settings, fullscreenDialog: fullscreenDialog);

  @override
  Duration get transitionDuration => const Duration(seconds: 3);
}

ただし、足場全体でトランジションを実行するのが最適ではない場合があります。データが大量にある場合や、特定のスペースに収まるように設計されている場合があります。その場合、本質的に「偽物」である、ヒーローのトランジションを実行したいもののバージョンを作成するオプション-つまり、2つのレイヤーのスタックがあります。1つはヒーローであり、背景色、足場、その他それ以外の場合は、トランジション中に表示したいし、一番下のレイヤーを完全に覆い隠す(つまり、背景が不透明度100%である)別のレイヤーには、アプリバーやその他の必要なものがあります。

それよりももっと良い方法がおそらくあるでしょう-例えば、あなたは 私がリンクしたブログ で述べられた方法を使ってヒーローを個別に指定することができます。

3
rmtmckenzie

追加 elevation:0完璧な結果を得るためにAppBarで!

0
Giorgio Modoni