Showing posts with label iOS. Show all posts
Showing posts with label iOS. Show all posts

Friday, 1 April 2016

Spannable string with custom renderer in Xamarin.Forms

Are you looking for a solution of how to highlight a keyword from a paragraph? Or change certain text to different font or decoration?  And, you wanted to achieve it in Xamarin.Forms?  Then, you are lucky!

If you have the experience of doing something similar on web page, or web app, then you may realise that the concept of how to do it for mobile app is quite similar.

In html, we can use <span>text</span> tag to wrap up the keyword we wanted. Or replace the keyword with <span> tag. Then apply some css style later. Job done.

Well, in mobile app. the concept will be:
In iOS, we use FontAttribute; In Android, we use Spannable string.

Example:
Here, I am going to show you how to highlight certain keyword from a sentence in a Label:

1. Create a custom Label in Xamarin.Forms project.
using System;
using Xamarin.Forms;

namespace SpanStr
{
 public class SpannableLabel : Label
 {
  #region Binding Declarations
  public static readonly BindableProperty SearchTermProperty =
   BindableProperty.Create<SpannableLabel, String>(p => p.SearchTerm, null);
  #endregion


  #region Public Properties
  public string SearchTerm
  {
   get { return (string)GetValue(SearchTermProperty); }
   set { SetValue(SearchTermProperty, value); }
  }
  #endregion
 }
}

Note: I have a bindable property named 'SearchTermProperty' and a public accessible method named "SearchTerm".  With this bindable property, this allow me to set the search keyword to my label. 

2. Create a HomePage content page. Place my spannable label into my home page layout.
using System;
using Xamarin.Forms;

namespace SpanStr
{
public class HomePage : ContentPage
{
Entry entrySearch;

SpannableLabel spannableLabel; 

public HomePage ()
{
BackgroundColor = Color.White;

entrySearch = new Entry {
Placeholder = "Search Term" ,
HorizontalOptions = LayoutOptions.FillAndExpand,
TextColor = Color.White,
BackgroundColor = Color.Gray
};

entrySearch.TextChanged += EntrySearch_Changed;

StackLayout searchLayout = new StackLayout {
Orientation = StackOrientation.Horizontal,
Children = {entrySearch}
};

spannableLabel = new SpannableLabel ();
spannableLabel.Text = "Hello Jeff Lim and Tim";
spannableLabel.TextColor = Color.Black;
spannableLabel.FontSize = 22;

Content = new StackLayout { 
Padding = 20,
Children = {
searchLayout,
spannableLabel,
},
HorizontalOptions = LayoutOptions.FillAndExpand
};
}

void EntrySearch_Changed (object sender, EventArgs e)
{
spannableLabel.SearchTerm = entrySearch.Text;
}
}
}
Note: I have a search entry / text field and a label here. Enter a text in text field will set the text to the searchTerm defined in Spannable custom class. 


3.  Create custom renderer in iOS project
using System;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
using UIKit;
using Foundation;
using SpanStr.iOS;
using SpanStr;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Linq;

[assembly: ExportRenderer (typeof(SpannableLabel), typeof(SpannableStringRendererIOS))]
namespace SpanStr.iOS
{
public class SpannableStringRendererIOS : LabelRenderer
{
private string searchTerm;

public SpannableStringRendererIOS ()
{
}

protected override void OnElementPropertyChanged (object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged (sender, e);
var firstAttr = new UIStringAttributes {
ForegroundColor = Color.Pink.ToUIColor(),
BackgroundColor = Color.Yellow.ToUIColor(),
Font = UIFont.FromName("Courier", 18f),
};
firstAttr.Font = UIFont.BoldSystemFontOfSize (18f);

if (Control != null) {
string originalText = Control.Text;
var spannableLabel = (SpannableLabel)sender;
searchTerm = spannableLabel.SearchTerm;

if (searchTerm != null) {
var allIndexOf =  Utils.AllIndexOf(originalText, searchTerm, StringComparison.OrdinalIgnoreCase);


var prettyString = new NSMutableAttributedString (originalText);
foreach (var i in allIndexOf) {
int startPosition = i;
int endPosition = searchTerm.Length;

if (startPosition >= 0) {
prettyString.SetAttributes (firstAttr, new NSRange (startPosition, endPosition));
}
}
Control.AttributedText = prettyString;
}
}
}


protected override void OnElementChanged (ElementChangedEventArgs<Label> e)
{
base.OnElementChanged (e);
}
}

public class Utils {
public static IList<int> AllIndexOf(string originText, string searchTerm, StringComparison comparisonType)
{
IList<int> allIndexOf = new List<int>();

var indexes = Regex.Matches(originText.ToLower(), searchTerm.ToLower()).Cast<Match>().Select(m => m.Index).ToList();

foreach (var position in indexes) {
allIndexOf.Add (position);
}
return allIndexOf;
}
}
}
Note: Because my bindable property is a property. So, I can handle it using OnPropertyChanged here. 
The AllIndexOf is the just a util tool to get all the occurrences of your keyword in a sentence.  
It is important to use 'NSMutableAttributedString'.  And, AttributedText that allowed you to pass attributed text to the control. 


3.  Create custom renderer in Android project
using System;
using Xamarin.Forms.Platform.Android;
using Xamarin.Forms;
using Android.Text.Style;
using SpanStr.Droid;
using SpanStr;
using Android.Text;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Linq;

