Performance: how to display huge texts smoothly using Recyclerview - performance

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?

Related

Performance problem while making a rich text editor in xamarin

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();

Layout inflater with DialogFragment

I'm trying to inflate my custom layout (a dialog fragment).
I have this in my function showDialog()
val inflatedView = layoutInflater.inflate(R.layout.alerts_dialog_remi, null)
mydialog = Dialog(this, R.style.DialogCustomTheme)
mydialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
mydialog.setContentView(R.layout.alerts_dialog_remi)
mydialog.setOnShowListener {
val text = inflatedView.findViewById<TextView>(R.id.alerte_title)
val lp = LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT)
text.setText("Text")}
mydialog.create()
txt = mydialog.findViewById(R.id.close_modal_alerte)
txt.isEnabled = true
txt.setOnClickListener{
mydialog.cancel()
}
mydialog.show()
}
But I don't see the "Text" string in my dialog fragment. I tried the put the inflatedView inside setOnShowListener, but it doesn't do anything either.
You don't need to inflate your view, because dialog.setContentView does just that for you.
What you need is to get the inflated view from inside your lambda.
Like this:
mydialog.setOnShowListener {
val text = it.view.findViewById<TextView>(R.id.alerte_title)

CharmListView Infinite Scroll

I need basically an event that triggers at each 200 records loaded, so more data can be loaded until the end of data.
I tried to extend CharmListCell and using the method updateItem like this:
#Override
public void updateItem(Model item, boolean empty) {
super.updateItem(item, empty);
currentItem = item;
if (!empty && item != null) {
update();
setGraphic(slidingTile);
} else {
setGraphic(null);
}
System.out.println(getIndex());
}
But the System.out.println(getIndex()); method returns -1;
I would like to call my backend method when the scroll down gets the end of last fetched block and so on, until get the end of data like the "infinite scroll" technique.
Thanks!
The CharmListCell doesn't expose the index of the underlying listView, but even if it did, that wouldn't be of much help to find out if you are scrolling over the end of the current list or not.
I'd suggest a different approach, which is also valid for a regular ListView, with the advantage of having the CharmListView features (mainly headers and the refresh indicator).
This short sample, created with a single view project using the Gluon IDE plugin and Charm 5.0.0, shows how to create a CharmListView control, and fill it with 30 items at a time. I haven't provided a factory cell, nor the headers, and for the sake of simplicity I'm just adding consecutive integers.
With a lookup, and after the view is shown (so the listView is added to the scene) we find the vertical ScrollBar of the listView, and then we add a listener to track its position. When it gets closer to 1, we simulate the load of another batch of items, with a pause transition that represents a heavy task.
Note the use of the refresh indicator. When new data is added, we scroll back to the first of the new items, so we can keep scrolling again.
public class BasicView extends View {
private final ObservableList<Integer> data;
private CharmListView<Integer, Integer> listView;
private final int batchSize = 30;
private PauseTransition pause;
public BasicView() {
data = FXCollections.observableArrayList();
listView = new CharmListView<>(data);
setOnShown(e -> {
ScrollBar scrollBar = null;
for (Node bar : listView.lookupAll(".scroll-bar")) {
if (bar instanceof ScrollBar && ((ScrollBar) bar).getOrientation().equals(Orientation.VERTICAL)) {
scrollBar = (ScrollBar) bar;
break;
}
}
if (scrollBar != null) {
scrollBar.valueProperty().addListener((obs, ov, nv) -> {
if (nv.doubleValue() > 0.95) {
addBatch();
}
});
addBatch();
}
});
setCenter(new VBox(listView));
}
private void addBatch() {
listView.setRefreshIndicatorVisible(true);
if (pause == null) {
pause = new PauseTransition(Duration.seconds(1));
pause.setOnFinished(f -> {
int size = data.size();
List<Integer> list = new ArrayList<>();
for (int i = size; i < size + batchSize; i++) {
list.add(i);
}
data.addAll(list);
listView.scrollTo(list.get(0));
listView.setRefreshIndicatorVisible(false);
});
} else {
pause.stop();
}
pause.playFromStart();
}
}
Note also that you could benefit from the setOnPullToRefresh() method, at any time. For instance, if you add this:
listView.setOnPullToRefresh(e -> addBatch());
whenever you go to the top of the list and drag it down (on a mobile device), it will make another call to load a new batch of items. Obviously, this is the opposite behavior as the "infinite scrolling", but it is possible as well with the CharmListView control.

Xamarin.Android Couchbase.Lite Map Reduce

I simply want to create a View that uses Map-Reduce to do this: Say I have Documents for the Automobile Industry. I would like the user to query for a particular Make - say Ford for example. I would like the user to provide the Ford value via an EditText, Tap a Button, and the "Count" be shown in a TextView. So, to clarify, I want to do a count of a certain type of Document using Map-Reduce. I have searched for over 100 hundred hours on this and have not found not one single example - REAL example I mean. (I have read all the docs, only generic examples - no actual examples)
I am an experienced programmer 15+ yrs exp - all I need is one example, and I am good to go.
Can someone please assist me with this?
Thanks,
Don
Here is my Actual Code:
string lMS = "MS:5"; // just to show what type of value I am using
var msCount = dbase.GetView ("count_ms");
msCount.SetMapReduce ((doc, emit) => {
if (doc.ContainsKey ("DT") && doc["DT"].Equals ("P")) {
if (doc.ContainsKey ("MS") && doc["MS"].Equals (_ms))
{
emit (doc ["id"], 1);
}
}
},
(keys, values, rereduce) => values.ToList().Count, "1");
var mscView = dbase.GetView ("count_ms");
var query = mscView.CreateQuery ();
query.StartKey = "MS:1";
query.EndKey = "MS:9999";
var queryResults = query.Run ();
var nr = queryResults.Count; // shows a value of 1 - wrong - should be 40
// the line below is to allow me to put a stop statement to read line above
var dummyForStop = nr;
Try setting something like
var docsByMakeCount = _database.GetView("docs_by_make_count");
docsByMakeCount.SetMapReduce((doc, emit) =>
{
if (doc.ContainsKey("Make"))
{
emit(doc["Make"], doc);
}
},
(keys, values, rereduce) => values.ToList().Count
, "1");
when you create your view.
and when you use it:
var docsByMake = _database.GetView("docs_by_make_count");
var query = docsByCity.CreateQuery();
query.StartKey = Make;
query.EndKey = Make;
var queryResults = query.Run();
MessageBox.Show(string.Format("{0} documents has been retrieved for that query", queryResults.Count));
if (queryResults.Count == 0) return;
var documents = queryResults.Select(result => JsonConvert.SerializeObject(result.Value, Formatting.Indented)).ToArray();
var commaSeperaterdDocs = "[" + string.Join(",", documents) + "]";
DocumentText = commaSeperaterdDocs;
In my case Make and DocumentText are properties.
There are some optimizations to be made here, like rereducing, but it's the straight forward way.

Determining object types in Qt

I have a series of QTextEdits and QLineEdits connected to a slot through a QSignalMapper(which emits a textChanged(QWidget*) signal). When the connected slot is called (pasted below), I need to be able to differentiate between the two so I know whether to call the text() or toPlainText() function. What's the easiest way to determine the subclass type of a QWidget?
void MainWindow::changed(QWidget *sender)
{
QTextEdit *temp = qobject_cast<QTextEdit *>(sender);
QString currentText = temp->toPlainText(); // or temp->text() if its
// a QLineEdit...
if(currentText.compare(""))
{
...
}
else
{
...
}
}
I was considering using try-catch but Qt doesn't seem to have very extensive support for Exceptions... Any ideas?
Actually, your solution is already almost there. In fact, qobject_cast will return NULL if it can't perform the cast. So try it on one of the classes, if it's NULL, try it on the other:
QString text;
QTextEdit *textEdit = qobject_cast<QTextEdit*>(sender);
QLineEdit *lineEdit = qobject_cast<QLineEdit*>(sender);
if (textEdit) {
text = textEdit->toPlainText();
} else if (lineEdit) {
text = lineEdit->text();
} else {
// Return an error
}
You can also use sender->metaObject()->className() so you won't make unnecesary casts. Specially if you have a lot of classes to test. The code will be like this:
QString text;
QString senderClass = sender->metaObject()->className();
if (senderClass == "QTextEdit") {
QTextEdit *textEdit = qobject_cast<QTextEdit*>(sender);
text = textEdit->toPlainText();
} else if (senderClass == "QLineEdit") {
QLineEdit *lineEdit = qobject_cast<QLineEdit*>(sender);
text = lineEdit->text();
} else {
// Return an error
}
I know is an old question but I leave this answer just in case it would be useful for somebody...

Resources