In the first part of this series we talked about how to create different UI cells to render chat messages depending on who sends the message. Also how to put the chat entry on the top of the keyboard when focused.
In this part we are going to talk about two things:
- Auto expand entry based on the text when typing
- Scroll to the last message.
Auto expand entry based on the text when typing

When typing a long message in an entry is really difficult to scroll horizontally to read the complete message. So we need an entry that can grow vertically based on the text content.
To achieve it we are going to use this (Extended Editor in Xamarin Forms) article, in which instead of using an entry we will use an editor.
We are going to use the same control and custom renderers of that previous article. Code here:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using System; | |
| using Xamarin.Forms; | |
| namespace ChatUIXForms.Controls | |
| { | |
| public class ExtendedEditorControl : Editor | |
| { | |
| public static BindableProperty PlaceholderProperty | |
| = BindableProperty.Create(nameof(Placeholder), typeof(string), typeof(ExtendedEditorControl)); | |
| public static BindableProperty PlaceholderColorProperty | |
| = BindableProperty.Create(nameof(PlaceholderColor), typeof(Color), typeof(ExtendedEditorControl), Color.LightGray); | |
| public static BindableProperty HasRoundedCornerProperty | |
| = BindableProperty.Create(nameof(HasRoundedCorner), typeof(bool), typeof(ExtendedEditorControl), false); | |
| public static BindableProperty IsExpandableProperty | |
| = BindableProperty.Create(nameof(IsExpandable), typeof(bool), typeof(ExtendedEditorControl), false); | |
| public bool IsExpandable | |
| { | |
| get { return (bool)GetValue(IsExpandableProperty); } | |
| set { SetValue(IsExpandableProperty, value); } | |
| } | |
| public bool HasRoundedCorner | |
| { | |
| get { return (bool)GetValue(HasRoundedCornerProperty); } | |
| set { SetValue(HasRoundedCornerProperty, value); } | |
| } | |
| public string Placeholder | |
| { | |
| get { return (string)GetValue(PlaceholderProperty); } | |
| set { SetValue(PlaceholderProperty, value); } | |
| } | |
| public Color PlaceholderColor | |
| { | |
| get { return (Color)GetValue(PlaceholderColorProperty); } | |
| set { SetValue(PlaceholderColorProperty, value); } | |
| } | |
| public ExtendedEditorControl() | |
| { | |
| TextChanged += OnTextChanged; | |
| } | |
| ~ExtendedEditorControl() | |
| { | |
| TextChanged -= OnTextChanged; | |
| } | |
| private void OnTextChanged(object sender, TextChangedEventArgs e) | |
| { | |
| if (IsExpandable) | |
| InvalidateMeasure(); | |
| } | |
| } | |
| } |
Android
We are going to limit the expansion of the editor to 5 lines, so when it gets to 5 lines typed the user will have to scroll vertically to read the previous text. We can do on this on the custom editor renderer by adding on OnElementChanged:
Control.SetMaxLines(5);
Code here:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using System.ComponentModel; | |
| using Android.Content; | |
| using Android.Content.Res; | |
| using Android.Graphics.Drawables; | |
| using ChatUIXForms.Controls; | |
| using ChatUIXForms.Droid.Renderers; | |
| using Xamarin.Forms; | |
| using Xamarin.Forms.Platform.Android; | |
| [assembly: ExportRenderer(typeof(ExtendedEditorControl), typeof(CustomEditorRenderer))] | |
| namespace ChatUIXForms.Droid.Renderers | |
| { | |
| public class CustomEditorRenderer : EditorRenderer | |
| { | |
| bool initial = true; | |
| Drawable originalBackground; | |
| public CustomEditorRenderer(Context context) : base(context) | |
| { | |
| } | |
| protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.Editor> e) | |
| { | |
| base.OnElementChanged(e); | |
| if (Control != null) | |
| { | |
| if (initial) | |
| { | |
| originalBackground = Control.Background; | |
| initial = false; | |
| } | |
| Control.SetMaxLines(5); | |
| } | |
| if (e.NewElement != null) | |
| { | |
| var customControl = (ExtendedEditorControl)Element; | |
| if (customControl.HasRoundedCorner) | |
| { | |
| ApplyBorder(); | |
| } | |
| if (!string.IsNullOrEmpty(customControl.Placeholder)) | |
| { | |
| Control.Hint = customControl.Placeholder; | |
| Control.SetHintTextColor(customControl.PlaceholderColor.ToAndroid()); | |
| } | |
| } | |
| } | |
| protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) | |
| { | |
| base.OnElementPropertyChanged(sender, e); | |
| var customControl = (ExtendedEditorControl)Element; | |
| if (ExtendedEditorControl.PlaceholderProperty.PropertyName == e.PropertyName) | |
| { | |
| Control.Hint = customControl.Placeholder; | |
| } | |
| else if (ExtendedEditorControl.PlaceholderColorProperty.PropertyName == e.PropertyName) | |
| { | |
| Control.SetHintTextColor(customControl.PlaceholderColor.ToAndroid()); | |
| } | |
| else if (ExtendedEditorControl.HasRoundedCornerProperty.PropertyName == e.PropertyName) | |
| { | |
| if (customControl.HasRoundedCorner) | |
| { | |
| ApplyBorder(); | |
| } | |
| else | |
| { | |
| this.Control.Background = originalBackground; | |
| } | |
| } | |
| } | |
| void ApplyBorder() | |
| { | |
| GradientDrawable gd = new GradientDrawable(); | |
| gd.SetCornerRadius(10); | |
| gd.SetStroke(2, Color.Black.ToAndroid()); | |
| this.Control.Background = gd; | |
| } | |
| } | |
| } |
iOS
On our chat input custom content view (ChatInputBarView) code behind we add a binding to the height of our custom content view so that is updated whenever our editor height changes:
if (Device.RuntimePlatform == Device.iOS)
{
this.SetBinding(HeightRequestProperty, new Binding("Height", BindingMode.OneWay, null, null, null, chatTextInput));
}
Note: This is not needed on Android which handles this by default
We are going to modify the custom renderer to handle the expand limit , invalidate height when there’s a new line and enable scrolling just when reaches the limit.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) | |
| { | |
| base.OnElementPropertyChanged(sender, e); | |
| var customControl = (ExtendedEditorControl)Element; | |
| if (e.PropertyName == Editor.TextProperty.PropertyName) | |
| { | |
| if (customControl.IsExpandable) | |
| { | |
| CGSize size = Control.Text.StringSize(Control.Font, Control.Frame.Size, UILineBreakMode.WordWrap); | |
| int numLines = (int)(size.Height / Control.Font.LineHeight); | |
| if (prevLines > numLines) | |
| { | |
| customControl.HeightRequest = -1; | |
| } | |
| else if (string.IsNullOrEmpty(Control.Text)) | |
| { | |
| customControl.HeightRequest = -1; | |
| } | |
| prevLines = numLines; | |
| } | |
| _placeholderLabel.Hidden = !string.IsNullOrEmpty(Control.Text); | |
| } | |
| else if (ExtendedEditorControl.HeightProperty.PropertyName == e.PropertyName) | |
| { | |
| if (customControl.IsExpandable) | |
| { | |
| CGSize size = Control.Text.StringSize(Control.Font, Control.Frame.Size, UILineBreakMode.WordWrap); | |
| int numLines = (int)(size.Height / Control.Font.LineHeight); | |
| if (numLines >= 5) | |
| { | |
| Control.ScrollEnabled = true; | |
| customControl.HeightRequest = previousHeight; | |
| } | |
| else | |
| { | |
| Control.ScrollEnabled = false; | |
| previousHeight = customControl.Height; | |
| } | |
| } | |
| } | |
| } |
Full Code here:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using System.Collections; | |
| using System.Collections.Generic; | |
| using System.ComponentModel; | |
| using ChatUIXForms.Controls; | |
| using ChatUIXForms.iOS.Renderers; | |
| using CoreGraphics; | |
| using Foundation; | |
| using UIKit; | |
| using Xamarin.Forms; | |
| using Xamarin.Forms.Platform.iOS; | |
| [assembly: ExportRenderer(typeof(ExtendedEditorControl), typeof(CustomEditorRenderer))] | |
| namespace ChatUIXForms.iOS.Renderers | |
| { | |
| public class CustomEditorRenderer : EditorRenderer | |
| { | |
| UILabel _placeholderLabel; | |
| double previousHeight = -1; | |
| int prevLines = 0; | |
| protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.Editor> e) | |
| { | |
| base.OnElementChanged(e); | |
| if (Control != null) | |
| { | |
| if (_placeholderLabel == null) | |
| { | |
| CreatePlaceholder(); | |
| } | |
| } | |
| if (e.NewElement != null) | |
| { | |
| var customControl = (ExtendedEditorControl)e.NewElement; | |
| if (customControl.IsExpandable) | |
| Control.ScrollEnabled = false; | |
| else | |
| Control.ScrollEnabled = true; | |
| if (customControl.HasRoundedCorner) | |
| Control.Layer.CornerRadius = 5; | |
| else | |
| Control.Layer.CornerRadius = 0; | |
| Control.InputAccessoryView = new UIView(CGRect.Empty); | |
| Control.ReloadInputViews(); | |
| } | |
| if (e.OldElement != null) | |
| { | |
| } | |
| } | |
| protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) | |
| { | |
| base.OnElementPropertyChanged(sender, e); | |
| var customControl = (ExtendedEditorControl)Element; | |
| if (e.PropertyName == Editor.TextProperty.PropertyName) | |
| { | |
| if (customControl.IsExpandable) | |
| { | |
| CGSize size = Control.Text.StringSize(Control.Font, Control.Frame.Size, UILineBreakMode.WordWrap); | |
| int numLines = (int)(size.Height / Control.Font.LineHeight); | |
| if (prevLines > numLines) | |
| { | |
| customControl.HeightRequest = -1; | |
| } | |
| else if (string.IsNullOrEmpty(Control.Text)) | |
| { | |
| customControl.HeightRequest = -1; | |
| } | |
| prevLines = numLines; | |
| } | |
| _placeholderLabel.Hidden = !string.IsNullOrEmpty(Control.Text); | |
| } | |
| else if (ExtendedEditorControl.PlaceholderProperty.PropertyName == e.PropertyName) | |
| { | |
| _placeholderLabel.Text = customControl.Placeholder; | |
| } | |
| else if (ExtendedEditorControl.PlaceholderColorProperty.PropertyName == e.PropertyName) | |
| { | |
| _placeholderLabel.TextColor = customControl.PlaceholderColor.ToUIColor(); | |
| } | |
| else if (ExtendedEditorControl.HasRoundedCornerProperty.PropertyName == e.PropertyName) | |
| { | |
| if (customControl.HasRoundedCorner) | |
| Control.Layer.CornerRadius = 5; | |
| else | |
| Control.Layer.CornerRadius = 0; | |
| } | |
| else if (ExtendedEditorControl.IsExpandableProperty.PropertyName == e.PropertyName) | |
| { | |
| if (customControl.IsExpandable) | |
| Control.ScrollEnabled = false; | |
| else | |
| Control.ScrollEnabled = true; | |
| } | |
| else if (ExtendedEditorControl.HeightProperty.PropertyName == e.PropertyName) | |
| { | |
| if (customControl.IsExpandable) | |
| { | |
| CGSize size = Control.Text.StringSize(Control.Font, Control.Frame.Size, UILineBreakMode.WordWrap); | |
| int numLines = (int)(size.Height / Control.Font.LineHeight); | |
| if (numLines >= 5) | |
| { | |
| Control.ScrollEnabled = true; | |
| customControl.HeightRequest = previousHeight; | |
| } | |
| else | |
| { | |
| Control.ScrollEnabled = false; | |
| previousHeight = customControl.Height; | |
| } | |
| } | |
| } | |
| } | |
| public void CreatePlaceholder() | |
| { | |
| var element = Element as ExtendedEditorControl; | |
| _placeholderLabel = new UILabel | |
| { | |
| Text = element?.Placeholder, | |
| TextColor = element.PlaceholderColor.ToUIColor(), | |
| BackgroundColor = UIColor.Clear | |
| }; | |
| var edgeInsets = Control.TextContainerInset; | |
| var lineFragmentPadding = Control.TextContainer.LineFragmentPadding; | |
| Control.AddSubview(_placeholderLabel); | |
| var vConstraints = NSLayoutConstraint.FromVisualFormat( | |
| "V:|-" + edgeInsets.Top + "-[PlaceholderLabel]-" + edgeInsets.Bottom + "-|", 0, new NSDictionary(), | |
| NSDictionary.FromObjectsAndKeys( | |
| new NSObject[] { _placeholderLabel }, new NSObject[] { new NSString("PlaceholderLabel") }) | |
| ); | |
| var hConstraints = NSLayoutConstraint.FromVisualFormat( | |
| "H:|-" + lineFragmentPadding + "-[PlaceholderLabel]-" + lineFragmentPadding + "-|", | |
| 0, new NSDictionary(), | |
| NSDictionary.FromObjectsAndKeys( | |
| new NSObject[] { _placeholderLabel }, new NSObject[] { new NSString("PlaceholderLabel") }) | |
| ); | |
| _placeholderLabel.TranslatesAutoresizingMaskIntoConstraints = false; | |
| Control.AddConstraints(hConstraints); | |
| Control.AddConstraints(vConstraints); | |
| } | |
| } | |
| } |
Scroll to the last message.
To achieve it there are two ways to do it:
1-Use the list scroll to the last message
You just have to indicate the list you want to scroll, the object to scroll (in this case the last message of the list) and if you want to animate the scrolling or not.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Device.BeginInvokeOnMainThread(() => | |
| ChatList.ScrollTo((this.BindingContext as ChatPageViewModel).Messages.Last(), ScrollToPosition.End, false) | |
| ); |
In our case as we create a content view to have our editor, we are going to use a command to indicate to the main page it has to scroll.
- Define scroll command on the chat page, so that when it gets executed scrolls list to last message:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using System; | |
| using System.Collections.Generic; | |
| using System.Linq; | |
| using System.Windows.Input; | |
| using ChatUIXForms.ViewModels; | |
| using Xamarin.Forms; | |
| namespace ChatUIXForms.Views | |
| { | |
| public partial class ChatPage : ContentPage | |
| { | |
| public ICommand ScrollListCommand { get; set; } | |
| public ChatPage() | |
| { | |
| InitializeComponent(); | |
| this.BindingContext = new ChatPageViewModel(); | |
| ScrollListCommand = new Command(() => | |
| { | |
| Device.BeginInvokeOnMainThread(() => | |
| ChatList.ScrollTo((this.BindingContext as ChatPageViewModel).Messages.Last(), ScrollToPosition.End, false) | |
| ); | |
| }); | |
| } | |
| } | |
| } |
- Execute scroll command when message is sent:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using System; | |
| using System.Collections.Generic; | |
| using ChatUIXForms.ViewModels; | |
| using Xamarin.Forms; | |
| namespace ChatUIXForms.Views.Partials | |
| { | |
| public partial class ChatInputBarView : ContentView | |
| { | |
| public ChatInputBarView() | |
| { | |
| InitializeComponent(); | |
| } | |
| public void Handle_Completed(object sender, EventArgs e) | |
| { | |
| (this.Parent.Parent.BindingContext as ChatPageViewModel).OnSendCommand.Execute(null); | |
| chatTextInput.Focus(); | |
| (this.Parent.Parent as ChatPage).ScrollListCommand.Execute(null); | |
| } | |
| } | |
| } |
The disadvantage of this approach is that each time you add new message you need to scroll to the latest message, so that you can see the last message was added. Also when loading the list of messages you will have to scroll to last message which is not a very smooth UX.
If you want to avoid this, you can use a different approach:
2-Rotate the list:
To achieve this we are going to rotate the ListView and ViewCells main layout to 180 degrees so that the list start from the bottom but by doing this the scroll bar now will appear at the left side. To fix that we are going to use the property FlowDirection (Released in the version 3.0 of Xamarin Forms).
So in the ListView we are going to set these two properties:
- FlowDirection=”RightToLeft”
- Rotation=”180″
And in the view cells main layout:
- FlowDirection=”LeftToRight”
- Rotation=”180″
Be aware that now that the ListView and ViewCells are rotated so you should consider that the bottom of your ListView is at the top, so for adding new messages we should add then at the first position because that’s the top of our ListView which is at the bottom.
We will have to change the code in our ViewModel, to add the new messages at the first position:
Messages.Insert(0, new Message() { Text = TextToSend, User = App.User });
The main advantage of this approach is that it always starts at the bottom, so you don’t need to scroll. If there are new messages and you are at the bottom you don’t need to scroll either.
Result:

Check the full source code here:
https://github.com/rdelrosario/ChatUIXForms
I decided to do a third part on this to cover even more, so see you in the third part. We will cover Jump to last message, UI improvements and moreā¦
Happy chat!
