diff --git a/packages/flutter/lib/src/rendering/view.dart b/packages/flutter/lib/src/rendering/view.dart index a17a31814d4..7dc47d2f962 100644 --- a/packages/flutter/lib/src/rendering/view.dart +++ b/packages/flutter/lib/src/rendering/view.dart @@ -244,9 +244,47 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin } void _updateSystemChrome() { + // Take overlay style from the place where a system status bar and system + // navigation bar are placed to update system style overlay. + // The center of the system navigation bar and the center of the status bar + // are used to get SystemUiOverlayStyle's to update system overlay appearance. + // + // Horizontal center of the screen + // V + // ++++++++++++++++++++++++++ + // | | + // | System status bar | <- Vertical center of the status bar + // | | + // ++++++++++++++++++++++++++ + // | | + // | Content | + // ~ ~ + // | | + // ++++++++++++++++++++++++++ + // | | + // | System navigation bar | <- Vertical center of the navigation bar + // | | + // ++++++++++++++++++++++++++ <- bounds.bottom final Rect bounds = paintBounds; - final Offset top = Offset(bounds.center.dx, _window.padding.top / _window.devicePixelRatio); - final Offset bottom = Offset(bounds.center.dx, bounds.center.dy - _window.padding.bottom / _window.devicePixelRatio); + // Center of the status bar + final Offset top = Offset( + // Horizontal center of the screen + bounds.center.dx, + // The vertical center of the system status bar. The system status bar + // height is kept as top window padding. + _window.padding.top / 2.0, + ); + // Center of the navigation bar + final Offset bottom = Offset( + // Horizontal center of the screen + bounds.center.dx, + // Vertical center of the system navigation bar. The system navigation bar + // height is kept as bottom window padding. The "1" needs to be subtracted + // from the bottom because available pixels are in (0..bottom) range. + // I.e. for a device with 1920 height, bound.bottom is 1920, but the most + // bottom drawn pixel is at 1919 position. + bounds.bottom - 1.0 - _window.padding.bottom / 2.0, + ); final SystemUiOverlayStyle? upperOverlayStyle = layer!.find(top); // Only android has a customizable system navigation bar. SystemUiOverlayStyle? lowerOverlayStyle; diff --git a/packages/flutter/test/rendering/view_chrome_style_test.dart b/packages/flutter/test/rendering/view_chrome_style_test.dart new file mode 100644 index 00000000000..1050b5edaad --- /dev/null +++ b/packages/flutter/test/rendering/view_chrome_style_test.dart @@ -0,0 +1,253 @@ +// 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. + +// @dart = 2.8 +import 'dart:ui'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('SystemChrome - style', () { + const double statusBarHeight = 25.0; + const double navigationBarHeight = 54.0; + const double deviceHeight = 960.0; + const double deviceWidth = 480.0; + const double devicePixelRatio = 2.0; + + void setupTestDevice() { + final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized() as TestWidgetsFlutterBinding; + const FakeWindowPadding padding = FakeWindowPadding( + top: statusBarHeight * devicePixelRatio, + bottom: navigationBarHeight * devicePixelRatio, + ); + + binding.window + ..viewPaddingTestValue = padding + ..paddingTestValue = padding + ..devicePixelRatioTestValue = devicePixelRatio + ..physicalSizeTestValue = const Size( + deviceWidth * devicePixelRatio, + deviceHeight * devicePixelRatio, + ); + } + + tearDown(() async { + SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle()); + await pumpEventQueue(); + }); + + group('status bar', () { + testWidgets( + 'statusBarColor isn\'t set for unannotated view', + (WidgetTester tester) async { + await tester.pumpWidget(const SizedBox.expand()); + await tester.pumpAndSettle(); + + expect(SystemChrome.latestStyle?.statusBarColor, isNull); + }, + ); + + testWidgets( + 'statusBarColor is set for annotated view', + (WidgetTester tester) async { + setupTestDevice(); + await tester.pumpWidget(const AnnotatedRegion( + value: SystemUiOverlayStyle( + statusBarColor: Colors.blue, + ), + child: SizedBox.expand(), + )); + await tester.pumpAndSettle(); + + expect( + SystemChrome.latestStyle?.statusBarColor, + Colors.blue, + ); + }, + variant: TargetPlatformVariant.mobile(), + ); + + testWidgets( + 'statusBarColor isn\'t set when view covers less than half of the system status bar', + (WidgetTester tester) async { + setupTestDevice(); + const double lessThanHalfOfTheStatusBarHeight = + statusBarHeight / 2.0 - 1; + await tester.pumpWidget(const Align( + alignment: Alignment.topCenter, + child: AnnotatedRegion( + value: SystemUiOverlayStyle( + statusBarColor: Colors.blue, + ), + child: SizedBox( + width: 100, + height: lessThanHalfOfTheStatusBarHeight, + ), + ), + )); + await tester.pumpAndSettle(); + + expect(SystemChrome.latestStyle?.statusBarColor, isNull); + }, + variant: TargetPlatformVariant.mobile(), + ); + + testWidgets( + 'statusBarColor is set when view covers more than half of tye system status bar', + (WidgetTester tester) async { + setupTestDevice(); + const double moreThanHalfOfTheStatusBarHeight = + statusBarHeight / 2.0 + 1; + await tester.pumpWidget(const Align( + alignment: Alignment.topCenter, + child: AnnotatedRegion( + value: SystemUiOverlayStyle( + statusBarColor: Colors.blue, + ), + child: SizedBox( + width: 100, + height: moreThanHalfOfTheStatusBarHeight, + ), + ), + )); + await tester.pumpAndSettle(); + + expect( + SystemChrome.latestStyle?.statusBarColor, + Colors.blue, + ); + }, + variant: TargetPlatformVariant.mobile(), + ); + }); + + group('navigation color (Android only)', () { + testWidgets( + 'systemNavigationBarColor isn\'t set for non Android device', + (WidgetTester tester) async { + setupTestDevice(); + await tester.pumpWidget(const AnnotatedRegion( + value: SystemUiOverlayStyle( + systemNavigationBarColor: Colors.blue, + ), + child: SizedBox.expand(), + )); + await tester.pumpAndSettle(); + + expect( + SystemChrome.latestStyle?.systemNavigationBarColor, + isNull, + ); + }, + variant: TargetPlatformVariant.only(TargetPlatform.iOS), + ); + + testWidgets( + 'systemNavigationBarColor isn\'t set for unannotated view', + (WidgetTester tester) async { + await tester.pumpWidget(const SizedBox.expand()); + await tester.pumpAndSettle(); + + expect(SystemChrome.latestStyle?.systemNavigationBarColor, isNull); + }, + variant: TargetPlatformVariant.only(TargetPlatform.android), + ); + + testWidgets( + 'systemNavigationBarColor is set for annotated view', + (WidgetTester tester) async { + setupTestDevice(); + await tester.pumpWidget(const AnnotatedRegion( + value: SystemUiOverlayStyle( + systemNavigationBarColor: Colors.blue, + ), + child: SizedBox.expand(), + )); + await tester.pumpAndSettle(); + + expect( + SystemChrome.latestStyle?.systemNavigationBarColor, + Colors.blue, + ); + }, + variant: TargetPlatformVariant.only(TargetPlatform.android), + ); + + testWidgets( + 'systemNavigationBarColor isn\'t set when view covers less than half of navigation bar', + (WidgetTester tester) async { + setupTestDevice(); + const double lessThanHalfOfTheNavigationBarHeight = + navigationBarHeight / 2.0 - 1; + await tester.pumpWidget(const Align( + alignment: Alignment.bottomCenter, + child: AnnotatedRegion( + value: SystemUiOverlayStyle( + systemNavigationBarColor: Colors.blue, + ), + child: SizedBox( + width: 100, + height: lessThanHalfOfTheNavigationBarHeight, + ), + ), + )); + await tester.pumpAndSettle(); + + expect(SystemChrome.latestStyle?.systemNavigationBarColor, isNull); + }, + variant: TargetPlatformVariant.only(TargetPlatform.android), + ); + + testWidgets( + 'systemNavigationBarColor is set when view covers more than half of navigation bar', + (WidgetTester tester) async { + setupTestDevice(); + const double moreThanHalfOfTheNavigationBarHeight = + navigationBarHeight / 2.0 + 1; + await tester.pumpWidget(const Align( + alignment: Alignment.bottomCenter, + child: AnnotatedRegion( + value: SystemUiOverlayStyle( + systemNavigationBarColor: Colors.blue, + ), + child: SizedBox( + width: 100, + height: moreThanHalfOfTheNavigationBarHeight, + ), + ), + )); + await tester.pumpAndSettle(); + + expect( + SystemChrome.latestStyle?.systemNavigationBarColor, + Colors.blue, + ); + }, + variant: TargetPlatformVariant.only(TargetPlatform.android), + ); + }); + }); +} + +class FakeWindowPadding implements WindowPadding { + const FakeWindowPadding({ + this.left = 0.0, + this.top = 0.0, + this.right = 0.0, + this.bottom = 0.0, + }); + + @override + final double left; + @override + final double top; + @override + final double right; + @override + final double bottom; +}