Xamarin Forms Auto adjust elements in ScrollView - xamarin

Xamarin Forms Version : 2.5
Complete and partial scroll look like below :
https://imgur.com/a/kvpBZaE
Height of ScrollView is 150 , and the height of each item in the scroll view is 50.
Requirement:
At any point in time the scroll always has to be in the complete scrolled position ie The vertical Scroll has to display only 3 items and the center item to be highlighted. If and when the items are scrolled and the number of items is anything other than 3(Partial Scroll) then the display has to auto adjust to three items.
Problem : There is no event on the scrollview which can be used to auto adjust the scroll.
Code
<ScrollView
x:Name="ListScrollViewer"
HorizontalOptions="FillAndExpand"
Orientation="Vertical">
<StackLayout x:Name="ItemsLayout" Spacing="0" />
</ScrollView>
public static readonly BindableProperty ItemsSourceProperty =
BindableProperty.Create(nameof(ItemsSource), typeof(IEnumerable), typeof(VerticalScrollView), null, BindingMode.Default, null, null);
public IEnumerable ItemsSource
{
get { return (IEnumerable)GetValue(ItemsSourceProperty); }
set
{
SetValue(ItemsSourceProperty, value);
var i = 0;
foreach (var item in value)
{
ItemsLayout.Children.Add(new TextView(item, i));
var tapGestureRecognizer = new TapGestureRecognizer()
{
CommandParameter = i,
Command = new Command<object>(OnTap),
NumberOfTapsRequired = 1,
};
ItemsLayout.Children.Last().GestureRecognizers.Add(tapGestureRecognizer);
i++;
}
}
}
Scroll View Usage:
<local:VerticalScrollView
x:Name="VerticalScroll"
BackgroundColor="Black"
HeightRequest="150"
HorizontalOptions="Center"
VerticalOptions="Center" />
Code Behind
public List<object> Options { get; set; } = new List<object>();
Options.Add(new { Name = "Val 1" });
Options.Add(new { Name = "Val 2" });
Options.Add(new { Name = "Val 3" });
Options.Add(new { Name = "Val 4" });
Options.Add(new { Name = "Val 5" });
VerticalScroll.ItemsSource = Options;

Related

CollectionView SelectionChange event not working properly in Android (.Net MAUI)

