diff options
author | Leif Åstrand <leif@vaadin.com> | 2015-03-27 13:57:51 +0200 |
---|---|---|
committer | Markus Koivisto <markus@vaadin.com> | 2015-04-15 11:31:39 +0300 |
commit | 9c9fc07f62ddaf5b9d27b0e09e91673a196e5a7f (patch) | |
tree | 9c75b3d6bbf3a0684deabb62db38df38b16fa4f3 | |
parent | 73c9c8ba75d8b8329a185534d952a6e468fe4b22 (diff) | |
download | vaadin-framework-9c9fc07f62ddaf5b9d27b0e09e91673a196e5a7f.tar.gz vaadin-framework-9c9fc07f62ddaf5b9d27b0e09e91673a196e5a7f.zip |
Reduce reflows when sizing columns (#17315)
This patch increases the reported fps from 10 to 17 in Chrome and from 5
to 10 in Firefox. No automatic test since performance testing on our
shared testing infrastructure would be quite error-prone.
Change-Id: I0bb6af250743058a8f32bb2df89da97660e94b52
5 files changed, 328 insertions, 38 deletions
diff --git a/client/src/com/vaadin/client/widgets/Escalator.java b/client/src/com/vaadin/client/widgets/Escalator.java index 3d4459a0cc..83c176d6fd 100644 --- a/client/src/com/vaadin/client/widgets/Escalator.java +++ b/client/src/com/vaadin/client/widgets/Escalator.java @@ -16,6 +16,7 @@ package com.vaadin.client.widgets; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; @@ -2010,9 +2011,8 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker return new Cell(domRowIndex, domColumnIndex, cellElement); } - double getMaxCellWidth(int colIndex) throws IllegalArgumentException { - double maxCellWidth = -1; - + void createAutoSizeElements(int colIndex, + Collection<TableCellElement> elements) { assert isAttached() : "Can't measure max width of cell, since Escalator is not attached to the DOM."; NodeList<TableRowElement> rows = root.getRows(); @@ -2041,24 +2041,9 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker cellClone.getStyle().clearWidth(); rowElement.insertBefore(cellClone, cellOriginal); - double requiredWidth = WidgetUtil - .getRequiredWidthBoundingClientRectDouble(cellClone); - - if (BrowserInfo.get().isIE()) { - /* - * IE browsers have some issues with subpixels. Occasionally - * content is overflown even if not necessary. Increase the - * counted required size by 0.01 just to be on the safe - * side. - */ - requiredWidth += 0.01; - } - maxCellWidth = Math.max(requiredWidth, maxCellWidth); - cellClone.removeFromParent(); + elements.add(cellClone); } - - return maxCellWidth; } private boolean cellIsPartOfSpan(TableCellElement cell) { @@ -3789,7 +3774,8 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker if (px < 0) { if (isAttached()) { - calculateWidth(); + autosizeColumns(Collections.singletonList(columns + .indexOf(this))); } else { /* * the column's width is calculated at Escalator.onLoad @@ -3843,10 +3829,6 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker } return false; } - - private void calculateWidth() { - calculatedWidth = getMaxCellWidth(columns.indexOf(this)); - } } private final List<Column> columns = new ArrayList<Column>(); @@ -4151,6 +4133,7 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker return; } + List<Integer> autosizeColumns = new ArrayList<Integer>(); for (Entry<Integer, Double> entry : indexWidthMap.entrySet()) { int index = entry.getKey().intValue(); double width = entry.getValue().doubleValue(); @@ -4160,10 +4143,15 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker } checkValidColumnIndex(index); - columns.get(index).setWidth(width); - + if (width >= 0) { + columns.get(index).setWidth(width); + } else { + autosizeColumns.add(index); + } } + autosizeColumns(autosizeColumns); + widthsArray = null; header.reapplyColumnWidths(); body.reapplyColumnWidths(); @@ -4174,6 +4162,64 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker recalculateElementSizes(); } + private void autosizeColumns(List<Integer> columns) { + if (columns.isEmpty()) { + return; + } + + // Must process columns in index order + Collections.sort(columns); + + Map<Integer, List<TableCellElement>> autoSizeElements = new HashMap<Integer, List<TableCellElement>>(); + try { + // Set up the entire DOM at once + for (int i = columns.size() - 1; i >= 0; i--) { + // Iterate backwards to not mess with the indexing + Integer colIndex = columns.get(i); + + ArrayList<TableCellElement> elements = new ArrayList<TableCellElement>(); + autoSizeElements.put(colIndex, elements); + + header.createAutoSizeElements(colIndex, elements); + body.createAutoSizeElements(colIndex, elements); + footer.createAutoSizeElements(colIndex, elements); + } + + // Extract all measurements & update values + for (Integer colIndex : columns) { + double maxWidth = Double.NEGATIVE_INFINITY; + List<TableCellElement> elements = autoSizeElements + .get(colIndex); + for (TableCellElement element : elements) { + + double cellWidth = WidgetUtil + .getRequiredWidthBoundingClientRectDouble(element); + + maxWidth = Math.max(maxWidth, cellWidth); + } + assert maxWidth >= 0 : "Got a negative max width for a column, which should be impossible."; + + if (BrowserInfo.get().isIE()) { + /* + * IE browsers have some issues with subpixels. + * Occasionally content is overflown even if not + * necessary. Increase the counted required size by 0.01 + * just to be on the safe side. + */ + maxWidth += 0.01; + } + + this.columns.get(colIndex).calculatedWidth = maxWidth; + } + } finally { + for (List<TableCellElement> list : autoSizeElements.values()) { + for (TableCellElement element : list) { + element.removeFromParent(); + } + } + } + } + private void checkValidColumnIndex(int index) throws IllegalArgumentException { if (!Range.withLength(0, getColumnCount()).contains(index)) { @@ -4193,18 +4239,6 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker return columns.get(index).getCalculatedWidth(); } - private double getMaxCellWidth(int colIndex) - throws IllegalArgumentException { - double headerWidth = header.getMaxCellWidth(colIndex); - double bodyWidth = body.getMaxCellWidth(colIndex); - double footerWidth = footer.getMaxCellWidth(colIndex); - - double maxWidth = Math.max(headerWidth, - Math.max(bodyWidth, footerWidth)); - assert maxWidth >= 0 : "Got a negative max width for a column, which should be impossible."; - return maxWidth; - } - /** * Calculates the width of the columns in a given range. * diff --git a/uitest/src/com/vaadin/tests/components/grid/GridResizeTerror.java b/uitest/src/com/vaadin/tests/components/grid/GridResizeTerror.java new file mode 100644 index 0000000000..365461caa9 --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/GridResizeTerror.java @@ -0,0 +1,45 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.components.grid; + +import com.vaadin.annotations.Widgetset; +import com.vaadin.server.VaadinRequest; +import com.vaadin.tests.util.ResizeTerrorizer; +import com.vaadin.tests.widgetset.TestingWidgetSet; +import com.vaadin.ui.Grid; +import com.vaadin.ui.UI; + +@Widgetset(TestingWidgetSet.NAME) +public class GridResizeTerror extends UI { + @Override + protected void init(VaadinRequest request) { + Grid grid = new Grid(); + + int cols = 10; + Object[] data = new Object[cols]; + + for (int i = 0; i < cols; i++) { + grid.addColumn("Col " + i); + data[i] = "Data " + i; + } + + for (int i = 0; i < 500; i++) { + grid.addRow(data); + } + + setContent(new ResizeTerrorizer(grid)); + } +} diff --git a/uitest/src/com/vaadin/tests/util/ResizeTerrorizer.java b/uitest/src/com/vaadin/tests/util/ResizeTerrorizer.java new file mode 100644 index 0000000000..f124305d8a --- /dev/null +++ b/uitest/src/com/vaadin/tests/util/ResizeTerrorizer.java @@ -0,0 +1,51 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.util; + +import com.vaadin.tests.widgetset.client.ResizeTerrorizerControlConnector.ResizeTerorrizerState; +import com.vaadin.ui.AbstractComponent; +import com.vaadin.ui.Component; +import com.vaadin.ui.VerticalLayout; + +public class ResizeTerrorizer extends VerticalLayout { + private final ResizeTerrorizerControl control; + + public class ResizeTerrorizerControl extends AbstractComponent implements + Component { + + public ResizeTerrorizerControl(Component target) { + getState().target = target; + } + + @Override + protected ResizeTerorrizerState getState() { + return (ResizeTerorrizerState) super.getState(); + } + } + + public ResizeTerrorizer(Component target) { + target.setWidth("700px"); + setSizeFull(); + addComponent(target); + setExpandRatio(target, 1); + control = new ResizeTerrorizerControl(target); + addComponent(control); + } + + public void setDefaultWidthOffset(int px) { + control.getState().defaultWidthOffset = px; + } +} diff --git a/uitest/src/com/vaadin/tests/widgetset/client/ResizeTerrorizerControlConnector.java b/uitest/src/com/vaadin/tests/widgetset/client/ResizeTerrorizerControlConnector.java new file mode 100644 index 0000000000..9fe037706b --- /dev/null +++ b/uitest/src/com/vaadin/tests/widgetset/client/ResizeTerrorizerControlConnector.java @@ -0,0 +1,157 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.widgetset.client; + +import com.google.gwt.animation.client.AnimationScheduler; +import com.google.gwt.animation.client.AnimationScheduler.AnimationCallback; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.IntegerBox; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.RequiresResize; +import com.vaadin.client.ui.AbstractComponentConnector; +import com.vaadin.client.ui.PostLayoutListener; +import com.vaadin.shared.AbstractComponentState; +import com.vaadin.shared.Connector; +import com.vaadin.shared.ui.Connect; +import com.vaadin.tests.util.ResizeTerrorizer; + +@Connect(ResizeTerrorizer.ResizeTerrorizerControl.class) +public class ResizeTerrorizerControlConnector extends + AbstractComponentConnector implements PostLayoutListener { + + public static class ResizeTerorrizerState extends AbstractComponentState { + public Connector target; + public int defaultWidthOffset = 200; + } + + public class ResizeTerrorizerControlPanel extends FlowPanel { + private Label results = new Label("Results"); + private IntegerBox startWidth = new IntegerBox(); + private IntegerBox endWidth = new IntegerBox(); + private final Button terrorizeButton = new Button("Terrorize", + new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + terrorize(startWidth.getValue(), endWidth.getValue(), + 1000); + } + }); + + public ResizeTerrorizerControlPanel() { + add(new Label("Start width")); + add(startWidth); + + add(new Label("End width")); + add(endWidth); + + add(terrorizeButton); + add(results); + + startWidth.getElement().setId("terror-start-width"); + endWidth.getElement().setId("terror-end-width"); + terrorizeButton.getElement().setId("terror-button"); + results.getElement().setId("terror-results"); + } + + private void showResults(String results) { + Integer temp = startWidth.getValue(); + startWidth.setValue(endWidth.getValue()); + endWidth.setValue(temp); + + this.results.setText(results); + } + } + + private void terrorize(final double startWidth, final double endWidth, + final double duration) { + final AbstractComponentConnector target = getTarget(); + + final AnimationScheduler scheduler = AnimationScheduler.get(); + AnimationCallback callback = new AnimationCallback() { + double startTime = -1; + int frameCount = 0; + + @Override + public void execute(double timestamp) { + frameCount++; + + boolean done = false; + if (startTime == -1) { + startTime = timestamp; + } + + double time = timestamp - startTime; + if (time > duration) { + time = duration; + done = true; + } + + double progress = time / duration; + double widthToSet = startWidth + (endWidth - startWidth) + * progress; + + // TODO Optionally inform LayoutManager as well + target.getWidget().setWidth(widthToSet + "px"); + if (target.getWidget() instanceof RequiresResize) { + ((RequiresResize) target.getWidget()).onResize(); + } + + if (!done) { + scheduler.requestAnimationFrame(this); + } else { + double fps = Math.round(frameCount / (duration / 1000)); + String results = frameCount + " frames, " + fps + " fps"; + + getWidget().showResults(results); + } + } + }; + scheduler.requestAnimationFrame(callback); + } + + private AbstractComponentConnector getTarget() { + return (AbstractComponentConnector) getState().target; + } + + @Override + public ResizeTerorrizerState getState() { + return (ResizeTerorrizerState) super.getState(); + } + + @Override + public ResizeTerrorizerControlPanel getWidget() { + return (ResizeTerrorizerControlPanel) super.getWidget(); + } + + @Override + protected ResizeTerrorizerControlPanel createWidget() { + return new ResizeTerrorizerControlPanel(); + } + + @Override + public void postLayout() { + if (getWidget().startWidth.getValue() == null) { + int width = getTarget().getWidget().getElement().getOffsetWidth(); + getWidget().startWidth.setValue(width); + getWidget().endWidth + .setValue(width + getState().defaultWidthOffset); + } + } + +} diff --git a/uitest/src/com/vaadin/tests/widgetset/rebind/TestWidgetRegistryGenerator.java b/uitest/src/com/vaadin/tests/widgetset/rebind/TestWidgetRegistryGenerator.java index 1bdbba2c36..c7b29e271b 100644 --- a/uitest/src/com/vaadin/tests/widgetset/rebind/TestWidgetRegistryGenerator.java +++ b/uitest/src/com/vaadin/tests/widgetset/rebind/TestWidgetRegistryGenerator.java @@ -136,6 +136,9 @@ public class TestWidgetRegistryGenerator extends Generator { } else if (!widgetType.getPackage().getName() .startsWith(TestWidgetConnector.class.getPackage().getName())) { return false; + } else if (widgetType.getEnclosingType() != null + && !widgetType.isStatic()) { + return false; } return true; |