adospace / reactorui-maui

MauiReactor is a MVU UI framework built on top of .NET MAUI
MIT License
568 stars 47 forks source link

Basic animation issue with invalidateComponent false #129

Closed MattePozzy closed 1 year ago

MattePozzy commented 1 year ago

Hi, I have created a basic animation but to make it works I need to invalidate all the components and re-render the enteire page, otherwise the animation doesn't work.

I have many heavy load pages, so I can' re-render the page.

There is a way to set invalidateComponent to false and make the animation works?

ezgif com-video-to-gif

Here a sample.

Thank you.

EDIT: also there is a way to animate the height only in one direction (eg. only from bottom to top)? Thank you.

adospace commented 1 year ago

Hi, the multiple render calls that you see are not caused by the animation but instead are required by the call to SetState(s => s.Status++): you are actually requesting a new render for every render. RxAnimations require that you use the SetState(s=>..) but the component is refreshed only once just to update the state: the animation works under the hood without re-render the complete component but just updating the dependency property marked with the WithAnimation() method.

Try this:

public override VisualNode Render()
{
    Debug.WriteLine("Render called");
    return new ContentPage
    {
        new Grid("auto,auto,auto,auto", "*,*")
        {
            new Label("Tap on each box").GridColumn(0).GridRow(0).GridColumnSpan(2).VCenter().HCenter(),

            new Label("Works --> invalidateComponent TRUE").GridColumn(0).GridRow(1).VCenter().HCenter(),
            new BoxView().GridColumn(0).GridRow(2)
                .HeightRequest(80).WidthRequest(80)
                .Color(Colors.Green)
                .Opacity(State.IsExpanded_Green ? 1 : 0.2)
                .HeightRequest(State.IsExpanded_Green ? 250 : 80)
                .WithAnimation(easing: Easing.Linear, duration: 1000)
                .OnTapped(() =>{
                    SetState(s => s.IsExpanded_Green = !s.IsExpanded_Green);
                }),
        }
        .VCenter().HCenter()
        .RowSpacing(25).ColumnSpacing(25)
        .Padding(30, 0)
    };
}

you should see only one message "Render called".

Regarding the animation from bottom to top, it's a layout issue in your code: setting Auto row height + VCenter() you are asking the grid to unlarge from the center to accommodate its content: try use HStart() on the grid.

MattePozzy commented 1 year ago

Hi, thank you but the render is called on every click on the box. I have used this code:

        public override VisualNode Render()
        {
            Debug.WriteLine("Render called");
            //SetState(s => s.Status++);
            return new ContentPage
            {
                new Grid("auto,auto,auto,auto", "*,*")
                {
                    new Label("Tap on each box").GridColumn(0).GridRow(0).GridColumnSpan(2).VCenter().HCenter(),

                    new Label("Works --> invalidateComponent TRUE").GridColumn(0).GridRow(1).VCenter().HCenter(),
                    new BoxView().GridColumn(0).GridRow(2)
                        .HeightRequest(80).WidthRequest(80)
                        .Color(Colors.Green)
                        .Opacity(State.IsExpanded_Green ? 1 : 0.2)
                        .HeightRequest(State.IsExpanded_Green ? 250 : 80)
                        .WithAnimation(easing: Easing.Linear, duration: 1000)
                        .OnTapped(() =>{
                            SetState(s => s.IsExpanded_Green = !s.IsExpanded_Green);
                        }),
                }
                .VEnd()
                .RowSpacing(25).ColumnSpacing(25)
                .Padding(30, 0)
            };
        }

ezgif com-video-to-gif (1)

adospace commented 1 year ago

yes is correct, it's called everytime you call SetState to update IsExpanded_Green; but is not called during animation

MattePozzy commented 1 year ago

yes is correct, it's called everytime you call SetState to update IsExpanded_Green; but is not called during animation

Ok, there is a way to avoid this? I need to set invalidateComponent to false like this, but doesn't work.

SetState(s => s.IsExpanded_Green = !s.IsExpanded_Green, false);

If I use this code, the animation doesn't work.

