mirror of https://github.com/flutter/samples.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
272 lines
7.4 KiB
272 lines
7.4 KiB
// Copyright 2023 The Flutter team. 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/material.dart';
|
|
import 'package:flutter/rendering.dart';
|
|
|
|
class FragmentShaded extends StatefulWidget {
|
|
final Widget child;
|
|
final String shaderName;
|
|
final int shaderDuration;
|
|
static const int dampenDuration = 1000;
|
|
static final Map<String, ui.FragmentProgram> fragmentPrograms = {};
|
|
static const List<String> fragmentProgramNames = [
|
|
'nothing',
|
|
'bw_split',
|
|
'color_split',
|
|
'row_offset',
|
|
'wavy_circ',
|
|
'wavy',
|
|
'wavy2'
|
|
];
|
|
|
|
const FragmentShaded({
|
|
required this.shaderName,
|
|
required this.shaderDuration,
|
|
required this.child,
|
|
super.key,
|
|
});
|
|
|
|
@override
|
|
State<FragmentShaded> createState() => FragmentShadedState();
|
|
}
|
|
|
|
class FragmentShadedState extends State<FragmentShaded>
|
|
with TickerProviderStateMixin {
|
|
late final AnimationController _controller;
|
|
late final Animation<double> _dampenAnimation;
|
|
late final Animation<double> _dampenCurve;
|
|
late final AnimationController _dampenController;
|
|
late AnimatingSamplerBuilder builder;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_controller = AnimationController(
|
|
vsync: this,
|
|
duration: Duration(milliseconds: widget.shaderDuration),
|
|
)..repeat(reverse: false);
|
|
_dampenController = AnimationController(
|
|
vsync: this,
|
|
duration: const Duration(milliseconds: FragmentShaded.dampenDuration),
|
|
);
|
|
_dampenCurve = CurvedAnimation(
|
|
parent: _dampenController,
|
|
curve: Curves.easeInOut,
|
|
);
|
|
_dampenAnimation =
|
|
Tween<double>(begin: 1.0, end: 0.0).animate(_dampenCurve);
|
|
initializeFragmentProgramsAndBuilder();
|
|
}
|
|
|
|
void initializeFragmentProgramsAndBuilder() async {
|
|
if (FragmentShaded.fragmentPrograms.isEmpty) {
|
|
for (String s in FragmentShaded.fragmentProgramNames) {
|
|
FragmentShaded.fragmentPrograms[s] =
|
|
await ui.FragmentProgram.fromAsset('shaders/$s.frag');
|
|
}
|
|
}
|
|
builder = AnimatingSamplerBuilder(_controller, _dampenAnimation,
|
|
FragmentShaded.fragmentPrograms[widget.shaderName]!.fragmentShader());
|
|
setState(() {});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
_dampenController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (null == FragmentShaded.fragmentPrograms[widget.shaderName]) {
|
|
setState(() {});
|
|
return const SizedBox(
|
|
width: 0,
|
|
height: 0,
|
|
);
|
|
}
|
|
return Transform.scale(
|
|
scale: 0.5,
|
|
child: ShaderSamplerBuilder(
|
|
builder,
|
|
child: widget.child,
|
|
),
|
|
);
|
|
}
|
|
|
|
void startDampening() {
|
|
_dampenController.forward();
|
|
}
|
|
}
|
|
|
|
class AnimatingSamplerBuilder extends SamplerBuilder {
|
|
AnimatingSamplerBuilder(
|
|
this.animation, this.dampenAnimation, this.fragmentShader) {
|
|
animation.addListener(notifyListeners);
|
|
dampenAnimation.addListener(notifyListeners);
|
|
}
|
|
|
|
final Animation<double> animation;
|
|
final Animation<double> dampenAnimation;
|
|
|
|
final ui.FragmentShader fragmentShader;
|
|
|
|
@override
|
|
void paint(ui.Image image, Size size, ui.Canvas canvas) {
|
|
// animation
|
|
fragmentShader.setFloat(0, animation.value);
|
|
// width
|
|
fragmentShader.setFloat(1, size.width);
|
|
// height
|
|
fragmentShader.setFloat(2, size.height);
|
|
// dampener
|
|
fragmentShader.setFloat(3, dampenAnimation.value);
|
|
// sampler
|
|
fragmentShader.setImageSampler(0, image);
|
|
|
|
canvas.drawRect(Offset.zero & size, Paint()..shader = fragmentShader);
|
|
}
|
|
}
|
|
|
|
abstract class SamplerBuilder extends ChangeNotifier {
|
|
void paint(ui.Image image, Size size, ui.Canvas canvas);
|
|
}
|
|
|
|
class ShaderSamplerBuilder extends StatelessWidget {
|
|
const ShaderSamplerBuilder(this.builder, {required this.child, super.key});
|
|
|
|
final SamplerBuilder builder;
|
|
final Widget child;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return RepaintBoundary(
|
|
child: _ShaderSamplerImpl(
|
|
builder,
|
|
child: child,
|
|
));
|
|
}
|
|
}
|
|
|
|
class _ShaderSamplerImpl extends SingleChildRenderObjectWidget {
|
|
const _ShaderSamplerImpl(this.builder, {super.child});
|
|
|
|
final SamplerBuilder builder;
|
|
|
|
@override
|
|
RenderObject createRenderObject(BuildContext context) {
|
|
return _RenderShaderSamplerBuilderWidget(
|
|
devicePixelRatio: MediaQuery.of(context).devicePixelRatio,
|
|
builder: builder,
|
|
);
|
|
}
|
|
|
|
@override
|
|
void updateRenderObject(
|
|
BuildContext context, covariant RenderObject renderObject) {
|
|
(renderObject as _RenderShaderSamplerBuilderWidget)
|
|
..devicePixelRatio = MediaQuery.of(context).devicePixelRatio
|
|
..builder = builder;
|
|
}
|
|
}
|
|
|
|
// 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 SamplerBuilder builder,
|
|
}) : _devicePixelRatio = devicePixelRatio,
|
|
_builder = builder;
|
|
|
|
/// 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;
|
|
if (_childRaster == null) {
|
|
return;
|
|
} else {
|
|
_childRaster?.dispose();
|
|
_childRaster = null;
|
|
markNeedsPaint();
|
|
}
|
|
}
|
|
|
|
/// The painter used to paint the child snapshot or child widgets.
|
|
SamplerBuilder get builder => _builder;
|
|
SamplerBuilder _builder;
|
|
set builder(SamplerBuilder value) {
|
|
if (value == builder) {
|
|
return;
|
|
}
|
|
builder.removeListener(markNeedsPaint);
|
|
_builder = value;
|
|
builder.addListener(markNeedsPaint);
|
|
markNeedsPaint();
|
|
}
|
|
|
|
ui.Image? _childRaster;
|
|
|
|
@override
|
|
void attach(PipelineOwner owner) {
|
|
builder.addListener(markNeedsPaint);
|
|
super.attach(owner);
|
|
}
|
|
|
|
@override
|
|
void detach() {
|
|
_childRaster?.dispose();
|
|
_childRaster = null;
|
|
builder.removeListener(markNeedsPaint);
|
|
super.detach();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
builder.removeListener(markNeedsPaint);
|
|
_childRaster?.dispose();
|
|
_childRaster = null;
|
|
super.dispose();
|
|
}
|
|
|
|
// Paint [child] with this painting context, then convert to a raster and detach all
|
|
// children from this layer.
|
|
ui.Image? _paintAndDetachToImage() {
|
|
final OffsetLayer offsetLayer = OffsetLayer();
|
|
final PaintingContext context =
|
|
PaintingContext(offsetLayer, Offset.zero & size);
|
|
super.paint(context, Offset.zero);
|
|
// This ignore is here because this method is protected by the `PaintingContext`. Adding a new
|
|
// method that performs the work of `_paintAndDetachToImage` would avoid the need for this, but
|
|
// that would conflict with our goals of minimizing painting context.
|
|
// ignore: invalid_use_of_protected_member
|
|
context.stopRecordingIfNeeded();
|
|
final ui.Image image = offsetLayer.toImageSync(Offset.zero & size,
|
|
pixelRatio: devicePixelRatio);
|
|
offsetLayer.dispose();
|
|
return image;
|
|
}
|
|
|
|
@override
|
|
void paint(PaintingContext context, Offset offset) {
|
|
if (size.isEmpty) {
|
|
_childRaster?.dispose();
|
|
_childRaster = null;
|
|
return;
|
|
}
|
|
_childRaster?.dispose();
|
|
_childRaster = _paintAndDetachToImage();
|
|
builder.paint(_childRaster!, size, context.canvas);
|
|
}
|
|
}
|