[assembly: ExportRenderer (typeof(SpannableLabel), typeof(SpannableStringRendererDroid))]
namespace SpanStr.Droid
{
public class SpannableStringRendererDroid : LabelRenderer
{
public SpannableStringRendererDroid ()
{
}

protected override void OnElementChanged (ElementChangedEventArgs<Label> e)
{
base.OnElementChanged (e);
}

protected override void OnElementPropertyChanged (object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged (sender, e);

var spannableLabel = (SpannableLabel)sender;
var originalText = spannableLabel.Text;
string searchTerm = spannableLabel.SearchTerm;

if (Control != null) {
if (!string.IsNullOrEmpty (searchTerm)) {

var allIndexOf =  Utils.AllIndexOf(originalText, searchTerm, StringComparison.OrdinalIgnoreCase);

SpannableString ss = new SpannableString (spannableLabel.Text);

foreach (var i in allIndexOf){
int startPosition = i;
int endPosition = startPosition + searchTerm.Length;

if (startPosition >= 0) {
ss.SetSpan (new BackgroundColorSpan (Android.Graphics.Color.Yellow), startPosition, endPosition, SpanTypes.Composing);
ss.SetSpan (new StyleSpan(Android.Graphics.TypefaceStyle.Bold), startPosition, endPosition, SpanTypes.Composing);
ss.SetSpan (new TypefaceSpan("Courier"), startPosition, endPosition, SpanTypes.Composing);
}
}
Control.TextFormatted = ss;

} else {
//reset;
Control.Text = originalText;
}
}
}
}


public class Utils {
public static IList<int> AllIndexOf(string originText, string searchTerm, StringComparison comparisonType)
{
IList<int> allIndexOf = new List<int>();

var indexes = Regex.Matches(originText.ToLower(), searchTerm.ToLower()).Cast<Match>().Select(m => m.Index).ToList();

foreach (var position in indexes) {
allIndexOf.Add (position);
}
return allIndexOf;
}
}
}


Note: SpannableString class it the key of success. This allows setting up the span to the spanned string. 
Besides, it is also important to use Control.TextFormatted to return your spanned string to label.  Control.Text is not allowed. 

JOB done :)

So, when you type 'Jef' as you keyword, then it will highlight the text for you. If you type 'im', both 'im' in Lim and Sim will be highlighted as well. 
  

Monday, 13 April 2015

Send email from iOS and Android via Xamarin Forms

Email service with default email composer
Send email from Xamarin Forms requires different implementation for iOS and Android. The default behaviour of using the default email service from each platform will populate a composer, which is visiable to the end user with the option to see, send, cancel, etc.

Concepts:  iOS

Compoment: MFMailComposeViewController
Present MFMailComposeViewController
In order to use the above iOS component as a dependency service, it is required to have a statement to present the view controller explicitly in your code. In this case, we use:
UIApplication.SharedApplication.KeyWindow.RootViewController.PresentViewController(mailer, truenull)


Dismiss MFMailComposeViewController
When finished sending the email, it is required to dismiss the view controller manually. The DismissViewController has to be inside the MainThread of iOS.
mailer.Finished += (s,e)=>{
    UIApplication.ShareApplication.InvokeMainThread(()=>{
        ((MFMailComposeViewController)s).DismissViewController(true,()=>{});
    });
}


Set Mime Type for an attachment
It is important to provide the right "Mime Type" to the attachment you plan to add to your email.

Concepts: Android

Intent:  Intent.ActionSend  (Not "Intent.ActionSendTo" ,  using this will cause some error when sending email to exchange or google)
Intent type: "message/rfc822"  (follow the receipe from Xamarin website: http://developer.xamarin.com/recipes/android/networking/email/send_an_email/)

Present Composer
Setting the Intent’s mime type to message/rfc822 causes the mail application to launch. If multiple applications are capable of handling mail, the user will get a list to choose from.
Note: user can choose, sending out a message via gmail, exchange, skype, hangout. whatsapp, etc.

File object:
With the given file path of the attachment, you still need the Java object to form the file object. But, use Android.Net.Uri.FromFile to form the uri to the file.
var file = new Java.IO.File(attachmentPath);
Use intent.PutExtra(Intent.ExtraStream, Android.Net.Uri.FromFile(file)


Start Android Activity:

It is important to start the Android activity via the right context.
In Xamarin Forms, we use: 
Forms.Context.StartActivity(intent)
//Not: this.Context.StartActivity(intent)



It is important to set "setReadable()" after obtaining a file object in Android.

Without this line, you would't get the attachment even though you can see the attachment appear in the email composer.
file.SetReadable(true, false)

Usage:
//Example in Xamarin.Forms 
var mailService = DependencyService.Get<IEmailService>(); 
if (mailService.CanSend){
    mailService.ShowDraft("Subject","Body",false"receipient email address""path to the attachment");
}else{
    //Please activate or setup your email account from your device settings.
}

Alternative approach:

MAILTO 
string strMailTo = @"mailto:jeff.wei-lim@artesiansolutions.com?Subject=test&Body=testBody";
Device.OpenUri(new Uri(strMailTo));

Note for using MailTo:
  1. user can choose, sending out a message via email client only (such as email app or gmail...)
  2. Unable to send an email with attachment.

How to run unit test for your Xamarin Application in AppCenter?

How to run unit test for your Xamarin application in AppCenter?  When we talk about Building and Distributing your Xamarin app, you m...