We've got a ScrollView which I've set with a VerticalOptions of "End", so that when we add content to it at runtime it 'grows' from the bottom.
We're scrolling to the end when adding content, with animation. This looks good when the ScrollView is full and is actually scrolling.
However, when content is added to the ScrollView, the new content appears immediately with no animation.
Any thoughts on how to animate the growth of the ScrollView as the new content is added? Ideally I'd like it to slide up, like the animated scroll when it's full.
We're using a RepeaterView as the content of the ScrollView, if that's relevant.
Relevant existing code below (we're using Forms with MvvmCross - hence an MVVM pattern):
ViewModel
private async Task NextClick()
{
var claimFlowQuestion = await GetClaimFlowQuestion(_currentIndexQuestion);
Questions.Add(ClaimFlowExtendFromClaimFlow(claimFlowQuestion));
// Trigger PropertyChanged so the Repeater updates
await RaisePropertyChanged(nameof(Questions));
// Trigger the QuestionAdded event so the ScrollView can scroll to the bottom (initiated in the xaml.cs code behind)
QuestionAdded?.Invoke(this, new EventArgs());
}
XAML
<ScrollView Grid.Row="1" x:Name="QuestionScrollView">
<StackLayout VerticalOptions="End"
Padding ="10,0,10,0"
IsVisible="{Binding Busy, Converter={StaticResource InvertedBooleanConvertor}}}">
<controls:RepeaterView
x:Name="QuestionRepeater"
Margin ="10"
AutomationId="IdQuestions"
Direction ="Column"
ItemsSource ="{Binding Questions}"
ClearChild ="false">
<controls:RepeaterView.ItemTemplate>
<DataTemplate>
<ViewCell>
<controlsClaimFlowControls:QuestionBlock
Margin ="0,20,0,20"
QuestionNumber ="{Binding Index}"
QuestionText ="{Binding QuestionText}"
QuestionDescription="{Binding QuestionDescription}"
ItemsSource ="{Binding Source}"
DisplayMemberPath ="{Binding DisplayPaths}"
QuestionType ="{Binding QuestionType}"
SelectedItem ="{Binding Value}"
IsEnabledBlock ="{Binding IsEnabled}" />
</ViewCell>
</DataTemplate>
</controls:RepeaterView.ItemTemplate>
</controls:RepeaterView>
</StackLayout>
</ScrollView>
XAML.cs
protected override void OnAppearing()
{
if (BindingContext != null)
{
MedicalClaimConditionPageModel model = (MedicalClaimConditionPageModel)this.BindingContext.DataContext;
model.QuestionAdded += Model_QuestionAdded;
}
base.OnAppearing();
}
protected override void OnDisappearing()
{
MedicalClaimConditionPageModel model = (MedicalClaimConditionPageModel)this.BindingContext.DataContext;
model.QuestionAdded -= Model_QuestionAdded;
base.OnDisappearing();
}
void Model_QuestionAdded(object sender, EventArgs e)
{
Device.BeginInvokeOnMainThread(async () =>
{
if (Device.RuntimePlatform == Device.iOS)
{
// We need a Task.Delay to allow the contained controls to repaint and have their new sizes
// Ultimately we should come up with a better resolution - the delay value can vary depending on device and OS version.
await Task.Delay(50);
}
await QuestionScrollView.ScrollToAsync(QuestionRepeater, ScrollToPosition.End, true);
});
}
A reasonable answer has been posted by user yelinzh on the Xamarin forums:
How about trying to use Custom Renderer such as the fading effect.
[assembly: ExportRenderer(typeof(ScrollView_), typeof(ScrollViewR))]
namespace App3.Droid
{
public class ScrollViewR : ScrollViewRenderer
{
public ScrollViewR(Context context) : base(context)
{
}
protected override void OnElementChanged(VisualElementChangedEventArgs e)
{
base.OnElementChanged(e);
this.SetFadingEdgeLength(300);
this.VerticalFadingEdgeEnabled = true;
}
}
}
This would probably work. In fact we've got for a different approach
(after feedback from the customer), where we're keeping the current question at the top of the screen and scrolling off upwards, so the content is no longer expanding.
Related
I have spent my recent hours on researching on how to make this possible
I have a bottom tabbed page with a home button. What I want to achieve is, when button is pressed for x seconds, a new page shall be opened. How should the custom renderer for the tabbed page look like?
I already have one custom renderer for the tabbed page that makes sure that the home page button is a floating action button. The custom renderer for the home page button:
[assembly: ExportRenderer(typeof(NavigationBar), typeof(CustomTabbedRenderer))]
namespace MysteryLocation.Droid
{
public class CustomTabbedRenderer : TabbedPageRenderer
{
Context context;
public CustomTabbedRenderer(Context context) : base(context)
{
this.context = context;
}
protected override void OnElementChanged(ElementChangedEventArgs<TabbedPage> e)
{
base.OnElementChanged(e);
if (e.OldElement == null && e.NewElement != null)
{
for (int i = 0; i <= this.ViewGroup.ChildCount - 1; i++)
{
var childView = this.ViewGroup.GetChildAt(i);
if (childView is ViewGroup viewGroup)
{
((ViewGroup)childView).SetClipChildren(false);
for (int j = 0; j <= viewGroup.ChildCount - 1; j++)
{
var childRelativeLayoutView = viewGroup.GetChildAt(j);
if (childRelativeLayoutView is BottomNavigationView bottomView)
{
FloatingActionButton button = new FloatingActionButton(context);
BottomNavigationView.LayoutParams parameters = new BottomNavigationView.LayoutParams(150, 150);
parameters.Gravity = GravityFlags.CenterHorizontal;
Drawable d = Resources.GetDrawable(Resource.Drawable.image);
button.SetScaleType(Android.Widget.ImageView.ScaleType.Center);
button.SetImageDrawable(d);
button.LayoutParameters = parameters;
bottomView.AddView(button);
}
}
}
}
}
}
}
}
==========
EDIT
==========
In my customtabbedrenderer I've added button.LongClick += Button_LongClick and button.Click += Button_Click with the methods
private void Button_LongClick(object sender, EventArgs e)
{
//open a page through pushasync
}
private void Button_Click(object sender, EventArgs e)
{
//navigate to the page as usual, as it does when there's no
"button.Click" specified in the custom renderer
}
Any ideas on how the code should look like in the given methods?
=========
EDIT 2
=========
I have solved on long click, a new page is being opened through
button.LongClick += (object sender, LongClickEventArgs args) =>
{
Application.Current.MainPage.Navigation.PushModalAsync(new NavigationPage(new Pass()));
};
The app works both on click and on long click now. The remaining problem is, how can I allow the same button to navigate to the page that it is supposed to when only doing a simple click? It navigates to that page when there's no "button.Click += Button_Click" and "button.LongClick += Button_Click1". But as soon as I add a longclick event, the button stops working on click.
==========================
SOLVED
==========================
Added Element.CurrentPage = Element.Children[2] in the method Button_Click method, simple as that.
Longpress may be problematic in XF. For one of my applications I used a double tap:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:ios="clr-namespace:Xamarin.Forms.PlatformConfiguration.iOSSpecific;assembly=Xamarin.Forms.Core"
xmlns:CustomCtrls ="clr-namespace:D4503.CustomControls"
xmlns:Views="clr-namespace:D4503.Views"
x:Class="D4503.Pages.MainPage"
BackgroundColor="{StaticResource ScreenBackColor}"
ios:Page.UseSafeArea="true">
<ContentPage.Content>
<StackLayout>
<Image Grid.Row="1" Grid.Column="0" x:Name="btnRed" Style="{StaticResource ColorButtonImage}" Source="stoplight_red.png"
Aspect="AspectFit">
<Image.GestureRecognizers>
<TapGestureRecognizer Tapped="Red_Button_Tapped"/>
<TapGestureRecognizer Tapped="Red_Button_Tapped_Double" NumberOfTapsRequired="2" />
</Image.GestureRecognizers>
</Image>
</StackLayout>
</ContentPage.Content>
</ContentPage>
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!");
}
}
Instead of having the standard NavigationBar on the mainpage, I want to hide that one and load a custom one. I think using a BoxView (edit:) frame is the best method for this. However, How do I bind the height(request) of a BoxView to a (hidden) NavigationBar, preferably in Xaml?
(frame of boxview should have the same method)
For two common controls, you can bind a height of one control to another control like this(take BoxView and a Button as an example):
<BoxView Color="Yellow"
x:Name="testbox"
WidthRequest="160"
HeightRequest="60"
VerticalOptions="Center"
HorizontalOptions="Center" />
<Button Text="test..."
BindingContext="{x:Reference testbox}"
WidthRequest="160"
HeightRequest="{Binding Path=HeightRequest}"/>
Update
You can get the NavigationBar height on each platform using custom renderer.
And Then binding the height of NavigationBar to Boxview by following the method above.
For Android:
public class NaviRendererForAndroid : NavigationPageRenderer
{
public CustomNaviForAndroid(Context context) : base(context)
{
}
protected override void
OnElementChanged(ElementChangedEventArgs<NavigationPage> e)
{
base.OnElementChanged(e);
var height = 0;
Resources resources = Context.Resources;
int resourceId = resources.GetIdentifier("navigation_bar_height", "dimen", "android");
if (resourceId > 0)
{
height = resources.GetDimensionPixelSize(resourceId);
}
}
}
For IOS:
public class NaviRendererForiOS : NavigationRenderer
{
protected override void OnElementChanged(VisualElementChangedEventArgs e)
{
base.OnElementChanged(e);
var height = NavigationBar.Bounds.Height;
}
}
I have a listview with view cell.Inside the listview I have a image( using absolute layout with ratio) which i use for some animation.To do animation I need the x and y coordinates. I am extending the image as
class MyImage : Image
{
public void AnimateImage(double value)
{
this.LayoutTo(new Rectangle(this.X, this.Y - (value), 20, value),
}
}
I need to get the x and y coordinates during the time of loading(not by using any events).Through this code am not getting the correct x,y coordinates. Value am getting with the help of bindable property.
What do you mean by Through this code am not getting the correct x,y coordinates.? What is wrong?
I use you code and it works on my side.
I create a ListView with image in ViewCell and is a layout by absolute layout:
<ListView x:Name="listView" RowHeight="200">
<ListView.ItemsSource>
<x:Array Type="{x:Type x:String}">
<x:String>mono</x:String>
<x:String>monodroid</x:String>
<x:String>monotouch</x:String>
<x:String>monorail</x:String>
</x:Array>
</ListView.ItemsSource>
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<AbsoluteLayout AbsoluteLayout.LayoutBounds="0,0,500,100" AbsoluteLayout.LayoutFlags="WidthProportional" Padding="5,0,0,0">
<local:MyImage Source="Images"
AbsoluteLayout.LayoutBounds=".5,1,.1,.5" AbsoluteLayout.LayoutFlags="All" />
</AbsoluteLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
And in the code behind, i use you code and the animation works well:
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
}
}
public class MyImage : Image{
public MyImage() {
NSTimer.CreateScheduledTimer(3, true, (obj) =>
{
AnimateImage(30);
});
}
public void AnimateImage(double value)
{
Console.WriteLine(this.X);
Console.WriteLine(this.Y);
this.LayoutTo(new Rectangle(this.X, this.Y - (value), 20, value), 500);
}
}
The Y of image reduce 50 every 3 seconds and the height of image change to 50.
Am I doing something different with you?
Here is a gif:
Update:
Add a little delay before call the animation as I mentioned in my comment:
public MyImage() {
Task.Delay(100).ContinueWith(t => AnimateImage(30));
}
I used a custom confirmation popup window, this the XAML:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Text="{Binding Content}" TextWrapping="Wrap" Width="150"/>
<StackPanel Orientation="Horizontal" Margin="6" Grid.Row="1">
<Button x:Name="YesBtn" Width="100" Content="OK" Click="OnOk_Click"/>
<Button x:Name="NoBtn" Width="100" Content="No" Click="OnNo_Click"/>
</StackPanel>
</Grid>
and this is the code behide:
public partial class CustomConfirmation : IInteractionRequestAware
{
public CustomConfirmation()
{
InitializeComponent();
}
public IConfirmation Confirmation
{
get { return this.DataContext as IConfirmation; }
set { this.DataContext = value; }
}
public string Title { get; set; }
public bool Confirmed { get; set; }
public INotification Notification { get; set; }
public Action FinishInteraction { get; set; }
private void OnOk_Click(object sender, RoutedEventArgs e)
{
if (FinishInteraction != null)
{
Confirmation.Confirmed= true;
FinishInteraction();
}
}
private void OnNo_Click(object sender, RoutedEventArgs e)
{
if (FinishInteraction != null)
{
Confirmation.Confirmed = false;
FinishInteraction();
}
}
}
In view model class i have :
two commands(DispalyLongTextCommand and DispalyShortTextCommand): one
to display long message and the other to display a short message
and i have InteractionRequest ConfirmationRequest
object initialized in ctor to raise intercations.
if I display the long message first my custom window resize its content to the hole message, it is OK!
but if a want to display the short message, my window keeps the previous size!
note : even i set the window SizeToContent style to WidthAndHeight but it not working.
<ei:Interaction.Triggers>
<prism:InteractionRequestTrigger SourceObject="{Binding ConfirmationRequest, Mode=TwoWay}">
<prism:PopupWindowAction IsModal="True" CenterOverAssociatedObject="True">
<prism:PopupWindowAction.WindowStyle>
<Style TargetType="Window">
<Setter Property="SizeToContent" Value="WidthAndHeight"/>
</Style>
</prism:PopupWindowAction.WindowStyle>
<prism:PopupWindowAction.WindowContent>
<local:CustomConfirmation/>
</prism:PopupWindowAction.WindowContent>
</prism:PopupWindowAction>
</prism:InteractionRequestTrigger>
</ei:Interaction.Triggers>
can you guide me,
thanks in advance
SOLUTION:
I fixed the problem by adding this code in the code behind of the custom popup window, :
public CustomConfirmationView()
{
InitializeComponent();
Loaded += CustomPopupView_Loaded;
}
private void CustomPopupView_Loaded(object sender, RoutedEventArgs e)
{
var parentWindow = this.Parent as Window;
if (parentWindow != null)
{
parentWindow.Measure(parentWindow.DesiredSize);
}
}
The WindowContent property is reused each time you show a new popup. So, what happens is that when you first show a popup, the CustomPopupView is visualized and the height is set based on the current content. Now, when you close the popup, and change the content to a larger message and then show it again, the CustomPopupView.Height has already been set by the previous action and isn't updated in time for the new Window to get the correct height. So you must now resize the Window to match the new size of the CustomPopupView height. So just add a little code to handle this in your code-behind like this:
public CustomPopupView()
{
InitializeComponent();
Loaded += CustomPopupView_Loaded;
}
private void CustomPopupView_Loaded(object sender, RoutedEventArgs e)
{
var parentWindow = this.Parent as Window;
if (parentWindow != null)
parentWindow.MinHeight = _txt.ActualHeight + 75;
}
Note: '_txt' is the name of the TextBlock with the Content binding.
I think this has to do with the default confirmation window that ships with Prism. The MinWidth and MinHeight are set in the XAML to 300 and 150, respectively. So, the window width/weight will never get any smaller no matter what the window content is. Overriding the window style will not be enough to do what you need.
You could download the Prism code and remove that limitation if you are comfortable enough with that. The source path to the file you would want to start with is below.
\Source\Wpf\Prism.Wpf\Interactivity\DefaultPopupWindows\DefaultConfirmationWindow.xaml
Either that, or ask the Prism team to see if they can make this more flexible, which is probably a better suggestion. You can post this as an issue on their GitHub page. https://github.com/PrismLibrary/Prism/issues