Can one use system icons on tabs in Xamarin Forms? - xamarin

Is there a way to specify a "system" icon to be displayed on a tab when using Xamarin Forms? I would like to use icons such as Favourites, Bookmark, History etc but I do not want to supply all my own images for the various platforms.
Using Xamarin.iOS one can use this syntax:
tab1.TabBarItem = new UITabBarItem (UITabBarSystemItem.Favorites, 0);
I can however not find how to do this in the cross-platform Xamarin.Forms project.
This is my current code:
var profilePage = new ContentPage {
Title = "Profile",
//This requires my own images to be added to the project whereas
//I wish to use built-in images which are platform specific for
//Favourites, Bookmark, More, etc...
//Icon = "Profile.png",
Content = new StackLayout {
Spacing = 20, Padding = 50,
VerticalOptions = LayoutOptions.Center,
Children = {
new Entry { Placeholder = "Username" },
new Entry { Placeholder = "Password", IsPassword = true },
new Button {
Text = "Login",
TextColor = Color.White,
BackgroundColor = Color.FromHex("77D065") }}}};
var settingsPage = new ContentPage {
Title = "Settings",
Content = new StackLayout {
Spacing = 20, Padding = 50,
VerticalOptions = LayoutOptions.Center,
Children = {
new Entry { Placeholder = "Username" },
new Entry { Placeholder = "Password", IsPassword = true }}}
};
MainPage = new TabbedPage { Children = {profilePage, settingsPage} };

For iOS
you need a custom renderer for your page. In my example, it is CustomTabsPage class. You cannot just use system icons to create a UIImage. We need to use UITabBarItem. The problem is that UITabBarItem doesn't allow changes to either title or image/icon. But, we can copy an image from it.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UIKit;
using ButtonRendererDemo;
using ButtonRendererDemo.iOS;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
[assembly: ExportRenderer(typeof(CustomTabsPage), typeof(CustomTabsPageRenderer))]
namespace ButtonRendererDemo.iOS
{
public class CustomTabsPageRenderer : TabbedRenderer
{
#region Sytem Image with custom title
public override void ViewWillAppear(bool animated)
{
base.ViewWillAppear(animated);
foreach (var item in TabBar.Items)
{
item.Image = GetTabIcon(item.Title);
}
}
private UIImage GetTabIcon(string title)
{
UITabBarItem item = null;
switch (title)
{
case "Profile":
item = new UITabBarItem(UITabBarSystemItem.Search, 0);
break;
case "Settings":
item = new UITabBarItem(UITabBarSystemItem.Bookmarks, 0);
break;
}
var img = (item != null) ? UIImage.FromImage(item.SelectedImage.CGImage, item.SelectedImage.CurrentScale, item.SelectedImage.Orientation) : new UIImage();
return img;
}
#endregion
}
}
For Android
things are easier
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ButtonRendererDemo;
using ButtonRendererDemo.Droid;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
using Xamarin.Forms.Platform.Android.AppCompat;
using Android.Support.Design.Widget;
[assembly: ExportRenderer(typeof(CustomTabsPage), typeof(CustomTabsPageRenderer))]
namespace ButtonRendererDemo.Droid
{
public class CustomTabsPageRenderer : TabbedPageRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<TabbedPage> e)
{
base.OnElementChanged(e);
//var layout = (TabLayout)ViewGroup.GetChildAt(1); //should be enough but just for robustness we use loop below
TabLayout layout = null;
for (int i = 0; i < ChildCount; i++)
{
layout = GetChildAt(i) as TabLayout;
if (layout != null)
break;
}
if (layout != null)
{
for (int tabIndex = 0; tabIndex < layout.TabCount; tabIndex++)
SetTabIcon(layout, tabIndex);
}
}
private void SetTabIcon(TabLayout layout, int tabIndex)
{
var tab = layout.GetTabAt(tabIndex);
switch (tabIndex)
{
case 0:
tab.SetIcon(Resource.Drawable.icon2);//from local resource
break;
case 1:
tab.SetIcon(Resource.Drawable.ic_media_play_dark);//from Android system, depends on version !
break;
}
}
}
}