I'm trying to create custom tabs control in .Net MAUI, for that, I had first tried it with ScrollView and BindableStackLayout control but in that, I'm facing a problem.
Reported here Custom tabs with ScrollView bug
So, as an alternative approach or work-around, I have tried to develop the same Tabs control using CollectionView.
This alternative approach is working fine in iOS but not working properly in Android.
There is one problem that is common in both Android and iOS. I have taken BoxView control as an Indicator for the selected tab. That I'm going to show only for the Selected tab but this just shows in the first tab, when I click on other tabs the tabs get changed but it does not get hidden from the first tab and get visible in the other selected tab.
I have used the visual state manager with white color for the selected state because it gives looks like an indicator which I',m trying to create using BoxView. But this also shows Selected item for Android only when that view gets loads for iOS I have to select the tab first then only it shows the selected color there.
Here is what I have done:
MainPage.xaml
<ContentPage.Content>
<Grid RowDefinitions="50, *" RowSpacing="0">
<CollectionView x:Name="TabsView"
Grid.Row="0"
ItemsSource="{Binding Tabs,Mode=TwoWay}"
ItemsUpdatingScrollMode="KeepItemsInView"
ItemSizingStrategy="MeasureAllItems"
SelectedItem="{Binding SelectedTab}"
SelectionChangedCommand="{Binding Path=BindingContext.TabChangedCommand,Source={x:Reference TabsView}}"
SelectionChangedCommandParameter="{Binding SelectedTab}"
SelectionMode="Single">
<CollectionView.ItemsLayout>
<LinearItemsLayout Orientation="Horizontal"/>
</CollectionView.ItemsLayout>
<CollectionView.ItemTemplate>
<DataTemplate>
<Grid RowSpacing="0" RowDefinitions="*, 3">
<Label Grid.Row="0"
Text="{Binding TabTitle}"
TextColor="White"
BackgroundColor="navy"
Padding="20,0"
VerticalTextAlignment="Center"
HorizontalTextAlignment="Center"
FontSize="12" />
<BoxView Grid.Row="1" Color="{Binding BoxColor}"
IsVisible="{Binding IsSelected}"/>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup Name="CommonStates">
<VisualState Name="Normal"/>
<VisualState Name="Selected">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="White" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<tabs:ParentRecordTabView Grid.Row="1"
IsVisible="{Binding IsParentRecordTabVisible}"
VerticalOptions="FillAndExpand"/>
<tabs:AdditionalInfoTabView Grid.Row="1"
IsVisible="{Binding IsAdditionalInfoTabVisible}"
VerticalOptions="FillAndExpand" />
</Grid>
</ContentPage.Content>
MainPageViewModel.cs
public class MainPageViewModel : BaseViewModel
{
public MainPageViewModel()
{
GetTabs();
}
private bool _isParentRecordTabVisible = true;
private bool _isAdditionalInfoTabVisible;
private ObservableCollection<TabViewModel> _tabs { get; set; }
private TabViewModel _selectedTab { get; set; }
public bool IsParentRecordTabVisible
{
get => _isParentRecordTabVisible;
set { _isParentRecordTabVisible = value; OnPropertyChanged(nameof(IsParentRecordTabVisible)); }
}
public bool IsAdditionalInfoTabVisible
{
get => _isAdditionalInfoTabVisible;
set { _isAdditionalInfoTabVisible = value; OnPropertyChanged(nameof(IsAdditionalInfoTabVisible)); }
}
public ObservableCollection<TabViewModel> Tabs
{
get => _tabs;
set { _tabs = value; OnPropertyChanged(nameof(Tabs)); }
}
public TabViewModel SelectedTab
{
get => _selectedTab;
set
{
_selectedTab = value;
OnPropertyChanged(nameof(SelectedTab));
}
}
public ICommand TabChangedCommand { get { return new Command<TabViewModel>(ChangeTabClick); } }
private void GetTabs()
{
Tabs = new ObservableCollection<TabViewModel>();
Tabs.Add(new TabViewModel { TabId = 1, IsSelected = true, TabTitle = "Parent record" });
Tabs.Add(new TabViewModel { TabId = 2, TabTitle = "Additional Info" });
Tabs.Add(new TabViewModel { TabId = 3, TabTitle = "Contacts" });
Tabs.Add(new TabViewModel { TabId = 4, TabTitle = "Previous inspections" });
Tabs.Add(new TabViewModel { TabId = 5, TabTitle = "Attachments" });
SelectedTab = Tabs.FirstOrDefault();
}
public void ChangeTabClick(TabViewModel tab)
{
Tabs.All((arg) =>
{
if (arg.TabId == tab.TabId)
{
arg.IsSelected = true;
}
else
{
arg.IsSelected = false;
}
return true;
});
SelectedTab = Tabs.Where(t => t.IsSelected == true).FirstOrDefault();
switch (SelectedTab.TabId)
{
case 1:
IsParentRecordTabVisible = true;
IsAdditionalInfoTabVisible = false;
break;
case 2:
IsParentRecordTabVisible = false;
IsAdditionalInfoTabVisible = true;
break;
}
}
}
TabViewModel.cs
public class TabViewModel : BaseViewModel
{
private bool _IsSelected;
public bool IsSelected
{
get { return _IsSelected; }
set
{
_IsSelected = value;
OnPropertyChanged(nameof(IsSelected));
}
}
private int _TabId;
public int TabId
{
get { return _TabId; }
set
{
_TabId = value;
OnPropertyChanged(nameof(TabId));
}
}
private string _TabTitle;
public string TabTitle
{
get { return _TabTitle; }
set
{
_TabTitle = value;
OnPropertyChanged(nameof(TabTitle));
}
}
}
Note: This same approach again works fine in Xamarin.Forms (Visual Studio 2019), this just not working in MAUI, so does anyone notice something like this?
How to Reproduce error: check github
Remove the BoxView default style in your project. Resource> Styles> Styles.xml
<Style TargetType="BoxView">
<Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
You could use the IsVisible property to show the BoxView or not instead of binding color with BoxColor. Remove the SelectionChangedCommand, SelectionChangedCommandParameter and VisualStateManager.VisualStateGroups in CollectionView as well.
<BoxView Grid.Row="1"
Color="Yellow"
IsVisible="{Binding IsSelected}"/>
Set the SelectedTab property like below.
public TabViewModel SelectedTab
{
get => _selectedTab;
set
{
_selectedTab = value;
SetSelection();
OnPropertyChanged(nameof(SelectedTab));
}
}
private void SetSelection()
{
foreach (var item in Tabs)
{
item.IsSelected = false;
}
SelectedTab.IsSelected = true;
}

