I want to draw material design Flat Button on android, for that i made this renderer:
public class NativeFlatButtonRenderer : ButtonRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<Button> e)
{
base.OnElementChanged(e);
var oldElement = e.OldElement as NativeFlatButton;
if (oldElement != null)
{
}
var newElement = e.NewElement as NativeFlatButton;
if (newElement != null)
{
Control.SetBackgroundResource(Android.Resource.Style.WidgetMaterialButtonBorderless);
}
}
}
and it throws Android.Content.Res.Resources+NotFoundException: Resource ID #0x1030259
when i change to
if (newElement != null)
{
var button = new AppCompatButton(Context, null, Android.Resource.Style.WidgetMaterialButtonBorderlessColored);
SetNativeControl(button);
}
its drawing but without commands etc. So how i can draw Flat Button with renderer on android?
That was quiet easy. Here is the ButtonRenderer method that creates native control:
protected override Android.Widget.Button CreateNativeControl()
{
return new Android.Widget.Button(this.Context);
}
So i only need to override it and there is my complete renderer:
public class NativeFlatButtonRenderer : ButtonRenderer
{
protected override Android.Widget.Button CreateNativeControl()
{
var button = new AppCompatButton(Context, null, Android.Resource.Style.WidgetMaterialButtonBorderlessColored);
return button;
}
}
Related
Please assist
I have been struggling in showing a trip on my carpooling app much like the one on an Uber. That is with two info windows for both start and end locations open with their respective addresses.
I have managed to render a custom info window the way I want to but can not make all the info windows open. How can I do one of the following:
Override the current logic that prevents google Maps to open more than one info window at a time.
Draw over the map to display a custom view right next to the Map pin.
I am using 'Xamarin.Forms.GoogleMaps'
Visual goal(Only top part of screen)
Xamarin.Android Custom Render Class
[assembly: ExportRenderer(typeof(CustomMap), typeof(CustomMapRenderer))]
namespace CoTripApp.Droid.CustomRennders
{
public class CustomMapRenderer : MapRenderer, GoogleMap.IInfoWindowAdapter
{
protected override void OnElementChanged(ElementChangedEventArgs<Map> e)
{
base.OnElementChanged(e);
if (e.OldElement != null)
{
//nativemap.infowindowclick -= oninfowindowclick;
}
if (e.NewElement != null)
{
var formsmap = (CustomMap)e.NewElement;
}
}
protected override void OnMapReady(GoogleMap nativeMap, Xamarin.Forms.GoogleMaps.Map map)
{
base.OnMapReady(nativeMap, map);
NativeMap.SetInfoWindowAdapter(this);
}
protected override void OnMarkerCreated(Pin pin, Marker marker)
{
marker.Position = new LatLng(pin.Position.Latitude, pin.Position.Longitude);
marker.Title = pin.Label;
marker.Snippet = pin.Address;
if (pin.Type.Equals(PinType.Generic))
{
marker.SetIcon(Android.Gms.Maps.Model.BitmapDescriptorFactory.FromResource(Resource.Drawable.circle));
}
else
{
marker.SetIcon(Android.Gms.Maps.Model.BitmapDescriptorFactory.FromResource(Resource.Drawable.circle_closed));
}
marker.ShowInfoWindow();
}
public Android.Views.View GetInfoContents(Marker marker)
{
Android.Views.View view;
var inflater = Android.App.Application.Context.GetSystemService(Context.LayoutInflaterService) as Android.Views.LayoutInflater;
if (inflater != null)
{
view = inflater.Inflate(Resource.Layout.CustomMapInfoWindow, null);
view.FindViewById<TextView>(Resource.Id.waypointNo).Text = marker.Title;
view.FindViewById<TextView>(Resource.Id.address).Text = marker.Snippet;
return view;
}
return null;
}
public Android.Views.View GetInfoWindow(Marker marker)
{
return null;
}
}
}
So i do some validations on my entries and i would like to change the color to red when the value is invalid.
Is there a way to do this in xamarin right now ?
I know that there's renderer to change the color permanently but i just want to change it based on a condition and leave it black when everything is ok.
Thank you.
You could overwrite OnElementPropertyChanged method in your CustomRenderer to achieve this.
For example:
for Android:
[assembly: ExportRenderer(typeof(Entry), typeof(UnderLineEntry))]
namespace EntryCa.Droid
{
class UnderLineEntry : EntryRenderer
{
public UnderLineEntry(Context context) : base(context)
{
}
protected override void OnElementChanged(ElementChangedEventArgs<Entry> e)
{
base.OnElementChanged(e);
if (Control == null || e.NewElement == null) return;
Control.BackgroundTintList = ColorStateList.ValueOf(Android.Graphics.Color.Black);
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName == Entry.TextProperty.PropertyName)
{
if (Control.Text.Length > 6) //this is your condition(For example, here is the length of the text content)
{
Control.BackgroundTintList = ColorStateList.ValueOf(Android.Graphics.Color.Red);
}
else
{
Control.BackgroundTintList = ColorStateList.ValueOf(Android.Graphics.Color.Black);
}
}
}
}
}
the ios is the similar to Android,also change it in the OnElementPropertyChanged method,if you want i could give you an example.
for ios.
[assembly: ExportRenderer(typeof(Entry), typeof(MyEntryRenderer))]
namespace EntryCa.iOS
{
public class MyEntryRenderer : EntryRenderer
{
private CALayer _line;
public override void LayoutSubviews()
{
base.LayoutSubviews();
Control.BorderStyle = UITextBorderStyle.None;
_line = new CALayer
{
BackgroundColor = UIColor.Black.CGColor,
Frame = new CGRect(0, Frame.Height, Frame.Width, 1f)
};
Control.Layer.AddSublayer(_line);
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName == Entry.TextProperty.PropertyName)
{
if (Control.Text.Length > 6)
{
_line.RemoveFromSuperLayer();
_line = new CALayer
{
BackgroundColor = UIColor.Red.CGColor,
Frame = new CGRect(0, Frame.Height, Frame.Width, 1f)
};
Control.Layer.AddSublayer(_line);
}
else
{
_line.RemoveFromSuperLayer();
_line = new CALayer
{
BackgroundColor = UIColor.Black.CGColor,
Frame = new CGRect(0, Frame.Height, Frame.Width, 1f)
};
Control.Layer.AddSublayer(_line);
}
}
}
}
}
To change the line, have to use a custom renderer like this.
Or just hide the line in the custom renderer and fake a line on the page, then you can control the color by Data trigger.
Alternatively, what about adding a frame or changing the background color instead of the entry.
I'm trying to create an extended version of the standard XF Map object:
public class RRMap: Map
{
public void DoSomethingOnMap() {
/* ... */
}
}
I also created an Android renderer (iOS will come later):
[assembly: ExportRenderer(typeof(RRMap), typeof(RRMapRendererAndroid))]
namespace MyApp.Droid.Renderers
{
public class RRMapRendererAndroid : MapRenderer
{
public RRMapRendererAndroid(Context context) : base(context) { }
protected override void OnElementChanged(Xamarin.Forms.Platform.Android.ElementChangedEventArgs<Map> e)
{
base.OnElementChanged(e);
if (e.NewElement != null)
{
Control.GetMapAsync(this);
}
}
protected override MarkerOptions CreateMarker(Pin pin)
{
var marker = new MarkerOptions();
marker.SetPosition(new LatLng(pin.Position.Latitude, pin.Position.Longitude));
marker.SetTitle(pin.Label);
marker.SetSnippet(pin.Address);
marker.SetIcon(BitmapDescriptorFactory.DefaultMarker(210));
return marker;
}
}
}
Everything is working fine so far: the map is rendered and pins are created with a custom color.
Unfortunately, I'm stuck on the implementation of DoSomethingOnMap method: it should be a method in the shared code, but it should be implemented in different ways, depending on the platform.
In other circumstances, I would create an interface using DependencyService for implementation, but in this particular case I can't figure out how to proceed.
The first solution is you can use a messaging-center, this can communicate between shared project and iOS/Android project.
Publish a message in the doSomethingOnMap method and anywhere you subscribed to the message will be triggered.
The second is create an event in your shared project and subscribe to that event in the renderer, I wrote both two solutions below:
In your shared project:
public class CustomMap : Map
{
public List<CustomPin> CustomPins { get; set; }
public event EventHandler CallToNativeMethod;
public void doSomething()
{
if (CallToNativeMethod != null)
CallToNativeMethod(this, new EventArgs());
}
public void doSomething(CustomMap myMap) {
MessagingCenter.Send<CustomMap>(this, "Hi");
}
}
In the renderer:
protected override void OnElementChanged(ElementChangedEventArgs<View> e)
{
base.OnElementChanged(e);
if (e.OldElement != null)
{
}
if (e.NewElement != null)
{
MessagingCenter.Subscribe<CustomMap>(this, "Hi", (sender) =>
{
// Do something whenever the "Hi" message is received
Console.WriteLine("hi");
});
((CustomMap)e.NewElement).CallToNativeMethod += (sender, arg) =>
{
Console.WriteLine("native method");
};
}
}
At anywhere you want to call this method:
private void Button_Clicked(object sender, System.EventArgs e)
{
customMap.doSomething();
customMap.doSomething(customMap);
}
I've many Entries and Labels in my Interface, what can I do to my page scrolldown if my keyboard is in front of any element? I tried to implement a scrollview in my xaml but no results...
I had a similar issue.
Best way to resolve this was to wrap the login part into a scrollview (similar to what you are doing already) and using custom renderers:
Android:
[assembly: ExportRenderer(typeof(ModernLogin), typeof(ModernLoginPageRenderer))]
namespace MyApp.Droid.Renderer
{
public class ModernLoginPageRenderer : PageRenderer
{
public ModernLoginPageRenderer(Context context) : base(context)
{
}
protected override void OnElementChanged(ElementChangedEventArgs<Page> e)
{
base.OnElementChanged(e);
// Deactivate SoftInput View Adjustment for this window
if (e.NewElement != null)
{
(this.Context as FormsApplicationActivity).Window.SetSoftInputMode(SoftInput.AdjustResize);
}
}
protected override void OnWindowVisibilityChanged([GeneratedEnum] ViewStates visibility)
{
// Enable SoftInput View Adjustment after moving away from this window
if (visibility == ViewStates.Gone)
{
(this.Context as FormsApplicationActivity).Window.SetSoftInputMode(SoftInput.AdjustPan);
}
base.OnWindowVisibilityChanged(visibility);
}
protected override void OnLayout(bool changed, int left, int top, int right, int bottom)
{
base.OnLayout(changed, left, top, right, bottom);
}
}
}
iOS:
[assembly: ExportRenderer(typeof(ModernLogin), typeof(ModernLoginPageRenderer))]
namespace MyApp.iOS.Renderer
{
public class ModernLoginPageRenderer : PageRenderer
{
NSObject observerHideKeyboard;
NSObject observerShowKeyboard;
public override void ViewDidLoad()
{
base.ViewDidLoad();
var cp = Element as ModernLogin;
if (cp != null)
{
foreach (var g in View.GestureRecognizers)
{
g.CancelsTouchesInView = true;
}
}
}
public override void ViewWillAppear(bool animated)
{
base.ViewWillAppear(animated);
observerHideKeyboard = NSNotificationCenter.DefaultCenter.AddObserver(UIKeyboard.WillHideNotification, OnKeyboardNotification);
observerShowKeyboard = NSNotificationCenter.DefaultCenter.AddObserver(UIKeyboard.WillShowNotification, OnKeyboardNotification);
}
public override void ViewWillDisappear(bool animated)
{
base.ViewWillDisappear(animated);
NSNotificationCenter.DefaultCenter.RemoveObserver(observerHideKeyboard);
NSNotificationCenter.DefaultCenter.RemoveObserver(observerShowKeyboard);
}
void OnKeyboardNotification(NSNotification notification)
{
if (!IsViewLoaded) return;
var frameBegin = UIKeyboard.FrameBeginFromNotification(notification);
var frameEnd = UIKeyboard.FrameEndFromNotification(notification);
var page = Element as ModernLogin;
if (page != null)
{
var padding = page.Padding;
page.Padding = new Thickness(padding.Left, padding.Top, padding.Right, padding.Bottom + frameBegin.Top - frameEnd.Top);
}
}
}
My problem occurs after I updated Xamarin.Forms and Xamarin.Forms.Maps to the new version (2.3.4).
After that I also updated all google play services in Android project (and a lot of libraries that I hate).
The main problem is that I have a custom MapRenderer for custom pins, in iOS and UWP works fine, but in Android version this custom MapRenderer brokes all the Map. Any property change or method call seems to be ignored.
For example I have a button to toggle the map type (Hybrid or Street) and that action never changes it. I also noticed (according this tutorial: https://developer.xamarin.com/guides/xamarin-forms/application-fundamentals/custom-renderer/map/customized-pin/) that the property "VisibleRegion" never changes so the following code never executes:
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName.Equals("VisibleRegion") && !_isDrawn)
{
// Do something with your custom map
}
}
Inside that if i used to populate my custom pins (like the tutorial above) and now my Map is always empty.
Now i populate my map inside the IOnMapReadyCallback and it works fine, but the I still have the bindings problem.
If I ignore the custom MapRendered (removing the assembly line) all the bindings starts working fine but my map now have the old pins and all customization is gone (obviously).
In the PCL I have things like MyMap.MoveToRegion(...) and MyMap.MapType = _currentType; but those instructions only works if a don't use a custom MapRenderer.
My custom MapRenderer is almost the same as the tutorial above.
The custom Map is created with C# and not with XAML, it doesn't have any XAML binding but any property change or method call like the MoveToRegion or MapType is totally ignored if i'm using the MapRenderer.
Any help?
Thanks
I already found the solution.
Looking at the source code, MapRenderer already implements IOnMapReadyCallback and if you remove the implementation in the custom MapRendered, everything starts working again (but with no customization).
MapRenderer saves the google map instance in the property NativeMap (also exists the property Map that is the Xamarin forms map instance) so we don't need to implement IOnMapReadyCallback any more. I think we need to be careful in the use of NativeMap because at the begining it could be null.
In the method I mentioned before now i do this:
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName.Equals("VisibleRegion") && !_isDrawn)
{
PopulateMap();
OnGoogleMapReady();
}
}
and the code I had in OnMapReady now goes inside OnGoogleMapReady():
private void OnGoogleMapReady()
{
if (_mapReady) return;
NativeMap.InfoWindowClick += OnInfoWindowClick;
NativeMap.SetInfoWindowAdapter(this);
_mapReady = true;
}
I also added this in OnElementChanged to remove any registered delegate in NativeMap
if (e.OldElement != null)
{
NativeMap.InfoWindowClick -= OnInfoWindowClick;
}
At the moment exists a Pull Request that implements OnMapReady as virtual method, so we can override it in our implementation and now be sure when NativeMap is not null, but for that we need to wait for a next release.
You can read more here -> https://forums.xamarin.com/discussion/92565/android-ionmapreadycallback-forms-2-3-4
I got the same issue and I solved it thanks to this answer on a Xamarin Forum.
This is my map renderer (Android part) to replace the marker's image of a pin :
[assembly: ExportRenderer(typeof(CustomMap), typeof(CustomMapRenderer))]
namespace MyNamespace.Droid
{
public class CustomMapRenderer : MapRenderer, GoogleMap.IInfoWindowAdapter, IOnMapReadyCallback
{
GoogleMap map;
List<CustomPin> customPins;
bool isDrawn;
protected override void OnElementChanged(Xamarin.Forms.Platform.Android.ElementChangedEventArgs<Map> e)
{
base.OnElementChanged(e);
if (e.OldElement != null)
{
map.InfoWindowClick -= OnInfoWindowClick;
}
if (e.NewElement != null)
{
var formsMap = (CustomMap)e.NewElement;
customPins = formsMap.CustomPins;
Control.GetMapAsync(this);
}
}
protected override void OnElementPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName.Equals ("VisibleRegion") && !isDrawn) {
map.Clear ();
foreach (var pin in customPins) {
var marker = new MarkerOptions();
marker.SetPosition (new LatLng(pin.Pin.Position.Latitude, pin.Pin.Position.Longitude));
marker.SetTitle (pin.Pin.Label);
marker.SetSnippet (pin.Pin.Address);
marker.SetIcon (BitmapDescriptorFactory.FromResource (Resource.Drawable.fake_ic_pin));
map.AddMarker (marker);
}
isDrawn = true;
}
}
protected override void OnLayout(bool changed, int l, int t, int r, int b)
{
base.OnLayout(changed, l, t, r, b);
if (changed)
{
isDrawn = false;
}
}
void OnInfoWindowClick(object sender, GoogleMap.InfoWindowClickEventArgs e)
{
var customPin = GetCustomPin(e.Marker);
if (customPin == null)
{
throw new Exception("Custom pin not found");
}
if (!string.IsNullOrWhiteSpace(customPin.Url))
{
var url = Android.Net.Uri.Parse(customPin.Url);
var intent = new Intent(Intent.ActionView, url);
intent.AddFlags(ActivityFlags.NewTask);
Android.App.Application.Context.StartActivity(intent);
}
}
void IOnMapReadyCallback.OnMapReady(GoogleMap googleMap)
{
InvokeOnMapReadyBaseClassHack(googleMap);
map = googleMap;
map.SetInfoWindowAdapter(this);
map.InfoWindowClick += OnInfoWindowClick;
}
public Android.Views.View GetInfoContents(Marker marker)
{
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.Pin.Position == position)
{
return pin;
}
}
return null;
}
void InvokeOnMapReadyBaseClassHack(GoogleMap googleMap)
{
System.Reflection.MethodInfo onMapReadyMethodInfo = null;
Type baseType = typeof(MapRenderer);
foreach (var currentMethod in baseType.GetMethods(System.Reflection.BindingFlags.NonPublic |
System.Reflection.BindingFlags.Instance |
System.Reflection.BindingFlags.DeclaredOnly))
{
if (currentMethod.IsFinal && currentMethod.IsPrivate)
{
if (string.Equals(currentMethod.Name, "OnMapReady", StringComparison.Ordinal))
{
onMapReadyMethodInfo = currentMethod;
break;
}
if (currentMethod.Name.EndsWith(".OnMapReady", StringComparison.Ordinal))
{
onMapReadyMethodInfo = currentMethod;
break;
}
}
}
if (onMapReadyMethodInfo != null)
{
onMapReadyMethodInfo.Invoke(this, new[] { googleMap });
}
}
}
}