Skip to content

Conversation

jonahwilliams
Copy link
Contributor

@jonahwilliams jonahwilliams commented May 16, 2018

In order to fix the unpredictability of SystemChrome changes, I've added the AnnotatedRegion API as proposed by @Hixie on #4630.

Changes

  • Adds AnnotatedRegion<T>, a widget which places an AnnotatedRegionLayer<T> in the layer tree.
  • Adds Layer.findRegion(Offset, Type) which searches for the last AnnotatedRegionLayer that matches Type and contains Offset
    • Checking that the offset is contained by the layer is not yet implemented.
  • Replaces direct calls to setSystemUiOverlayStyle with AnnotatedRegion<SystemUiOverlayStyle>
  • Calls setSystemUiOverlayStyle after the layer tree is built using findRegion.
  • Updates light and dark themes to always have dark app bars for Android - this is so enabling the default themes is non-breaking.

Questions

  • Can this change be submitted (which already massively improves SystemChrome behavior) before I update it with the proper hitTesting behavior?
  • Should we default to a dark theme if an AnnotatedRegion isn't found?
    • Consider a case where our app has a light theme but no explicit AnnotatedRegion. We then push a route with an AnnotatedRegion and a dark theme. When we pop this route, currently the system ui theme remains dark.

Edit

Unfortunately I swapped the meaning of the brightness values for iOS in the engine, so I need to fix that separately

@jonahwilliams
Copy link
Contributor Author

Getting closer, math is a bit off though.

@@ -184,6 +186,14 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox>
}
}

void _updateSystemChrome() {
final SystemUiOverlayStyle overlayStyle = layer.findRegion(
Offset.zero,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The suggestion I had was for when we just had one colour for everything. Now that we have two colours (at least), we probably want to independently get the details for the top and the bottom.

For the top, I'd probably aim for a point half way across the width, and half of the way down the top padding.
Similarly for the other sources.

_latestStyle = _pendingStyle;
}
_pendingStyle = null;
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we might want to keep the old behaviour around in case anyone else wants to use this API, and doesn't have a way to guarantee they only call it once per frame.

}

/// Render object for the [AnnotatedRegion].
class AnnotatedRegionRenderObject<T> extends RenderProxyBox {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't this be in proxy_box?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The render_object, or the whole annotated_region API?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the render object.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved

@Hixie
Copy link
Contributor

Hixie commented May 17, 2018

This looks fantastic.

before I update it with the proper hitTesting behavior?

not sure what you mean by "proper"? This seems pretty good already.

Should we default to a dark theme if an AnnotatedRegion isn't found?

We could have a default. I'd be tempted to just say if there's no information, we don't change it.

I would recommend adding more tests, e.g. to check if it works with crazy layer types like the leader/follower layers, if it works with scrolling of slivers, etc.

@Hixie
Copy link
Contributor

Hixie commented May 17, 2018

When you fix whatever angered the bots, please see if you can also fix the _verifyNoBadImportsInFlutter logic to not crash when handling whatever you did... thanks :-)

import 'package:flutter/widgets.dart';

void main() => runApp(const Center(child: const Text('Hello, world!', textDirection: TextDirection.ltr)));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't change the hello world example. The whole point of this example is to be a one liner.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry commited by mistake, was using this to debug values - will revert

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