Open popup at specific position - Xamarin.Forms

I have a xamarin.forms app which uses Rg.Plugins.Popup. I have a list view with an "edit" icon on each view cell. I am opening the popup when we click on this icon. The problem is the popup will open at the center of screen no matter what where the listview cells position. How can we open the popup exactly at the position of listview cell which being clicked? Is it possible? Any help is appreciated.
For now, Rg.Plugins.Popup support the Animations for Right, Left, Center, Top, Bottom.
For more details of Animations, you could check the link below. https://github.com/rotorgames/Rg.Plugins.Popup/wiki/Animations#custom-animations
Applying a custom animations in a xaml file:
class UserAnimation : MoveAnimation
{
private double _defaultTranslationY;
public UserAnimation()
{
DurationIn = DurationOut = 300;
EasingIn = Easing.SinOut;
EasingOut = Easing.SinIn;
PositionIn = MoveAnimationOptions.Right;
PositionOut = MoveAnimationOptions.Right;
}
public override void Preparing(View content, PopupPage page)
{
base.Preparing(content, page);
page.IsVisible = false;
if (content == null) return;
_defaultTranslationY = content.TranslationY;
}
public override void Disposing(View content, PopupPage page)
{
base.Disposing(content, page);
page.IsVisible = true;
if (content == null) return;
content.TranslationY = _defaultTranslationY;
}
public async override Task Appearing(View content, PopupPage page)
{
var taskList = new List<Task>();
taskList.Add(base.Appearing(content, page));
if (content != null)
{
var topOffset = GetTopOffset(content, page);
var leftOffset = GetLeftOffset(content, page);
taskList.Add(content.TranslateTo(content.Width, _defaultTranslationY, DurationIn, EasingIn));
};
page.IsVisible = true;
await Task.WhenAll(taskList);
}
public async override Task Disappearing(View content, PopupPage page)
{
var taskList = new List<Task>();
taskList.Add(base.Disappearing(content, page));
if (content != null)
{
_defaultTranslationY = content.TranslationX - content.Width;
var topOffset = GetTopOffset(content, page);
var leftOffset = GetLeftOffset(content, page);
taskList.Add(content.TranslateTo(leftOffset, _defaultTranslationY, DurationOut, EasingOut));
};
await Task.WhenAll(taskList);
}
}
Usage:
<pages:PopupPage.Animation>
<animations:UserAnimation />
</pages:PopupPage.Animation>
<StackLayout HorizontalOptions="Center" VerticalOptions="Center">
<Frame BackgroundColor="Silver">
<StackLayout Spacing="20">
<Label
FontSize="16"
HorizontalOptions="Center"
Text="User Animation" />
<Button Clicked="OnClose" Text="Close" />
</StackLayout>
</Frame>
</StackLayout>
OnClose event:
private void OnClose(object sender, EventArgs e)
{
PopupNavigation.Instance.PopAsync();
}
If you wannna Left, Top, Botton, Center for the Popup, you could change the MoveAnimationOptions of your custom animations.
PositionIn = MoveAnimationOptions.Right;
PositionOut = MoveAnimationOptions.Right;

Position View below RelativeLayout

