Performance problem while making a rich text editor in xamarin - performance

We want to make a rich (but not too rich) text editor with xamarin such that it is possible to:
Highlight some words
Position the cursor anywhere with the mouse or touch
Show the position of the cursor
Our attempt is using a Label to display text, and when the user click on the label in reality a hidden Entry is focused. To reposition the cursor we split the FormattedString in more Span, one for each character, so we know which is tapped or clicked.
The problem now is performance, as the content grows the time of updating the view grows a lot. For example just for adding one character can take 3000 ms if the content length is around 30 character. By the way it takes just 10 ms if the content length is little like 1-5 characters. The time to update the label is not linear on the length of text.
Here I put the code where the performance problem occur, and I want ask you if there is a problem in my code or maybe I can't do this in xamarin in this way.
Stopwatch stopwatch = Stopwatch.StartNew();
string newText = e.NewTextValue;
string oldText = (e.OldTextValue ?? string.Empty);
List<string> newParts = newText.Split('#').ToList();
List<string> oldParts = oldText.Split('#').ToList();
// ...
foreach (string part in newParts)
{
string placeholderCode = $"#{part}#";
string textToShow = isPlaceholder ? placeholders[placeholderCode] : part;
if (isPlaceholder)
{
editorPosition += placeholderCode.Length;
}
Span tmpSpan = new Span();
foreach (char character in textToShow)
{
Span item = viewer.FormattedText.Spans.ElementAtOrDefault(currentSpan++);
if (item == null)
{
item = new Span();
TapGestureRecognizer gestureRecognizer = new TapGestureRecognizer();
gestureRecognizer.Tapped += delegate {
editor.CursorPosition = spanToEditorPosition[item];
FocusEditor();
};
item.GestureRecognizers.Add(gestureRecognizer);
viewer.FormattedText.Spans.Add(item);
}
if (isPlaceholder)
{
tmpSpan.FontAttributes = FontAttributes.Bold;
}
else
{
tmpSpan.FontAttributes = FontAttributes.None;
editorPosition++;
}
tmpSpan.Text = character.ToString();
if (tmpSpan.Text != item.Text || tmpSpan.FontAttributes != item.FontAttributes)
{
item.Text = tmpSpan.Text;
item.FontAttributes = tmpSpan.FontAttributes;
}
spanToEditorPosition.Add(item, editorPosition);
}
isPlaceholder = !isPlaceholder;
}
stopwatch.Stop();

Related

Performance: how to display huge texts smoothly using Recyclerview

I'm building a note application that must be able to display long texts (10000+ characters)
According to Best practices for text on Android (Google I/O '18), I followed some tips to achieve that:
Break huge texts into parts and display them using recyclerview
Prefetch Text with PrecomputedTextCompat
My code is looking like this:
inner class DescriptionViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val textView: AppCompatTextView = itemView.adapter_description_text
fun bindView(descriptionModel: NoteUIModel.DescriptionModel) {
textView.setTextColor(displayTextColor)
textView.setTextFuture(PrecomputedTextCompat.getTextFuture(
descriptionModel.descriptionCell.description,
TextViewCompat.getTextMetricsParams(textView),
null
))
}
}
with (note_details_recycler) {
layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false)
adapter = mAdapter
}
viewModel.viewModelScope.launch {
mAdapter.submitContent(NoteUIModel.fromNote(note))
}
The problem:
companion object {
suspend fun fromNote(note: Note): ArrayList<NoteUIModel> = withContext(Dispatchers.Default) {
val list: ArrayList<NoteUIModel> = arrayListOf()
val words = note.description.split(" ", ignoreCase = true)
val paragraphs = words.chunked(30)
paragraphs.forEach {
list.add(
DescriptionModel(
DescriptionCell(
it.joinToString().replace(",", "")
)
)
)
}
list.add(DateModel(DateCell(note.timestamp)))
list.add(TagsModel(TagsCell(note.tags)))
list
}
}
I'll need an algorithm like that to break the text preventing words from being divided into 2 views.
I'm still having delay and a little bit of stuttering to display the texts
The question is:
Is that still worth to have an algorithm making operation across the entire text anyway?
There is a cheaper way to break the text?

How can I replace an image in Google Documents?

