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. 
  

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