blob: c1ebc9249c24c893a59c879519e09c555b083b6e [file] [log] [blame]
// Copyright 2014 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';
import '../common.dart';
// Various tests to verify that the Opacity layer propagates the opacity to various
// combinations of children that can apply it themselves.
// See https://github.com/flutter/flutter/issues/75697
class OpacityPeepholePage extends StatelessWidget {
const OpacityPeepholePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Opacity Peephole tests')),
body: ListView(
key: const Key(kOpacityScrollableName),
children: <Widget>[
for (OpacityPeepholeCase variant in allOpacityPeepholeCases)
ElevatedButton(
key: Key(variant.route),
child: Text(variant.name),
onPressed: () {
Navigator.pushNamed(context, variant.route);
},
),
],
),
);
}
}
typedef ValueBuilder = Widget Function(double v);
typedef AnimationBuilder = Widget Function(Animation<double> animation);
double _opacity(double v) => v * 0.5 + 0.25;
int _red(double v) => (v * 255).round();
int _green(double v) => _red(1 - v);
int _blue(double v) => 0;
class OpacityPeepholeCase {
OpacityPeepholeCase.forValue({required String route, required String name, required ValueBuilder builder})
: this.forAnimation(
route: route,
name: name,
builder: (Animation<double> animation) => AnimatedBuilder(
animation: animation,
builder: (BuildContext context, Widget? child) => builder(animation.value),
),
);
OpacityPeepholeCase.forAnimation({required this.route, required this.name, required AnimationBuilder builder})
: animationBuilder = builder;
final String route;
final String name;
final AnimationBuilder animationBuilder;
Widget buildPage(BuildContext context) {
return VariantPage(variant: this);
}
}
List<OpacityPeepholeCase> allOpacityPeepholeCases = <OpacityPeepholeCase>[
// Tests that Opacity can hand down value to a simple child
OpacityPeepholeCase.forValue(
route: kOpacityPeepholeOneRectRouteName,
name: 'One Big Rectangle',
builder: (double v) {
return Opacity(
opacity: _opacity(v),
child: Container(
width: 300,
height: 400,
color: Color.fromARGB(255, _red(v), _green(v), _blue(v)),
),
);
}
),
// Tests that a column of Opacity widgets can individually hand their values down to simple children
OpacityPeepholeCase.forValue(
route: kOpacityPeepholeColumnOfOpacityRouteName,
name: 'Column of Opacity',
builder: (double v) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
for (int i = 0; i < 10; i++, v = 1 - v)
Opacity(
opacity: _opacity(v),
child: Padding(
padding: const EdgeInsets.all(5),
child: Container(
width: 300,
height: 30,
color: Color.fromARGB(255, _red(v), _green(v), _blue(v)),
),
),
),
],
);
},
),
// Tests that an Opacity can hand value down to a cached child
OpacityPeepholeCase.forValue(
route: kOpacityPeepholeOpacityOfCachedChildRouteName,
name: 'Opacity of Cached Child',
builder: (double v) {
// ChildV starts as a constant so the same color pattern always appears and the child will be cached
double childV = 0;
return Opacity(
opacity: _opacity(v),
child: RepaintBoundary(
child: SizedBox(
width: 300,
height: 400,
child: Stack(
children: <Widget>[
for (double i = 0; i < 100; i += 10, childV = 1 - childV)
Positioned.fromRelativeRect(
rect: RelativeRect.fromLTRB(i, i, i, i),
child: Container(
color: Color.fromARGB(255, _red(childV), _green(childV), _blue(childV)),
),
),
],
),
),
),
);
}
),
// Tests that an Opacity can hand a value down to a Column of simple non-overlapping children
OpacityPeepholeCase.forValue(
route: kOpacityPeepholeOpacityOfColumnRouteName,
name: 'Opacity of Column',
builder: (double v) {
return Opacity(
opacity: _opacity(v),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
for (int i = 0; i < 10; i++, v = 1 - v)
Padding(
padding: const EdgeInsets.all(5),
// RepaintBoundary here to avoid combining children into 1 big Picture
child: RepaintBoundary(
child: Container(
width: 300,
height: 30,
color: Color.fromARGB(255, _red(v), _green(v), _blue(v)),
),
),
),
],
),
);
},
),
// Tests that an entire grid of Opacity objects can hand their values down to their simple children
OpacityPeepholeCase.forValue(
route: kOpacityPeepholeGridOfOpacityRouteName,
name: 'Grid of Opacity',
builder: (double v) {
double rowV = v;
double colV = rowV;
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
for (int i = 0; i < 10; i++, rowV = 1 - rowV, colV = rowV)
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
for (int j = 0; j < 7; j++, colV = 1 - colV)
Opacity(
opacity: _opacity(colV),
child: Padding(
padding: const EdgeInsets.all(5),
child: Container(
width: 30,
height: 30,
color: Color.fromARGB(255, _red(colV), _green(colV), _blue(colV)),
),
),
),
],
),
],
);
},
),
// tests if an Opacity can hand its value down to a 2D grid of simple non-overlapping children.
// The success of this case would depend on the sophistication of the non-overlapping tests.
OpacityPeepholeCase.forValue(
route: kOpacityPeepholeOpacityOfGridRouteName,
name: 'Opacity of Grid',
builder: (double v) {
double rowV = v;
double colV = rowV;
return Opacity(
opacity: _opacity(v),
child: SizedBox(
width: 300,
height: 400,
child: Stack(
children: <Widget>[
for (int i = 0; i < 10; i++, rowV = 1 - rowV, colV = rowV)
for (int j = 0; j < 7; j++, colV = 1 - colV)
Positioned.fromRect(
rect: Rect.fromLTWH(j * 40 + 5, i * 40 + 5, 30, 30),
// RepaintBoundary here to avoid combining the 70 children into a single Picture
child: RepaintBoundary(
child: Container(
color: Color.fromARGB(255, _red(colV), _green(colV), _blue(colV)),
),
),
),
],
),
),
);
},
),
// tests if an Opacity can hand its value down to a Column of non-overlapping rows of non-overlapping simple children.
// This test only requires linear non-overlapping tests to succeed.
OpacityPeepholeCase.forValue(
route: kOpacityPeepholeOpacityOfColOfRowsRouteName,
name: 'Opacity of Column of Rows',
builder: (double v) {
double rowV = v;
double colV = v;
return Opacity(
opacity: _opacity(v),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
for (int i = 0; i < 10; i++, rowV = 1 - rowV, colV = rowV)
Padding(
padding: const EdgeInsets.only(top: 5, bottom: 5),
// RepaintBoundary here to separate each row into a separate layer child
child: RepaintBoundary(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
for (int j = 0; j < 7; j++, colV = 1 - colV)
Padding(
padding: const EdgeInsets.only(left: 5, right: 5),
// RepaintBoundary here to prevent the row children combining into a single Picture
child: RepaintBoundary(
child: Container(
width: 30,
height: 30,
color: Color.fromARGB(255, _red(colV), _green(colV), _blue(colV)),
),
),
),
],
),
),
),
],
),
);
},
),
OpacityPeepholeCase.forAnimation(
route: kOpacityPeepholeFadeTransitionTextRouteName,
name: 'FadeTransition text',
builder: (Animation<double> animation) {
return FadeTransition(
opacity: Tween<double>(begin: 0.25, end: 0.75).animate(animation),
child: const SizedBox(
width: 300,
height: 400,
child: Center(
child: Text('Hello, World',
style: TextStyle(fontSize: 48),
),
),
),
);
},
),
OpacityPeepholeCase.forValue(
route: kOpacityPeepholeGridOfRectsWithAlphaRouteName,
name: 'Grid of Rectangles with alpha',
builder: (double v) {
return Opacity(
opacity: _opacity(v),
child: SizedBox.expand(
child: CustomPaint(
painter: RectGridPainter((Canvas canvas, Size size) {
const int numRows = 10;
const int numCols = 7;
const double rectWidth = 30;
const double rectHeight = 30;
final double hGap = (size.width - numCols * rectWidth) / (numCols + 1);
final double vGap = (size.height - numRows * rectHeight) / (numRows + 1);
final double gap = min(hGap, vGap);
final double xOffset = (size.width - (numCols * (rectWidth + gap) - gap)) * 0.5;
final double yOffset = (size.height - (numRows * (rectHeight + gap) - gap)) * 0.5;
final Paint rectPaint = Paint();
for (int r = 0; r < numRows; r++, v = 1 - v) {
final double y = yOffset + r * (rectHeight + gap);
double cv = v;
for (int c = 0; c < numCols; c++, cv = 1 - cv) {
final double x = xOffset + c * (rectWidth + gap);
rectPaint.color = Color.fromRGBO(_red(cv), _green(cv), _blue(cv), _opacity(cv));
final Rect rect = Rect.fromLTWH(x, y, rectWidth, rectHeight);
canvas.drawRect(rect, rectPaint);
}
}
}),
),
),
);
},
),
OpacityPeepholeCase.forValue(
route: kOpacityPeepholeGridOfAlphaSaveLayerRectsRouteName,
name: 'Grid of alpha SaveLayers of Rectangles',
builder: (double v) {
return Opacity(
opacity: _opacity(v),
child: SizedBox.expand(
child: CustomPaint(
painter: RectGridPainter((Canvas canvas, Size size) {
const int numRows = 10;
const int numCols = 7;
const double rectWidth = 30;
const double rectHeight = 30;
final double hGap = (size.width - numCols * rectWidth) / (numCols + 1);
final double vGap = (size.height - numRows * rectHeight) / (numRows + 1);
final double gap = min(hGap, vGap);
final double xOffset = (size.width - (numCols * (rectWidth + gap) - gap)) * 0.5;
final double yOffset = (size.height - (numRows * (rectHeight + gap) - gap)) * 0.5;
final Paint rectPaint = Paint();
final Paint layerPaint = Paint();
for (int r = 0; r < numRows; r++, v = 1 - v) {
final double y = yOffset + r * (rectHeight + gap);
double cv = v;
for (int c = 0; c < numCols; c++, cv = 1 - cv) {
final double x = xOffset + c * (rectWidth + gap);
rectPaint.color = Color.fromRGBO(_red(cv), _green(cv), _blue(cv), 1.0);
layerPaint.color = Color.fromRGBO(255, 255, 255, _opacity(cv));
final Rect rect = Rect.fromLTWH(x, y, rectWidth, rectHeight);
canvas.saveLayer(null, layerPaint);
canvas.drawRect(rect, rectPaint);
canvas.restore();
}
}
}),
),
),
);
},
),
OpacityPeepholeCase.forValue(
route: kOpacityPeepholeColumnOfAlphaSaveLayerRowsOfRectsRouteName,
name: 'Grid with alpha SaveLayer on Rows',
builder: (double v) {
return Opacity(
opacity: _opacity(v),
child: SizedBox.expand(
child: CustomPaint(
painter: RectGridPainter((Canvas canvas, Size size) {
const int numRows = 10;
const int numCols = 7;
const double rectWidth = 30;
const double rectHeight = 30;
final double hGap = (size.width - numCols * rectWidth) / (numCols + 1);
final double vGap = (size.height - numRows * rectHeight) / (numRows + 1);
final double gap = min(hGap, vGap);
final double xOffset = (size.width - (numCols * (rectWidth + gap) - gap)) * 0.5;
final double yOffset = (size.height - (numRows * (rectHeight + gap) - gap)) * 0.5;
final Paint rectPaint = Paint();
final Paint layerPaint = Paint();
for (int r = 0; r < numRows; r++, v = 1 - v) {
final double y = yOffset + r * (rectHeight + gap);
layerPaint.color = Color.fromRGBO(255, 255, 255, _opacity(v));
canvas.saveLayer(null, layerPaint);
double cv = v;
for (int c = 0; c < numCols; c++, cv = 1 - cv) {
final double x = xOffset + c * (rectWidth + gap);
rectPaint.color = Color.fromRGBO(_red(cv), _green(cv), _blue(cv), 1.0);
final Rect rect = Rect.fromLTWH(x, y, rectWidth, rectHeight);
canvas.drawRect(rect, rectPaint);
}
canvas.restore();
}
}),
),
),
);
},
),
];
class RectGridPainter extends CustomPainter {
RectGridPainter(this.painter);
final void Function(Canvas canvas, Size size) painter;
@override
void paint(Canvas canvas, Size size) => painter(canvas, size);
@override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}
Map<String, WidgetBuilder> opacityPeepholeRoutes = <String, WidgetBuilder>{
for (OpacityPeepholeCase variant in allOpacityPeepholeCases)
variant.route: variant.buildPage,
};
class VariantPage extends StatefulWidget {
const VariantPage({super.key, required this.variant});
final OpacityPeepholeCase variant;
@override
State<VariantPage> createState() => VariantPageState();
}
class VariantPageState extends State<VariantPage> with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: const Duration(seconds: 4));
_controller.repeat(reverse: true);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.variant.name),
),
body: Center(
child: widget.variant.animationBuilder(_controller),
),
);
}
}