public override VisualNode Render()
        {
            Debug.WriteLine("Render called");
            //SetState(s => s.Status++);
            return new ContentPage
            {
                new Grid("auto,auto,auto,auto", "*,*")
                {
                    new Label("Tap on each box").GridColumn(0).GridRow(0).GridColumnSpan(2).VCenter().HCenter(),

                    new Label("invalidateComponent FALSE").GridColumn(0).GridRow(1).VCenter().HCenter(),
                    new BoxView().GridColumn(0).GridRow(2)
                        .HeightRequest(80).WidthRequest(80)
                        .Color(Colors.Green)
                        .Opacity(() => State.IsExpanded_Green ? 1 : 0.2)
                        .HeightRequest(() => State.IsExpanded_Green ? 250 : 80)
                        .WithAnimation(easing: Easing.Linear, duration: 1000)
                        .OnTapped(() => {
                            SetState(s => s.IsExpanded_Green = !s.IsExpanded_Green, false);
                        }),
                }
                .VEnd()
                .RowSpacing(25).ColumnSpacing(25)
                .Padding(30, 0)
            };
        }

I need to avoid the re-render when I set IsExpanded_Green to fire the animation.

adospace commented 1 year ago

No, is not possible (at least using rxAnimation). Maybe you can try the AnimationController but the code I guess would be more complicated. In the end, you can simply fall back to MAUI class animations: Get a reference to the BoxView: new BoxView(boxViewRef => _boxViewRef = boxViewRef) and call _boxViewRef.FadeTo(..) in the OnTappedEvent. Anyway, I'm not sure why you're worried about the render pass: it's optimized enough to handle the majority of the cases.

MattePozzy commented 1 year ago

I have a listview and when I run the animation the app freeze beacause of the re-render. Here a video.

adospace commented 1 year ago

hum, ok, it's hard to say what causes it, seems like the parent component render is called during the animation, can you add a Debug.Writeline("Render") in the render method containing the listview? just to see if it's called many times during the animation

MattePozzy commented 1 year ago

EDIT forget it. the problem is also with only the labels. I can not understand.

Hi! While i was making an example to send, i found the problem.

I created a custom label which is actually a VStack containing two labels (one for description and one for the data).

Apparently if there are too many VStacks in the list item template the render freezes and also the animations.

think I'll rewrite the itemTemplate using only label.

This is the custom control

 public partial class VolosLabel : VStack
    {
        private readonly Label Descr = new();
        private readonly Label Value = new();

        public VolosLabel()
        {
            Descr.Style(Utility.GetResources<MauiControls.Style>("LblDescr"));
            Value.Style(Utility.GetResources<MauiControls.Style>("LblValore"));
            Value.LineBreakMode(LineBreakMode.WordWrap);
            Add(Descr);
            Add(Value);
        }

        public VolosLabel Descrizione(string text)
        {
            Descr.Text(text);
            return this;
        }

        public VolosLabel Valore(string text)
        {
            Value.Text(text);
            return this;
        }

        public VolosLabel ValoreTextColorHex(string hexColor)
        {
            Value.TextColor(Color.FromArgb(hexColor));
            return this;
        }

        public VolosLabel SetLineBreakMode(LineBreakMode type)
        {
            Descr.LineBreakMode(type);
            Value.LineBreakMode(type);
            return this;
        }

        public VolosLabel ValoreAsHtml(bool siNo)
        {
            if (siNo)
            {
                Value.TextType(TextType.Html);
                Value.FontSize(FontSize.Normal);
                Value.FontFamily("Arial");
            }
            return this;
        }
    }

and this is the ItemTemplate:

