Skip to content

Instantly share code, notes, and snippets.

@take4blue
Created May 8, 2023 07:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save take4blue/1b1a4c44e4f4fbb961d6575a807fe6e6 to your computer and use it in GitHub Desktop.
Save take4blue/1b1a4c44e4f4fbb961d6575a807fe6e6 to your computer and use it in GitHub Desktop.
popupmenuの中身をtile形式で表示するためのウィジェット類
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