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