return new ViewCell {
new VStack() {
                new Grid("auto,auto,auto,auto,auto", "20,*,*,*,*,*,*,10")
                {
                    #region Riga colorata
                    new BoxView().GridRow(0).GridColumn(0).GridRowSpan(5).Margin(new Thickness(0,0,10,0)).WidthRequest(10).Color(Color.FromArgb(item.Colore)),
                    #endregion

                    #region Numero/Anno Wo + icona offline + icona mio wo   
                    new VolosLabel().GridRow(0).GridColumn(1).GridColumnSpan(6)
                        .Descrizione(State.TS.Translate("OFL_workorder.LblWO"))
                        .Valore(item.NumWoAnno),
                    new Label().GridRow(0).GridColumn(6)
                        .VerticalTextAlignment(TextAlignment.Center).HorizontalTextAlignment(TextAlignment.End)
                        .IsVisible(item.Offline).FontFamily(Costanti.Font.MaterialDesignIcons.ToString()).FontSize(FontSize.Large)
                        .Text(MaterialFontIcons.TabletCellphone).TextColor(Utility.GetResources<Color>("Primary")),
                    new Label().GridRow(1).GridColumn(6)
                        .VerticalTextAlignment(TextAlignment.Center).HorizontalTextAlignment(TextAlignment.End)
                        .IsVisible(item.MioWo).FontFamily(Costanti.Font.MaterialDesignIcons.ToString()).FontSize(FontSize.Large)
                        .Text(MaterialFontIcons.Crown).TextColor(Utility.GetResources<Color>("Primary")),
                    #endregion

                    #region Cliente, Sito e indirizzo completo
                    new VolosLabel().GridRow(1).GridColumn(1).GridColumnSpan(6)
                        .Descrizione(State.TS.Translate("OFL_workorder.LblDatiAnagrafici"))
                        .Valore(item.DatiAnagrafici),
                    #endregion

                    #region Macchina + tipo wo
                        new VolosLabel().GridRow(2).GridColumn(1).GridColumnSpan(2)
                            .Descrizione(State.TS.Translate("OFL_workorder.LblMacchina"))
                            .Valore(item.MacchinaFull),
                        new VolosLabel().GridRow(2).GridColumn(3).GridColumnSpan(4)
                            .Descrizione(State.TS.Translate("OFL_workorder.LblTipoWO"))
                            .Valore(item.TipoWo),
                    #endregion

                    #region severity + Data apertura + Data scadenza
                        new VolosLabel().GridRow(3).GridColumn(1).GridColumnSpan(2)
                            .Descrizione(State.TS.Translate("OFL_workorder.LblSeverity"))
                            .Valore(item.Severity),
                        new VolosLabel().GridRow(3).GridColumn(3).GridColumnSpan(2)
                            .Descrizione(State.TS.Translate("OFL_workorder.LblDataApertura"))
                            .Valore(item.Data_apertura.Value.ToString(UtenteMobile.UteLoggato.formatoData)),
                        new VolosLabel().GridRow(3).GridColumn(5).GridColumnSpan(2)
                            .Descrizione(State.TS.Translate("OFL_workorder.LblDataScadenza"))
                            .Valore(item.Data_scadenza.Value.ToString(UtenteMobile.UteLoggato.formatoData)),
                    #endregion

                    #region Passaggio Successivo
                        new VolosLabel().GridRow(4).GridColumn(1).GridColumnSpan(6)
                            .Descrizione(State.TS.Translate("OFL_workorder.LblProgressStatus"))
                            .Valore(item.ProgressStatus)
                    #endregion
                    },
                    new BoxView().HeightRequest(1).Color(Utility.GetResources<Color>("Gray400"))
            },
 new MenuFlyout
                    {
                        MenuPerWin(State.TS.Translate("OFL_allegato.Allegati"), MaterialFontIcons.Paperclip),
                        MenuPerWin(State.TS.Translate("OFL_WorkOrder.Note"), MaterialFontIcons.NoteMultiple),
                        MenuPerWin(State.TS.Translate("OFL_WorkOrder.impostaCoordinateWo"), MaterialFontIcons.MapMarker).OnClicked(async (obj, evt) => await ImpostaCoordinate(obj, evt)),

                        MenuPerWin(State.TS.Translate("OFL_WorkOrder.anteprimaStampa"), MaterialFontIcons.Printer),
                        MenuPerWin(State.TS.Translate("ofl_nota.Attivita"), MaterialFontIcons.TagMultiple),
                        MenuPerWin(State.TS.Translate("OFL_MARKETING.titoloDialogWarningOpportunities"), MaterialFontIcons.Briefcase),

                        MenuPerWin(State.TS.Translate("OFL_WorkOrder.impostaOffline"), MaterialFontIcons.TabletCellphone).OnClicked(async (obj, evt) =>
                        {
                            MauiControls.MenuFlyoutItem menu = obj as MauiControls.MenuFlyoutItem;
                            WorkOrderLista woSel = menu.BindingContext as WorkOrderLista;
                            await ImpostaOffline(woSel);
                        }),
                    }
 };
MattePozzy commented 1 year ago

I have solved by using a method like this one:

new BoxView(boxViewRef => _boxViewRef = boxViewRef) and call _boxViewRef.FadeTo(..)