@@ -536,7 +536,10 @@ class OffsetLayer extends ContainerLayer {

@override
Object findRegion(Offset offset, Type type) {
return super.findRegion(offset + this.offset, type);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please name the argument something that doesn't shadow a member variable.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

@@ -536,7 +536,10 @@ class OffsetLayer extends ContainerLayer {

@override
Object findRegion(Offset offset, Type type) {
return super.findRegion(offset + this.offset, type);
if (offset <= this.offset) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why?

systemNavigationBarDividerColor: lowerOverlayStyle.systemNavigationBarDividerColor,
));
} else if (upperOverlayStyle != null || lowerOverlayStyle != null) {
SystemChrome.setSystemUIOverlayStyle(upperOverlayStyle ?? lowerOverlayStyle);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems weird that when an upper overlay style goes out of view, suddenly the lower overlay style controls the top system ui overlay.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

S find<S>(Offset regionOffset) {
if (_invertedTransform == null) {
_invertedTransform ??= new Matrix4.zero();
_transform?.copyInto(_invertedTransform)?.invert();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's a method or constructor or something that'll get you a new matrix that represents the inverse of another, which'll save you having to manually create the new one.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

@override
S find<S>(Offset regionOffset) {
final Offset transformed = regionOffset - offset;
return super.find<S>(transformed);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i don't mind which we do, but we should be consistent in the style between this one and LeaderLayer's.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

}
final Vector4 vector = new Vector4(regionOffset.dx, regionOffset.dy, 0.0, 1.0);
final Vector4 result = _invertedTransform.transform(vector);
return super.find<S>(new Offset(result[0] - linkedOffset.dx, result[1] - linkedOffset.dy));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this doesn't seem to handle the case of link.leader == null && !showWhenUnlinked. I assume in that case we should just return null?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

/// a [Size] is provided to this layer, then find will check if the provided
/// offset is within the bounds of the layer.
class AnnotatedRegionLayer<T> extends ContainerLayer {
/// Creates a new annotated layer.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These docs don't match our style guide.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Because they don't tell you anything you can't tell from the identifiers. Maybe mention the contract on size, e.g. that if null it won't clip, or something.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe say value can't be null? and assert it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

/// The [size] is optionally used to clip the hit-testing of [find].
///
/// If not provided, all offsets are considered to be contained within this
/// layer, unless a parent layer applies a clip.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a parent -> an ancestor

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

super.debugFillProperties(properties);
properties.add(new DiagnosticsProperty<T>('value', value));
if (size != null)
properties.add(new DiagnosticsProperty<Size>('size', size));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can always add this property, even if it's null. Just set defaultValue to null.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

/// * [Layer.find], for an example of how this value is retrieved.
/// * [AnnotatedRegionLayer], the layer this render object creates.
class AnnotatedRegionRenderObject<T> extends RenderProxyBox {
/// A value which can be retrieved using [Layer.find].
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The lack of a constructor is very atypical for RenderObject subclasses.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

/// * [AnnotatedRegionLayer], the layer this render object creates.
class AnnotatedRegionRenderObject<T> extends RenderProxyBox {
/// A value which can be retrieved using [Layer.find].
T value;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't changing value cause markNeedsPaint?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

T value;

/// Whether the render object will pass its [size] to the [AnnotatedRegionLayer].
bool sized;
Copy link
Contributor

@Hixie Hixie Jun 18, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similarly here, shouldn't mutating sized cause markNeedsPaint?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

if (child != null) {
final AnnotatedRegionLayer<T> layer = new AnnotatedRegionLayer<T>(value, size: sized ? size : null);
context.pushLayer(layer, super.paint, offset);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do you only push the layer if there's a child?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

@override
void updateRenderObject(BuildContext context, AnnotatedRegionRenderObject<T> renderObject) {
renderObject.value = value;
renderObject.sized = sized;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prefer cascade operator; see other updateRenderObject implementations

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

@jonahwilliams
Copy link
Contributor Author

Found issue with TestRecordingCanvas and with the location of the annotated region layer in app bar. Both fixes, should be all green

/// object to clip the results of [Layer.findRegion].
///
/// Neither [value] nor [sized] can be null.
AnnotatedRegionRenderObject(T value, bool sized)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw the naming scheme is usually RenderFoo for the render object, and Foo for the widget.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

: assert(value != null),
assert(sized != null),
_value = value,
_sized = sized;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

child?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't that set by the element?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The render object layer can be used directly, it doesn't have to be used via widgets. When used directly, the first child is usually set in the constructor. Look at the other classes in this file. :-)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ahhh right, fixed

@jonahwilliams
Copy link
Contributor Author

yaaay it passed

@jonahwilliams
Copy link
Contributor Author

Friendly ping. I've been informed this feature is a blocker for customer mulligan.

@Hixie
Copy link
Contributor

Hixie commented Jun 22, 2018

LGTM

@Hixie
Copy link
Contributor

Hixie commented Jun 22, 2018

Thanks for doing this, this is awesome!

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants