Is it possible to change the constraints of a RelativeLayout after they have been set one time?
In the documentation I see methods like GetBoundsConstraint(BindableObject), but I think you can only use them if you have a XAML file. For now I'm trying to do this in code. I get null if I try
RelativeLayout.GetBoundsConstraint(this.label);
The only way I found out is to remove the children from the layout and add it with the new constraints again.
Example:
public class TestPage : ContentPage
{
private RelativeLayout relativeLayout;
private BoxView view;
private bool moreWidth = false;
public TestPage()
{
this.view = new BoxView
{
BackgroundColor = Color.Yellow,
};
Button button = new Button
{
Text = "change",
TextColor = Color.Black,
};
button.Clicked += Button_Clicked;
this.relativeLayout = new RelativeLayout();
this.relativeLayout.Children.Add(this.view,
Constraint.Constant(0),
Constraint.Constant(0),
Constraint.Constant(80),
Constraint.RelativeToParent((parent) =>
{
return parent.Height;
}));
this.relativeLayout.Children.Add(button,
Constraint.RelativeToParent((parent) =>
{
return parent.Width / 2;
}),
Constraint.RelativeToParent((parent) =>
{
return parent.Height / 2;
}));
this.Content = this.relativeLayout;
}
private void Button_Clicked(object sender, EventArgs e)
{
double width = 0;
if(this.moreWidth)
{
width = 120;
}
else
{
width = 80;
}
var c= BoundsConstraint.FromExpression((Expression<Func<Rectangle>>)(() => new Rectangle(0, 0, width, this.Content.Height)), new View[0]);
RelativeLayout.SetBoundsConstraint(this.view, c);
this.relativeLayout.ForceLayout();
this.moreWidth = !this.moreWidth;
}
}
It does not officially possible with the current version of Xamarin Forms. The RelativeLayout container only recomputes constraints when adding/removing items from its children collection (it caches the solved constraints - presumable for performance). Even though the various constraints are implemented as Bindable Properties, they still do not get recomputed when changed.
I assume that the intention is to someday respect constraint updates, which would be useful with animations for example, but for now it doesn't appear to work that way.
HOWEVER, I took a look at the decompiled source for RelativeLayout and it is possible to hack together a way around it - but it might not suit your needs, depending on how much functionality you require and how complex your constraint definitions are.
See this example code (the key part is setting the constraint using SetBoundsConstraint, which overrides the internally computed bounds of the added view - and then calling ForceLayout()):
public partial class App : Application
{
public App ()
{
var label = new Label {
Text = "Test",
HorizontalTextAlignment = TextAlignment.Center,
VerticalTextAlignment = TextAlignment.Center,
BackgroundColor = Color.Silver
};
var layout = new RelativeLayout ();
layout.Children.Add (label,
Constraint.Constant (50),
Constraint.Constant (100),
Constraint.Constant (260),
Constraint.Constant (30));
MainPage = new ContentPage {
Content = layout
};
var fwd = true;
layout.Animate ("bounce",
(delta) => {
var d = fwd ? delta : 1.0 - delta;
var y = 100.0 + (50.0 * d);
var c = BoundsConstraint.FromExpression ((Expression<Func<Rectangle>>)(() => new Rectangle (50, y, 260, 30)), new View [0]);
RelativeLayout.SetBoundsConstraint(label, c);
layout.ForceLayout ();
}, 16, 800, Easing.SinInOut, (f, b) => {
// reset direction
fwd = !fwd;
}, () => {
// keep bouncing
return true;
});
}
}
Yes. This possible.
Layout code:
<StackLayout RelativeLayout.XConstraint="{Binding XConstaint}" ...>
VM code:
public Constraint XConstaint
{
get => _xConstaint;
set { SetFieldValue(ref _xConstaint, value, nameof(XConstaint)); }
}
public override void OnAppearing()
{
base.OnAppearing();
XConstaint = Constraint.RelativeToParent((parent) => { return parent.Width - 128; });
}
If i understood you correctly , I would try the following
Implement SizeChanged event for relativeLayout
like :-
var relativeLayout = new RelativeLayout ();
relativeLayout.SizeChanged += someEventForTesting;
and invoke the event whenever you want to resize the relative layout or some particular child.
public void someEventForTesting(Object sender , EventArgs)
{
var layout = (RelativeLayout)sender ;
//here you have access to layout and every child , you can add them again with new constraint .
}
Related
I'm trying to make a horizontal UIStackView with 2 images and a label:
The first image ico_qol should be anchored to the left side of the view.
The second image ico_edit should be anchored to the right side of the view.
The label should be anchored in between the two images.
The view should be sized to the height of the images which are the same. It should span the entire screen width.
What I'm trying to achieve is something like this (selected elements in blue):
Here is my code:
public partial class ViewController : UIViewController
{
public ViewController(IntPtr handle) : base(handle)
{
}
public override void ViewDidLoad()
{
base.ViewDidLoad();
UIStackView header = new UIStackView();
header.Axis = UILayoutConstraintAxis.Horizontal;
View.AddSubview(header);
//< color name = "healthy_green" >#409900</color>
var healthy_green = new UIColor(red: 0.25f, green: 0.60f, blue: 0.00f, alpha: 1.00f);
//< color name = "optimistic_orange" >#FF5F00</color>
var optimistic_orange = new UIColor(red: 1.00f, green: 0.37f, blue: 0.00f, alpha: 1.00f);
// Enable Auto Layout
header.TranslatesAutoresizingMaskIntoConstraints = false;
var ic_qol_image = UIImage.FromBundle("ic_qol");
ic_qol_image.ApplyTintColor(healthy_green);
var ic_qol = new UIImageView(ic_qol_image);
ic_qol.SizeToImage();
var qol_title = new UILabel { TranslatesAutoresizingMaskIntoConstraints = false, Text = #"Quality of Life" };
var ic_edit_image = UIImage.FromBundle("ic_edit");
ic_edit_image.ApplyTintColor(optimistic_orange);
var ic_edit = new UIImageView(ic_edit_image);
ic_edit.SizeToImage();
var qolHeaderViews = new UIView[] { ic_qol, qol_title, ic_edit };
header.AddSubviews(qolHeaderViews);
header.Anchor(top: View.SafeAreaLayoutGuide.TopAnchor, leading: View.LeadingAnchor, trailing: View.TrailingAnchor);
ic_qol.Anchor(top: header.TopAnchor, leading: header.LeadingAnchor);
ic_edit.Anchor(top: header.TopAnchor, trailing: header.TrailingAnchor);
qol_title.Anchor(top: View.SafeAreaLayoutGuide.TopAnchor, leading: ic_qol.RightAnchor, trailing: ic_edit.RightAnchor, padding: new UIEdgeInsets(0, 10, 0, 0)); <=== fails here.
}
public override void DidReceiveMemoryWarning()
{
base.DidReceiveMemoryWarning();
// Release any cached data, images, etc that aren't in use.
}
}
internal static class extensions
{
// https://stackoverflow.com/a/48601685/15186
internal static void SizeToImage(this UIImageView uIImageView)
{
//Grab loc
var xC = uIImageView.Center.X;
var yC = uIImageView.Center.Y;
//Size to fit
uIImageView.Frame = new CGRect(x: 0, y: 0, width: uIImageView.Image.Size.Width / 2, height: uIImageView.Image.Size.Height / 2);
//Move to loc
uIImageView.Center = new CGPoint(x: xC, y: yC);
}
internal static void FillParentView(this UIView uIView)
{
uIView.Anchor(top: uIView.Superview?.TopAnchor, leading: uIView.Superview?.LeadingAnchor, bottom: uIView.Superview?.BottomAnchor, trailing: uIView.Superview.TrailingAnchor);
}
internal static void AnchorSize(this UIView uIView, UIView to)
{
uIView.WidthAnchor.ConstraintEqualTo(to.WidthAnchor).Active = true;
uIView.HeightAnchor.ConstraintEqualTo(to.HeightAnchor).Active = true;
}
internal static void Anchor(this UIView uIView, NSLayoutYAxisAnchor top = null, NSLayoutXAxisAnchor leading = null, NSLayoutYAxisAnchor bottom = null, NSLayoutXAxisAnchor trailing = null, UIEdgeInsets padding = default, CGSize size= default)
{
uIView.TranslatesAutoresizingMaskIntoConstraints = false;
if (top != null)
{
uIView.TopAnchor.ConstraintEqualTo(top, padding.Top).Active = true;
}
if (leading != null)
{
uIView.LeadingAnchor.ConstraintEqualTo(leading, padding.Left).Active = true; <=== fails here.
}
if (bottom != null)
{
uIView.BottomAnchor.ConstraintEqualTo(bottom, -padding.Bottom).Active = true;
}
if (trailing != null)
{
uIView.TrailingAnchor.ConstraintEqualTo(trailing, -padding.Right).Active = true;
}
if ( size.Width != 0)
{
uIView.WidthAnchor.ConstraintEqualTo(size.Width).Active = true;
}
if ( size.Height != 0)
{
uIView.HeightAnchor.ConstraintEqualTo(size.Height).Active = true;
}
}
}
I'm trying to constraint the Leading anchor of the Label to the trailing anchor of the image but it fails with the following exception:
A constraint cannot be made between <NSLayoutXAxisAnchor:0x280bcb980 "UILabel:0x1015132a0'Quality of Life'.leading">' and '<NSLayoutXAxisAnchor:0x280bcb840 "UIImageView:0x101510ea0.right">' because their units are not compatible.
I understand that their units may be incompatible but how can I do that anyways? How to make the units compatible?
Credits: The extensions methods I'm using are originally created during this video tutorial. I ported them to c#.
You can achieve this layout easily with a "horizontal" stack view.
Note: I don't use xamarin / c#, so I'll just describe what you want to do.
create the stack view
set its properties to
Axis: Horizontal
Distribution: Fill
Alignment: Center (this is the vertical alignment of the subviews)
Spacing: 8 (you probably want a little space between subviews)
create 2 image views
load an image into each image view
set width and height constraints on the image views as desired (it looks like you are using 1/2 the height and width of your images?)
create a label
add the image views and label to the stack view as arranged subviews
header.AddArrangedSubview(ic_qol);
header.AddArrangedSubview(qol_title);
header.AddArrangedSubview(ic_edit);
add the stack view to the view
View.AddSubview(header);
The last step will be to add constraints to put the stack view at the top, spanning the width of the view:
// Get the parent view's layout
var margins = View.LayoutMarginsGuide;
// Pin the top edge of the stackView to the margin
header.TopAnchor.ConstraintEqualTo (margins.TopAnchor).Active = true;
// Pin the leading edge of the stackView to the margin
header.LeadingAnchor.ConstraintEqualTo (margins.LeadingAnchor).Active = true;
// Pin the trailing edge of the stackView to the margin
header.TrailingAnchor.ConstraintEqualTo (margins.TrailingAnchor).Active = true;
The result should look about like this:
Each imageView is constrained to 40-pts width and height-equalTo-width. The label has a cyan background to make it easy to see the layout, and text alignment is center. The outlines are just there so we can see the stackView's frame.
and at runtime, without the frame outlines:
I'm trying to make what I think will be a nice effect for an app - a series of images (think wallpaper) will be constantly scrolling in the background during a view. I started prototyping this in Xamarin.Forms, creating a custom control. Planned on a diagonal translation but started with the most basic approach and still ran into some issues fairly quickly, namely that it is not entirely smooth as it gets a bit choppy here and there (even when using caching and just a 10kb image) and 2) if user executes an action that's more involved it may cause a lag and the images get rendered more closely together than they should be. Is there a way to fix up this approach so that it's as smooth as possible and doesn't interfere (or get interfered with) the other UI elements, or is there a far superior approach for something like this - anyone ever tackle this? Please let me know, thanks.
FlyingImageBackground.cs
public class FlyingImageBackground : ContentView
{
public static readonly BindableProperty FlyingImageProperty =
BindableProperty.Create(nameof(FlyingImage), typeof(ImageSource), typeof(FlyingImageBackground), default(ImageSource), BindingMode.TwoWay, propertyChanged: OnFlyingImageChanged);
public ImageSource FlyingImage
{
get => (ImageSource)GetValue(FlyingImageProperty);
set => SetValue(FlyingImageProperty, value);
}
private AbsoluteLayout canvas;
public FlyingImageBackground()
{
this.canvas = new AbsoluteLayout()
{
HorizontalOptions = LayoutOptions.FillAndExpand,
VerticalOptions = LayoutOptions.FillAndExpand
};
this.canvas.SizeChanged += Canvas_SizeChanged;
Content = this.canvas;
}
~FlyingImageBackground() => this.canvas.SizeChanged -= Canvas_SizeChanged;
private static void OnFlyingImageChanged(BindableObject bindable, object oldValue, object newValue)
{
var control = (FlyingImageBackground)bindable;
control.BringToLife();
}
private void BringToLife()
{
if (this.canvas.Width <= 0 || this.canvas.Height <= 0)
return;
Device.StartTimer(TimeSpan.FromSeconds(1), () =>
{
Device.BeginInvokeOnMainThread(async () =>
{
await SendImageWave();
});
return this.canvas.IsVisible;
});
}
private async Task SendImageWave()
{
var startingX = -100;
var endingX = this.canvas.Width;
if (endingX <= 0)
return;
endingX += 100;
var yPositions = Enumerable.Range(0, (int)this.canvas.Height).Where(x => x % 90 == 0).ToList();
var imgList = new List<CachedImage>();
foreach (var yPos in yPositions)
{
var img = new CachedImage
{
Source = FlyingImage,
HeightRequest = 50
};
imgList.Add(img);
this.canvas.Children.Add(img, new Point(startingX, yPos));
}
await Task.WhenAll(
imgList.Select(x => x.TranslateTo(endingX, 0, 10000)));
//.Concat(imgList.Select(x => x.TranslateTo(startingX, 0, uint.MinValue))));
imgList.ForEach(x =>
{
this.canvas.Children.Remove(x);
x = null;
});
imgList = null;
}
private void Canvas_SizeChanged(object sender, EventArgs e)
{
BringToLife();
}
}
Usage example:
Just put it into a Grid in a ContentPage along with the main content:
e.g.:
<ContentPage.Content>
<Grid>
<controls:FlyingImageBackground FlyingImage="fireTruck.png" />
<StackLayout HorizontalOptions="Center">
<Button
Text="I'm a button!" />
<Label
FontAttributes="Bold,Italic"
Text="You're a good man, old sport!!!"
TextDecorations="Underline" />
</StackLayout>
</Grid>
</ContentPage.Content>
Switched to SkiaSharp and much better results. The animation appears smooth and if the flow gets interrupted the images maintain their appropriate distance. Also realized in the first draft using the built-in Xamarin Animations that I screwed up the check for when to run it; .IsVisible prop will remain true even if the page isn't even on the screen anymore, so in this new version needed to bind to a property that tells me if the page is actually active or not (based on when it gets navigated to and navigated away from) and if not then stop the animation. This is still just handling the horizontal scrolling effect for now. Hope someone else finds it useful, and any other improvements would be welcome, please just comment/post an answer!
[DesignTimeVisible(true)]
public class FlyingImageBackgroundSkia : ContentView
{
public static readonly BindableProperty IsActiveProperty =
BindableProperty.Create(
nameof(IsActive),
typeof(bool),
typeof(FlyingImageBackground),
default(bool),
BindingMode.TwoWay,
propertyChanged: OnPageActivenessChanged);
private SKCanvasView canvasView;
private SKBitmap resourceBitmap;
private Stopwatch stopwatch = new Stopwatch();
// consider making these bindable props
private float percentComplete;
private float imageSize = 40;
private float columnSpacing = 100;
private float rowSpacing = 100;
private float framesPerSecond = 60;
private float cycleTime = 1; // in seconds, for a single column
public FlyingImageBackgroundSkia()
{
this.canvasView = new SKCanvasView();
this.canvasView.PaintSurface += OnCanvasViewPaintSurface;
this.Content = this.canvasView;
string resourceID = "XamarinTestProject.Resources.Images.fireTruck.png";
Assembly assembly = GetType().GetTypeInfo().Assembly;
using (Stream stream = assembly.GetManifestResourceStream(resourceID))
{
this.resourceBitmap = SKBitmap.Decode(stream);
}
}
~FlyingImageBackgroundSkia() => this.resourceBitmap.Dispose();
public bool IsActive
{
get => (bool)GetValue(IsActiveProperty);
set => SetValue(IsActiveProperty, value);
}
private static async void OnPageActivenessChanged(BindableObject bindable, object oldValue, object newValue)
{
var control = (FlyingImageBackgroundSkia)bindable;
await control.AnimationLoop();
}
private async Task AnimationLoop()
{
this.stopwatch.Start();
while (IsActive)
{
this.percentComplete = (float)(this.stopwatch.Elapsed.TotalSeconds % this.cycleTime) / this.cycleTime; // always between 0 and 1
this.canvasView.InvalidateSurface(); // trigger redraw
await Task.Delay(TimeSpan.FromSeconds(1.0 / this.framesPerSecond)); // non-blocking
}
this.stopwatch.Stop();
}
private void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
var xPositions = Enumerable.Range(0, info.Width + (int)this.columnSpacing).Where(x => x % (int)this.columnSpacing == 0).ToList();
xPositions.Insert(0, -(int)this.columnSpacing);
var yPositions = Enumerable.Range(0, info.Height + (int)this.rowSpacing).Where(x => x % (int)this.rowSpacing == 0).ToList();
yPositions.Insert(0, -(int)this.rowSpacing);
if (this.resourceBitmap != null)
{
foreach (var xPos in xPositions)
{
var xPosNow = xPos + (this.rowSpacing * this.percentComplete);
foreach (var yPos in yPositions)
{
canvas.DrawBitmap(
this.resourceBitmap,
new SKRect(xPosNow, yPos, xPosNow + this.imageSize, yPos + this.imageSize));
}
}
}
}
}
I customize the background color and its tint color with this code:
var blackBGColor = new UIColor(new nfloat(40) / 255f,
new nfloat(40) / 255f,
new nfloat(40) / 255f, 1f);
this.MoreNavigationController.TopViewController.View.BackgroundColor = blackBGColor;
var moreTableView = (UITableView)this.MoreNavigationController.TopViewController.View;
moreTableView.TintColor = UIColor.White;
foreach (var cell in moreTableView.VisibleCells)
{
cell.BackgroundColor = blackBGColor;
cell.TextLabel.TextColor = UIColor.White;
var selectedView = new UIView
{
BackgroundColor = UIColor.DarkGray
};
cell.SelectedBackgroundView = selectedView;
}
this.MoreNavigationController.NavigationBar.BarStyle = UIBarStyle.Black;
this.MoreNavigationController.NavigationBar.Translucent = false;
this.MoreNavigationController.NavigationBar.TintColor = UIColor.White;
this.MoreNavigationController.NavigationBar.BarTintColor = new UIColor(24f / 255f, 24f / 255f, 24f / 255f, 1f);
But I couldn't change the badge color inside UIMoreNavigationController.
I've tried this:
this.MoreNavigationController.TopViewController.TabBarItem.BadgeColor = UIColor.White
but it's not working.
Tried this one too inside WillShowViewController:
this.ViewControllers[4].TabBarItem.BadgeColor = UIColor.White
but still not working.
Is there any way to change the badge color?
UPDATE:
After investigating the hierarchy of MoreNavigationController, apparently the badge value for Priority and DueBy tab is assign to a UILabel inside _UITableViewCellBadgeNeue. The hierarchy is:
this.MoreNavigationController.ViewControllers[0]: this is a UIMoreListController
Get the View and cast it to UITableView because that View is a _UIMoreListTableView
Then iterate inside that tableview VisibleCells, check the IEnumerator and in the forth object there is _UITableViewCellBadgeNeue
The SubViews[0] inside _UITableViewCellBadgeNeue is a UILabel and the label's text is the badge value.
Based on that, I change the label TextColor and BackgroundColor in WillShowViewController. It works but I need to go back and forth from Priority/DueBy tab to More tab. It never works on the first time.
[Export("navigationController:willShowViewController:animated:")]
public void WillShowViewController(UINavigationController navigationController, UIViewController viewController, bool animated)
{
if (this.MoreNavigationController.ViewControllers.Contains(viewController))
{
this.ViewControllers[4].TabBarItem.BadgeValue = this.ViewModel.SelectedPrioritiesFilter.Count > 0 ? this.ViewModel.SelectedPrioritiesFilter.Count.ToString() : null;
this.ViewControllers[5].TabBarItem.BadgeValue = this.ViewModel.SelectedDueByFilter != null ? "1" : null;
//this is the code to change the color
var vc = this.MoreNavigationController.ViewControllers[0];
var moreTableView = (UITableView)vc.View;
foreach (var cell in moreTableView.VisibleCells)
{
var enumerator = cell.GetEnumerator();
var i = 0;
while (enumerator.MoveNext())
{
//_UITableViewCellBadgeNeue is in the forth object
if(i == 3)
{
//_UITableViewCellBadgeNeue is a UIView
if (enumerator.Current is UIView)
{
var current = (UIView)enumerator.Current;
if (current != null)
{
if (current.Subviews.Length > 0)
{
var label = (UILabel)current.Subviews[0];
label.TextColor = UIColor.White;
label.BackgroundColor = UIColor.Clear;
}
}
}
}
i++;
}
}
}
}
I reproduced your issue and figured the reason out.
It is because the TableView has not finished rendering(drawing) when you retrieve the view hierarchy in WillShowViewController.
My test
View Hierarchy in WillShowViewController
UITableViewCellContentView
UIButton
View Hierarchy in DidShowViewController
UITableViewCellContentView
UIButton
_UITableViewCellBadgeNeue
_UITableViewCellSeparatorView
UITableViewCellContentView
UIButton
_UITableViewCellSeparatorView
So you just need replace the WillShowViewController with DidShowViewController.
PS: Small Suggestion
To avoid the change of View Hierarchy by Apple, you can use the condition like if (view.Class.Name.ToString() == "_UITableViewCellBadgeNeue") instead of judging which level the _UITableViewCellBadgeNeue is.
So I am using Xamarin and trying to create an image renderer that when tapped will show the UIDatePicker for iOS. I have the renderer working great for Android, but I can not get the date picker to display on iOS. My code is below for the renderer and I debugged into it and I get all the way through mTextField.BecomeFirstResponder(); which I "thought" would make the date picker display.
Can anyone point me in the right direction? I just need that picker to display on the screen. Once I can get it to display I can handle it like I want.
private void _DatePickerHasBeenTapped( object sender, EventArgs e )
{
if( mDatePickerDialog != null )
{
mDatePickerDialog.TouchCancel -= _DatePickerDialogOnCancelEvent;
mDatePickerDialog.ValueChanged -= _DatePickerDialogOnValueChanged;
}
var date = mDatePicker.PickerDate == DateTime.MinValue
? DateTime.Today
: mDatePicker.PickerDate;
mTextField = new UITextField();
mDatePickerDialog = new UIDatePicker { Mode = UIDatePickerMode.Date, TimeZone = new NSTimeZone( "UTC" ) };
mDatePickerDialog.ValueChanged += _DatePickerDialogOnValueChanged;
mDatePickerDialog.SetDate( date.ToNSDate(), false );
var width = UIScreen.MainScreen.Bounds.Width;
var toolbar = new UIToolbar( new RectangleF( 0, 0, (float)width, 44 ) ) { BarStyle = UIBarStyle.Default, Translucent = true };
var spacer = new UIBarButtonItem( UIBarButtonSystemItem.FlexibleSpace );
var doneButton = new UIBarButtonItem( UIBarButtonSystemItem.Done, ( o, a ) => mDatePickerDialog.ResignFirstResponder() );
toolbar.SetItems( new[] { spacer, doneButton }, false );
mTextField.InputView = mDatePickerDialog;
mTextField.InputAccessoryView = toolbar;
if( mTextField.CanBecomeFirstResponder )
{
mTextField.BecomeFirstResponder();
}
}
Your method would work if the UITextField was actually being displayed. Now you could hack it by adding the UITextField to the VC View hierarchy but offsetting it with a negative cgrect to get it off screen and removing it after in the UIBarButtonItem handler:
var mTextField = new UITextView(new CGRect(-1000, -1000, 10, 10));
yourViewController.Add(mTextField);
Note: I've seen responder failures with off-screen controls and avoid them when I need them to be faux responders, but I know others that do it... user beware... :-/
Or:
You could design a UIViewController either programmatically or from a storyboard that contains the picker and a close button and present that when the user taps your image. Catch the tap on the close button, dismiss the controller and send your picker date somewhere...
Or:
For something like this I normally use a popover style view controller.
Example:
var parentVC = this; // This would the "main" Form's ViewController, obtain it within your renderer
PopoverDelegate popoverDelegate = null;
var datePicker = new UIDatePicker(new CGRect(0, 0, parentVC.View.Frame.Width, 300));
popoverDelegate = new PopoverDelegate(datePicker, (date) =>
{
// Post the date change back to Forms via an event, etc...
Console.WriteLine($"Date Choosen: {date}");
datePicker.Dispose();
popoverDelegate.Dispose();
});
using (var popOverStyleVC = new UIViewController
{
ModalPresentationStyle = UIModalPresentationStyle.Popover
})
{
var pc = popOverStyleVC.PopoverPresentationController;
{
pc.Delegate = popoverDelegate;
pc.SourceView = parentVC.View;
pc.SourceRect = imageButton.Frame; // a CGRect where you want the popup arrow to point
}
popOverStyleVC.PreferredContentSize = datePicker.Frame.Size;
popOverStyleVC.View.Add(datePicker);
parentVC.PresentViewController(popOverStyleVC, true, null);
}
Results in:
The missing part from above it the UIPopoverPresentationControllerDelegate as you have to return UIModalPresentationStyle.None via UIModalPresentationStyle GetAdaptivePresentationStyle other the popover is not displayed properly, this is also where I catch the dismissal and provide a callback with the date.
UIPopoverPresentationControllerDelegate:
public class PopoverDelegate : UIPopoverPresentationControllerDelegate
{
public delegate void DatePicked(NSDate date);
UIDatePicker datePicker;
DatePicked datePicked;
public PopoverDelegate(UIDatePicker datePicker, DatePicked datePicked)
{
this.datePicked = datePicked;
this.datePicker = datePicker;
}
public override UIModalPresentationStyle GetAdaptivePresentationStyle(UIPresentationController forPresentationController)
{
return UIModalPresentationStyle.None;
}
public override void DidDismissPopover(UIPopoverPresentationController popoverPresentationController)
{
datePicked(datePicker.Date);
}
}
I'm having some trouble with gestures in xamarin forms. I've used an example I found from the web that wraps an Image control in a PanContainer which handles panning gestures:
using System;
using Xamarin.Forms;
namespace TestGestures
{
public class PanContainer : ContentView
{
double x, y;
public PanContainer ()
{
// Set PanGestureRecognizer.TouchPoints to control the
// number of touch points needed to pan
var panGesture = new PanGestureRecognizer ();
panGesture.PanUpdated += OnPanUpdated;
GestureRecognizers.Add (panGesture);
}
void OnPanUpdated (object sender, PanUpdatedEventArgs e)
{
switch (e.StatusType) {
case GestureStatus.Running:
// Translate and ensure we don't pan beyond the wrapped user interface element bounds.
Content.TranslationX = Math.Max (Math.Min (0, x + e.TotalX), -Math.Abs (Content.Width - App.ScreenWidth));
Content.TranslationY = Math.Max (Math.Min (0, y + e.TotalY), -Math.Abs (Content.Height - App.ScreenHeight));
break;
case GestureStatus.Completed:
// Store the translation applied during the pan
x = Content.TranslationX;
y = Content.TranslationY;
break;
}
}
}
}
Wrapper usage:
public HomePageCS ()
{
Content = new AbsoluteLayout {
Padding = new Thickness (20),
Children = {
new PanContainer {
Content = new Image {
Source = ImageSource.FromFile ("MonoMonkey.jpg"),
WidthRequest = 1024,
HeightRequest = 768
}
}
}
};
}
This example works absolutely fine, no problems at all.
The problems start when I replace the image that is wrapped in the PanContainer with a control that inherits from View and has a customer renderer for each platform. When I do this the OnPanUpdated method never gets called.
Does anyone know why this happens on a View and is there any workaround to get it to work?