Xamarin Show iOS UIDatePicker from Image Tap - c#

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);
}
}

Related

How to animate an element where the trigger is a data change on the property you want to animate

I want to animate my WPF window. It's set to size to content, so when the content changes, the window changes size. I don't want it to snap to a new size though, when it determines it needs a new size I want it to lerp between the old and new value with an animation.
How do I set this up?
EDIT:
I am using SizeToContent=WidthAndHeight" as a parameter for my window, and want to animate when it automatically sizes to different content being shown.
You may override the OnPropertyChanged method and start an animation of any property you like.
For example, the Width property:
private DoubleAnimation widthAnimation;
protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
{
if (e.Property == WidthProperty &&
!double.IsNaN((double)e.OldValue) &&
widthAnimation == null)
{
widthAnimation = new DoubleAnimation
{
Duration = TimeSpan.FromSeconds(1),
From = (double)e.OldValue,
To = (double)e.NewValue
};
widthAnimation.Completed += (s, a) =>
{
widthAnimation = null;
BeginAnimation(WidthProperty, null);
};
BeginAnimation(WidthProperty, widthAnimation);
}
else
{
base.OnPropertyChanged(e);
}
}

iOS Change badge color inside UIMoreNavigationController in UITabBarController

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.

How to handle the Tap of a ViewCell defined in code in Xamarin.Forms?

I have a multiple ViewCell and ImageCell subclasses inside a TableView.
I would like to execute some code when the user taps the cell. The whole cell highlights whenever it's touched, but I don't see any event handler to use here.
Isn't there a XAML Tapped equivalent for this but in code only?
private void SetTableView()
{
Content = new TableView
{
HasUnevenRows = true,
Intent = TableIntent.Menu,
Root = new TableRoot()
{
new TableSection()
{
new ProfileCell(ImageSource.FromFile("profile_placeholder.png"), "Casa de Férias")
},
new TableSection()
{
new InfoCell()
{
Type = InfoCellTypes.Pending,
Text = "Avaliação do Imóvel",
Detail = "Estado do Processo"
}
}
}
};
}
I'm sure there must be some API that handles this. Maybe I'm just not looking in the right place?
Thank you!
This can be accomplished by adding a TapGestureRecognizer to the cell's view layout.
var absoluteLayout = new AbsoluteLayout
{
Children =
{
photo,
editImage,
label
}
};
var tapGestureRecognizer = new TapGestureRecognizer();
tapGestureRecognizer.NumberOfTapsRequired = 1;
tapGestureRecognizer.Tapped += (s, e) => {
// handle the tap
};
absoluteLayout.GestureRecognizers.Add(tapGestureRecognizer);
View = absoluteLayout;
EDIT:
Or, a better alternative, using the Tapped property of the ViewCell, so it doesn't break the "Tap Animation":
Tapped += new EventHandler((e, s) => {
if (command.CanExecute(null))
{
command.Execute(null);
}
});

Xamarin.Forms: Change RelativeLayout constraints afterwards

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 .
}

Fill color of a single point in a ScatterSeries

So, I have tried to create a ScatterSeries using OxyPlot under Xamarin.Forms for which I want a point to be filled when a user interacts with it.
The plot itself is rendered just fine, and interaction seemingly does too.
The problem is that once I click on a point, it is not at first filled using the SelectionColor of the PlotModel. I have to drag the canvas in order for it to update, whereafter the specific point is updated and filled with the SelectionColor.
I instantiate the PlotModel like so:
var model = new PlotModel()
{
SelectionColor = OxyColors.Red,
// ...
};
Hereafter I instantiate the my ScatterSeries:
var scatterSeries = new ScatterSeries()
{
SelectionMode = SelectionMode.Single,
// ...
};
Next I create a List of ScatterPoints:
var scatterPoints = new List<ScatterPoint>
{
new ScatterPoint(0, 4),
// ...
};
Lastly, I listen to the TouchStarted event of the ScatterSeries:
scatterSeries.TouchStarted += (sender, e) =>
{
var xCoordinate = e.Position.X;
var yCoordinate = e.Position.Y;
var screenPoint = new ScreenPoint(xCoordinate, yCoordinate);
var point = lineSeries.GetNearestPoint(screenPoint, false);
var index = (int)point.Index;
scatterSeries.SelectItem(index);
// I expect it to refresh and update the PlotModel here:
model.InvalidatePlot(true);
e.Handled = true;
};
As mentioned this seems to work fine, except for that the PlotModel is not updated immediately with the SelectionColor - I have to drag the canvas before it updates.
Any ideas?

Categories