Context of the problem:
I do have a StackLayout with a lot of entries. When the user taps on an entry I do want to show below the tapped entry an info box. This info box should visually be above the next entry (kind of like a tooltip). The entry can have a dynamic height.
What is my approach:
Using a RelativeLayout it should be possible to position views outside the bounds of the RelativeLayout which represents the entry.
Something like this:
<StackLayout>
<BoxView BackgroundColor="Green" HeightRequest="150" ></BoxView>
<RelativeLayout BackgroundColor="Yellow" x:Name="container">
<Label Text="This is the entry"></Label>
<BoxView BackgroundColor="Aqua"
RelativeLayout.YConstraint="{ConstraintExpression Type=RelativeToView, ElementName=container, Property=Y, Factor=1, Constant=100}"></BoxView>
</RelativeLayout>
<BoxView BackgroundColor="Green" HeightRequest="150" ></BoxView>
</StackLayout>
In this sample code the green BoxView's are kind of the entries before and after the one I do want to show. This is the result:
This makes actually sense, as I've linked to the Y-Property of the container and added 100 using "Constant".
And this is what I do want to archive:
I want to have a StackLayout with multiple entries. Whenever I click on one of this entries (yellow) right below an info should appear (blue).
How do I have to specify the YConstraint on the BoxView (which should illustrate the info window) to archive my goal? Or am I on a wrong path and another solution fits better?
I write a demo about your needs, here is running GIF.
First of all, I create content view.
<ContentView.Content>
<RelativeLayout x:Name="container" BackgroundColor="Yellow">
<Entry Text="This is the entry" x:Name="MyEntry" Focused="MyEntry_Focused" Unfocused="MyEntry_Unfocused">
</Entry>
</RelativeLayout>
</ContentView.Content>
Here is background code about content view.
public partial class FloatEntry : ContentView
{
BoxView boxView;
public FloatEntry()
{
InitializeComponent();
boxView = new BoxView();
boxView.BackgroundColor = Color.Red;
boxView.WidthRequest = 200;
}
private void MyEntry_Focused(object sender, FocusEventArgs e)
{
container.Children.Add(boxView,Constraint.RelativeToView(MyEntry, (Parent, sibling) =>
{
return sibling.X + 100;
}), Constraint.RelativeToView(MyEntry, (parent, sibling) =>
{
return sibling.Y + 50;
}));
container.RaiseChild(boxView);
}
private void MyEntry_Unfocused(object sender, FocusEventArgs e)
{
container.Children.Remove(boxView);
}
}
}
But If you used this way to achieve it, you want to BoxView to cover the below Entry. You have to put the content view to a RelativeLayout as well.
<RelativeLayout x:Name="myRl">
<myentry:FloatEntry x:Name="myfloat" HorizontalOptions="StartAndExpand" HeightRequest="50" >
<myentry:FloatEntry.GestureRecognizers>
<TapGestureRecognizer Tapped="TapGestureRecognizer_Tapped"/>
</myentry:FloatEntry.GestureRecognizers>
</myentry:FloatEntry>
<myentry:FloatEntry HorizontalOptions="StartAndExpand" HeightRequest="50"
RelativeLayout.YConstraint="{ConstraintExpression Type=RelativeToView, ElementName=myfloat, Property=Y, Factor=1, Constant=50}"
>
</myentry:FloatEntry>
</RelativeLayout>
Here is layout background code.
public partial class Page1 : ContentPage
{
public Page1()
{
InitializeComponent();
}
private void TapGestureRecognizer_Tapped(object sender, EventArgs e)
{
// I need to use following method to move the Boxview cover the blew Entry
myRl.RaiseChild(myfloat);
}
}
A more generic approach would be to write your own control which could be named as InfoBoxPopup (bascially a ContentPage) which you open manually once the Entry gets Focused and Close it on Unfocus.
Just be sure that you have on top of every page a grid panel defined.
In the InfoBox.xaml you define your custom style (panel, label, margins, IsInputTransparent?, etc. to show the custom text or other stuff)
public partial class InfoBoxPopup : ContentView
{
public static readonly BindableProperty TextProperty = BindableProperty.Create(nameof(Text), typeof(string), typeof(InfoBoxPopup));
public InfoBoxPopup()
{
InitializeComponent();
}
public string? Text
{
get => (string?)GetValue(TextProperty);
set => SetValue(TextProperty, value);
}
public void Show()
{
var rootGrid = GetCurrentPageGrid();
var rowsCount = rootGrid.RowDefinitions.Count;
if (rowsCount > 1)
{
Grid.SetRowSpan(this, rowsCount);
}
rootGrid.Children.Add(this);
}
public void Close()
{
var rootGrid = (Grid)Parent;
rootGrid.Children.Remove(this);
}
private static Grid GetCurrentPageGrid()
{
var shellView = (ShellView)Application.Current.MainPage;
var contentPage = (ContentPage)shellView.CurrentPage;
if (contentPage.Content is Grid grid) { return grid; }
var actualPanel = contentPage.Content;
for (int i = 0; i < 10; i++)
{
var children = actualPanel.LogicalChildren;
var childGrid = children.OfType<Grid>().FirstOrDefault();
if (childGrid != null) { return childGrid; }
actualPanel = children.OfType<View>().FirstOrDefault();
}
throw new ArgumentException("No Grid panel could identified to place the info box!");
}
}

