Instantly share code, notes, and snippets.
Created
May 8, 2023 07:09
-
Save take4blue/1b1a4c44e4f4fbb961d6575a807fe6e6 to your computer and use it in GitHub Desktop.
popupmenuの中身をtile形式で表示するためのウィジェット類
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:flutter/material.dart'; | |
/// Popup用のTile</br> | |
/// 呼び出し側は[padding]を[EdgeInsets.zero]にする必要あり</br> | |
/// subtitleのスタイルはLabel Medium/On surface variantを使用する。</br> | |
/// スタイルは https://m2.material.io/components/menus#specs, | |
/// https://m3.material.io/components/menus/specs を参考にしている。 | |
class PopupMenuTile extends StatelessWidget { | |
const PopupMenuTile({ | |
super.key, | |
required this.title, | |
this.subtitle, | |
this.leading, | |
this.trailing, | |
this.minWidth, | |
this.height = kMinInteractiveDimension, | |
}); | |
/// ListTileのtitle相当部分に表示するウィジェット(テキスト) | |
final Widget title; | |
/// ListTileのsubtitle相当部分に表示するウィジェット(テキスト) | |
final Widget? subtitle; | |
/// ListTileのleading相当部分に表示するウィジェット(アイコン) | |
final Widget? leading; | |
/// ListTileのtrailing相当部分に表示するウィジェット(アイコン) | |
final Widget? trailing; | |
/// 最低幅の指定</br> | |
/// 2段目のメニューのヘッダーとして使用する場合に1段目のメニュー幅と合わせるのに使用している。 | |
final double? minWidth; | |
/// 最低高さ | |
final double height; | |
@override | |
Widget build(BuildContext context) { | |
final useMaterial3 = Theme.of(context).useMaterial3; | |
// subtextのスタイルは | |
// https://m3.material.io/components/menus/specs | |
// https://m3.material.io/components/lists/specs | |
// 上の2つの仕様を考慮し、Label Medium/On surface variantにした。 | |
final TextStyle? substyle = Theme.of(context) | |
.textTheme | |
.labelMedium | |
?.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant); | |
final titles = (subtitle == null) | |
? title | |
: Column( | |
mainAxisAlignment: MainAxisAlignment.start, | |
crossAxisAlignment: CrossAxisAlignment.start, | |
children: [ | |
title, | |
substyle != null | |
? DefaultTextStyle(style: substyle, child: subtitle!) | |
: subtitle! | |
], | |
); | |
// tile部分の生成 | |
final tiles = Row( | |
crossAxisAlignment: CrossAxisAlignment.center, | |
children: [ | |
if (leading != null) ...[ | |
leading!, | |
// leadingアイコンとの隙間はM3はスペック通り、M2はデスクトップ仕様にしている | |
SizedBox(width: useMaterial3 ? 12 : 20), | |
], | |
titles, | |
if (trailing != null) ...[ | |
const Spacer(), | |
trailing!, | |
], | |
], | |
); | |
// 高さは[height]、幅は[minWidth]を制約条件にする | |
// paddingは親側がEdgeInsets.zeroであると仮定している。 | |
return ConstrainedBox( | |
constraints: BoxConstraints(minHeight: height, minWidth: minWidth ?? 0), | |
child: Container( | |
// paddingはM3はスペック通り、M2はMinimum and maximum widthに書かれているものを利用 | |
padding: EdgeInsets.symmetric(horizontal: useMaterial3 ? 12 : 16), | |
child: tiles, | |
), | |
); | |
} | |
} | |
/// heckedPopupMenuItemの子供を[ListTile]から[PopupMenuTile]に変更したもの。</br> | |
/// 最低限の派生にしたかったのだけどアニメーションメンバがプライベートなので全てコピーしてきた。 | |
class CheckedPopupMenuItemP<T> extends PopupMenuItem<T> { | |
/// Creates a popup menu item with a checkmark. | |
/// | |
/// By default, the menu item is [enabled] but unchecked. To mark the item as | |
/// checked, set [checked] to true. | |
/// | |
/// The `checked` and `enabled` arguments must not be null. | |
const CheckedPopupMenuItemP({ | |
super.key, | |
super.value, | |
this.checked = false, | |
super.enabled, | |
super.padding = EdgeInsets.zero, | |
super.height, | |
super.mouseCursor, | |
super.child, | |
}) : assert(checked != null); | |
/// Whether to display a checkmark next to the menu item. | |
/// | |
/// Defaults to false. | |
/// | |
/// When true, an [Icons.done] checkmark is displayed. | |
/// | |
/// When this popup menu item is selected, the checkmark will fade in or out | |
/// as appropriate to represent the implied new state. | |
final bool checked; | |
/// The widget below this widget in the tree. | |
/// | |
/// Typically a [Text]. An appropriate [DefaultTextStyle] is put in scope for | |
/// the child. The text should be short enough that it won't wrap. | |
/// | |
/// This widget is placed in the [ListTile.title] slot of a [ListTile] whose | |
/// [ListTile.leading] slot is an [Icons.done] icon. | |
@override | |
Widget? get child => super.child; | |
@override | |
PopupMenuItemState<T, CheckedPopupMenuItemP<T>> createState() => | |
_CheckedPopupMenuItemPState<T>(); | |
} | |
class _CheckedPopupMenuItemPState<T> | |
extends PopupMenuItemState<T, CheckedPopupMenuItemP<T>> | |
with SingleTickerProviderStateMixin { | |
static const Duration _fadeDuration = Duration(milliseconds: 150); | |
late AnimationController _controller; | |
Animation<double> get _opacity => _controller.view; | |
@override | |
void initState() { | |
super.initState(); | |
_controller = AnimationController(duration: _fadeDuration, vsync: this) | |
..value = widget.checked ? 1.0 : 0.0 | |
..addListener(() => setState(() {/* animation changed */})); | |
} | |
@override | |
void handleTap() { | |
// This fades the checkmark in or out when tapped. | |
if (widget.checked) { | |
_controller.reverse(); | |
} else { | |
_controller.forward(); | |
} | |
super.handleTap(); | |
} | |
@override | |
Widget buildChild() { | |
// ListTile -> PopupMenuTile | |
return PopupMenuTile( | |
leading: FadeTransition( | |
opacity: _opacity, | |
child: Icon(_controller.isDismissed ? null : Icons.done), | |
), | |
title: widget.child!, | |
); | |
} | |
} | |
/// PopupMenuButtonのデフォルトを変更したもの | |
/// - [padding]のデフォルトを[EdgeInsets.zero]に。 | |
/// - [offset]のデフォルトを[const Offset(0, -8)]に。 | |
class PopupMenuButtonP<T> extends PopupMenuButton<T> { | |
const PopupMenuButtonP({ | |
super.key, | |
required super.itemBuilder, | |
super.initialValue, | |
super.onOpened, | |
super.onSelected, | |
super.onCanceled, | |
super.tooltip, | |
super.elevation, | |
super.shadowColor, | |
super.surfaceTintColor, | |
super.padding = EdgeInsets.zero, | |
super.child, | |
super.splashRadius, | |
super.icon, | |
super.iconSize, | |
super.offset = const Offset(0, -8), | |
super.enabled = true, | |
super.shape, | |
super.color, | |
super.enableFeedback, | |
super.constraints, | |
super.position, | |
super.clipBehavior, | |
}); | |
} | |
/// PopupMenuItemのデフォルトを変更したもの | |
/// - [padding]を[EdgeInsets.zero]に。 | |
class PopupMenuItemP<T> extends PopupMenuItem<T> { | |
const PopupMenuItemP({ | |
super.key, | |
super.value, | |
super.onTap, | |
super.enabled = true, | |
super.height, | |
super.padding = EdgeInsets.zero, | |
super.textStyle, | |
super.labelTextStyle, | |
super.mouseCursor, | |
required super.child, | |
}); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment