namespace JiDangJia.Views;
// A bounded native carousel follows the finger and wraps at both ends. Each page
// owns a vertical scroller, rather than placing the carousel in an unbounded stack.
public sealed class LoopPager : ContentView
{
private readonly CarouselView carousel;
private readonly ScrollView tabScroll;
private readonly Label[] labels;
private readonly BoxView[] lines;
private readonly View[] tabViews;
public LoopPager(string[] names,int selected,Func<int,View> create,Action<int> changed)
{
labels=new Label[names.Length];lines=new BoxView[names.Length];tabViews=new View[names.Length];
carousel=new CarouselView{Loop=true,IsSwipeEnabled=true,HorizontalScrollBarVisibility=ScrollBarVisibility.Never,VerticalScrollBarVisibility=ScrollBarVisibility.Never,PeekAreaInsets=0};
carousel.ItemTemplate=new DataTemplate(()=>
{
var host=new ContentView{HorizontalOptions=LayoutOptions.Fill,VerticalOptions=LayoutOptions.Fill};
host.BindingContextChanged+=(_,_)=>{if(host.Content is IDisposable old)old.Dispose();host.Content=null;if(host.BindingContext is int index)host.Content=create(index);};return host;
});
var tabs=new HorizontalStackLayout{Spacing=20,Padding=new Thickness(18,7,18,7)};
for(int i=0;i<names.Length;i++)
{
int index=i;labels[i]=Ui.Text(names[i],15);lines[i]=new BoxView{HeightRequest=3,CornerRadius=2};
var label=labels[i];label.HorizontalTextAlignment=TextAlignment.Center;var item=Ui.Stack(label,lines[i]);item.Spacing=5;item.MinimumHeightRequest=44;item.VerticalOptions=LayoutOptions.Center;
tabViews[i]=Ui.Tap(item,()=>{if(carousel.IsLoaded)carousel.ScrollTo(index,position:ScrollToPosition.Center,animate:false);else carousel.Position=index;return Task.CompletedTask;},names[i]);tabs.Add(tabViews[i]);
}
tabScroll=new ScrollView{Content=tabs,Orientation=ScrollOrientation.Horizontal,HorizontalScrollBarVisibility=ScrollBarVisibility.Never};
var grid=new Grid{RowDefinitions=[new(GridLength.Auto),new(GridLength.Star)]};grid.Add(tabScroll);grid.Add(carousel,0,1);Content=grid;
UpdateTabs(selected);carousel.ItemsSource=Enumerable.Range(0,names.Length).ToList();carousel.Position=selected;
carousel.PositionChanged+=(_,e)=>{if(e.CurrentPosition<0||e.CurrentPosition>=names.Length)return;changed(e.CurrentPosition);UpdateTabs(e.CurrentPosition);};
}
private void UpdateTabs(int selected)
{
for(int i=0;i<labels.Length;i++){bool active=i==selected;labels[i].TextColor=active?Ui.Ink:Ui.Muted;labels[i].FontAttributes=active?FontAttributes.Bold:FontAttributes.None;lines[i].Color=active?Ui.Gold:Colors.Transparent;}
if(tabScroll.IsLoaded)_=tabScroll.ScrollToAsync(tabViews[selected],ScrollToPosition.MakeVisible,true);
}
}
internal sealed class PageScroller : RefreshView,IDisposable
{
private Action? release;
public PageScroller(Action? release=null){this.release=release;}
public void Dispose(){var action=release;release=null;action?.Invoke();}
}
namespace JiDangJia.Views;
// A bounded native carousel follows the finger and wraps at both ends. Each page
// owns a vertical scroller, rather than placing the carousel in an unbounded stack.
public sealed class LoopPager : ContentView
{
private readonly CarouselView carousel;
private readonly ScrollView tabScroll;
private readonly Label[] labels;
private readonly BoxView[] lines;
private readonly View[] tabViews;
public LoopPager(string[] names,int selected,Func<int,View> create,Action<int> changed)
{
labels=new Label[names.Length];lines=new BoxView[names.Length];tabViews=new View[names.Length];
carousel=new CarouselView{Loop=true,IsSwipeEnabled=true,HorizontalScrollBarVisibility=ScrollBarVisibility.Never,VerticalScrollBarVisibility=ScrollBarVisibility.Never,PeekAreaInsets=0};
carousel.ItemTemplate=new DataTemplate(()=>
{
var host=new ContentView{HorizontalOptions=LayoutOptions.Fill,VerticalOptions=LayoutOptions.Fill};
host.BindingContextChanged+=(_,_)=>{if(host.Content is IDisposable old)old.Dispose();host.Content=null;if(host.BindingContext is int index)host.Content=create(index);};return host;
});
var tabs=new HorizontalStackLayout{Spacing=20,Padding=new Thickness(18,7,18,7)};
for(int i=0;i<names.Length;i++)
{
int index=i;labels[i]=Ui.Text(names[i],15);lines[i]=new BoxView{HeightRequest=3,CornerRadius=2};
var label=labels[i];label.HorizontalTextAlignment=TextAlignment.Center;var item=Ui.Stack(label,lines[i]);item.Spacing=5;item.MinimumHeightRequest=44;item.VerticalOptions=LayoutOptions.Center;
tabViews[i]=Ui.Tap(item,()=>{if(carousel.IsLoaded)carousel.ScrollTo(index,position:ScrollToPosition.Center,animate:false);else carousel.Position=index;return Task.CompletedTask;},names[i]);tabs.Add(tabViews[i]);
}
tabScroll=new ScrollView{Content=tabs,Orientation=ScrollOrientation.Horizontal,HorizontalScrollBarVisibility=ScrollBarVisibility.Never};
var grid=new Grid{RowDefinitions=[new(GridLength.Auto),new(GridLength.Star)]};grid.Add(tabScroll);grid.Add(carousel,0,1);Content=grid;
UpdateTabs(selected);carousel.ItemsSource=Enumerable.Range(0,names.Length).ToList();carousel.Position=selected;
carousel.PositionChanged+=(_,e)=>{if(e.CurrentPosition<0||e.CurrentPosition>=names.Length)return;changed(e.CurrentPosition);UpdateTabs(e.CurrentPosition);};
}
private void UpdateTabs(int selected)
{
for(int i=0;i<labels.Length;i++){bool active=i==selected;labels[i].TextColor=active?Ui.Ink:Ui.Muted;labels[i].FontAttributes=active?FontAttributes.Bold:FontAttributes.None;lines[i].Color=active?Ui.Gold:Colors.Transparent;}
if(tabScroll.IsLoaded)_=tabScroll.ScrollToAsync(tabViews[selected],ScrollToPosition.MakeVisible,true);
}
}
internal sealed class PageScroller : RefreshView,IDisposable
{
private Action? release;
public PageScroller(Action? release=null){this.release=release;}
public void Dispose(){var action=release;release=null;action?.Invoke();}
}