Make inactive mode-switch button bleed to the header's true edge
The inactive Plan/Fly button used to sit inset by the header's own padding on all sides, leaving a visible strip of the header tint around it instead of filling the header exactly like the HTML prototype. CSS solves this with a negative margin, which Flutter's Padding widget rejects (padding.isNonNegative assertion) - restructured so only the active button stays wrapped in the inset padding, while the inactive one is a raw Row child that naturally touches the header container's true top/bottom/outer edges and gets clipped to its rounded corner. Also adds a short gradient from the header's own tint into the button's solid color at the seam, softening what used to be a hard color edge.
This commit is contained in:
@@ -29,9 +29,26 @@ import 'map_search_controls.dart';
|
|||||||
/// (FlyMapControls) - Ortssuche/Home entfallen dort, da im Fly-Modus keine
|
/// (FlyMapControls) - Ortssuche/Home entfallen dort, da im Fly-Modus keine
|
||||||
/// Missionsbearbeitung stattfindet und "Home" (erster Wegpunkt) waehrend
|
/// Missionsbearbeitung stattfindet und "Home" (erster Wegpunkt) waehrend
|
||||||
/// des Flugs kein sinnvoller Bezugspunkt mehr ist.
|
/// des Flugs kein sinnvoller Bezugspunkt mehr ist.
|
||||||
|
///
|
||||||
|
/// Der inaktive Moduswechsel-Button (im HTML-Demonstrator per negativem
|
||||||
|
/// CSS-Margin geloest, `#topBarWrap.mode-edit #flyModeBtn{margin:-5px -10px
|
||||||
|
/// -5px 0}`) sitzt hier bewusst als ROHES Row-Kind ohne umgebendes Padding
|
||||||
|
/// (Flutters `Padding`-Widget erlaubt anders als CSS-Margin keine negativen
|
||||||
|
/// Werte, siehe `padding.isNonNegative`-Assertion) - dadurch beruehrt er von
|
||||||
|
/// selbst alle drei Aussenkanten der Leiste (oben/unten/aussen) und wird von
|
||||||
|
/// deren abgerundetem Container (clipBehavior: Clip.antiAlias) exakt an der
|
||||||
|
/// Kontur beschnitten. Der aktive Button bleibt dagegen ueber ein
|
||||||
|
/// zusaetzliches Padding im Leisteninneren eingerueckt.
|
||||||
class TopModeBar extends ConsumerWidget {
|
class TopModeBar extends ConsumerWidget {
|
||||||
const TopModeBar({super.key});
|
const TopModeBar({super.key});
|
||||||
|
|
||||||
|
/// Einrueckung des jeweils AKTIVEN Moduswechsel-Buttons gegenueber der
|
||||||
|
/// Leistenkante (HTML-Demonstrator: `#topBarWrap{padding:5px 10px}`) - der
|
||||||
|
/// inaktive Button verzichtet bewusst darauf, siehe Klassendoku oben.
|
||||||
|
static const _outerPaddingH = 10.0;
|
||||||
|
static const _outerPaddingV = 5.0;
|
||||||
|
static const _pillRadius = 26.0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final isFly = context.select((AppModeCubit c) => c.state.mode == AppMode.fly);
|
final isFly = context.select((AppModeCubit c) => c.state.mode == AppMode.fly);
|
||||||
@@ -50,6 +67,20 @@ class TopModeBar extends ConsumerWidget {
|
|||||||
MediaQuery.of(context).padding.right,
|
MediaQuery.of(context).padding.right,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final middleContent = isFly
|
||||||
|
? const [
|
||||||
|
HeaderWindPill(),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Expanded(child: FlightModePill()),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
FlyMapControls(),
|
||||||
|
]
|
||||||
|
: const [
|
||||||
|
HeaderWindPill(),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Expanded(child: MapSearchControls()),
|
||||||
|
];
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
top: topInset,
|
top: topInset,
|
||||||
@@ -59,12 +90,18 @@ class TopModeBar extends ConsumerWidget {
|
|||||||
child: FractionallySizedBox(
|
child: FractionallySizedBox(
|
||||||
widthFactor: 0.77,
|
widthFactor: 0.77,
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
// Schneidet den bis an den Rand reichenden inaktiven
|
||||||
|
// Moduswechsel-Button (siehe Klassendoku) exakt an der
|
||||||
|
// abgerundeten Leistenkontur ab, statt dass er rechteckig ueber
|
||||||
|
// die Rundung hinaussteht - dieselbe Kontur uebernimmt gleich
|
||||||
|
// auch die Eckenrundung fuer beide Buttons, eine eigene
|
||||||
|
// BorderRadius je Button ist dadurch nicht mehr noetig.
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: activeTint.withValues(alpha: 0.55),
|
color: activeTint.withValues(alpha: 0.55),
|
||||||
borderRadius: const BorderRadius.only(
|
borderRadius: const BorderRadius.only(
|
||||||
bottomLeft: Radius.circular(26),
|
bottomLeft: Radius.circular(_pillRadius),
|
||||||
bottomRight: Radius.circular(26),
|
bottomRight: Radius.circular(_pillRadius),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -72,34 +109,28 @@ class TopModeBar extends ConsumerWidget {
|
|||||||
_ModeButton(
|
_ModeButton(
|
||||||
label: 'Plan',
|
label: 'Plan',
|
||||||
isActive: !isFly,
|
isActive: !isFly,
|
||||||
|
activeTint: activeTint,
|
||||||
inactiveColor: DmcColors.planModeColor,
|
inactiveColor: DmcColors.planModeColor,
|
||||||
cornerRadius: const BorderRadius.only(
|
isLeftButton: true,
|
||||||
bottomLeft: Radius.circular(26),
|
|
||||||
),
|
|
||||||
onTap: () => context.read<AppModeCubit>().toPlan(),
|
onTap: () => context.read<AppModeCubit>().toPlan(),
|
||||||
),
|
),
|
||||||
if (isFly) ...[
|
Expanded(
|
||||||
const SizedBox(width: 8),
|
child: Padding(
|
||||||
const HeaderWindPill(),
|
padding: EdgeInsets.only(
|
||||||
const SizedBox(width: 8),
|
left: 8,
|
||||||
const Expanded(child: FlightModePill()),
|
right: isFly ? 8 : 12,
|
||||||
const SizedBox(width: 8),
|
top: _outerPaddingV,
|
||||||
const FlyMapControls(),
|
bottom: _outerPaddingV,
|
||||||
const SizedBox(width: 8),
|
),
|
||||||
] else ...[
|
child: Row(children: middleContent),
|
||||||
const SizedBox(width: 8),
|
),
|
||||||
const HeaderWindPill(),
|
),
|
||||||
const SizedBox(width: 8),
|
|
||||||
const Expanded(child: MapSearchControls()),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
],
|
|
||||||
_ModeButton(
|
_ModeButton(
|
||||||
label: 'Fly',
|
label: 'Fly',
|
||||||
isActive: isFly,
|
isActive: isFly,
|
||||||
|
activeTint: activeTint,
|
||||||
inactiveColor: DmcColors.flyModeColor,
|
inactiveColor: DmcColors.flyModeColor,
|
||||||
cornerRadius: const BorderRadius.only(
|
isLeftButton: false,
|
||||||
bottomRight: Radius.circular(26),
|
|
||||||
),
|
|
||||||
onTap: () {
|
onTap: () {
|
||||||
context.read<AppModeCubit>().toFly();
|
context.read<AppModeCubit>().toFly();
|
||||||
// Eintritt in den Fly-Modus setzt immer auf Waypoint
|
// Eintritt in den Fly-Modus setzt immer auf Waypoint
|
||||||
@@ -126,29 +157,62 @@ class _ModeButton extends StatelessWidget {
|
|||||||
const _ModeButton({
|
const _ModeButton({
|
||||||
required this.label,
|
required this.label,
|
||||||
required this.isActive,
|
required this.isActive,
|
||||||
|
required this.activeTint,
|
||||||
required this.inactiveColor,
|
required this.inactiveColor,
|
||||||
required this.cornerRadius,
|
required this.isLeftButton,
|
||||||
required this.onTap,
|
required this.onTap,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String label;
|
final String label;
|
||||||
final bool isActive;
|
final bool isActive;
|
||||||
|
|
||||||
|
/// Aktuelle Kopfleisten-Toenung (TopModeBar.activeTint) - dient nur als
|
||||||
|
/// Startfarbe des Farbverlaufs im inaktiven Zustand (siehe unten), der
|
||||||
|
/// aktive Button hat ohnehin keinen eigenen Hintergrund.
|
||||||
|
final Color activeTint;
|
||||||
final Color inactiveColor;
|
final Color inactiveColor;
|
||||||
final BorderRadius cornerRadius;
|
|
||||||
|
/// true = Plan-Button (linkes Ende der Leiste), false = Fly-Button
|
||||||
|
/// (rechtes Ende) - bestimmt sowohl die Seite der Einrueckung im aktiven
|
||||||
|
/// Zustand als auch die Verlaufsrichtung des Farbverlaufs im inaktiven.
|
||||||
|
final bool isLeftButton;
|
||||||
|
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GestureDetector(
|
final button = GestureDetector(
|
||||||
onTap: isActive ? null : onTap,
|
onTap: isActive ? null : onTap,
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: EdgeInsets.symmetric(
|
padding: EdgeInsets.symmetric(
|
||||||
horizontal: 24,
|
horizontal: 24,
|
||||||
vertical: isActive ? 6 : 11,
|
// Grosszuegiger als der aktive Zustand (6), damit dieser
|
||||||
|
// Button - roh, ohne umgebendes Leisten-Padding, siehe
|
||||||
|
// TopModeBar-Klassendoku - garantiert der hoechste Row-Inhalt
|
||||||
|
// bleibt und dadurch die Leiste exakt auf ihre volle Hoehe
|
||||||
|
// (Text + diese Polsterung) bringt, statt von einem anderen
|
||||||
|
// Zeileninhalt (z.B. den 34px hohen Pillen in der Mitte)
|
||||||
|
// ueberragt zu werden und sichtbaren Rand stehen zu lassen.
|
||||||
|
vertical: isActive ? 6 : 16,
|
||||||
|
),
|
||||||
|
decoration: isActive
|
||||||
|
? null
|
||||||
|
: BoxDecoration(
|
||||||
|
// Kurzer Farbverlauf von der Kopfleisten-eigenen Toenung zur
|
||||||
|
// vollen Button-Farbe statt einer harten Kante an der
|
||||||
|
// Nahtstelle zum mittleren Inhalt (Nutzerwunsch: "linearer
|
||||||
|
// kurzer Farbverlauf zwischen button und rest header") - nur
|
||||||
|
// die ersten 30% der Breite blenden ein, der Rest bleibt
|
||||||
|
// solide, damit der Text weiterhin auf vollem Kontrast sitzt.
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: isLeftButton ? Alignment.centerRight : Alignment.centerLeft,
|
||||||
|
end: isLeftButton ? Alignment.centerLeft : Alignment.centerRight,
|
||||||
|
colors: [
|
||||||
|
activeTint.withValues(alpha: 0.55),
|
||||||
|
inactiveColor.withValues(alpha: 0.85),
|
||||||
|
],
|
||||||
|
stops: const [0.0, 0.3],
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: isActive ? null : inactiveColor.withValues(alpha: 0.85),
|
|
||||||
borderRadius: isActive ? null : cornerRadius,
|
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
label,
|
label,
|
||||||
@@ -160,5 +224,20 @@ class _ModeButton extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!isActive) return button;
|
||||||
|
|
||||||
|
// Nur der aktive Button bleibt gegenueber der Leistenkante eingerueckt
|
||||||
|
// (HTML-Demonstrator: unveraendertes #topBarWrap-Padding gilt fuer ihn
|
||||||
|
// weiterhin) - der inaktive bleibt roh (siehe Klassendoku).
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
top: TopModeBar._outerPaddingV,
|
||||||
|
bottom: TopModeBar._outerPaddingV,
|
||||||
|
left: isLeftButton ? TopModeBar._outerPaddingH : 0,
|
||||||
|
right: isLeftButton ? 0 : TopModeBar._outerPaddingH,
|
||||||
|
),
|
||||||
|
child: button,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user