mirror of
https://github.com/flutter/samples.git
synced 2025-11-11 23:39:14 +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:
37
next_gen_ui_demo/lib/common/reactive_widget.dart
Normal file
37
next_gen_ui_demo/lib/common/reactive_widget.dart
Normal file
@@ -0,0 +1,37 @@
|
||||
// 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:flutter/material.dart';
|
||||
|
||||
import 'ticking_builder.dart';
|
||||
|
||||
typedef ReactiveWidgetBuilder = Widget Function(
|
||||
BuildContext context, double time, Size bounds);
|
||||
|
||||
/// ReactiveWidget forces repainting a subtree on
|
||||
/// each frame for ambient animation.
|
||||
class ReactiveWidget extends StatefulWidget {
|
||||
const ReactiveWidget({
|
||||
super.key,
|
||||
required this.builder,
|
||||
});
|
||||
final ReactiveWidgetBuilder builder;
|
||||
@override
|
||||
State<ReactiveWidget> createState() => _ReactiveWidgetState();
|
||||
}
|
||||
|
||||
class _ReactiveWidgetState extends State<ReactiveWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TickingBuilder(
|
||||
builder: (_, time) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return widget.builder(context, time, constraints.biggest);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
359
next_gen_ui_demo/lib/common/shader_effect.dart
Normal file
359
next_gen_ui_demo/lib/common/shader_effect.dart
Normal file
@@ -0,0 +1,359 @@
|
||||
// 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:ui' as ui;
|
||||
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
|
||||
/**
|
||||
* This is an unfinished, pre-release effect for Flutter Animate:
|
||||
* https://pub.dev/packages/flutter_animate
|
||||
*
|
||||
* It includes a copy of `AnimatedSampler` from Flutter Shaders:
|
||||
* https://github.com/jonahwilliams/flutter_shaders
|
||||
*
|
||||
* Once `AnimatedSampler` (or equivalent) is stable, or included in the core
|
||||
* SDK, this effect will be updated, tested, refined, and added to the
|
||||
* effects.dart file.
|
||||
*/
|
||||
|
||||
// TODO: document.
|
||||
|
||||
/// An effect that lets you apply an animated fragment shader to a target.
|
||||
@immutable
|
||||
class ShaderEffect extends Effect<double> {
|
||||
const ShaderEffect({
|
||||
super.delay,
|
||||
super.duration,
|
||||
super.curve,
|
||||
this.shader,
|
||||
this.update,
|
||||
ShaderLayer? layer,
|
||||
}) : layer = layer ?? ShaderLayer.replace,
|
||||
super(
|
||||
begin: 0,
|
||||
end: 1,
|
||||
);
|
||||
|
||||
final ui.FragmentShader? shader;
|
||||
final ShaderUpdateCallback? update;
|
||||
final ShaderLayer layer;
|
||||
|
||||
@override
|
||||
Widget build(
|
||||
BuildContext context,
|
||||
Widget child,
|
||||
AnimationController controller,
|
||||
EffectEntry entry,
|
||||
) {
|
||||
double ratio = 1 / MediaQuery.of(context).devicePixelRatio;
|
||||
Animation<double> animation = buildAnimation(controller, entry);
|
||||
return getOptimizedBuilder<double>(
|
||||
animation: animation,
|
||||
builder: (_, __) {
|
||||
return AnimatedSampler(
|
||||
(image, size, canvas) {
|
||||
EdgeInsets? insets;
|
||||
if (update != null) {
|
||||
insets = update!(shader!, animation.value, size, image);
|
||||
}
|
||||
Rect rect = Rect.fromLTWH(0, 0, size.width, size.height);
|
||||
rect = insets?.inflateRect(rect) ?? rect;
|
||||
|
||||
void drawImage() {
|
||||
canvas.save();
|
||||
canvas.scale(ratio, ratio);
|
||||
canvas.drawImage(image, Offset.zero, Paint());
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
if (layer == ShaderLayer.foreground) drawImage();
|
||||
if (shader != null) canvas.drawRect(rect, Paint()..shader = shader);
|
||||
if (layer == ShaderLayer.background) drawImage();
|
||||
},
|
||||
enabled: shader != null,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension ShaderEffectExtensions<T> on AnimateManager<T> {
|
||||
/// Adds a [shader] extension to [AnimateManager] ([Animate] and [AnimateList]).
|
||||
T shader({
|
||||
Duration? delay,
|
||||
Duration? duration,
|
||||
Curve? curve,
|
||||
ui.FragmentShader? shader,
|
||||
ShaderUpdateCallback? update,
|
||||
ShaderLayer? layer,
|
||||
}) =>
|
||||
addEffect(ShaderEffect(
|
||||
delay: delay,
|
||||
duration: duration,
|
||||
curve: curve,
|
||||
shader: shader,
|
||||
update: update,
|
||||
layer: layer,
|
||||
));
|
||||
}
|
||||
|
||||
enum ShaderLayer { foreground, background, replace }
|
||||
|
||||
/// Function signature for [ShaderEffect] update handlers.
|
||||
typedef ShaderUpdateCallback = EdgeInsets? Function(
|
||||
ui.FragmentShader shader, double value, Size size, ui.Image image);
|
||||
|
||||
/******************************************************************************/
|
||||
// TODO: add this as a dependency instead of copying it in once it is stable:
|
||||
// https://github.com/jonahwilliams/flutter_shaders
|
||||
|
||||
// Copyright 2013 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.
|
||||
|
||||
/// A callback for the [AnimatedSamplerBuilder] widget.
|
||||
typedef AnimatedSamplerBuilder = void Function(
|
||||
ui.Image image,
|
||||
Size size,
|
||||
ui.Canvas canvas,
|
||||
);
|
||||
|
||||
/// A widget that allows access to a snapshot of the child widgets for painting
|
||||
/// with a sampler applied to a [FragmentProgram].
|
||||
///
|
||||
/// When [enabled] is true, the child widgets will be painted into a texture
|
||||
/// exposed as a [ui.Image]. This can then be passed to a [FragmentShader]
|
||||
/// instance via [FragmentShader.setSampler].
|
||||
///
|
||||
/// If [enabled] is false, then the child widgets are painted as normal.
|
||||
///
|
||||
/// Caveats:
|
||||
/// * Platform views cannot be captured in a texture. If any are present they
|
||||
/// will be excluded from the texture. Texture-based platform views are OK.
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// Providing an image to a fragment shader using
|
||||
/// [FragmentShader.setImageSampler].
|
||||
///
|
||||
/// ```dart
|
||||
/// Widget build(BuildContext context) {
|
||||
/// return AnimatedSampler(
|
||||
/// (ui.Image image, Size size, Canvas canvas) {
|
||||
/// shader
|
||||
/// ..setFloat(0, size.width)
|
||||
/// ..setFloat(1, size.height)
|
||||
/// ..setImageSampler(0, image);
|
||||
/// canvas.drawRect(Offset.zero & size, Paint()..shader = shader);
|
||||
/// },
|
||||
/// child: widget.child,
|
||||
/// );
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// See also:
|
||||
/// * [SnapshotWidget], which provides a similar API for the purpose of
|
||||
/// caching during expensive animations.
|
||||
class AnimatedSampler extends StatelessWidget {
|
||||
/// Create a new [AnimatedSampler].
|
||||
const AnimatedSampler(
|
||||
this.builder, {
|
||||
required this.child,
|
||||
super.key,
|
||||
this.enabled = true,
|
||||
});
|
||||
|
||||
/// A callback used by this widget to provide the children captured in
|
||||
/// a texture.
|
||||
final AnimatedSamplerBuilder builder;
|
||||
|
||||
/// Whether the children should be captured in a texture or displayed as
|
||||
/// normal.
|
||||
final bool enabled;
|
||||
|
||||
/// The child widget.
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _ShaderSamplerBuilder(
|
||||
builder,
|
||||
enabled: enabled,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ShaderSamplerBuilder extends SingleChildRenderObjectWidget {
|
||||
const _ShaderSamplerBuilder(
|
||||
this.builder, {
|
||||
super.child,
|
||||
required this.enabled,
|
||||
});
|
||||
|
||||
final AnimatedSamplerBuilder builder;
|
||||
final bool enabled;
|
||||
|
||||
@override
|
||||
RenderObject createRenderObject(BuildContext context) {
|
||||
return _RenderShaderSamplerBuilderWidget(
|
||||
devicePixelRatio: MediaQuery.of(context).devicePixelRatio,
|
||||
builder: builder,
|
||||
enabled: enabled,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(
|
||||
BuildContext context, covariant RenderObject renderObject) {
|
||||
(renderObject as _RenderShaderSamplerBuilderWidget)
|
||||
..devicePixelRatio = MediaQuery.of(context).devicePixelRatio
|
||||
..builder = builder
|
||||
..enabled = enabled;
|
||||
}
|
||||
}
|
||||
|
||||
// A render object that conditionally converts its child into a [ui.Image]
|
||||
// and then paints it in place of the child.
|
||||
class _RenderShaderSamplerBuilderWidget extends RenderProxyBox {
|
||||
// Create a new [_RenderSnapshotWidget].
|
||||
_RenderShaderSamplerBuilderWidget({
|
||||
required double devicePixelRatio,
|
||||
required AnimatedSamplerBuilder builder,
|
||||
required bool enabled,
|
||||
}) : _devicePixelRatio = devicePixelRatio,
|
||||
_builder = builder,
|
||||
_enabled = enabled;
|
||||
|
||||
@override
|
||||
OffsetLayer updateCompositedLayer(
|
||||
{required covariant _ShaderSamplerBuilderLayer? oldLayer}) {
|
||||
final _ShaderSamplerBuilderLayer layer =
|
||||
oldLayer ?? _ShaderSamplerBuilderLayer(builder);
|
||||
layer
|
||||
..callback = builder
|
||||
..size = size
|
||||
..devicePixelRatio = devicePixelRatio;
|
||||
return layer;
|
||||
}
|
||||
|
||||
/// The device pixel ratio used to create the child image.
|
||||
double get devicePixelRatio => _devicePixelRatio;
|
||||
double _devicePixelRatio;
|
||||
set devicePixelRatio(double value) {
|
||||
if (value == devicePixelRatio) {
|
||||
return;
|
||||
}
|
||||
_devicePixelRatio = value;
|
||||
markNeedsCompositedLayerUpdate();
|
||||
}
|
||||
|
||||
/// The painter used to paint the child snapshot or child widgets.
|
||||
AnimatedSamplerBuilder get builder => _builder;
|
||||
AnimatedSamplerBuilder _builder;
|
||||
set builder(AnimatedSamplerBuilder value) {
|
||||
if (value == builder) {
|
||||
return;
|
||||
}
|
||||
_builder = value;
|
||||
markNeedsCompositedLayerUpdate();
|
||||
}
|
||||
|
||||
bool get enabled => _enabled;
|
||||
bool _enabled;
|
||||
set enabled(bool value) {
|
||||
if (value == enabled) {
|
||||
return;
|
||||
}
|
||||
_enabled = value;
|
||||
markNeedsPaint();
|
||||
markNeedsCompositingBitsUpdate();
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isRepaintBoundary => alwaysNeedsCompositing;
|
||||
|
||||
@override
|
||||
bool get alwaysNeedsCompositing => enabled;
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
if (size.isEmpty || !_enabled) {
|
||||
return;
|
||||
}
|
||||
assert(offset == Offset.zero);
|
||||
return super.paint(context, offset);
|
||||
}
|
||||
}
|
||||
|
||||
/// A [Layer] that uses an [AnimatedSamplerBuilder] to create a [ui.Picture]
|
||||
/// every time it is added to a scene.
|
||||
class _ShaderSamplerBuilderLayer extends OffsetLayer {
|
||||
_ShaderSamplerBuilderLayer(this._callback);
|
||||
|
||||
Size get size => _size;
|
||||
Size _size = Size.zero;
|
||||
set size(Size value) {
|
||||
if (value == size) {
|
||||
return;
|
||||
}
|
||||
_size = value;
|
||||
markNeedsAddToScene();
|
||||
}
|
||||
|
||||
double get devicePixelRatio => _devicePixelRatio;
|
||||
double _devicePixelRatio = 1.0;
|
||||
set devicePixelRatio(double value) {
|
||||
if (value == devicePixelRatio) {
|
||||
return;
|
||||
}
|
||||
_devicePixelRatio = value;
|
||||
markNeedsAddToScene();
|
||||
}
|
||||
|
||||
AnimatedSamplerBuilder get callback => _callback;
|
||||
AnimatedSamplerBuilder _callback;
|
||||
set callback(AnimatedSamplerBuilder value) {
|
||||
if (value == callback) {
|
||||
return;
|
||||
}
|
||||
_callback = value;
|
||||
markNeedsAddToScene();
|
||||
}
|
||||
|
||||
ui.Image _buildChildScene(Rect bounds, double pixelRatio) {
|
||||
final ui.SceneBuilder builder = ui.SceneBuilder();
|
||||
final Matrix4 transform =
|
||||
Matrix4.diagonal3Values(pixelRatio, pixelRatio, 1);
|
||||
builder.pushTransform(transform.storage);
|
||||
addChildrenToScene(builder);
|
||||
builder.pop();
|
||||
return builder.build().toImageSync(
|
||||
(pixelRatio * bounds.width).ceil(),
|
||||
(pixelRatio * bounds.height).ceil(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void addToScene(ui.SceneBuilder builder) {
|
||||
if (size.isEmpty) return;
|
||||
final ui.Image image = _buildChildScene(
|
||||
offset & size,
|
||||
devicePixelRatio,
|
||||
);
|
||||
final ui.PictureRecorder pictureRecorder = ui.PictureRecorder();
|
||||
final Canvas canvas = Canvas(pictureRecorder);
|
||||
try {
|
||||
callback(image, size, canvas);
|
||||
} finally {
|
||||
image.dispose();
|
||||
}
|
||||
final ui.Picture picture = pictureRecorder.endRecording();
|
||||
builder.addPicture(offset, picture);
|
||||
}
|
||||
}
|
||||
28
next_gen_ui_demo/lib/common/shader_painter.dart
Normal file
28
next_gen_ui_demo/lib/common/shader_painter.dart
Normal file
@@ -0,0 +1,28 @@
|
||||
// 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:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ShaderPainter extends CustomPainter {
|
||||
ShaderPainter(this.shader, {this.update});
|
||||
|
||||
final FragmentShader shader;
|
||||
final void Function(FragmentShader, Size)? update;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
update?.call(shader, size);
|
||||
canvas.drawRect(
|
||||
Rect.fromLTWH(0, 0, size.width, size.height),
|
||||
Paint()..shader = shader,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant ShaderPainter oldDelegate) {
|
||||
return oldDelegate.shader != shader || oldDelegate.update != update;
|
||||
}
|
||||
}
|
||||
40
next_gen_ui_demo/lib/common/ticking_builder.dart
Normal file
40
next_gen_ui_demo/lib/common/ticking_builder.dart
Normal file
@@ -0,0 +1,40 @@
|
||||
// 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:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
|
||||
/// TickingBuilder is for ambient animation to be run
|
||||
/// on each frame.
|
||||
class TickingBuilder extends StatefulWidget {
|
||||
const TickingBuilder({super.key, required this.builder});
|
||||
final Widget Function(BuildContext context, double time) builder;
|
||||
@override
|
||||
State<TickingBuilder> createState() => _TickingBuilderState();
|
||||
}
|
||||
|
||||
class _TickingBuilderState extends State<TickingBuilder>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final Ticker _ticker;
|
||||
double _time = 0.0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_ticker = createTicker(_handleTick)..start();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_ticker.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _handleTick(Duration elapsed) {
|
||||
setState(() => _time = elapsed.inMilliseconds.toDouble() / 1000.0);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => widget.builder.call(context, _time);
|
||||
}
|
||||
31
next_gen_ui_demo/lib/common/ui_scaler.dart
Normal file
31
next_gen_ui_demo/lib/common/ui_scaler.dart
Normal file
@@ -0,0 +1,31 @@
|
||||
// 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 'package:flutter/material.dart';
|
||||
|
||||
class UiScaler extends StatelessWidget {
|
||||
const UiScaler({
|
||||
super.key,
|
||||
required this.child,
|
||||
required this.alignment,
|
||||
this.referenceHeight = 1080,
|
||||
});
|
||||
|
||||
final int referenceHeight;
|
||||
final Widget child;
|
||||
final Alignment alignment;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenSize = MediaQuery.of(context).size;
|
||||
final double scale = min(screenSize.height / referenceHeight, 1.0);
|
||||
return Transform.scale(
|
||||
scale: scale,
|
||||
alignment: alignment,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user