CALayer Custom Property Animation with Xamarin - ios

i am really frustrated, because I am trying to animate a Pie Chart (Arc Segment with a transparent hole) on iOS with CoreAnimation since the last week.
At the Moment, I am drawing the ArcSegment with a CAShapeLayer with its Path-Property. It looks great, but I can't animate this property.
I want to animate the Layer-Property like Radius, Segments etc, with a CABasicAnimation.
Is there anyone here, who can tell me how to solve this?
Thank you.
Regards
Ronny
public class ArcSegmentLayer : CAShapeLayer {
private const string StartAngleProperty = "StartAngle";
private const string EndAngleProperty = "EndAngle";
public static void RegisterProperties() {
ObjCProperties.RegisterDynamicProperty(typeof(ArcSegmentLayer), StartAngleProperty, typeof(float));
ObjCProperties.RegisterDynamicProperty(typeof(ArcSegmentLayer), EndAngleProperty, typeof(float));
}
public ArcSegmentLayer() { }
[Export("initWithLayer:")]
public ArcSegmentLayer(ArcSegmentLayer layer) {
this.LineWidth = layer.LineWidth;
this.Frame = layer.Frame;
this.FillColor = layer.FillColor;
this.StrokeColor = layer.StrokeColor;
this.Segments = layer.Segments;
this.Margin = layer.Margin;
}
#region Properties
public float StartAngle {
get { return ObjCProperties.GetFloatProperty(Handle, StartAngleProperty); }
set {
ObjCProperties.SetFloatProperty(Handle, StartAngleProperty, value);
}
}
public float EndAngle {
get { return ObjCProperties.GetFloatProperty(Handle, EndAngleProperty); }
set {
ObjCProperties.SetFloatProperty(Handle, EndAngleProperty, value);
}
}
public nint Segments {
get { return segments; }
set {
if (segments != value) {
segments = value;
this.SetNeedsDisplay();
}
}
}
public nfloat Margin {
get {
return margin;
}
set {
if (margin != value) {
margin = value;
this.SetNeedsDisplay();
}
}
}
#endregion
[Export("needsDisplayForKey:")]
public static bool NeedsDisplayForKey(NSString key) {
return key == StartAngleProperty
|| key == EndAngleProperty
|| key == "Margin"
|| key == "Segments"
|| key == "LineWidth"
|| key == "StrokeColor"
|| CALayer.NeedsDisplayForKey(key);
}
[Export("display")]
public override void Display() {
base.Display();
Console.WriteLine(this.EndAngle);
this.Path = CreateSegments().CGPath;
}
[Export("actionForKey:")]
public override NSObject ActionForKey(string eventKey) {
/*
if (eventKey == EndAngleProperty) {
CABasicAnimation animation = CABasicAnimation.FromKeyPath(eventKey);
animation.TimingFunction = CAMediaTimingFunction.FromName(CAMediaTimingFunction.Linear);
animation.From = new NSNumber(this.EndAngle); //PresentationLayer.ValueForKey(new NSString(eventKey));
//animation.Duration = CATransition. 1;
animation.Duration = 0;
return animation;
} else if (eventKey == StartAngleProperty) {
CABasicAnimation animation = CABasicAnimation.FromKeyPath(eventKey);
animation.TimingFunction = CAMediaTimingFunction.FromName(CAMediaTimingFunction.Linear);
animation.From = new NSNumber(this.StartAngle);
animation.Duration = 0;
return animation;
}*/
return base.ActionForKey(eventKey);
}
private UIBezierPath CreateSegments() {
var path = new UIBezierPath();
nfloat segmentSize = (nfloat)(360.0 / (nfloat)this.Segments);
nfloat startSegAngle = 0;
nfloat endSegAngle = startSegAngle + segmentSize;
if (this.Segments > 1) {
var fromSeg = (nint)((((double)this.Segments) * this.StartAngle) / 360.0);
var toSeg = (nint)((((double)this.Segments) * this.EndAngle) / 360.0);
for (var seg = 0; seg < this.Segments; seg++) {
var hiddenLayer = !(seg >= fromSeg && seg < toSeg);
if (!hiddenLayer) {
path.AppendPath(
this.CreateSegmentPath(
startSegAngle, endSegAngle - this.Margin));
}
startSegAngle += segmentSize;
endSegAngle += segmentSize;
}
} else if (this.Segments == 1) {
path.AppendPath(this.CreateSegmentPath(this.StartAngle, this.EndAngle));
}
return path;
}
private UIBezierPath CreateSegmentPath(nfloat startSegAngle, nfloat endSegAngle) {
var center = new CGPoint(x: this.Bounds.Width / 2f, y: this.Bounds.Height / 2f);
var radius = (nfloat)Math.Max(this.Bounds.Width, this.Bounds.Height) / 2f - this.LineWidth / 2f;
var path = UIBezierPath.FromArc(
center,
radius,
Deg2Rad(startSegAngle - 90f),
Deg2Rad(endSegAngle - 90f),
true);
path.MoveTo(center);
path.ClosePath();
path.Stroke();
return path;
}
private static nfloat Deg2Rad(nfloat value) {
return (nfloat)(floatPI / 180.0 * value);
}
private static readonly nfloat floatPI = (nfloat)Math.PI;
private nint segments;
private nfloat margin;
}
[DesignTimeVisible(true)]
public partial class ArcSegmentView : UIView {
public ArcSegmentView(IntPtr handle) : base(handle) {
this.strokeColor = UIColor.Black.CGColor;
}
#region Properties
[Export("StartAngle"), Browsable(true)]
public nfloat StartAngle {
get { return startAngle; }
set {
if (startAngle != value) {
startAngle = value;
((ArcSegmentLayer)this.Layer).StartAngle = (float)value;
this.SetNeedsDisplay();
}
}
}
[Export("EndAngle"), Browsable(true)]
public nfloat EndAngle {
get { return endAngle; }
set {
if (endAngle != value) {
endAngle = value;
((ArcSegmentLayer)this.Layer).EndAngle = (float)value;
this.SetNeedsDisplay();
}
}
}
[Export("Segments"), Browsable(true)]
public nint Segments {
get { return segments; }
set {
if (segments != value) {
segments = value;
((ArcSegmentLayer)this.Layer).Segments = value;
this.SetNeedsDisplay();
}
}
}
[Export("Margin"), Browsable(true)]
public nfloat Margin {
get { return margin; }
set {
if (margin != value) {
margin = value;
((ArcSegmentLayer)this.Layer).Margin = value;
this.SetNeedsDisplay();
}
}
}
[Export("LineWidth"), Browsable(true)]
public nfloat LineWidth {
get { return lineWidth; }
set {
if (lineWidth != value) {
lineWidth = value;
((ArcSegmentLayer)this.Layer).LineWidth = value;
this.SetNeedsDisplay();
}
}
}
[Export("StrokeColor"), Browsable(true)]
public CGColor StrokeColor {
get { return strokeColor; }
set {
if (StrokeColor != value) {
strokeColor = value;
((ArcSegmentLayer)this.Layer).StrokeColor = value;
//this.SetNeedsDisplay();
}
}
}
#endregion
[Export("layerClass")]
static Class LayerClass() {
return new Class(typeof(ArcSegmentLayer));
}
private nfloat lineWidth;
private nfloat margin;
private nint segments;
private nfloat startAngle;
private nfloat endAngle;
private CGColor strokeColor;
}
public partial class ViewController : UIViewController {
protected ViewController(IntPtr handle) : base(handle) { }
public override void ViewDidLoad() {
base.ViewDidLoad();
arcSegment.StartAngle = 45;
arcSegment.EndAngle = 90;
arcSegment.Margin = 2;
arcSegment.StrokeColor = UIColor.Red.CGColor;
arcSegment.Segments = 70;
arcSegment.LineWidth = 10;
CABasicAnimation animation = CABasicAnimation.FromKeyPath("EndAngle");
animation.TimingFunction = CAMediaTimingFunction.FromName(CAMediaTimingFunction.Linear);
animation.From = new NSNumber(45);
animation.To = new NSNumber(360);
animation.Duration = 10;
arcSegment.Layer.AddAnimation(animation, "EndAngle");
}
}

We have a sample that shows how to do this:
https://github.com/xamarin/ios-samples/tree/master/CustomPropertyAnimation
In particular:
https://github.com/xamarin/ios-samples/blob/master/CustomPropertyAnimation/AppDelegate.cs

Related

Xamarin: How to avoid internal ListView from change its content size when showing keyboard on iOS

Im creating this content page:
Content = new StackLayout()
{
Spacing = 0,
Orientation = StackOrientation.Vertical,
Children = {
(listView = new ListView
{
HasUnevenRows = true,
SeparatorVisibility = SeparatorVisibility.None,
IsPullToRefreshEnabled = true,
HorizontalOptions = LayoutOptions.FillAndExpand,
VerticalOptions = LayoutOptions.FillAndExpand,
ItemsSource = listItems,
ItemTemplate = new MyDataTemplateSelector(userName),
BackgroundColor = Constants.Cor_ChatFundo
}),
(grid = new Grid
{
RowSpacing = 1,
ColumnSpacing = 2,
Padding = new Thickness(5),
BackgroundColor = Color.White,
VerticalOptions = LayoutOptions.End,
HorizontalOptions = LayoutOptions.FillAndExpand,
ColumnDefinitions =
{
new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) },
new ColumnDefinition { Width = new GridLength(1, GridUnitType.Auto) }
},
RowDefinitions =
{
new RowDefinition { Height = new GridLength(40) }
}
})
}
};
grid.Children.Add(sendMessageEntry = new Entry
{
FontSize = 18,
HeightRequest = 30,
Placeholder = "Type here...",
Keyboard = Keyboard.Chat
}, 0, 0);
grid.Children.Add(buttonSend = new Button
{
Text = "Send"
}, 1, 0);
I'm using a modified version of the KeyboardOverlapRenderer to move the entire page UP when the keyboard is shown.
The modified version of the KeyboardOverlapRenderer is to handle the suggestion bar above the iOS8 keyboard... the original version doesn't handle that.
The KeyboardOverlapRenderer class:
using System;
using Xamarin.Forms.Platform.iOS;
using Foundation;
using UIKit;
using Xamarin.Forms;
using CoreGraphics;
using EficienciaEnergetica.iOS.KeyboardOverlap;
using System.Diagnostics;
using EficienciaEnergetica.ContentPages;
[assembly: ExportRenderer(typeof(Page), typeof(KeyboardOverlapRenderer))]
namespace EficienciaEnergetica.iOS.KeyboardOverlap
{
[Preserve(AllMembers = true)]
public class KeyboardOverlapRenderer : PageRenderer
{
Rectangle initialViewState;
NSObject _keyboardShowObserver;
NSObject _keyboardHideObserver;
private bool _pageWasShiftedUp;
private double _activeViewBottom;
private bool _isKeyboardShown;
public static void StaticInit()
{
var now = DateTime.Now;
Debug.WriteLine("Keyboard Overlap plugin initialized {0}", now);
}
public override void ViewWillAppear(bool animated)
{
base.ViewWillAppear(animated);
var page = Element as ContentPage;
if (page != null)
{
var contentScrollView = page.Content as ScrollView;
if (contentScrollView != null)
return;
initialViewState = Element.Bounds;
RegisterForKeyboardNotifications();
}
}
public override void ViewWillDisappear(bool animated)
{
base.ViewWillDisappear(animated);
UnregisterForKeyboardNotifications();
}
void RegisterForKeyboardNotifications()
{
if (_keyboardShowObserver == null)
_keyboardShowObserver = NSNotificationCenter.DefaultCenter.AddObserver(UIKeyboard.WillShowNotification, OnKeyboardShow);
if (_keyboardHideObserver == null)
_keyboardHideObserver = NSNotificationCenter.DefaultCenter.AddObserver(UIKeyboard.WillHideNotification, OnKeyboardHide);
}
void UnregisterForKeyboardNotifications()
{
_isKeyboardShown = false;
if (_keyboardShowObserver != null)
{
NSNotificationCenter.DefaultCenter.RemoveObserver(_keyboardShowObserver);
_keyboardShowObserver.Dispose();
_keyboardShowObserver = null;
}
if (_keyboardHideObserver != null)
{
NSNotificationCenter.DefaultCenter.RemoveObserver(_keyboardHideObserver);
_keyboardHideObserver.Dispose();
_keyboardHideObserver = null;
}
}
protected virtual void OnKeyboardShow(NSNotification notification)
{
if (!IsViewLoaded)
return;
_isKeyboardShown = true;
var activeView = View.FindFirstResponder();
if (activeView == null)
return;
var keyboardFrame = UIKeyboard.FrameEndFromNotification(notification);
var isOverlapping = activeView.IsKeyboardOverlapping(View, keyboardFrame);
if (!isOverlapping)
return;
if (isOverlapping)
{
System.Diagnostics.Debug.WriteLine(keyboardFrame);
_activeViewBottom = activeView.GetViewRelativeBottom(View);
ShiftPageUp(keyboardFrame.Height, _activeViewBottom);
}
}
private void OnKeyboardHide(NSNotification notification)
{
if (!IsViewLoaded)
return;
_isKeyboardShown = false;
var keyboardFrame = UIKeyboard.FrameEndFromNotification(notification);
if (_pageWasShiftedUp)
ShiftPageDown(keyboardFrame.Height, _activeViewBottom);
}
private void ShiftPageUp(nfloat keyboardHeight, double activeViewBottom)
{
var pageFrame = initialViewState;// Element.Bounds;
var newY = pageFrame.Y + CalculateShiftByAmount(pageFrame.Height, keyboardHeight, activeViewBottom);
Element.LayoutTo(new Rectangle(pageFrame.X, newY,
pageFrame.Width, pageFrame.Height));
_pageWasShiftedUp = true;
}
private void ShiftPageDown(nfloat keyboardHeight, double activeViewBottom)
{
Element.LayoutTo(initialViewState);
_pageWasShiftedUp = false;
}
private double CalculateShiftByAmount(double pageHeight, nfloat keyboardHeight, double activeViewBottom)
{
return (pageHeight - activeViewBottom) - keyboardHeight;
}
}
}
The problem I have is when editing the entry. In iOS it shows the keyboard, but the content of the internal listview seems to allocate an empty space for the keyboard also, and it is possible to scroll down the list more than the elements that are inside it.
Is it possible to disable the listview keyboard notification behaviour in this situation? The ListView is not the main component in this page.
I found a solution for me!
Set lv.HeightRequest on the entry focus!
Seems to be

how to expand customPinWinod in xamarin forms maps

I have this custom renderer for iOS and Im uisng xamarin forms maps:
public class CustomMapRenderer : MapRenderer
{
UIView customPinView;
ObservableCollection<CustomPin> customPins;
protected override void OnElementChanged(ElementChangedEventArgs<View> e)
{
base.OnElementChanged(e);
if (e.OldElement != null)
{
var nativeMap = Control as MKMapView;
nativeMap.GetViewForAnnotation = null;
nativeMap.CalloutAccessoryControlTapped -= OnCalloutAccessoryControlTapped;
nativeMap.DidSelectAnnotationView -= OnDidSelectAnnotationView;
nativeMap.DidDeselectAnnotationView -= OnDidDeselectAnnotationView;
}
if (e.NewElement != null)
{
var formsMap = (CustomMap)e.NewElement;
var nativeMap = Control as MKMapView;
customPins = formsMap.CustomPins;
nativeMap.GetViewForAnnotation = GetViewForAnnotation;
nativeMap.CalloutAccessoryControlTapped += OnCalloutAccessoryControlTapped;
nativeMap.DidSelectAnnotationView += OnDidSelectAnnotationView;
nativeMap.DidDeselectAnnotationView += OnDidDeselectAnnotationView;
}
}
protected override MKAnnotationView GetViewForAnnotation(MKMapView mapView, IMKAnnotation annotation)
{
MKAnnotationView annotationView = null;
if (annotation is MKUserLocation)
return null;
var customPin = GetCustomPin(annotation as MKPointAnnotation);
if (customPin == null)
{
throw new Exception("Custom pin not found");
}
annotationView = mapView.DequeueReusableAnnotation(customPin.Label);
if (annotationView == null)
{
annotationView = new CustomMKAnnotationView(annotation, customPin.Label);
annotationView.Image = UIImage.FromFile("pin.png");
annotationView.CalloutOffset = new CGPoint(0, 0);
annotationView.LeftCalloutAccessoryView = new UIImageView(UIImage.FromFile("monkey.png"));
annotationView.RightCalloutAccessoryView = UIButton.FromType(UIButtonType.DetailDisclosure);
((CustomMKAnnotationView)annotationView).Name = customPin.Label;
}
annotationView.CanShowCallout = true;
return annotationView;
}
void OnCalloutAccessoryControlTapped(object sender, MKMapViewAccessoryTappedEventArgs e)
{
CustomMKAnnotationView customView = e.View as CustomMKAnnotationView;
if (!string.IsNullOrWhiteSpace(customView.Url))
{
UIApplication.SharedApplication.OpenUrl(new Foundation.NSUrl(customView.Url));
}
}
void OnDidSelectAnnotationView(object sender, MKAnnotationViewEventArgs e)
{
CustomMKAnnotationView customView = e.View as CustomMKAnnotationView;
customPinView = new UIView();
var label = new UILabel();
label.Text = "Tutorial";
label.Font.WithSize(36);
customPinView.AddSubview(label);
customPinView.Frame = new CGRect(0, 0, 200, 84);
var image = new UIImageView(new CGRect(0, 0, 200, 84));
image.Image = UIImage.FromFile("xamarin.png");
customPinView.AddSubview(image);
customPinView.Center = new CGPoint(0, -(e.View.Frame.Height + 75));
e.View.AddSubview(customPinView);
}
void OnDidDeselectAnnotationView(object sender, MKAnnotationViewEventArgs e)
{
if (!e.View.Selected)
{
customPinView.RemoveFromSuperview();
customPinView.Dispose();
customPinView = null;
}
}
CustomPin GetCustomPin(MKPointAnnotation annotation)
{
var position = new Position(annotation.Coordinate.Latitude, annotation.Coordinate.Longitude);
foreach (var pin in customPins)
{
if (pin.Position == position)
{
return pin;
}
}
return null;
}
}
And I want to add some more things below, and I dont know why it doesnt resize this pin window?
Here is the outcome:
UPDATE:
Here is Android renderer which works fine. From my CUstomPinModel, I need dynamically create labels:
public class CustomMapRenderer : MapRenderer, GoogleMap.IInfoWindowAdapter
{
private LatLngBounds.Builder _builder;
private GoogleMap _map;
ObservableCollection<CustomPin> customPins { get; set; }
Context context;
public CustomMapRenderer(Context context) : base(context)
{
this.context = context;
customPins = new ObservableCollection<CustomPin>();
_builder = new LatLngBounds.Builder();
}
protected override void OnElementChanged(Xamarin.Forms.Platform.Android.ElementChangedEventArgs<Map> e)
{
base.OnElementChanged(e);
if (e.OldElement != null)
{
NativeMap.InfoWindowClick -= OnInfoWindowClick;
}
if (e.NewElement != null)
{
var formsMap = (CustomMap)e.NewElement;
customPins = formsMap.CustomPins;
}
}
protected override void OnMapReady(GoogleMap map)
{
base.OnMapReady(map);
NativeMap.InfoWindowClick += OnInfoWindowClick;
NativeMap.SetInfoWindowAdapter(this);
if (_map == null)
{
_map = map;
}
}
protected override MarkerOptions CreateMarker(Pin pin)
{
CustomPin customPin = (CustomPin)pin;
var marker = new MarkerOptions();
LatLng position = new LatLng(pin.Position.Latitude, pin.Position.Longitude);
marker.SetPosition(new LatLng(pin.Position.Latitude, pin.Position.Longitude));
marker.SetTitle(pin.Label);
marker.SetSnippet(pin.Address);
marker.SetIcon(BitmapDescriptorFactory.FromResource(Resource.Drawable.pin));
_builder.Include(position);
LatLngBounds bounds = _builder.Build();
CameraUpdate cu = CameraUpdateFactory.NewLatLngBounds(bounds, 20);
_map.MoveCamera(cu);
return marker;
}
void OnInfoWindowClick(object sender, GoogleMap.InfoWindowClickEventArgs e)
{
var customPin = GetCustomPin(e.Marker);
if (customPin == null)
{
throw new Exception("Custom pin not found");
}
}
public Android.Views.View GetInfoContents(Marker marker)
{
var inflater = Android.App.Application.Context.GetSystemService(Context.LayoutInflaterService) as Android.Views.LayoutInflater;
if (inflater != null)
{
Android.Views.View view;
var customPin = GetCustomPin(marker);
if (customPin == null)
{
throw new Exception("Custom pin not found");
}
view = inflater.Inflate(Resource.Layout.MapInfoWindow, null);
LinearLayout linearLayout = (LinearLayout)view.FindViewById(Resource.Id.InfoWindowProps);
if(customPin != null && customPin.InfoBox != null && customPin.InfoBox.DetailsObjectInfos.Count > 0)
{
for (int i = 0; i < customPin.InfoBox.DetailsObjectInfos.Count; i++)
{
TextView t1 = new TextView(context);
t1.Text = customPin.InfoBox.DetailsObjectInfos[i].BoldLabelTitle + customPin.InfoBox.DetailsObjectInfos[i].LabelValue;
linearLayout.AddView(t1);
}
}
return view;
}
return null;
}
public Android.Views.View GetInfoWindow(Marker marker)
{
return null;
}
CustomPin GetCustomPin(Marker annotation)
{
var position = new Position(annotation.Position.Latitude, annotation.Position.Longitude);
foreach (var pin in customPins)
{
if (pin.Position == position)
{
return pin;
}
}
return null;
}
}
Also, I set in my layout correct naming in order to be able to find resource by id:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:id="#+id/InfoWindowImage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:adjustViewBounds="true"
android:src="#drawable/monkey" />
<LinearLayout
android:id="#+id/InfoWindowProps"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_margin="10dp"
android:layout_marginRight="261.5dp" >
</LinearLayout>
</LinearLayout>
UPDATE #2:
My model for CustomPin:
public class CustomPin : Pin, INotifyPropertyChanged
{
public string PinIcon { get; set; }
public Color Color { get; set; }
public InfoBoxMapModel InfoBox { get; set; } = new InfoBoxMapModel();
public int? Id { get; set; }
public int? ClassId { get; set; }
public string FavoriteLabel { get; set; }
public bool? _isFavorite { get; set; }
public bool? IsFavorite
{
get { return _isFavorite; }
set { _isFavorite = value; OnPropertyChanged(); }
}
}
and InfoBoxModel which is containing my string labels:
public class InfoBoxMapModel
{
//base64
public string ImageString { get; set; }
public ImageSource PinImageSource { get; set; }
public List<DetailsObjectInfo> DetailsObjectInfos { get; set; } = new List<DetailsObjectInfo>();
public int CountDetailsItemsRows { get; set; }
}
public class DetailsObjectInfo
{
public string BoldLabelTitle { get; set; }
public string LabelValue { get; set; }
}
UPDATE #3: Here is the example how it works in android:
All these rows of strings are concatenation of DetailsObjectInfo => BoldLabelTitle + ": " + LabelValue
So, I want to render DetailsObjectyInfos List in white callout pin cloud.
If need to add subviews dynamically , the one way that you need to calculate the Frame of each Control (Label).Such as follow:
for (int i = 0; i < 3; i++)
{
var label = new UILabel();
label.Frame = new CGRect(50*i, 0, 45, 84);
label.Text = "Tutorial:"+i;
label.Font.WithSize(36);
customPinView.AddSubview(label);
}
The another way is using UIStackView as RootView as follow:
void OnDidSelectAnnotationView(object sender, MKAnnotationViewEventArgs e)
{
CustomMKAnnotationView customView = e.View as CustomMKAnnotationView;
//customPinView = new UIView();
UIStackView customPinView = new UIStackView();
if (customView.Name.Equals("Xamarin"))
{
//var label = new UILabel(new CGRect(-50, 0, 50, 84));
for (int i = 0; i < 3; i++)
{
var label = new UILabel();
label.Text = "Tutorial:"+i;
label.Font.WithSize(36);
customPinView.AddArrangedSubview(label);
}
customPinView.Frame = new CGRect(0, 0, 300, 84);
customPinView.Axis = UILayoutConstraintAxis.Horizontal;
customPinView.Distribution = UIStackViewDistribution.EqualSpacing;
customPinView.Spacing = 10;
customPinView.Alignment = UIStackViewAlignment.Fill;
customPinView.Center = new CGPoint(0, -(e.View.Frame.Height + 75));
//customPinView.BackgroundColor = UIColor.Yellow;
e.View.AddSubview(customPinView);
}
}
The effect:
=============================Update #1===============================
MainPage declare the Lable text of Pin:
public partial class MapPage : ContentPage
{
public MapPage()
{
InitializeComponent();
CustomPin pin = new CustomPin
{
Type = PinType.Place,
Position = new Position(37.79752, -122.40183),
Label = "Xamarin San Francisco Office",
Address = "394 Pacific Ave, San Francisco CA",
Name = "Xamarin",
Url = "http://xamarin.com/about/"
};
customMap.CustomPins = new List<CustomPin> { pin };
customMap.Pins.Add(pin);
customMap.MoveToRegion(MapSpan.FromCenterAndRadius(new Position(37.79752, -122.40183), Distance.FromMiles(1.0)));
}
}
=============================Update #2#3===============================
Your idea is right . I have found the solution that replace the adress string wtih my custom Labels .
There is a DetailCalloutAccessoryView of MKAnnotationView , if we can use custome View for it , it will show the wants.
Modify it inside the GetViewForAnnotation method :
protected override MKAnnotationView GetViewForAnnotation(MKMapView mapView, IMKAnnotation annotation)
{
MKAnnotationView annotationView = null;
if (annotation is MKUserLocation)
return null;
var customPin = GetCustomPin(annotation as MKPointAnnotation);
if (customPin == null)
{
throw new Exception("Custom pin not found");
}
annotationView = mapView.DequeueReusableAnnotation(customPin.Name);
if (annotationView == null)
{
annotationView = new CustomMKAnnotationView(annotation, customPin.Name);
annotationView.Image = UIImage.FromFile("pin.png");
annotationView.CalloutOffset = new CGPoint(0, 0);
//annotationView.LeftCalloutAccessoryView = new UIImageView(UIImage.FromFile("monkey.png"));
UIImageView uIImageView = new UIImageView(UIImage.FromFile("monkey.png"));
uIImageView.Frame = new CGRect(0, 0, 75, 100);
annotationView.LeftCalloutAccessoryView = uIImageView;
annotationView.RightCalloutAccessoryView = UIButton.FromType(UIButtonType.DetailDisclosure);
((CustomMKAnnotationView)annotationView).Name = customPin.Name;
((CustomMKAnnotationView)annotationView).Url = customPin.Url;
customPinView = new UIStackView();
for (int i = 0; i < 3; i++)
{
var label = new UILabel();
label.Text = "Tutorial: " + i;
label.BackgroundColor = UIColor.White;
label.Font.WithSize(36);
customPinView.AddArrangedSubview(label);
}
customPinView.Frame = new CGRect(0, 0, 300, 84);
customPinView.Axis = UILayoutConstraintAxis.Vertical;
customPinView.Distribution = UIStackViewDistribution.EqualSpacing;
customPinView.Spacing = 1;
customPinView.Alignment = UIStackViewAlignment.Fill;
annotationView.DetailCalloutAccessoryView = customPinView;
}
annotationView.CanShowCallout = true;
return annotationView;
}
This is the full renderer code :
public class CustomMapRenderer : MapRenderer
{
UIStackView customPinView;
List<CustomPin> customPins;
protected override void OnElementChanged(ElementChangedEventArgs<View> e)
{
base.OnElementChanged(e);
if (e.OldElement != null)
{
var nativeMap = Control as MKMapView;
nativeMap.GetViewForAnnotation = null;
nativeMap.CalloutAccessoryControlTapped -= OnCalloutAccessoryControlTapped;
nativeMap.DidSelectAnnotationView -= OnDidSelectAnnotationView;
nativeMap.DidDeselectAnnotationView -= OnDidDeselectAnnotationView;
}
if (e.NewElement != null)
{
var formsMap = (CustomMap)e.NewElement;
var nativeMap = Control as MKMapView;
customPins = formsMap.CustomPins;
nativeMap.GetViewForAnnotation = GetViewForAnnotation;
nativeMap.CalloutAccessoryControlTapped += OnCalloutAccessoryControlTapped;
nativeMap.DidSelectAnnotationView += OnDidSelectAnnotationView;
nativeMap.DidDeselectAnnotationView += OnDidDeselectAnnotationView;
}
}
protected override MKAnnotationView GetViewForAnnotation(MKMapView mapView, IMKAnnotation annotation)
{
MKAnnotationView annotationView = null;
if (annotation is MKUserLocation)
return null;
var customPin = GetCustomPin(annotation as MKPointAnnotation);
if (customPin == null)
{
throw new Exception("Custom pin not found");
}
annotationView = mapView.DequeueReusableAnnotation(customPin.Name);
if (annotationView == null)
{
annotationView = new CustomMKAnnotationView(annotation, customPin.Name);
annotationView.Image = UIImage.FromFile("pin.png");
annotationView.CalloutOffset = new CGPoint(0, 0);
//annotationView.LeftCalloutAccessoryView = new UIImageView(UIImage.FromFile("monkey.png"));
UIImageView uIImageView = new UIImageView(UIImage.FromFile("monkey.png"));
uIImageView.Frame = new CGRect(0, 0, 75, 100);
annotationView.LeftCalloutAccessoryView = uIImageView;
annotationView.RightCalloutAccessoryView = UIButton.FromType(UIButtonType.DetailDisclosure);
((CustomMKAnnotationView)annotationView).Name = customPin.Name;
((CustomMKAnnotationView)annotationView).Url = customPin.Url;
customPinView = new UIStackView();
for (int i = 0; i < 3; i++)
{
var label = new UILabel();
label.Text = "Tutorial: " + i;
label.BackgroundColor = UIColor.White;
label.Font.WithSize(36);
customPinView.AddArrangedSubview(label);
}
customPinView.Frame = new CGRect(0, 0, 300, 84);
customPinView.Axis = UILayoutConstraintAxis.Vertical;
customPinView.Distribution = UIStackViewDistribution.EqualSpacing;
customPinView.Spacing = 1;
customPinView.Alignment = UIStackViewAlignment.Fill;
annotationView.DetailCalloutAccessoryView = customPinView;
}
annotationView.CanShowCallout = true;
return annotationView;
}
void OnCalloutAccessoryControlTapped(object sender, MKMapViewAccessoryTappedEventArgs e)
{
CustomMKAnnotationView customView = e.View as CustomMKAnnotationView;
if (!string.IsNullOrWhiteSpace(customView.Url))
{
UIApplication.SharedApplication.OpenUrl(new Foundation.NSUrl(customView.Url));
}
}
void OnDidSelectAnnotationView(object sender, MKAnnotationViewEventArgs e)
{
CustomMKAnnotationView customView = e.View as CustomMKAnnotationView;
//customPinView = new UIView();
if (customView.Name.Equals("Xamarin"))
{
//customPinView.Frame = new CGRect(0, 0, 200, 84);
//var image = new UIImageView(new CGRect(0, 0, 200, 84));
//image.Image = UIImage.FromFile("xamarin.png");
//customPinView.AddSubview(image);
//customPinView.Center = new CGPoint(0, -(e.View.Frame.Height + 75));
//e.View.AddSubview(customPinView);
}
}
void OnDidDeselectAnnotationView(object sender, MKAnnotationViewEventArgs e)
{
if (!e.View.Selected)
{
customPinView.RemoveFromSuperview();
customPinView.Dispose();
customPinView = null;
}
}
CustomPin GetCustomPin(MKPointAnnotation annotation)
{
var position = new Position(annotation.Coordinate.Latitude, annotation.Coordinate.Longitude);
foreach (var pin in customPins)
{
if (pin.Position == position)
{
return pin;
}
}
return null;
}
}
The effect:

Multi Column Picker for Xamarin iOS

I am building a multi-column picker control, and I have some issues with the xamarin.ios renderer.
I have two columns in the picker. One for the month and another for the year. I have got the values binded to the picker control properly. However when I make a selection the app crashes.
The error is Specified cast is not valid.
Stacktrace is
at Xamarin.Forms.Platform.iOS.PickerRendererBase`1[TControl].OnEnded
(System.Object sender, System.EventArgs eventArgs) [0x0000b] in
<0648e2dffe9e4201b8c6e274ced6579f>:0 at
UIKit.UIControlEventProxy.Activated () [0x00004] in /Library/Frameworks/Xamarin.iOS.framework/Versions/12.16.0.5/src/Xamarin.iOS/UIKit/UIControl.cs:38
My ios Custom Renderer code is as follows:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using CoreGraphics;
using UIKit;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
[assembly: ExportRenderer(typeof(YearMonthPicker), typeof(YearMonthPickerRenderer))]
namespace TestApp.iOS.Renderers
{
public class YearMonthPickerRenderer : PickerRenderer
{
private MonthYear _monthYear = new MonthYear();
private UILabel _monthLabel;
private UILabel _yearLabel;
public YearMonthPickerRenderer()
{
_monthYear.Months = new List<string>();
_monthYear.Years = new List<int>();
InitValues();
}
private void InitValues()
{
_monthYear.Months = new List<string>{
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"Sepetember",
"October",
"November",
"December"
};
}
protected override void OnElementChanged(ElementChangedEventArgs<Picker> e)
{
base.OnElementChanged(e);
if (e.NewElement != null)
{
var customPicker = e.NewElement as YearMonthPicker;
var startYear = customPicker.StartYear;
var endYear = customPicker.EndYear;
do
{
_monthYear.Years.Add(startYear);
startYear++;
} while (startYear <= endYear);
if (_monthLabel == null)
{
_monthLabel = new UILabel();
_monthLabel.Text = customPicker.MonthLabel;
_monthLabel.TextColor = customPicker.LabelColor.ToUIColor();
_monthLabel.Font = _monthLabel.Font.WithSize(14.0f);
}
if (_yearLabel == null)
{
_yearLabel = new UILabel();
_yearLabel.Text = customPicker.YearLabel;
_yearLabel.TextColor = customPicker.LabelColor.ToUIColor();
_yearLabel.Font = _yearLabel.Font.WithSize(14.0f);
}
if (Control is UITextField textField)
{
var pickerView = textField.InputView as UIPickerView;
var yearMonthModel = new YearMonthModel(textField, _monthYear);
var yearMonthDelegate = new YearMonthPickerDelegate(textField, _monthYear);
pickerView.Model = yearMonthModel;
pickerView.Delegate = yearMonthDelegate;
textField.BorderStyle = UITextBorderStyle.None;
textField.AddSubview(_monthLabel);
textField.AddSubview(_yearLabel);
}
}
var toolbar = new UIToolbar(new CGRect(0.0f, 0.0f, Control.Frame.Size.Width, 44.0f));
toolbar.Items = new[]
{
new UIBarButtonItem(UIBarButtonSystemItem.FlexibleSpace),
new UIBarButtonItem("Done",
UIBarButtonItemStyle.Done,
delegate {
Control.ResignFirstResponder();
})
};
if (this.Control != null)
{
Control.InputAccessoryView = toolbar;
}
}
public override void LayoutSubviews()
{
base.LayoutSubviews();
_monthLabel.Frame = new CGRect(0, -10,
Control.Frame.Width / 2,
Control.Frame.Height - 5);
_yearLabel.Frame = new CGRect(Control.Frame.Width / 2, -10,
Control.Frame.Width / 2,
Control.Frame.Height - 5);
}
protected override void Dispose(bool disposing)
{
_monthLabel?.Dispose();
_yearLabel?.Dispose();
_monthLabel = null;
_yearLabel = null;
base.Dispose(disposing);
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
}
}
public class YearMonthModel : UIPickerViewModel
{
private UITextField _uITextField;
private string _selectedMonth = string.Empty;
private string _selectedYear = string.Empty;
private MonthYear _monthYear;
public YearMonthModel(UITextField uITextField, MonthYear monthYear)
{
_uITextField = uITextField;
_monthYear = monthYear;
}
public override nint GetComponentCount(UIPickerView pickerView)
{
return 2;
}
public override nint GetRowsInComponent(UIPickerView pickerView, nint component)
{
if (component == 0)
return _monthYear.Months.Count;
else
return _monthYear.Years.Count;
}
public override string GetTitle(UIPickerView pickerView, nint row, nint component)
{
if (component == 0)
return _monthYear.Months[(int)row];
else
return _monthYear.Years[(int)row].ToString();
}
public override void Selected(UIPickerView pickerView, nint row, nint component)
{
if (component == 0)
_selectedMonth = _monthYear.Months[(int)row];
if (component == 1)
_selectedYear = _monthYear.Years[(int)row].ToString();
_uITextField.Text = $"{_selectedMonth} {_selectedYear}";
}
public override nfloat GetComponentWidth(UIPickerView picker, nint component)
{
if (component == 0)
return 140f;
else
return 100f;
}
public override nfloat GetRowHeight(UIPickerView picker, nint component)
{
return 40f;
}
}
public class YearMonthPickerDelegate : UIPickerViewDelegate
{
private UITextField _uITextField;
private string _selectedMonth = string.Empty;
private string _selectedYear = string.Empty;
private MonthYear _monthYear;
public YearMonthPickerDelegate(UITextField uITextField, MonthYear monthYear)
{
_uITextField = uITextField;
_monthYear = monthYear;
}
public override string GetTitle(UIPickerView pickerView, nint row, nint component)
{
if (component == 0)
return _monthYear.Months[(int)row];
else
return _monthYear.Years[(int)row].ToString();
}
public override void Selected(UIPickerView pickerView, nint row, nint component)
{
if (component == 0)
_selectedMonth = _monthYear.Months[(int)row];
if (component == 1)
_selectedYear = _monthYear.Years[(int)row].ToString();
_uITextField.Text = $"{_selectedMonth} {_selectedYear}";
}
}
public class MonthYear
{
public List<string> Months { get; set; }
public List<int> Years { get; set; }
}
}
Edit 1
I am adding the YearMonthPicker class
public class YearMonthPicker : Picker
{
public YearMonthPicker()
{
}
public static readonly BindableProperty StartYearProperty =
BindableProperty.Create(nameof(StartYear), typeof(int), typeof(YearMonthPicker), 1900);
public int StartYear
{
get { return (int)GetValue(StartYearProperty); }
set { SetValue(StartYearProperty, value); }
}
public static readonly BindableProperty EndYearProperty =
BindableProperty.Create(nameof(EndYear), typeof(int), typeof(YearMonthPicker), 9999);
public int EndYear
{
get { return (int)GetValue(EndYearProperty); }
set { SetValue(EndYearProperty, value); }
}
public static readonly BindableProperty YearLabelProperty =
BindableProperty.Create(nameof(YearLabel), typeof(string), typeof(YearMonthPicker), string.Empty);
public string YearLabel
{
get { return (string)GetValue(YearLabelProperty); }
set { SetValue(YearLabelProperty, value); }
}
public static readonly BindableProperty MonthLabelProperty =
BindableProperty.Create(nameof(MonthLabel), typeof(string), typeof(YearMonthPicker), string.Empty);
public string MonthLabel
{
get { return (string)GetValue(MonthLabelProperty); }
set { SetValue(MonthLabelProperty, value); }
}
public static readonly BindableProperty LabelColorProperty =
BindableProperty.Create(nameof(LabelColor), typeof(Color), typeof(YearMonthPicker), default(Color));
public Color LabelColor
{
get { return (Color)GetValue(LabelColorProperty); }
set { SetValue(LabelColorProperty, value); }
}
}
I appreciate if someone can help me identify what causes the crash.
Finally, I found your problem is you did not assign the pickerView to the textField's inputView.
To assign the picker to the textField's inputView, use below code:
if (Control is UITextField textField)
{
//Change here
var pickerView = new UIPickerView();
Control.InputView = pickerView;
var yearMonthModel = new YearMonthModel(textField, _monthYear);
var yearMonthDelegate = new YearMonthPickerDelegate(textField, _monthYear);
pickerView.Model = yearMonthModel;
pickerView.Delegate = yearMonthDelegate;
textField.BorderStyle = UITextBorderStyle.None;
textField.AddSubview(_monthLabel);
textField.AddSubview(_yearLabel);
}

Bindable Property is not updating view on iOS

I am creating a cross platform app on xamarin.forms. I needed a custom renderer with gradient background and i've created it and it works well on both android and ios.
However, when i want to change the colors of the gradient background, it does not work on iOS.
Test.xaml.cs
namespace KiaiDay
{
public class TesteViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private Color _startColor = Color.Green;
public Color SStartColor
{
get => _startColor;
set { _startColor = value; OnPropertyChanged(nameof(SStartColor)); }
}
private Color _endColor = Color.Blue;
public Color EEndColor
{
get => _endColor;
set { _endColor = value; OnPropertyChanged(nameof(EEndColor)); }
}
public ICommand Select
{
get => new Command(() =>
{
SStartColor = Color.Red;
EEndColor = Color.Brown;
});
}
#region INotifyPropertyChanged Implementation
void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
if (PropertyChanged == null)
return;
PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class teste : ContentPage
{
public teste()
{
BindingContext = new TesteViewModel();
InitializeComponent();
}
}
}
Test.xaml
<ContentPage.Content>
<StackLayout HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand">
<local:ShadowFrame StartColor="{Binding SStartColor}" EndColor="{Binding EEndColor}" HorizontalOptions="Center" VerticalOptions="Center">
<Label Text="Teste"/>
<local:ShadowFrame.GestureRecognizers>
<TapGestureRecognizer NumberOfTapsRequired="1" Command="{Binding Select}"/>
</local:ShadowFrame.GestureRecognizers>
</local:ShadowFrame>
</StackLayout>
</ContentPage.Content>
Custom Renderer in PCL
public class ShadowFrame : Frame
{
public static BindableProperty ElevationProperty = BindableProperty.Create(nameof(Elevation), typeof(float), typeof(ShadowFrame), default(float));
public static readonly BindableProperty StartColorProperty = BindableProperty.Create(nameof(StartColor), typeof(Color), typeof(ShadowFrame), default(Color));
public static readonly BindableProperty EndColorProperty = BindableProperty.Create(nameof(EndColor), typeof(Color), typeof(ShadowFrame), default(Color));
public float Elevation
{
get { return (float)GetValue(ElevationProperty); }
set { SetValue(ElevationProperty, value); }
}
public Color StartColor
{
get { return (Color)GetValue(StartColorProperty); }
set { SetValue(StartColorProperty, value); }
}
public Color EndColor
{
get { return (Color)GetValue(EndColorProperty); }
set { SetValue(EndColorProperty, value); }
}
}
iOS Renderer(i think the problem is here)
[assembly: ExportRenderer(typeof(ShadowFrame), typeof(ShadowFrameRenderer))]
namespace KiaiDay.iOS.Renderers
{
public class ShadowFrameRenderer : FrameRenderer
{
CAGradientLayer gradientLayer;
private Color StartColor { get; set; }
private Color EndColor { get; set; }
CGRect rect;
public static void Initialize()
{
// empty, but used for beating the linker
}
public ShadowFrameRenderer() => gradientLayer = new CAGradientLayer();
public override void Draw(CGRect rect)
{
this.rect = rect;
var stack = (ShadowFrame)this.Element;
StartColor = stack.StartColor;
EndColor = stack.EndColor;
CGColor startColor = this.StartColor.ToCGColor();
CGColor endColor = this.EndColor.ToCGColor();
#region for Vertical Gradient
//var gradientLayer = new CAGradientLayer();
#endregion
#region for Horizontal Gradient
//var gradientLayer = new CAGradientLayer()
//{
// StartPoint = new CGPoint(0, 0.5),
// EndPoint = new CGPoint(1, 0.5)
//};
#endregion
gradientLayer.Frame = rect;
gradientLayer.Colors = new CGColor[] {
startColor,
endColor
};
NativeView.Layer.InsertSublayer(gradientLayer, 0);
base.Draw(rect);
}
protected override void OnElementChanged(ElementChangedEventArgs<Frame> e)
{
base.OnElementChanged(e);
var stack = (ShadowFrame)e.NewElement;
if (e.OldElement != null || Element == null)
{
return;
}
try
{
this.StartColor = stack.StartColor;
this.EndColor = stack.EndColor;
UpdateShadow();
Draw(rect);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine("ERROR:", ex.Message);
}
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
var stack = (ShadowFrame) sender;
if (e.PropertyName == ShadowFrame.ElevationProperty.PropertyName)
{
UpdateShadow();
}
if (e.PropertyName == ShadowFrame.StartColorProperty.PropertyName)
{
this.StartColor = stack.StartColor;
}
if (e.PropertyName == ShadowFrame.EndColorProperty.PropertyName)
{
this.EndColor = stack.EndColor;
}
}
private void UpdateShadow()
{
var shadowFrame = (ShadowFrame)Element;
// Update shadow to match better material design standards of elevation
Layer.ShadowRadius = shadowFrame.Elevation;
Layer.ShadowColor = UIColor.Gray.CGColor;
Layer.ShadowOffset = new CGSize(2, 2);
Layer.ShadowOpacity = 0.80f;
Layer.ShadowPath = UIBezierPath.FromRect(Layer.Bounds).CGPath;
Layer.MasksToBounds = false;
}
}
}
I would like to update iOS view immediatly after i change the color through the command.
If you change a property and the view doesn't update the issue has to be in OnElementPropertyChanged.
You need to call UpdateShadow() and Draw().
After digging a lot i think i have found a solution:
iOS Custom Renderer :
public class GradientStackColorRenderer : VisualElementRenderer<Frame>
{
private Color StartColor { get; set; }
private Color EndColor { get; set; }
public override CGRect Frame
{
get
{
return base.Frame;
}
set
{
if (value.Width > 0 && value.Height > 0)
{
foreach (var layer in NativeView?.Layer.Sublayers.Where(layer => layer is CAGradientLayer))
layer.Frame = new CGRect(0, 0, value.Width, value.Height);
}
base.Frame = value;
}
}
protected override void OnElementChanged(ElementChangedEventArgs<StackLayout> e)
{
base.OnElementChanged(e);
if (e.OldElement == null)
{
try
{
var stack = e.NewElement as GradientColorStack;
this.StartColor = stack.StartColor;
this.EndColor = stack.EndColor;
AdicionarGradiente();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(#"ERROR:", ex.Message);
}
}
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
var stack = this.Element as GradientColorStack;
if(e.PropertyName == GradientColorStack.StartColorProperty.PropertyName)
{
this.StartColor = stack.StartColor;
AdicionarGradiente();
}
if (e.PropertyName == GradientColorStack.EndColorProperty.PropertyName)
{
this.EndColor = stack.EndColor;
AdicionarGradiente();
}
}
public void AdicionarGradiente()
{
var gradient = new CAGradientLayer();
gradient.CornerRadius = NativeView.Layer.CornerRadius = 5;
gradient.Colors = new CGColor[] { StartColor.ToCGColor(), EndColor.ToCGColor() };
var layer = NativeView?.Layer.Sublayers.LastOrDefault();
NativeView?.Layer.InsertSublayerBelow(gradient, layer);
}
public static CGColor ToCGColor(Color color)
{
return new CGColor(CGColorSpace.CreateSrgb(), new nfloat[] { (float)color.R, (float)color.G, (float)color.B, (float)color.A });
}
}

Monotouch dialog with MvvmCross cell reuse mixes up cell data when scrolling

I am building an app with quite a number sub classed monotouch dialog RootElements . Part of customising the look and feel of the cell was adding a notification view on the far right of the cell based on boolean value bound from the viewmodel, as part of the validation of that specific cell.
This works great if all the cells never go out of view. But when scrolling comes into play, these UIViews added to the cells get mixed up.
I have tried overriding the CellKey getter and an assortment of other options to no avail, any assistance would be greatly appreciated. Thank you.
public class PgpStyledRootElement : RootElement, IElementSizing, IValidatableElement
{
float _height;
bool _isRequired = false;
UIView notificationView;
protected IMvxMessenger _messenger;
protected MvxSubscriptionToken _token;
public WeakReference WeakNotificationView { get; set;}
public UIView NotificationView
{
get
{
if (WeakNotificationView == null || !WeakNotificationView.IsAlive)
return null;
return WeakNotificationView.Target as UIView;
}
}
private UITableViewCell _activeCell;
public UITableViewCell ActiveCell
{
get
{
if(_activeCell == null)
{
_activeCell = this.GetActiveCell ();
}
return _activeCell;
}
set
{
_activeCell = value;
}
}
private bool _isValid = true;
public bool IsValid
{
get { return _isValid; }
set
{
_isValid = value;
if (_isValid)
{
UIApplication.SharedApplication.InvokeOnMainThread (RemoveNotificationView);
}
else
{
UIApplication.SharedApplication.InvokeOnMainThread (AddNotificationView);
}
}
}
public PgpStyledRootElement(string caption, bool isRequired, Group group = null) : base(caption, group) {
}
public PgpStyledRootElement(string caption, bool isRequired, float height, bool fullPageWidth, Group group = null) : base(caption, group) {
_height = height;
}
public PgpStyledRootElement(string caption, bool isRequired, float height, Group group = null, string defaultDetailText = null) : base(caption, group) {
_height = height;
_isRequired = isRequired;
_messenger = Mvx.Resolve<IMvxMessenger> ();
_token = _messenger.Subscribe<ScreenWillRotateMessage> (HandleScreenWillRotate);
}
public PgpStyledRootElement (string caption, bool isRequired, float height, Func<RootElement, UIViewController> createOnSelected) : base (caption, createOnSelected)
{
this.Sections = new List<Section> ();
_height = height;
_messenger = Mvx.Resolve<IMvxMessenger> ();
_token = _messenger.Subscribe<ScreenWillRotateMessage> (HandleScreenWillRotate);
}
public PgpStyledRootElement(string caption, float height, Group group = null) : base(caption, group) {
_height = height;
}
protected override UITableViewCell GetCellImpl (UITableView tv)
{
var cell = base.GetCellImpl (tv);
HandleInitialLoadValidation (cell);
return cell;
}
protected override void UpdateCellDisplay (UITableViewCell cell)
{
base.UpdateCellDisplay (cell);
if (cell != null) {
cell.TextLabel.TextColor = CaptureListingsStyleConfig.DefaultTableCellTextColor;
cell.SetNeedsLayout ();
}
}
public void HandleInitialLoadValidation (UITableViewCell cell)
{
if (_isRequired)
{
if (!IsValid)
{
AddInitialNotificationView (cell);
}
}
}
static void GetRequiredIndicatorFrame (UITableViewCell cell, out float indicatorPointX, out float indicatorHeight)
{
if (UIApplication.SharedApplication.StatusBarOrientation == UIInterfaceOrientation.Portrait || UIApplication.SharedApplication.StatusBarOrientation == UIInterfaceOrientation.PortraitUpsideDown) {
indicatorPointX = cell.Frame.Width + 120;
indicatorHeight = cell.Frame.Height;
}
else {
indicatorPointX = cell.Frame.Width * 2 + 56;
indicatorHeight = cell.Frame.Height;
}
}
static void GetRequiredIndicatorFrameForRotationChange (UITableViewCell cell, out float indicatorPointX, out float indicatorHeight)
{
if (UIApplication.SharedApplication.StatusBarOrientation == UIInterfaceOrientation.Portrait || UIApplication.SharedApplication.StatusBarOrientation == UIInterfaceOrientation.PortraitUpsideDown) {
indicatorPointX = cell.Frame.Width - 263;
indicatorHeight = cell.Frame.Height;
}
else {
indicatorPointX = cell.Frame.Width + 249;
indicatorHeight = cell.Frame.Height;
}
}
void HandleScreenWillRotate (ScreenWillRotateMessage obj)
{
RectangleF newFrame;
if(NotificationView != null)
{
const int indicatorpointY = 0;
const int indicatorWidth = 7;
float indicatorPointX;
float indicatorHeight;
GetRequiredIndicatorFrameForRotationChange (ActiveCell, out indicatorPointX, out indicatorHeight);
CreateNewNotificationViewFrame (out newFrame, indicatorpointY, indicatorWidth, indicatorPointX, indicatorHeight);
NotificationView.Frame = newFrame;
}
}
static void CreateNewNotificationViewFrame (out RectangleF newFrame, int indicatorpointY, int indicatorWidth, float indicatorPointX, float indicatorHeight)
{
newFrame = new RectangleF (indicatorPointX, indicatorpointY, indicatorWidth, indicatorHeight);
}
public void AddInitialNotificationView (UITableViewCell cell)
{
if (cell != null) {
const int indicatorpointY = 0;
const int indicatorWidth = 7;
float indicatorPointX;
float indicatorHeight;
GetRequiredIndicatorFrame (cell, out indicatorPointX, out indicatorHeight);
if (Util.UserInterfaceIdiomIsPhone) {
indicatorPointX = cell.Frame.Width - indicatorWidth;
}
AddViewToCell (cell, indicatorpointY, indicatorWidth, indicatorPointX, indicatorHeight);
}
}
public void AddNotificationView ()
{
if (ActiveCell != null) {
const int indicatorpointY = 0;
const int indicatorWidth = 7;
var indicatorPointX = ActiveCell.Frame.Width - indicatorWidth;
var indicatorHeight = ActiveCell.Frame.Height;
if (Util.UserInterfaceIdiomIsPhone) {
indicatorPointX = ActiveCell.Frame.Width - indicatorWidth;
}
AddViewToCell (ActiveCell, indicatorpointY, indicatorWidth, indicatorPointX, indicatorHeight);
}
}
void AddViewToCell (UITableViewCell cell, int indicatorpointY, int indicatorWidth, float indicatorPointX, float indicatorHeight)
{
if (NotificationView == null) {
notificationView = new UIView (new RectangleF (indicatorPointX, indicatorpointY, indicatorWidth, indicatorHeight));
notificationView.BackgroundColor = UIColor.Red;
WeakNotificationView = new WeakReference (notificationView);
cell.Add (NotificationView);
}
}
public void RemoveNotificationView()
{
if(NotificationView != null)
{
NotificationView.RemoveFromSuperview ();
WeakNotificationView = null; //This should technically not be necessary, but lets do it anyway
}
}
#region IElementSizing implementation
public float GetHeight (UITableView tableView, NSIndexPath indexPath)
{
if(_height == 0.0f)
return CaptureListingsStyleConfig.DefaultTextCellHeight; //return CaptureListingsStyleConfig.DefaultRootCellHeight ();
return _height;
}
#endregion
}
Whenever a cell is required for display, then the DialogViewController calls the Element's GetCell method. This method then executes GetCellImpl followed by UpdateCellDisplay.
In the case of reuse, I suspect your RootElement is returning the reused Cell in GetCellImpl (because that's what the base class returns) and then not fully updating it. You could try modifying your UpdateCellDisplay method so that it updates the cell display to reflect the current state of your Element - i.e. adding/removing the notification display.

Resources