In order to use icons, you need a platform-specific structure. In Xamarin.Forms, for this to work:
Icon = "youricon.png";
You need:
iOS: put the image in /Resources/youricon.png
Android: put the image in /Resources/drawable/youricon.png
Win Phone: put the image in the Windows Phone application project root.

You should write custom TabbedPage renderer for iOS project:
public class YourTabbedRenderer : TabbedRenderer {
public override void ViewWillAppear(bool animated)
{
base.ViewWillAppear(animated);
var items = TabBar.Items;
for (var i = 0; i < items.Length; i++) {
// set your icons here, you could do some string comparison (check tab title or tab icon resource name)
// items[i].Image =
// items[i].SelectedImage =
}
}
}
http://developer.xamarin.com/guides/cross-platform/xamarin-forms/custom-renderer/

Related

Xamarin bug happens when linker is set to "Link All". Can't use DependencyService

Right now, I have to have my linker set to "Link All" in order to submit to the App Store because of the deprecated UIWebView. While doing this, I had to add [Preserve(AllMembers = true)] to my DataStores to make them work this way, but then I ran into this issue.
The following line will return null:
var dp = DependencyService.Get<ITextMeter>();
After looking into the solution, it seemed the best answer would be to add the following line into the AppDelegate:
DependencyService.Register<ITextMeter, TextMeterImplementation>();
Once I did that, I started receiving this exception:
DependencyService: System.MissingMethodException: Default constructor not found for [Interface]
https://forums.xamarin.com/discussion/71072/dependencyservice-system-missingmethodexception-default-constructor-not-found-for-interface
I just want to find a working solution that will allow everything to work with the linker set to "Link All". Thanks in advance.
ITextMeter:
using System;
using Xamarin.Forms.Internals;
namespace RedSwipe.Services
{
public interface ITextMeter
{
double MeasureTextSize(string text, double width, double fontSize, string fontName = null);
}
}
TextMeterImplementation:
using System.Drawing;
using Foundation;
using RedSwipe.iOS.Services;
using RedSwipe.Services;
using UIKit;
[assembly: Xamarin.Forms.Dependency(typeof(TextMeterImplementation))]
namespace RedSwipe.iOS.Services
{
public class TextMeterImplementation : ITextMeter
{
public double MeasureTextSize(string text, double width, double fontSize, string fontName = null)
{
var nsText = new NSString(text);
var boundSize = new SizeF((float)width, float.MaxValue);
var options = NSStringDrawingOptions.UsesFontLeading | NSStringDrawingOptions.UsesLineFragmentOrigin;
if (fontName == null)
{
fontName = "HelveticaNeue";
}
var attributes = new UIStringAttributes
{
Font = UIFont.FromName(fontName, (float)fontSize)
};
var sizeF = nsText.GetBoundingRect(boundSize, options, attributes, null).Size;
//return new Xamarin.Forms.Size((double)sizeF.Width, (double)sizeF.Height);
return (double)sizeF.Height;
}
}
}
Add this [Preserve (AllMembers = true)] in your TextMeterImplementation before class implementation.
Your code would be like:
using System.Drawing;
using Foundation;
using RedSwipe.iOS.Services;
using RedSwipe.Services;
using UIKit;
using Xamarin.Forms.Internals; // Add This import
[assembly: Xamarin.Forms.Dependency(typeof(TextMeterImplementation))]
namespace RedSwipe.iOS.Services
{
[Preserve (AllMembers = true)]
public class TextMeterImplementation : ITextMeter
{
public double MeasureTextSize(string text, double width, double fontSize, string fontName = null)
{
var nsText = new NSString(text);
var boundSize = new SizeF((float)width, float.MaxValue);
var options = NSStringDrawingOptions.UsesFontLeading | NSStringDrawingOptions.UsesLineFragmentOrigin;
if (fontName == null)
{
fontName = "HelveticaNeue";
}
var attributes = new UIStringAttributes
{
Font = UIFont.FromName(fontName, (float)fontSize)
};
var sizeF = nsText.GetBoundingRect(boundSize, options, attributes, null).Size;
//return new Xamarin.Forms.Size((double)sizeF.Width, (double)sizeF.Height);
return (double)sizeF.Height;
}
}
}