Xamarin Forms - Change View's location in a Grid

I have a grid on my page with a few items.
I want to change the location/span of these items in C#.
On Xaml I can do this:
<Grid>
<Label Grid.Row="1" Grid.RowSpan="2"/>
</Grid>
I could do grd.Children.Remove and then grd.Children.Add in the new position, but is there another way?
public MyPage()
{
var grid = new Grid();
var lbl1 = new Label { Text= "lbl1" };
var lbl2 = new Label { Text = "lbl2" };
grid.Children.Add(lbl1);
grid.Children.Add(lbl2);
Grid.SetRow(lbl1, 1);
Grid.SetRow(lbl2, 0);
this.Content = grid;
}
However I would recommend not to do it dynamically but once in the constructor. More info in the official doc.

Xamarin.Form iOS custom height ProgressBar issue

UPDATED:
So, I'm unable to create IOS custom height ProgressBar.
I use the latest version of Xamarin.Forms.
.cs file:
public class SplashScreenProgressBar : ProgressBar
{
public static readonly BindableProperty TintColorProperty =
BindableProperty.Create<CustomProgressBar, Color>( p => p.TintColor, Color.Green);
public Color TintColor
{
get { return (Color) GetValue(TintColorProperty); }
set { SetValue(TintColorProperty, value); }
}
public static readonly BindableProperty HeightExtendedProperty =
BindableProperty.Create("HeightExtended", typeof(double), typeof(SplashScreenProgressBar), 10.0);
public double HeightExtended
{
get { return (double) GetValue(HeightExtendedProperty); }
set { SetValue(HeightExtendedProperty, value); }
}
public static readonly BindableProperty BackgroundColorExtendedProperty =
BindableProperty.Create("BackgroundColorExtended", typeof(Color), typeof(SplashScreenProgressBar),
Color.White);
public Color BackgroundColorExtended
{
get { return (Color) GetValue(BackgroundColorExtendedProperty); }
set { SetValue(BackgroundColorExtendedProperty, value); }
}
}
Here is iOS renderer:
public class SplashScreenProgressBarRenderer : ProgressBarRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<ProgressBar> e)
{
base.OnElementChanged(e);
var element = (SplashScreenProgressBar)Element;
this.Control.ProgressTintColor = element.TintColor.ToUIColor();
this.Control.TrackTintColor = element.BackgroundColorExtended.ToUIColor();
}
public override void LayoutSubviews()
{
base.LayoutSubviews();
var element = (SplashScreenProgressBar)Element;
var X = 1.0f;
var Y = (System.nfloat)element.HeightExtended;
CGAffineTransform transform = CGAffineTransform.MakeScale(X, Y);
this.Control.Transform = transform;
this.Control.ClipsToBounds = true;
this.Control.Layer.MasksToBounds = true;
this.Control.CornerRadius = 5;
}
}
xaml file:
<StackLayout HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand" BackgroundColor="White" >
<StackLayout HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand" Margin="50" BackgroundColor="White" >
<views:SplashScreenProgressBar x:Name="Progress"
TintColor="#5FA5F9"
HorizontalOptions="FillAndExpand"
VerticalOptions="Center"
BackgroundColorExtended="#FFF" />
</StackLayout>
</StackLayout>
But this way doesn't work.
I googled and tried almost all samples which I've found, but nothing happened.
Screenshot:
As you see on the screenshot corner radius is applied to ProgressBar, but height(scale) isn't applied.
In PCL
StackLayout is overlapped with status bar.
Add Margin on it.
<StackLayout Margin="50" xxxxx
In Renderer
ClipsToBounds ,Layer.MasksToBounds ,Layer.CornerRadius should be set on the Control not the Renderer
this.Control.Transform = transform;
this.Control.ClipsToBounds = true;
this.Control.Layer.MasksToBounds = true;
this.Control.Layer.CornerRadius = 5;
When use the custom renderer in ios, it always occupy the whole area of parent element. So, u need to update the progress bar frame once again in layoutsubview.
bool is rendered;
public override void LayoutSubviews()
{
if(!rendered)
{
Frame = new CGRect(x,y,width,height);
setNeedsdisplay
}
rendered=true;
}
So, I spent a lot of hours to researching and investigation.
And seems there xamarin.forms iOS bug for progress bar rounded corners.

Resources