Unverified Commit b7a6c169 authored by amirh's avatar amirh Committed by GitHub

Improve AndroidView resize jank. (#20284)

Resizing an AndroidView happens asynchronously (as it requires thread
hopping from the ui thread to the platform thread).
While waiting for the resize to complete the framework does exactly when
the embedded view has been resized. This leads to pretty bad jank as the
framework might paint the embedded view with a wrong size.

We're working around this by freezing the texture frame while resizing
until we are sure that the next frame has the new size. This is how it
looks with the workaround:

This is how it looks before and after the workaround:

Before (janky) |  After (less janky)
:--------|---------:
![resize_before](https://user-images.githubusercontent.com/1024117/43669639-51bfd0ae-9739-11e8-8cbe-96e36f2460d2.gif) | ![resize_after](https://user-images.githubusercontent.com/1024117/43669647-62e885a6-9739-11e8-8aa4-beb74e979995.gif)

This depends on flutter/engine#5938. Additionaly right now the engine completes
the resize call immediately, a following PR will change it to complete
after a frame with the new size is ready.
parent 5177a37f
......@@ -216,6 +216,7 @@ class TextureLayer extends Layer {
TextureLayer({
@required this.rect,
@required this.textureId,
this.freeze = false,
}): assert(rect != null), assert(textureId != null);
/// Bounding rectangle of this layer.
......@@ -224,6 +225,8 @@ class TextureLayer extends Layer {
/// The identity of the backend texture.
final int textureId;
final bool freeze;
@override
void addToScene(ui.SceneBuilder builder, Offset layerOffset) {
final Rect shiftedRect = rect.shift(layerOffset);
......@@ -232,6 +235,7 @@ class TextureLayer extends Layer {
offset: shiftedRect.topLeft,
width: shiftedRect.width,
height: shiftedRect.height,
freeze: freeze,
);
}
......
......@@ -97,17 +97,21 @@ class RenderAndroidView extends RenderBox {
_sizePlatformView();
}
Size _currentAndroidViewSize;
Future<Null> _sizePlatformView() async {
if (_state == _PlatformViewState.resizing) {
return;
}
_state = _PlatformViewState.resizing;
markNeedsPaint();
Size targetSize;
do {
targetSize = size;
await _viewController.setSize(size);
await _viewController.setSize(targetSize);
_currentAndroidViewSize = targetSize;
// We've resized the platform view to targetSize, but it is possible that
// while we were resizing the render object's size was changed again.
// In that case we will resize the platform view again.
......@@ -122,9 +126,30 @@ class RenderAndroidView extends RenderBox {
if (_viewController.textureId == null)
return;
// Clip the texture if it's going to paint out of the bounds of the renter box
// (see comment in _paintTexture for an explanation of when this happens).
if (size.width < _currentAndroidViewSize.width || size.height < _currentAndroidViewSize.height) {
context.pushClipRect(true, offset, offset & size, _paintTexture);
return;
}
_paintTexture(context, offset);
}
void _paintTexture(PaintingContext context, Offset offset) {
// As resizing the Android view happens asynchronously we don't know exactly when is a
// texture frame with the new size is ready for consumption.
// TextureLayer is unaware of the texture frame's size and always maps it to the
// specified rect. If the rect we provide has a different size from the current texture frame's
// size the texture frame will be scaled.
// To prevent unwanted scaling artifacts while resizing we freeze the texture frame, until
// we know that a frame with the new size is in the buffer.
// This guarantees that the size of the texture frame we're painting is always
// _currentAndroidViewSize.
context.addLayer(new TextureLayer(
rect: offset & size,
rect: offset & _currentAndroidViewSize,
textureId: _viewController.textureId,
freeze: _state == _PlatformViewState.resizing,
));
}
......
......@@ -25,6 +25,8 @@ class FakePlatformViewsController {
int _textureCounter = 0;
Completer<void> resizeCompleter;
void registerViewType(String viewType) {
_registeredViewTypes.add(viewType);
}
......@@ -86,7 +88,7 @@ class FakePlatformViewsController {
return new Future<Null>.sync(() => null);
}
Future<dynamic> _resize(MethodCall call) {
Future<dynamic> _resize(MethodCall call) async {
final Map<dynamic, dynamic> args = call.arguments;
final int id = args['id'];
final double width = args['width'];
......@@ -98,6 +100,9 @@ class FakePlatformViewsController {
message: 'Trying to resize a platform view with unknown id: $id',
);
if (resizeCompleter != null) {
await resizeCompleter.future;
}
_views[id].size = new Size(width, height);
return new Future<Null>.sync(() => null);
......
......@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
......@@ -49,20 +51,36 @@ void main() {
),
);
viewsController.resizeCompleter = new Completer<void>();
await tester.pumpWidget(
const Center(
child: SizedBox(
width: 400.0,
height: 200.0,
width: 100.0,
height: 50.0,
child: AndroidView(viewType: 'webview'),
),
),
);
final Layer textureParentLayer = tester.layers[tester.layers.length - 2];
expect(textureParentLayer, isInstanceOf<ClipRectLayer>());
final ClipRectLayer clipRect = textureParentLayer;
expect(clipRect.clipRect, new Rect.fromLTWH(0.0, 0.0, 100.0, 50.0));
expect(
viewsController.views,
unorderedEquals(<FakePlatformView>[
new FakePlatformView(currentViewId + 1, 'webview', const Size(200.0, 100.0))
]),
);
viewsController.resizeCompleter.complete();
await tester.pump();
expect(
viewsController.views,
unorderedEquals(<FakePlatformView>[
new FakePlatformView(currentViewId + 1, 'webview', const Size(400.0, 200.0))
new FakePlatformView(currentViewId + 1, 'webview', const Size(100.0, 50.0))
]),
);
});
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment