mirror of
https://github.com/flutter/samples.git
synced 2025-11-10 14:58:34 +00:00
Next Gen UI demo (#1778)
First pass at a Next Generation UI demo app. The UI needs work, feedback gratefully accepted. ## Pre-launch Checklist - [x] I read the [Flutter Style Guide] _recently_, and have followed its advice. - [x] I signed the [CLA]. - [x] I read the [Contributors Guide]. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-devrel channel on [Discord]. <!-- Links --> [Flutter Style Guide]: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo [CLA]: https://cla.developers.google.com/ [Discord]: https://github.com/flutter/flutter/wiki/Chat [Contributors Guide]: https://github.com/flutter/samples/blob/main/CONTRIBUTING.md
This commit is contained in:
312
next_gen_ui_demo/lib/title_screen_5b/title_screen.dart
Normal file
312
next_gen_ui_demo/lib/title_screen_5b/title_screen.dart
Normal file
@@ -0,0 +1,312 @@
|
||||
// Copyright 2023 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:math';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
|
||||
import '../assets.dart';
|
||||
import '../orb_shader/orb_shader_config.dart';
|
||||
import '../orb_shader/orb_shader_widget.dart';
|
||||
import '../styles.dart';
|
||||
import '../title_screen/title_screen.dart';
|
||||
import 'title_screen_ui.dart';
|
||||
|
||||
class TitleScreen extends TitleScreenBase {
|
||||
const TitleScreen({super.key, required super.callback});
|
||||
|
||||
@override
|
||||
State<TitleScreen> createState() => _TitleScreenState();
|
||||
}
|
||||
|
||||
class _TitleScreenState extends State<TitleScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
final _orbKey = GlobalKey<OrbShaderWidgetState>();
|
||||
|
||||
/// Editable Settings
|
||||
/// 0-1, receive lighting strength
|
||||
final _minReceiveLightAmt = .35;
|
||||
final _maxReceiveLightAmt = .7;
|
||||
|
||||
/// 0-1, emit lighting strength
|
||||
final _minEmitLightAmt = .5;
|
||||
final _maxEmitLightAmt = 1;
|
||||
|
||||
/// Internal
|
||||
var _mousePos = Offset.zero;
|
||||
|
||||
Color get _emitColor =>
|
||||
AppColors.emitColors[_difficultyOverride ?? _difficulty];
|
||||
Color get _orbColor =>
|
||||
AppColors.orbColors[_difficultyOverride ?? _difficulty];
|
||||
|
||||
/// Currently selected difficulty
|
||||
int _difficulty = 0;
|
||||
|
||||
/// Currently focused difficulty (if any)
|
||||
int? _difficultyOverride;
|
||||
double _orbEnergy = 0;
|
||||
double _minOrbEnergy = 0;
|
||||
|
||||
double get _finalReceiveLightAmt {
|
||||
final light =
|
||||
lerpDouble(_minReceiveLightAmt, _maxReceiveLightAmt, _orbEnergy) ?? 0;
|
||||
return light + _pulseEffect.value * .05 * _orbEnergy;
|
||||
}
|
||||
|
||||
double get _finalEmitLightAmt {
|
||||
return lerpDouble(_minEmitLightAmt, _maxEmitLightAmt, _orbEnergy) ?? 0;
|
||||
}
|
||||
|
||||
late final AnimationController _pulseEffect;
|
||||
|
||||
Duration _getRndPulseDuration() => 100.ms + 200.ms * Random().nextDouble();
|
||||
|
||||
double _getMinEnergyForDifficulty(int difficulty) {
|
||||
if (difficulty == 1) {
|
||||
return .3;
|
||||
} else if (difficulty == 2) {
|
||||
return .6;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_pulseEffect = AnimationController(
|
||||
vsync: this,
|
||||
duration: _getRndPulseDuration(),
|
||||
lowerBound: -1,
|
||||
upperBound: 1,
|
||||
);
|
||||
_pulseEffect.forward();
|
||||
_pulseEffect.addListener(_handlePulseEffectUpdate);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pulseEffect.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _handlePulseEffectUpdate() {
|
||||
if (_pulseEffect.status == AnimationStatus.completed) {
|
||||
_pulseEffect.reverse();
|
||||
_pulseEffect.duration = _getRndPulseDuration();
|
||||
} else if (_pulseEffect.status == AnimationStatus.dismissed) {
|
||||
_pulseEffect.duration = _getRndPulseDuration();
|
||||
_pulseEffect.forward();
|
||||
}
|
||||
}
|
||||
|
||||
void _handleDifficultyPressed(int value) {
|
||||
setState(() => _difficulty = value);
|
||||
_bumpMinEnergy();
|
||||
}
|
||||
|
||||
Future<void> _bumpMinEnergy([double amount = 0.1]) async {
|
||||
setState(() {
|
||||
_minOrbEnergy = _getMinEnergyForDifficulty(_difficulty) + amount;
|
||||
});
|
||||
await Future<void>.delayed(.2.seconds);
|
||||
setState(() {
|
||||
_minOrbEnergy = _getMinEnergyForDifficulty(_difficulty);
|
||||
});
|
||||
}
|
||||
|
||||
void _handleStartPressed() => _bumpMinEnergy(0.3);
|
||||
|
||||
void _handleDifficultyFocused(int? value) {
|
||||
setState(() {
|
||||
_difficultyOverride = value;
|
||||
if (value == null) {
|
||||
_minOrbEnergy = _getMinEnergyForDifficulty(_difficulty);
|
||||
} else {
|
||||
_minOrbEnergy = _getMinEnergyForDifficulty(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Update mouse position so the orbWidget can use it, doing it here prevents
|
||||
/// btns from blocking the mouse-move events in the widget itself.
|
||||
void _handleMouseMove(PointerHoverEvent e) {
|
||||
setState(() {
|
||||
_mousePos = e.localPosition;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: MouseRegion(
|
||||
onHover: _handleMouseMove,
|
||||
child: _AnimatedColors(
|
||||
orbColor: _orbColor,
|
||||
emitColor: _emitColor,
|
||||
builder: (_, orbColor, emitColor) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
widget.callback(orbColor);
|
||||
});
|
||||
return Stack(
|
||||
children: [
|
||||
/// Bg-Base
|
||||
Image.asset(AssetPaths.titleBgBase),
|
||||
|
||||
/// Bg-Receive
|
||||
_LitImage(
|
||||
color: orbColor,
|
||||
imgSrc: AssetPaths.titleBgReceive,
|
||||
pulseEffect: _pulseEffect,
|
||||
lightAmt: _finalReceiveLightAmt,
|
||||
),
|
||||
|
||||
/// Orb
|
||||
Positioned.fill(
|
||||
child: Stack(
|
||||
children: [
|
||||
// Orb
|
||||
OrbShaderWidget(
|
||||
key: _orbKey,
|
||||
mousePos: _mousePos,
|
||||
minEnergy: _minOrbEnergy,
|
||||
config: OrbShaderConfig(
|
||||
ambientLightColor: orbColor,
|
||||
materialColor: orbColor,
|
||||
lightColor: orbColor,
|
||||
),
|
||||
onUpdate: (energy) => setState(() {
|
||||
_orbEnergy = energy;
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
/// Mg-Base
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleMgBase,
|
||||
color: orbColor,
|
||||
pulseEffect: _pulseEffect,
|
||||
lightAmt: _finalReceiveLightAmt,
|
||||
),
|
||||
|
||||
/// Mg-Receive
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleMgReceive,
|
||||
color: orbColor,
|
||||
pulseEffect: _pulseEffect,
|
||||
lightAmt: _finalReceiveLightAmt,
|
||||
),
|
||||
|
||||
/// Mg-Emit
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleMgEmit,
|
||||
color: emitColor,
|
||||
pulseEffect: _pulseEffect,
|
||||
lightAmt: _finalEmitLightAmt,
|
||||
),
|
||||
|
||||
/// Fg-Rocks
|
||||
Image.asset(AssetPaths.titleFgBase),
|
||||
|
||||
/// Fg-Receive
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleFgReceive,
|
||||
color: orbColor,
|
||||
pulseEffect: _pulseEffect,
|
||||
lightAmt: _finalReceiveLightAmt,
|
||||
),
|
||||
|
||||
/// Fg-Emit
|
||||
_LitImage(
|
||||
imgSrc: AssetPaths.titleFgEmit,
|
||||
color: emitColor,
|
||||
pulseEffect: _pulseEffect,
|
||||
lightAmt: _finalEmitLightAmt,
|
||||
),
|
||||
|
||||
/// UI
|
||||
Positioned.fill(
|
||||
child: TitleScreenUi(
|
||||
difficulty: _difficulty,
|
||||
onDifficultyFocused: _handleDifficultyFocused,
|
||||
onDifficultyPressed: _handleDifficultyPressed,
|
||||
onStartPressed: _handleStartPressed,
|
||||
),
|
||||
),
|
||||
],
|
||||
).animate().fadeIn(duration: 1.seconds, delay: .3.seconds);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LitImage extends StatelessWidget {
|
||||
const _LitImage({
|
||||
required this.color,
|
||||
required this.imgSrc,
|
||||
required this.pulseEffect,
|
||||
required this.lightAmt,
|
||||
});
|
||||
final Color color;
|
||||
final String imgSrc;
|
||||
final AnimationController pulseEffect;
|
||||
final double lightAmt;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final hsl = HSLColor.fromColor(color);
|
||||
return ListenableBuilder(
|
||||
listenable: pulseEffect,
|
||||
child: Image.asset(imgSrc),
|
||||
builder: (context, child) {
|
||||
return ColorFiltered(
|
||||
colorFilter: ColorFilter.mode(
|
||||
hsl.withLightness(hsl.lightness * lightAmt).toColor(),
|
||||
BlendMode.modulate,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AnimatedColors extends StatelessWidget {
|
||||
const _AnimatedColors({
|
||||
required this.emitColor,
|
||||
required this.orbColor,
|
||||
required this.builder,
|
||||
});
|
||||
|
||||
final Color emitColor;
|
||||
final Color orbColor;
|
||||
|
||||
final Widget Function(BuildContext context, Color orbColor, Color emitColor)
|
||||
builder;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final duration = .5.seconds;
|
||||
return TweenAnimationBuilder(
|
||||
tween: ColorTween(begin: emitColor, end: emitColor),
|
||||
duration: duration,
|
||||
builder: (_, emitColor, __) {
|
||||
return TweenAnimationBuilder(
|
||||
tween: ColorTween(begin: orbColor, end: orbColor),
|
||||
duration: duration,
|
||||
builder: (context, orbColor, __) {
|
||||
return builder(context, orbColor!, emitColor!);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
301
next_gen_ui_demo/lib/title_screen_5b/title_screen_ui.dart
Normal file
301
next_gen_ui_demo/lib/title_screen_5b/title_screen_ui.dart
Normal file
@@ -0,0 +1,301 @@
|
||||
// Copyright 2023 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:extra_alignments/extra_alignments.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:focusable_control_builder/focusable_control_builder.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../assets.dart';
|
||||
import '../common/shader_effect.dart';
|
||||
import '../common/ticking_builder.dart';
|
||||
import '../common/ui_scaler.dart';
|
||||
import '../styles.dart';
|
||||
|
||||
class TitleScreenUi extends StatelessWidget {
|
||||
const TitleScreenUi({
|
||||
super.key,
|
||||
required this.difficulty,
|
||||
required this.onDifficultyPressed,
|
||||
required this.onDifficultyFocused,
|
||||
required this.onStartPressed,
|
||||
});
|
||||
|
||||
final int difficulty;
|
||||
final void Function(int difficulty) onDifficultyPressed;
|
||||
final void Function(int? difficulty) onDifficultyFocused;
|
||||
final VoidCallback onStartPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 40, horizontal: 50),
|
||||
child: Stack(
|
||||
children: [
|
||||
/// Title Text
|
||||
const TopLeft(
|
||||
child: UiScaler(
|
||||
alignment: Alignment.topLeft,
|
||||
child: _TitleText(),
|
||||
),
|
||||
),
|
||||
|
||||
/// Difficulty Btns
|
||||
BottomLeft(
|
||||
child: UiScaler(
|
||||
alignment: Alignment.bottomLeft,
|
||||
child: _DifficultyBtns(
|
||||
difficulty: difficulty,
|
||||
onDifficultyPressed: onDifficultyPressed,
|
||||
onDifficultyFocused: onDifficultyFocused,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
/// StartBtn
|
||||
BottomRight(
|
||||
child: UiScaler(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 20, right: 40),
|
||||
child: _StartBtn(onPressed: onStartPressed),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TitleText extends StatelessWidget {
|
||||
const _TitleText();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget content = Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Gap(20),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Transform.translate(
|
||||
offset: Offset(-(TextStyles.h1.letterSpacing! * .5), 0),
|
||||
child: Text('OUTPOST', style: TextStyles.h1),
|
||||
),
|
||||
Image.asset(AssetPaths.titleSelectedLeft, height: 65),
|
||||
Text('57', style: TextStyles.h2),
|
||||
Image.asset(AssetPaths.titleSelectedRight, height: 65),
|
||||
],
|
||||
).animate().fadeIn(delay: .8.seconds, duration: .7.seconds),
|
||||
Text('INTO THE UNKNOWN', style: TextStyles.h3)
|
||||
.animate()
|
||||
.fadeIn(delay: 1.seconds, duration: .7.seconds),
|
||||
],
|
||||
);
|
||||
return Consumer<Shaders?>(
|
||||
builder: (context, shaders, _) {
|
||||
if (shaders == null) return content;
|
||||
return TickingBuilder(
|
||||
builder: (context, time) {
|
||||
return AnimatedSampler(
|
||||
(image, size, canvas) {
|
||||
const double overdrawPx = 30;
|
||||
shaders.ui
|
||||
..setFloat(0, size.width)
|
||||
..setFloat(1, size.height)
|
||||
..setFloat(2, time)
|
||||
..setImageSampler(0, image);
|
||||
Rect rect = Rect.fromLTWH(-overdrawPx, -overdrawPx,
|
||||
size.width + overdrawPx, size.height + overdrawPx);
|
||||
canvas.drawRect(rect, Paint()..shader = shaders.ui);
|
||||
},
|
||||
child: content,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DifficultyBtns extends StatelessWidget {
|
||||
const _DifficultyBtns({
|
||||
required this.difficulty,
|
||||
required this.onDifficultyPressed,
|
||||
required this.onDifficultyFocused,
|
||||
});
|
||||
|
||||
final int difficulty;
|
||||
final void Function(int difficulty) onDifficultyPressed;
|
||||
final void Function(int? difficulty) onDifficultyFocused;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_DifficultyBtn(
|
||||
label: 'Casual',
|
||||
selected: difficulty == 0,
|
||||
onPressed: () => onDifficultyPressed(0),
|
||||
onHover: (over) => onDifficultyFocused(over ? 0 : null),
|
||||
)
|
||||
.animate()
|
||||
.fadeIn(delay: 1.3.seconds, duration: .35.seconds)
|
||||
.slide(begin: const Offset(0, .2)),
|
||||
_DifficultyBtn(
|
||||
label: 'Normal',
|
||||
selected: difficulty == 1,
|
||||
onPressed: () => onDifficultyPressed(1),
|
||||
onHover: (over) => onDifficultyFocused(over ? 1 : null),
|
||||
)
|
||||
.animate()
|
||||
.fadeIn(delay: 1.5.seconds, duration: .35.seconds)
|
||||
.slide(begin: const Offset(0, .2)),
|
||||
_DifficultyBtn(
|
||||
label: 'Hardcore',
|
||||
selected: difficulty == 2,
|
||||
onPressed: () => onDifficultyPressed(2),
|
||||
onHover: (over) => onDifficultyFocused(over ? 2 : null),
|
||||
)
|
||||
.animate()
|
||||
.fadeIn(delay: 1.7.seconds, duration: .35.seconds)
|
||||
.slide(begin: const Offset(0, .2)),
|
||||
const Gap(20),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DifficultyBtn extends StatelessWidget {
|
||||
const _DifficultyBtn({
|
||||
required this.selected,
|
||||
required this.onPressed,
|
||||
required this.onHover,
|
||||
required this.label,
|
||||
});
|
||||
final String label;
|
||||
final bool selected;
|
||||
final VoidCallback onPressed;
|
||||
final void Function(bool hasFocus) onHover;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FocusableControlBuilder(
|
||||
onPressed: onPressed,
|
||||
onHoverChanged: (_, state) => onHover.call(state.isHovered),
|
||||
builder: (_, state) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: SizedBox(
|
||||
width: 250,
|
||||
height: 60,
|
||||
child: Stack(
|
||||
children: [
|
||||
/// Bg with fill and outline
|
||||
AnimatedOpacity(
|
||||
opacity: (!selected && (state.isHovered || state.isFocused))
|
||||
? 1
|
||||
: 0,
|
||||
duration: .3.seconds,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF00D1FF).withOpacity(.1),
|
||||
border: Border.all(color: Colors.white, width: 5),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
if (state.isHovered || state.isFocused) ...[
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF00D1FF).withOpacity(.1),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
/// cross-hairs (selected state)
|
||||
if (selected) ...[
|
||||
CenterLeft(
|
||||
child: Image.asset(AssetPaths.titleSelectedLeft),
|
||||
),
|
||||
CenterRight(
|
||||
child: Image.asset(AssetPaths.titleSelectedRight),
|
||||
),
|
||||
],
|
||||
|
||||
/// Label
|
||||
Center(
|
||||
child: Text(label.toUpperCase(), style: TextStyles.btn),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StartBtn extends StatefulWidget {
|
||||
const _StartBtn({required this.onPressed});
|
||||
final VoidCallback onPressed;
|
||||
|
||||
@override
|
||||
State<_StartBtn> createState() => _StartBtnState();
|
||||
}
|
||||
|
||||
class _StartBtnState extends State<_StartBtn> {
|
||||
AnimationController? _btnAnim;
|
||||
bool _wasHovered = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FocusableControlBuilder(
|
||||
cursor: SystemMouseCursors.click,
|
||||
onPressed: widget.onPressed,
|
||||
builder: (_, state) {
|
||||
if ((state.isHovered || state.isFocused) &&
|
||||
!_wasHovered &&
|
||||
_btnAnim?.status != AnimationStatus.forward) {
|
||||
_btnAnim?.forward(from: 0);
|
||||
}
|
||||
_wasHovered = (state.isHovered || state.isFocused);
|
||||
return SizedBox(
|
||||
width: 520,
|
||||
height: 100,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(child: Image.asset(AssetPaths.titleStartBtn)),
|
||||
if (state.isHovered || state.isFocused) ...[
|
||||
Positioned.fill(
|
||||
child: Image.asset(AssetPaths.titleStartBtnHover)),
|
||||
],
|
||||
Center(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Text('START MISSION',
|
||||
style: TextStyles.btn
|
||||
.copyWith(fontSize: 24, letterSpacing: 18)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
.animate(autoPlay: false, onInit: (c) => _btnAnim = c)
|
||||
.shimmer(duration: .7.seconds, color: Colors.black),
|
||||
)
|
||||
.animate()
|
||||
.fadeIn(delay: 2.3.seconds)
|
||||
.slide(begin: const Offset(0, .2));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user