Showing posts with label Android. Show all posts
Showing posts with label Android. 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. 
  

Thursday, 6 August 2015

Xamarin Forms - Mail service for Android - Send email with multiple attachments

Done number of research, testing, refactoring, etc. TBH, It was quite hard to get it work correctly.
Here are the list of websites that I've been through before I got it work, but none of them not really solve my problem... :(
https://gist.github.com/prashantvc/5213961
http://stackoverflow.com/questions/6972210/android-email-sqlite-database
http://stackoverflow.com/questions/23847561/send-email-with-html-content

How to send email from Android with Attachment? or with multiple attachments? 

A long my development and testing, I have figured out:

1. Intent ActionSend
If you use Intent.ActionSend, then you will basically allow to send an email with 1 attachment only.
var intent = new Intent(Intent.ActionSend);

This work well with:
intent.PutExtra(Intent.ExtraStream,  Android.Net.Uri.FromFile (fileIn)  );


2.Intent.ActionSendMultiple
However, if you prefer to send multiple attachments, then you need this:
var intent = new Intent(Intent.ActionSendMultiple); 

This will not work with
intent.PutExtra(Intent.ExtraStream,  Android.Net.Uri.FromFile (fileIn)  );

You need:
intent.PutParcelableArrayListExtra(Intent.ExtraStream, new List<IParcelable>(uris)); 

3. And,...
If you hit the problem where .... you file path is correct, but unable to attach file to your email client (such as Ms Outlook or gmail),. then you need this:
File fileIn = new File(file);
fileIn.SetReadable(true, false); 

The mail activity might not enough rights to read your file. Try to add myFile.setReadable(true, false) before adding to Attachments array.


Here is my example, it has been written as dependencies service for Xamarin.Forms:

[assembly: DependencyAttribute(typeof(MailServiceDroid))]
namespace Jeff.Droid
{
    public class MailServiceDroid : IMailService
    {
        public bool CanSend
        {
            get { return true; }
        }

        public void ShowDraft(string subject, string body, bool html, string to, MessageOrigin origin, IEnumerable<string> attachments = null)
        {
            ShowDraft (subject, body, html, new[] { to }, new string[] { }, new string[] { }, origin, attachments);
        }

        public void ShowDraft(string subject, string body, bool html, string[] to, string[] cc, string[] bcc, MessageOrigin origin, IEnumerable<string> attachments = null)
        {
            //var intent = new Intent(Intent.ActionSend); // This is for single attachment.
            var intent = new Intent(Intent.ActionSendMultiple);

            //intent.SetType ("message/rfc822"); //Set the type according to the attachment type. 
            intent.SetType("application/octet-stream"); // db3
            intent.PutExtra(Intent.ExtraEmail, to);
            intent.PutExtra(Intent.ExtraCc, cc);
            intent.PutExtra(Intent.ExtraBcc, bcc);
            intent.PutExtra(Intent.ExtraSubject, subject ?? string.Empty);

            if (html)
            {
                intent.PutExtra(Intent.ExtraText, Android.Text.Html.FromHtml(body));
            }
            else
            {
                IList<string> x = new List<string>();
                intent.PutStringArrayListExtra(Intent.ExtraText, x);
            }

            if (attachments != null) {
                IntentExtensions.AddAttachments (intent, attachments);
            }

            //Forms.Context.StartActivity (intent); //
            Forms.Context.StartActivity(Intent.CreateChooser(intent, "Send mail..."));
        }
    }


    public static class IntentExtensions
    {
        public static void AddAttachments(this Intent intent, IEnumerable<string> attachments)
        {
            var uris = new List<IParcelable>();
            foreach (var file in attachments)
            {
                File fileIn = new File(file);
                fileIn.SetReadable(true, false); 
                if (fileIn.CanWrite ()) {
                    Android.Net.Uri u = Android.Net.Uri.FromFile (fileIn);
                    uris.Add (u);


                    //This work for: //var intent = new Intent(Intent.ActionSend);
                    //intent.PutExtra(Intent.ExtraStream, u);
                }
            }
            intent.PutParcelableArrayListExtra(Intent.ExtraStream, new List<IParcelable>(uris));
        }

    }
}



Give me a 'like' if you this help! /Jeff

  

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