| // 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), | |
| ), | |
| ); | |
| } | |
| } |