I'm trying to insert images into Google Docs (other GSuite apps later) from an Add-On. I've succeeded in fetching the image and inserting it when getCursor() returns a valid Position. When there is a selection (instead of a Cursor), I can succeed if it's text that's selected by walking up to the Parent of the selected text and inserting the image at the start of the paragraph (not perfect, but OK).
UPDATE: It seems that I was using a deprecated method (getSelectedElements()), but that didn't fix the issue. It seems the issue is only with wrapped images as well (I didn't realize that the type of the object changed when you changed it to a wrapped text).
However, when an wrapped-text Image (presumably a PositionedImage) is highlighted (with the rotate and resize handles visible in blue), both getSelection() and getCursor() return null. This is a problem as I would like to be able to get that image and replace it with the one I'm inserting.
Here's my code... any help would be great.
var response = UrlFetchApp.fetch(imageTokenURL);
var selection = DocumentApp.getActiveDocument().getSelection();
if (selection)
{
Logger.log("Got Selection");
var replaced = false;
var elements = selection.getRangeElements();
if (elements.length === 1
&& elements[0].getElement().getType() === DocumentApp.ElementType.INLINE_IMAGE)
{
//replace the URL -- this never happens
}
//otherwise, we take the first element and work from there:
var firstElem = elements[0].getElement();
Logger.log("First Element Type = " + firstElem.getType());
if (firstElem.getType() == DocumentApp.ElementType.PARAGRAPH)
{
var newImage = firstElem.asParagraph().insertInlineImage(0, response);
newImage.setHeight(200);
newImage.setWidth(200);
}
else if (firstElem.getType() == DocumentApp.ElementType.TEXT)
{
var p = firstElem.getParent();
if (p.getType() == DocumentApp.ElementType.PARAGRAPH)
{
var index = p.asParagraph().getChildIndex(firstElem);
var newImage = p.asParagraph().insertInlineImage(index, response);
newImage.setHeight(200);
newImage.setWidth(200);
}
}
} else {
Logger.log("Checking Cursor");
var cursor = DocumentApp.getActiveDocument().getCursor();
if (cursor)
{
Logger.log("Got Cursor: " + cursor);
var newImage = cursor.insertInlineImage(response);
var p = cursor.getElement();
var size=200;
newImage.setHeight(size);
newImage.setWidth(size);
}
}
You are using the deprecated 'getSelectedElements()' method of the Range class. You may notice it's crossed out in the autocomplete selection box.
Instead, use the 'getRangeElements()' method. After selecting the image in the doc, the code below worked for me:
var range = doc.getSelection();
var element = range.getRangeElements()[0].getElement();
Logger.log(element.getType() == DocumentApp.ElementType.INLINE_IMAGE); //logs 'true'

Xamarin Forms complexe clickable label

I'm trying to create a string that have classic work & linked word.
I have this sentence :
I accept the terms of use and the privacy policy
In this sentence I want to have the words in bold clickable.
I already tried with an Horizontal StackLayout, Labels and Buttons but the result do not be like a simple sentence...
Any idea ?
Use a TapGestureRecognizer on a label:
var terms = new Label() { Text = "terms of use" };
var termsTapped = new TapGestureRecognizer();
termsTapped.Tapped += (o,e) =>
{
//do something
};
terms.GestureRecognizers.Add(termsTapped);
var stack = new StackLayout()
{
Orientation = StackOrientation.Horizontal
};
stack.Children.Add(new Label() { Text = "I accept the " });
stack.Children.Add(terms);
... and the same with your privacy policy.
EDIT:
If you want use this functionallity in a single lable, you could implement your own Span which is clickable and work with FormattedString.

Xamarin children not being added

I have this annoying problem and I don't know how to solve this.
In Xamarin Forms, I'm trying to draw a dynamic layout, for this I load a list of elements (this works). Now i'm trying to display the label for it, so I loop through all the items and add a label for every item. The problem is that the page stays empty.
Yes I initialized the _layout variable as a StackLayout and I also made a ScrollView, then I set the scrollview's content to the _layout variable. But still my page stays empty. I can't share the actual code but I rewrote it using different names.
private void DrawItems()
{
var items = (List<Item>)_database.GetItems();
foreach(var item in items)
{
DrawItem(item);
}
}
private void DrawItem(Item item)
{
AddLabel(item);
}
private void AddLabel(Item item)
{
if (string.IsNullOrEmpty(item.Text)) return;
var label = new Label
{
Text = (!string.IsNullOrEmpty(item.Number)) ? string.Format("{0}: {1}", item.Number, item.Text) : item.Text,
FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label))
};
_layout.Children.Add(label);
}
For some weird reason, when I start debugging (put a break on var label ...) the label get's created but when I put a break on _layout.Children.Add(label), this never gets called.
When changing UI elements, you need to do it on the main thread.
Try this:
Device.BeginInvokeOnMainThread(() =>
{
_layout.Children.Add(label);
}
I've tried the following code, and it works for me. I'm using Xamarin.Forms 1.4.0.6341
public class MyPage : ContentPage
{
StackLayout stack;
public MyPage()
{
var scroll = new ScrollView();
stack = new StackLayout();
var btn = new Button {Text = "Add Label"};
btn.Clicked += (sender, args) => stack.Children.Add(new Label {Text = "Test"});
stack.Children.Add(btn);
AddLabelEverySecond();
scroll.Content = stack;
Content = scroll;
}
private async void AddLabelEverySecond()
{
for (var i = 0; i < 10; i++)
{
await Task.Delay(1000);
stack.Children.Add(new Label { Text = "1 second" });
}
}
}
Is this applicable to your code? Maybe you could tell us where DrawItems is called from?

Android: How to end animation of a listitem using valueanimator?

I am using the ValueAnimator to make one row in my list pulse from dark blue to light blue finitely. However, I need to check for a boolean when the rows load and when it gets set to false I need the view to go back to its original non-pulsing state. What is the best way of doing this?
My code is as follows -
if(isHighlighted(post)) {
String titleText = title.getText().toString();
title.setText(titleText);
title.setTextColor(Color.WHITE);
timeStamp.setTextColor(Color.WHITE);
highLighterStartColor = resources.getColor( R.color.active_blue );
highLighterEndColor = resources.getColor( R.color.teal );
ValueAnimator va = ObjectAnimator.ofInt(view, "backgroundColor", highLighterStartColor, highLighterEndColor);
if(va != null) {
va.setDuration(750);
va.setEvaluator(new ArgbEvaluator());
va.setRepeatCount(ValueAnimator.INFINITE);
va.setRepeatMode(ValueAnimator.REVERSE);
va.start();
}
} else {
title.setTextAppearance(activity.getApplicationContext(), R.style.medium_text);
timeStamp.setTextAppearance(activity.getApplicationContext(), R.style.timestamp_text);
}

Resources