PulltoRefresh + await FadeTo+TranslateTo animation combination Crash on Xamarin.forms

I'm making app with using Xamarin.forms PCL.
I'm having really hard time to solve this issue.
I've spent several days but couldn't solve.
I'm using this nuget
PullToRefresh
https://github.com/jamesmontemagno/Xamarin.Forms-PullToRefreshLayout
xabre ble plugin
https://github.com/xabre/xamarin-bluetooth-le
When I use this at the same time AND Add some animation on the page.
It gives this runtime exception when I try to refresh. (It happens very rare like 1 time on 10 times)
The Crash happens ONLY iOS.
It's very rare. You can try to refresh more than 10 times. You will see.
It happens only actual iOS Device.
Foundation.MonoTouchException: Objective-C exception thrown. Name:
NSGenericException Reason: *** Collection <__NSSetM: 0x14feb8b0> was
mutated while being enumerated.
I know well what this is and when this happen.
It happens when I try to access deleted item on list or different thread.
So I made very simple source code to look this issue simply.
I don't have any list or array on my code.
Well, it happens again.
https://github.com/myallb/test_pulltorefresh
This is my sample source code for reproducing this issue. If you can help me, please look this code.
The Crash happens ONLY iOS.
It's very rare. You can try to refresh more than 10 times. You will see.
It happens only actual iOS Device.
Thanks so much.
Full source code
using Xamarin.Forms;
using Plugin.BLE.Abstractions.Contracts;
using Plugin.BLE;
using Plugin.BLE.Abstractions.EventArgs;
using System;
using System.Diagnostics;
using Refractored.XamForms.PullToRefresh;
using System.Threading.Tasks;
namespace test
{
public partial class testPage : ContentPage
{
public static IAdapter Adapter { set; get; }
public PullToRefreshLayout RefreshView = null;
AbsoluteLayout layout;
public testPage()
{
InitializeComponent();
Adapter = CrossBluetoothLE.Current.Adapter;
if (Adapter != null)
{
Adapter.DeviceAdvertised += OnEvent_DeviceAdvertised;
Adapter.DeviceConnected += OnEvent_DeviceConnected;
Adapter.DeviceConnectionLost += OnEvent_DeviceConnectionLost;
Adapter.DeviceDisconnected += OnEvent_DeviceDisconnected;
Adapter.DeviceDiscovered += OnEvent_DeviceDiscovered;
Device.StartTimer(TimeSpan.FromSeconds(5), Timer_ScanDevice);
}
else {
}
layout = new AbsoluteLayout()
{
BackgroundColor = Color.Purple,
};
ScrollView scrollview = new ScrollView()
{
VerticalOptions = LayoutOptions.FillAndExpand,
HorizontalOptions = LayoutOptions.FillAndExpand,
Content = layout
};
RefreshView = new PullToRefreshLayout
{
VerticalOptions = LayoutOptions.FillAndExpand,
HorizontalOptions = LayoutOptions.FillAndExpand,
Content = scrollview,
RefreshColor = Color.Red,
RefreshCommand = new Command(RefreshStart)
};
RefreshView.IsPullToRefreshEnabled = true;
Content = RefreshView;
Device.StartTimer(new TimeSpan(0, 0, 1), ani);
}
bool ani()
{
Label z = new Label()
{
Text = "Z",
TextColor = Color.White,
FontAttributes = FontAttributes.Bold,
FontSize = new Random().Next(22, 35)
};
AbsoluteLayout.SetLayoutBounds(z, new Rectangle(0.67 + new Random().Next(0, 10) / 100.0, 0.13 + new Random().Next(0, 10) / 100.0, 40, 40));
AbsoluteLayout.SetLayoutFlags(z, AbsoluteLayoutFlags.PositionProportional);
layout.Children.Add(z);
Device.BeginInvokeOnMainThread(async () =>
{
Task t1 = z.FadeTo(0, 3500);
Task t2 = z.TranslateTo(0, -70, 3500, Easing.SinInOut);
await Task.WhenAll(t1, t2);
layout.Children.Remove(z);
});
return true;
}
void RefreshStart()
{
Debug.WriteLine("RefreshStart");
if (RefreshView != null)
RefreshView.IsRefreshing = true;
Device.BeginInvokeOnMainThread(async () =>
{
await Task.Delay(20);
Debug.WriteLine("RefreshEnd");
RefreshView.IsRefreshing = false;
});
}
bool Timer_ScanDevice()
{
Adapter.StartScanningForDevicesAsync();
return true;
}
void OnEvent_DeviceAdvertised(object sender, DeviceEventArgs a)
{
Debug.WriteLine("OnEvent_DeviceAdvertised");
}
void OnEvent_DeviceDiscovered(object sender, DeviceEventArgs a)
{
Debug.WriteLine("OnEvent_DeviceDiscovered");
}
void OnEvent_DeviceConnected(object sender, DeviceEventArgs a)
{
Debug.WriteLine("OnEvent_DeviceConnected");
}
void OnDeviceProcessError(IDevice device, string message)
{
Debug.WriteLine("OnDeviceProcessError");
}
void OnEvent_DeviceConnectionLost(object sender, DeviceErrorEventArgs a)
{
Debug.WriteLine("OnEvent_DeviceConnectionLost");
}
void OnEvent_DeviceDisconnected(object sender, DeviceEventArgs a)
{
Debug.WriteLine("OnEvent_DeviceDisconnected");
}
}
}

How to load an Image from Assets in Xamarin.Forms on Android?

I am using the following code:
var baseUrl = DependencyService.Get<IBaseUrl> ().Get ();
var backgroundImage = new Image () {
Source = FileImageSource.FromFile (
System.IO.Path.Combine (baseUrl, "Content", "images", "background-2.jpg")
)
};
Where the DependencyServices for iOS and Androids are as below:
// iOS
[assembly: Xamarin.Forms.Dependency (typeof (BaseUrl_iOS))]
namespace TuneProtectApp.iOS
{
public class BaseUrl_iOS : IBaseUrl
{
public BaseUrl_iOS () { }
public string Get ()
{
return NSBundle.MainBundle.BundlePath;
}
}
}
// Android
[assembly: Xamarin.Forms.Dependency (typeof (BaseUrl_Droid))]
namespace TuneProtectApp.Droid
{
public class BaseUrl_Droid : IBaseUrl
{
public BaseUrl_Droid () {}
public string Get ()
{
return "file:///android_asset/";
}
}
}
The backgroundImage loads fine on iOS but not on Android. How to load an Image from Assets in Xamarin.Forms on Android?
In my Xamarin.forms (shared) app I have a registration-page, where the user also have to select an image for his avatar. Based on the sex of the user, I show a male or a female symbol-image as default (the user then can select another, if he want to do).
I have implemented it as follows:
First created a sub-directory \Embedded for all projects (iOS, Android and WP) (directly in the project-root of each project-type).
Then added the two .jpg’s to the new directories in all projects.
In my app I have a global variable (GV.cEmbeddedAblage)
This is filled in startup-code:
string cNameSpace = "";
switch (Device.OS)
{
case TargetPlatform.WinPhone:
cNameSpace = "MatrixGuide.WinPhone";
break;
case TargetPlatform.iOS:
cNameSpace = "MatrixGuide.iOS";
break;
case TargetPlatform.Android:
cNameSpace = "MatrixGuide.Droid";
break;
//
}
GV.cEmbeddedAblage = cNameSpace + ".Embedded.";
Further, I create a global byte-array for the images (example to male):
static Byte[] _SymbolMann;
public static Byte[] ByteSymbolMann
{
get { return _SymbolMann; }
set { _SymbolMann = value; }
}
I then easily can access the images from shared code (on the registration-page) with (e.g.):
Generate the path, load image in byte-array (if not already loaded):
string cFilename = "";
if (GV.ByteSymbolMann == null) // not yet loaded - only load one time
{
cFilename = GV.cEmbeddedAblage + "SymbolMann.jpg";
var assembly = this.GetType().GetTypeInfo().Assembly;
byte[] buffer;
using (Stream s = assembly.GetManifestResourceStream(cFilename))
{
long length = s.Length;
buffer = new byte[length];
s.Read(buffer, 0, (int)length);
GV.ByteSymbolMann = buffer;
}
}
Fill another byte.array (with selected (loaded) male- / female- image):
AvatarErfassung = GV.ByteSymbolMann;
create the image on the page:
var Avatar = new Image { HeightRequest = 70, WidthRequest = 70, HorizontalOptions = LayoutOptions.Start };
Overtake the selected image as Source to the Image:
Avatar.Source = ImageSource.FromStream(() => new MemoryStream(AvatarErfassung));
You should be able to do it similar...

Placing a SearchBar in the top/navigation bar

In a Forms project, is it possible to place a SearchBar such that it appears in the top/navigation bar of the app? What I want to achieve is something along the lines of the Android Youtube app, just cross-platform:
To do this, you should write a renderer for your Page
There is my implementation for iOS (with custom 'searchField')
using CoreGraphics;
using Foundation;
using MyControls;
using MyRenderer;
using UIKit;
using Xamarin.Forms;
[assembly: ExportRenderer(typeof(MySearchContentPage), typeof(MySearchContentPageRenderer))]
namespace IOS.Renderer
{
using Xamarin.Forms.Platform.iOS;
public class MySearchContentPageRenderer : PageRenderer
{
public override void ViewWillAppear(bool animated)
{
base.ViewWillAppear(animated);
SetSearchToolbar();
}
public override void WillMoveToParentViewController(UIKit.UIViewController parent)
{
base.WillMoveToParentViewController(parent);
if (parent != null)
{
parent.NavigationItem.RightBarButtonItem = NavigationItem.RightBarButtonItem;
parent.NavigationItem.TitleView = NavigationItem.TitleView;
}
}
private void SetSearchToolbar()
{
var element = Element as MySearchContentPage;
if (element == null)
{
return;
}
var width = NavigationController.NavigationBar.Frame.Width;
var height = NavigationController.NavigationBar.Frame.Height;
var searchBar = new UIStackView(new CGRect(0, 0, width * 0.85, height));
searchBar.Alignment = UIStackViewAlignment.Center;
searchBar.Axis = UILayoutConstraintAxis.Horizontal;
searchBar.Spacing = 3;
var searchTextField = new MyUITextField();
searchTextField.BackgroundColor = UIColor.FromRGB(239, 239, 239);
NSAttributedString strAttr = new NSAttributedString("Search", foregroundColor: UIColor.FromRGB(146, 146, 146));
searchTextField.AttributedPlaceholder = strAttr;
searchTextField.SizeToFit();
// Delete button
UIButton textDeleteButton = new UIButton(new CGRect(0, 0, searchTextField.Frame.Size.Height + 5, searchTextField.Frame.Height));
textDeleteButton.Font = UIFont.FromName("FontAwesome", 18f);
textDeleteButton.BackgroundColor = UIColor.Clear;
textDeleteButton.SetTitleColor(UIColor.FromRGB(146, 146, 146), UIControlState.Normal);
textDeleteButton.SetTitle("\uf057", UIControlState.Normal);
textDeleteButton.TouchUpInside += (sender, e) =>
{
searchTextField.Text = string.Empty;
};
searchTextField.RightView = textDeleteButton;
searchTextField.RightViewMode = UITextFieldViewMode.Always;
// Border
searchTextField.BorderStyle = UITextBorderStyle.RoundedRect;
searchTextField.Layer.BorderColor = UIColor.FromRGB(239, 239, 239).CGColor;
searchTextField.Layer.BorderWidth = 1;
searchTextField.Layer.CornerRadius = 5;
searchTextField.EditingChanged += (sender, e) =>
{
element.SetValue(MySearchContentPage.SearchTextProperty, searchTextField.Text);
};
searchBar.AddArrangedSubview(searchTextField);
var searchbarButtonItem = new UIBarButtonItem(searchBar);
NavigationItem.SetRightBarButtonItem(searchbarButtonItem, true);
NavigationItem.TitleView = new UIView();
if (ParentViewController != null)
{
ParentViewController.NavigationItem.RightBarButtonItem = NavigationItem.RightBarButtonItem;
ParentViewController.NavigationItem.TitleView = NavigationItem.TitleView;
}
}
}
}
Also, there is some discussion:How to include view in NavigationBar of Xamarin Forms?
I hope, you understood the main idea.

Bind text with links to RichTextBox

I need to bind text which may contain hyperlinks to RichTextBox so it could show text as normal text and links as hyperlinks.
For example I have following text:
Join us on social networks
http://www.facebook.com/
I want that links in a text be hyperlinks so the result in RichTextBox would be like this:
Join us on social networks
http://www.facebook.com/
I implemented what I need
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Text.RegularExpressions;
using System.Windows.Media;
namespace NazarGrynko.UI.Controls
{
public class MyRichTextBox : RichTextBox
{
private const string UrlPattern = #"(http|ftp|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,#?^=%&:/~\+#]*[\w\-\#?^=%&/~\+#])?";
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register("Text", typeof (string), typeof(MyRichTextBox ), new PropertyMetadata(default(string), TextPropertyChanged));
public string Text
{
get { return (string) GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
private static void TextPropertyChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
{
var richTextBox = (MyRichTextBox)dependencyObject;
var text = (string) dependencyPropertyChangedEventArgs.NewValue;
int textPosition = 0;
var paragraph = new Paragraph();
var urlMatches = Regex.Matches(text, UrlPattern);
foreach (Match urlMatch in urlMatches)
{
int urlOccurrenceIndex = text.IndexOf(urlMatch.Value, textPosition, StringComparison.Ordinal);
if (urlOccurrenceIndex == 0)
{
var hyperlink = new Hyperlink
{
NavigateUri = new Uri(urlMatch.Value),
TargetName = "_blank",
Foreground = Application.Current.Resources["PhoneAccentBrush"] as Brush
};
hyperlink.Inlines.Add(urlMatch.Value);
paragraph.Inlines.Add(hyperlink);
textPosition += urlMatch.Value.Length;
}
else
{
paragraph.Inlines.Add(text.Substring(textPosition, urlOccurrenceIndex - textPosition));
textPosition += urlOccurrenceIndex - textPosition;
var hyperlink = new Hyperlink
{
NavigateUri = new Uri(urlMatch.Value),
TargetName = "_blank",
Foreground = Application.Current.Resources["PhoneAccentBrush"] as Brush
};
hyperlink.Inlines.Add(urlMatch.Value);
paragraph.Inlines.Add(hyperlink);
textPosition += urlMatch.Value.Length;
}
}
if (urlMatches.Count == 0)
{
paragraph.Inlines.Add(text);
}
richTextBox.Blocks.Add(paragraph);
}
}
}
Using example:
<MyRichTextBox Text="{Binding Message}"/>
Parse the Hyperlink, and create following structure (With C#, of course):
<RichTextBlock>
<Run>Hello World!</Run>
<Hyperlink NavigateUri="http://www.stackoverflow.com">http://www.stackoverflow.com</Hyperlink>
Thanks for the solution!
One minor modification I made was right at the end, I replaced the check on the count, with a line that just adds a substring of the full text, this way it does not truncate everything after the last URL, all text is retained.
paragraph.Inlines.Add(text.Substring(textPosition, text.Length - textPosition));
//if (urlMatches.Count == 0)
//{
// paragraph.Inlines.Add(text);
//}

Resources