Closed nor0x closed 7 months ago
Were you modifying the value using:
SetState(state => state.Scale = ###);
Look at the Receipe Sampe App video here: https://github.com/adospace/mauireactor-samples/tree/main
https://github.com/adospace/mauireactor-samples/blob/main/RecipeApp/Pages/RecipeComponent.cs
Do you mean something like this:
class MainPageScrollingAnimationState
{
public ObservableCollection<IndexedPersonWithAddress> Persons { get; set; }
public double ScrollY { get; set; }
}
class MainPageScrollingAnimation : Component<MainPageScrollingAnimationState>
{
private const double _itemSize = 128;
private static List<IndexedPersonWithAddress> GenerateSamplePersons(int count)
{
var random = new Random();
var firstNames = new[] { "John", "Jim", "Joe", "Jack", "Jane", "Jill", "Jerry", "Jude", "Julia", "Jenny" };
var lastNames = new[] { "Brown", "Green", "Black", "White", "Blue", "Red", "Gray", "Smith", "Doe", "Jones" };
var cities = new[] { "New York", "London", "Sidney", "Tokyo", "Paris", "Berlin", "Mumbai", "Beijing", "Cairo", "Rio" };
var persons = new List<IndexedPersonWithAddress>();
for (int i = 0; i < count; i++)
{
var firstName = firstNames[random.Next(0, firstNames.Length)];
var lastName = lastNames[random.Next(0, lastNames.Length)];
var age = random.Next(20, 60);
var city = cities[random.Next(0, cities.Length)];
var address = $"{city} No. {random.Next(1, 11)} Lake Park";
persons.Add(new IndexedPersonWithAddress(i, firstName, lastName, age, address));
}
return persons;
}
protected override void OnMounted()
{
State.Persons = new ObservableCollection<IndexedPersonWithAddress>(GenerateSamplePersons(100));
base.OnMounted();
}
public override VisualNode Render()
{
return new ContentPage
{
new CollectionView()
.ItemsSource(State.Persons, RenderItem)
.OnScrolled((s,e) => SetState(s => s.ScrollY = e.VerticalOffset, false))
//new ScrollView
//{
// new VStack
// {
// State.Persons.Select(RenderItem)
// }
//}
//.OnScrolled((s,e) => SetState(s => s.ScrollY = e.ScrollY, false))
};
}
private VisualNode RenderItem(IndexedPersonWithAddress item)
{
return new Border
{
new Label($"Item {item.Index}")
.TextColor(Colors.White)
.Center()
}
.StrokeThickness(0)
.StrokeCornerRadius(34, 34, 0, 0)
.HeightRequest(_itemSize)
.BackgroundColor(Colors.BlueViolet)
.ScaleX(() => 0.5 + GetPercOffset(item) * 0.5)
.Opacity(() => 0.2 + GetPercOffset(item) * 0.8)
;
}
private double GetPercOffset(IndexedPersonWithAddress item)
{
var itemScrollY = item.Index * _itemSize;
if (itemScrollY < State.ScrollY - _itemSize)
{
return 0.0;
}
else if (itemScrollY > State.ScrollY + _itemSize)
{
return 1.0;
}
return (itemScrollY - (State.ScrollY - _itemSize)) / (_itemSize * 2);
}
}
PS: the sample contains both implementations CollectionView vs ScrollView+Stack
thank you very much @adospace for this. didn't notice that there is sample code available for my question. Will have a look at this now. https://github.com/adospace/mauireactor-samples/blob/main/Controls/CollectionViewTestApp/Pages/MainPageScrollingAnimation.cs
EDIT:
i tried modifying the sample to have a separate component as the item in the CollectionView
and got the following - could you point me to what I'm doing wrong here? The updates from OnScrolling
are not continously passed to the PersonComponent
https://github.com/adospace/reactorui-maui/assets/3210391/afd2f634-6ea5-4179-906c-c1a358a0c60f
here is my attempt:
class MainPageScrollingAnimationState
{
public ObservableCollection<IndexedPersonWithAddress> Persons { get; set; }
public double ScrollY { get; set; }
}
class MainPageScrollingAnimation : Component<MainPageScrollingAnimationState>
{
private const double _itemSize = 128;
private static List<IndexedPersonWithAddress> GenerateSamplePersons(int count)
{
var random = new Random();
var firstNames = new[] { "John", "Jim", "Joe", "Jack", "Jane", "Jill", "Jerry", "Jude", "Julia", "Jenny" };
var lastNames = new[] { "Brown", "Green", "Black", "White", "Blue", "Red", "Gray", "Smith", "Doe", "Jones" };
var cities = new[] { "New York", "London", "Sidney", "Tokyo", "Paris", "Berlin", "Mumbai", "Beijing", "Cairo", "Rio" };
var persons = new List<IndexedPersonWithAddress>();
for (int i = 0; i < count; i++)
{
var firstName = firstNames[random.Next(0, firstNames.Length)];
var lastName = lastNames[random.Next(0, lastNames.Length)];
var age = random.Next(20, 60);
var city = cities[random.Next(0, cities.Length)];
var address = $"{city} No. {random.Next(1, 11)} Lake Park";
persons.Add(new IndexedPersonWithAddress(i, firstName, lastName, age, address));
}
return persons;
}
protected override void OnMounted()
{
State.Persons = new ObservableCollection<IndexedPersonWithAddress>(GenerateSamplePersons(100));
base.OnMounted();
}
public override VisualNode Render()
{
return new ContentPage
{
new CollectionView()
.ItemsSource(State.Persons, RenderItem)
.OnScrolled((s,e) => SetState(s => s.ScrollY = e.VerticalOffset, false))
};
}
private VisualNode RenderItem(IndexedPersonWithAddress item)
=> new PersonComponent()
.Item(item)
.PercOffset(GetPercOffset(item));
private double GetPercOffset(IndexedPersonWithAddress item)
{
var itemScrollY = item.Index * _itemSize;
if (itemScrollY < State.ScrollY - _itemSize)
{
return 0.0;
}
else if (itemScrollY > State.ScrollY + _itemSize)
{
return 1.0;
}
return (itemScrollY - (State.ScrollY - _itemSize)) / (_itemSize * 2);
}
}
class PesonComponentState
{
public double PercOffset { get; set; }
}
class PersonComponent : Component<PesonComponentState>
{
IndexedPersonWithAddress _item;
double _itemSize = 128;
double _percOffset;
public PersonComponent Item(IndexedPersonWithAddress item)
{
_item = item;
return this;
}
public PersonComponent PercOffset(double percOffset)
{
SetState(s => s.PercOffset = percOffset);
return this;
}
public override VisualNode Render()
{
return new Border
{
new Label($"Item {_item.Index}")
.TextColor(Colors.White)
.Center()
}
.StrokeThickness(0)
.StrokeCornerRadius(34, 34, 0, 0)
.HeightRequest(_itemSize)
.BackgroundColor(Colors.BlueViolet)
.ScaleX(() => 0.5 + State.PercOffset * 0.5)
.Opacity(() => 0.2 + State.PercOffset * 0.8)
.WithAnimation()
.WithKey(_item.Index);
}
}
Hi, if you want to use a component for your items you have to wrap it inside a container (i.e. Grid) and modify its opacity and scale based on the scroll vertical offset:
class MainPageScrollingAnimationWithComponentState
{
public ObservableCollection<IndexedPersonWithAddress> Persons { get; set; }
public double ScrollY { get; set; }
}
class MainPageScrollingAnimationWithComponent : Component<MainPageScrollingAnimationWithComponentState>
{
private const double _itemSize = 128;
private static List<IndexedPersonWithAddress> GenerateSamplePersons(int count)
{
var random = new Random();
var firstNames = new[] { "John", "Jim", "Joe", "Jack", "Jane", "Jill", "Jerry", "Jude", "Julia", "Jenny" };
var lastNames = new[] { "Brown", "Green", "Black", "White", "Blue", "Red", "Gray", "Smith", "Doe", "Jones" };
var cities = new[] { "New York", "London", "Sidney", "Tokyo", "Paris", "Berlin", "Mumbai", "Beijing", "Cairo", "Rio" };
var persons = new List<IndexedPersonWithAddress>();
for (int i = 0; i < count; i++)
{
var firstName = firstNames[random.Next(0, firstNames.Length)];
var lastName = lastNames[random.Next(0, lastNames.Length)];
var age = random.Next(20, 60);
var city = cities[random.Next(0, cities.Length)];
var address = $"{city} No. {random.Next(1, 11)} Lake Park";
persons.Add(new IndexedPersonWithAddress(i, firstName, lastName, age, address));
}
return persons;
}
protected override void OnMounted()
{
State.Persons = new ObservableCollection<IndexedPersonWithAddress>(GenerateSamplePersons(100));
base.OnMounted();
}
public override VisualNode Render()
{
return new ContentPage
{
new CollectionView()
.ItemsSource(State.Persons, RenderItem)
.OnScrolled((s,e) => SetState(s => s.ScrollY = e.VerticalOffset, false))
};
}
private VisualNode RenderItem(IndexedPersonWithAddress item)
=> new Grid
{
new PersonComponent()
.Item(item)
}
.ScaleX(() => 0.5 + GetPercOffset(item) * 0.5)
.Opacity(() => 0.2 + GetPercOffset(item) * 0.8);
private double GetPercOffset(IndexedPersonWithAddress item)
{
var itemScrollY = item.Index * _itemSize;
if (itemScrollY < State.ScrollY - _itemSize)
{
return 0.0;
}
else if (itemScrollY > State.ScrollY + _itemSize)
{
return 1.0;
}
return (itemScrollY - (State.ScrollY - _itemSize)) / (_itemSize * 2);
}
}
class PersonComponent : Component
{
IndexedPersonWithAddress _item;
readonly double _itemSize = 128;
public PersonComponent Item(IndexedPersonWithAddress item)
{
_item = item;
return this;
}
public override VisualNode Render()
{
return new Border
{
new Label($"Item {_item.Index}")
.TextColor(Colors.White)
.Center()
}
.StrokeThickness(0)
.StrokeCornerRadius(34, 34, 0, 0)
.HeightRequest(_itemSize)
.BackgroundColor(Colors.BlueViolet);
}
}
This is because we have to update Opacity and Scale without invalidating the entire component (which will cause a serious performance issue in this specific case).
To avoid full component refresh you have to call SetState with invalidateComponent parameter set to false, plus, you need to use a callback function to update directly the Opacity and Scale dependency properties:
alright - understood! thanks again, still getting used to how stuff works in MVU 👋
Sorry @adospace for coming back to this issue, I don't want to open new ones for every thing that I notice which is somewhat related to this.
I have implemented your suggestion for wrapping the component in a Grid
and applying the Scale
and Opactiy
changes to this wrapper - works just fine 👍
I now have continued the development of my usecase and I somehow need to apply an animation to a child VisualNode
in the component as well (the blue border in my sample code). Is this something that can be done without invalidating the whole component? It works fine without using a component for the CollectionView Item like this:
private VisualNode RenderItem(IndexedPersonWithAddress item)
{
return new Border
{
new VStack()
{
new Border()
.BackgroundColor(Colors.Blue)
.WidthRequest(50)
.HeightRequest(50)
.VCenter()
.HCenter()
/* ______ this animation but inside a component ______ */
.Opacity(() => 0.2 + GetPercOffset(item) * 0.8)
.WithAnimation()
.WithKey(item.Index),
new Label($"Item {item.Index}")
.TextColor(Colors.White)
.Center()
}
}
.StrokeThickness(0)
.StrokeCornerRadius(34, 34, 0, 0)
.HeightRequest(_itemSize)
.BackgroundColor(Colors.BlueViolet)
.ScaleX(() => 0.5 + GetPercOffset(item) * 0.5)
.WithAnimation()
.WithKey(item.Index)
;
}
Hi, sorry for the late reply, I was busy this week. Regarding your issue, I think it's more a c# task than a specific MauiReactor thing. I mean, the problem is (considering this specific case) is to not invalidate the entire component to make a smooth animation.
So I guess, we can use a code like the following where I pass the scrolling event down to the items. Each item component can attach the event, and on call back update its UI without invalidating the entire component.
class MainPageScrollingAnimationWithComponentState
{
public ObservableCollection<IndexedPersonWithAddress> Persons { get; set; }
}
class MainPageScrollingAnimationWithComponent : Component<MainPageScrollingAnimationWithComponentState>
{
event EventHandler<double> Scrolled;
private static List<IndexedPersonWithAddress> GenerateSamplePersons(int count)
{
var random = new Random();
var firstNames = new[] { "John", "Jim", "Joe", "Jack", "Jane", "Jill", "Jerry", "Jude", "Julia", "Jenny" };
var lastNames = new[] { "Brown", "Green", "Black", "White", "Blue", "Red", "Gray", "Smith", "Doe", "Jones" };
var cities = new[] { "New York", "London", "Sidney", "Tokyo", "Paris", "Berlin", "Mumbai", "Beijing", "Cairo", "Rio" };
var persons = new List<IndexedPersonWithAddress>();
for (int i = 0; i < count; i++)
{
var firstName = firstNames[random.Next(0, firstNames.Length)];
var lastName = lastNames[random.Next(0, lastNames.Length)];
var age = random.Next(20, 60);
var city = cities[random.Next(0, cities.Length)];
var address = $"{city} No. {random.Next(1, 11)} Lake Park";
persons.Add(new IndexedPersonWithAddress(i, firstName, lastName, age, address));
}
return persons;
}
protected override void OnMounted()
{
State.Persons = new ObservableCollection<IndexedPersonWithAddress>(GenerateSamplePersons(100));
base.OnMounted();
}
public override VisualNode Render()
{
return new ContentPage
{
new CollectionView()
.ItemsSource(State.Persons, RenderItem)
.OnScrolled((s,e) => Scrolled?.Invoke(s, e.VerticalOffset))
};
}
private VisualNode RenderItem(IndexedPersonWithAddress item)
=> new PersonComponent()
.WhenScroll(ref Scrolled)
.Item(item);
}
class PersonComponentState
{
public double ScrollY { get; set; }
}
class PersonComponent : Component<PersonComponentState>
{
IndexedPersonWithAddress _item;
readonly double _itemSize = 128;
public PersonComponent Item(IndexedPersonWithAddress item)
{
_item = item;
return this;
}
public PersonComponent WhenScroll(ref EventHandler<double> scrolled)
{
scrolled += (s, verticalOffset) => SetState(s => s.ScrollY = verticalOffset, false);
return this;
}
public override VisualNode Render()
{
double GetPercOffset()
{
var itemScrollY = _item.Index * _itemSize;
if (itemScrollY < State.ScrollY - _itemSize)
{
return 0.0;
}
else if (itemScrollY > State.ScrollY + _itemSize)
{
return 1.0;
}
return (itemScrollY - (State.ScrollY - _itemSize)) / (_itemSize * 2);
}
return new Border
{
new VStack()
{
new Border()
.BackgroundColor(Colors.Blue)
.WidthRequest(50)
.HeightRequest(50)
.VCenter()
.HCenter()
.ScaleX(() => 1.0 + GetPercOffset()),
new Label($"Item {_item.Index}")
.TextColor(Colors.White)
.Center()
}
}
.StrokeThickness(0)
.StrokeCornerRadius(34, 34, 0, 0)
.HeightRequest(_itemSize)
.BackgroundColor(Colors.BlueViolet)
//.ScaleX(() => 0.5 + GetPercOffset() * 0.5)
.Opacity(() => 0.2 + GetPercOffset() * 0.8)
;
}
}
Feel free to leave the task open or re-open it in case you need more support.
Just revisited the sample and I found that the above code contains a memory leak, please look at the updated sample:
this is the part changed:
IndexedPersonWithAddress _item;
private Action<EventHandler<double>> _subscribeToScrollEvent;
readonly double _itemSize = 128;
public PersonComponent Item(IndexedPersonWithAddress item)
{
_item = item;
return this;
}
public PersonComponent WhenScroll(Action<EventHandler<double>> subscribeToScrollEvent)
{
_subscribeToScrollEvent = subscribeToScrollEvent;
return this;
}
protected override void OnMounted()
{
//System.Diagnostics.Debug.WriteLine($"OnMounted (Item {_item.Index})");
_subscribeToScrollEvent?.Invoke(OnParentScrolled);
base.OnMounted();
}
void OnParentScrolled(object sender, double verticalOffset)
{
SetState(s => s.ScrollY = verticalOffset);
}
i'm following the code here (https://github.com/adospace/mauireactor-samples/commit/631cff0b21b5f993571ca0eec7f304bac1e779b7#diff-dacab46b064ae55b6f850e4b47d44b421c4c36bb7ac05b469a8e0c34e015a276) and it works perfectly. thanks again 🙌
Hello again 👋 I have a CollectionView with various items and now I want to polish the UI. The plan is to animate the topmost upon scrolling (Scale In and Out). Similar to the MAUI coupons sample https://github.com/jsuarezruiz/netmaui-coupons-app-challenge/tree/main
I was wondering how I can invalidate the item in the
CollectionView
. I have already tried adding aScale
property to the modeland modify that value in the
OnScrolled
event. But this doesn't updated with every scroll event.Is there a sample or some info on how to achieve such a scroll animation scenario? Thank you