Multiple IsVisible performance question

PaulCharltonPaulCharlton GBMember ✭✭
edited September 2014 in Xamarin.Forms

Hi Guys,

I'm having a little problem that I've recreated below in that setting the IsVisible property of several child controls seems to take a lot longer than I was expecting. There's a timer at the top which shows the initial load is a approx 70ms whereas setting 20 labels takes upwards of 3000ms, even regenrating the content .

I've tried a couple of options including

  • setting the parent to invisible while the labels are set

  • removing the children from the list and re-adding

but to no avail.

I was assuming that this was being caused by the UI being redrawn each time the IsVisible is changed and was hoping there may be a way of disabling the redraw of the screen until I'm finished. I've noticed there's a batch procedure for properties of the same view, but can't find anything for the same property of different views. Is there anything like this?

Of course the other option would be to recreate the page when the visibility changes and while that is fine in my test app, the original code is a touch more involved and the initial load itself is +3000ms.

As a bit of background, the need for this essentially arises from workflow, I have a page that when a view is interacted with (a check box for instance) this then effects the visibility of other views on this page.

Code:
<br /> public class PlayPage : ContentPage<br /> {<br /> public PlayPage()<br /> {<br /> this.Content = GenerateContent();<br /> }</p> <pre><code> public View GenerateContent() { var totst = DateTime.Now; var layout = new StackLayout(); layout.Spacing = 10; layout.VerticalOptions = LayoutOptions.FillAndExpand; layout.Orientation = StackOrientation.Vertical; layout.HorizontalOptions = LayoutOptions.FillAndExpand; BackgroundColor = Color.Blue; var loadtime = new Label { Text = "(ms): " }; var totoggle = new StackLayout(); for (int i = 0; i < 30; i++) { totoggle.Children.Add(new Label { Text = "Label Toggle #" + i }); } var toggle = new Button { Text = "Toggle Visiblity" }; toggle.Clicked += (sender, args) => { var st = DateTime.Now; bool newvis = !totoggle.Children.First().IsVisible; foreach (var child in totoggle.Children) { child.IsVisible = newvis; } loadtime.Text += (newvis ? "show" : "hide") + ", " + (DateTime.Now - st).TotalMilliseconds + "; "; }; var togglep = new Button { Text = "Toggle Visiblity (with parent)" }; togglep.Clicked += (sender, args) => { var st = DateTime.Now; bool newvis = !totoggle.Children.First().IsVisible; //totoggle.BatchBegin(); totoggle.IsVisible = false; foreach (var child in totoggle.Children) { child.IsVisible = newvis; } totoggle.IsVisible = true; //totoggle.BatchCommit(); loadtime.Text += (newvis ? "show" : "hide") + ", " + (DateTime.Now - st).TotalMilliseconds + "; "; }; var toggle2 = new Button { Text = "Attach/Detach" }; var children = new List<View>(); toggle2.Clicked += (sender, args) => { var st = DateTime.Now; bool newvis = children.Any(); if (newvis) { foreach (var child in children) { totoggle.Children.Add(child); } children.Clear(); } else { children = totoggle.Children.ToList(); totoggle.Children.Clear(); } loadtime.Text += (newvis ? "attach" : "detach") + ", " + (DateTime.Now - st).TotalMilliseconds + "; "; }; var toggle2p = new Button { Text = "Attach/Detach (with parent)" }; var childrenp = new List<View>(); toggle2p.Clicked += (sender, args) => { var st = DateTime.Now; bool newvis = childrenp.Any(); if (newvis) { this.Content = null; //layout.Children.Remove(totoggle); foreach (var child in childrenp) { totoggle.Children.Add(child); } childrenp.Clear(); this.Content = layout; //layout.Children.Add(totoggle); } else { childrenp = totoggle.Children.ToList(); totoggle.Children.Clear(); } loadtime.Text += (newvis ? "attach" : "detach") + ", " + (DateTime.Now - st).TotalMilliseconds + "; "; }; var regen = new Button { Text = "Regenerate" }; regen.Clicked += (sender, args) => { var st = DateTime.Now; //this.Content = null; //this.Content = GenerateContent(); GenerateContent(); loadtime.Text += "regen, " + (DateTime.Now - st).TotalMilliseconds + "; "; }; layout.Children.Add(loadtime); layout.Children.Add(toggle); layout.Children.Add(togglep); layout.Children.Add(toggle2); layout.Children.Add(toggle2p); layout.Children.Add(regen); layout.Children.Add(totoggle); loadtime.Text += "initial load, " + (DateTime.Now - totst).TotalMilliseconds + "; "; return layout; } }

Thanks in advance,

Paul.

Posts

  • MihaMarkicMihaMarkic SI ✭✭✭✭

    That's a real problem. I assume Forms doesn't postpone the recomposition and rendering phase thus the layout recomposition is done each time you change a label visibility and rendering is done as well (guessing here).
    I guess a simple solution would be having a way to postpone these processes while you are manipulating the visibility (or other properties that affect it) through Layout methods.

  • PaulCharltonPaulCharlton GBMember ✭✭

    That's what I was hoping for although I can't find any documentation around it. I was also thinking of maybe writing child classes for each view and hiding the IsVisible property and controlling when Notify was called, (just assuming here, haven't tried it).

  • MihaMarkicMihaMarkic SI ✭✭✭✭

    Ha, I've made it 5x times faster. Interested? :)

  • PaulCharltonPaulCharlton GBMember ✭✭

    Go on, I'm intrigued......

  • MihaMarkicMihaMarkic SI ✭✭✭✭
    edited September 2014

    Here is a dirty proof of concept.
    Derived StackLayout.

    public class RhStackLayout: StackLayout
        {
            public bool Suppress { get; set; }
            protected override void LayoutChildren(double x, double y, double width, double height)
            {
                if (!Suppress)
                    base.LayoutChildren(x, y, width, height);
            }
    
            protected override SizeRequest OnSizeRequest(double widthConstraint, double heightConstraint)
            {
                if (!Suppress)
                    return base.OnSizeRequest(widthConstraint, heightConstraint);
                else
                    return new SizeRequest(new Size(100, 100));
            }
    
        }
    

    and test code:

    var totoggle = new RhStackLayout(); .... var st = DateTime.Now; bool newvis = !totoggle.Children.First().IsVisible; totoggle.Suppress = true; View lastChild = totoggle.Children.Last(); foreach (var child in totoggle.Children) { if (child == lastChild) totoggle.Suppress = false; child.IsVisible = newvis; } loadtime.Text += (newvis ? "show" : "hide") + ", " + (DateTime.Now - st).TotalMilliseconds + "; ";

    Basically I just suppress the two methods that gets called and are expensive, until I change the last child where I let StackLayout do its job.

  • MihaMarkicMihaMarkic SI ✭✭✭✭
    edited September 2014

    So, in order to do this properly, Xamarin should help us a bit.

    1) make BatchBegin and BatchCommit at least virtual

    2) split BatchCommit into two methods, we need RefreshLayout as well

    3) override both BatchBegin and Commit in Layouts to do as I did above.

  • PaulCharltonPaulCharlton GBMember ✭✭

    Bob on, that's a lot quicker, cheers.

    I would say, even if there was a PauseUi/ResumeUi on the page then the UI changes were stored and applied all at once. It's been a long time since I've dabbled but I'm sure win forms did something similar.....

    Thanks for your help.

  • MihaMarkicMihaMarkic SI ✭✭✭✭

    You're welcome. Hopefully that this get sorted out right in the Forms somehow. And yes, WinForms did similar :) while WPF is a bit smarter and does similar automatically.

  • MaruMaru DEMember ✭✭

    Is this making loading times with layouts faster in general or is this only applying to IsVisible?

  • PaulCharltonPaulCharlton GBMember ✭✭

    I'm only changing IsVisible, but based on Miha's comments, doing a bit of reading around the methods above and ForceLayout/InvalidateLayout it seems that the above is stopping the UI from drawing the children, so in theory it would apply to anything that was causing the UI to refresh.

    Just to note these improvements are seen after the layout has fully loaded.

  • MihaMarkicMihaMarkic SI ✭✭✭✭

    Yep, it addresses batch changes when it comes to existing layout (not only StackLayout but it could be ported to any Layout). Whatever property you change that it forces layout recomposition.

    As you see from MyStackLayout it just stops method propagation (LayoutChildren and OnSizeRequest) until you want them to really happen.

  • MaruMaru DEMember ✭✭

    Just to note these improvements are seen after the layout has fully loaded

    Yep, it addresses batch changes when it comes to existing layout

    So do I understand it right that this method doesn't improve the initial loading time of a layout?

  • PaulCharltonPaulCharlton GBMember ✭✭

    this is without actual evidence but I don't believe it improve initial loading time.

  • MihaMarkicMihaMarkic SI ✭✭✭✭

    It depends on how you do the layout. I can imagine that if you block the root Layout and/or each layout in the tree and make sure only the last full layout request fires it might improve the overall loading time as well.

  • With buttons do this and the performance will be WAAAAYYYYY better:

       public static void UpdateButtonVisible(Button button, bool isVisibled)
            {
                button.IsEnabled = isVisibled;
                button.Opacity = isVisibled ? 1 : 0;
            }
    
Sign In or Register to comment.