Enhance in-note search highlight

This commit is contained in:
Stefan Niedermann 2020-03-26 11:21:49 +01:00
parent f9754f0013
commit 9a95143f4e
7 changed files with 76 additions and 33 deletions

View file

@ -24,7 +24,6 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.view.ViewCompat;
import androidx.preference.PreferenceManager;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
@ -35,12 +34,14 @@ import it.niedermann.owncloud.notes.R;
import it.niedermann.owncloud.notes.databinding.FragmentNoteEditBinding;
import it.niedermann.owncloud.notes.model.CloudNote;
import it.niedermann.owncloud.notes.model.ISyncCallback;
import it.niedermann.owncloud.notes.util.DisplayUtils;
import it.niedermann.owncloud.notes.util.MarkDownUtil;
import it.niedermann.owncloud.notes.util.NotesTextWatcher;
import it.niedermann.owncloud.notes.util.format.ContextBasedFormattingCallback;
import it.niedermann.owncloud.notes.util.format.ContextBasedRangeFormattingCallback;
import static androidx.core.view.ViewCompat.isAttachedToWindow;
import static it.niedermann.owncloud.notes.util.DisplayUtils.searchAndColor;
public class NoteEditFragment extends SearchableBaseNoteFragment {
private static final String LOG_TAG_AUTOSAVE = "AutoSave";
@ -238,11 +239,10 @@ public class NoteEditFragment extends SearchableBaseNoteFragment {
}
@Override
protected void colorWithText(String newText) {
if (binding != null && ViewCompat.isAttachedToWindow(binding.editContent)) {
binding.editContent.setText(DisplayUtils.searchAndColor(getContent(), new SpannableString
(getContent()), newText, getResources().getColor(R.color.primary)),
TextView.BufferType.SPANNABLE);
protected void colorWithText(@NonNull String newText, @Nullable Integer current) {
if (binding != null && isAttachedToWindow(binding.editContent)) {
binding.editContent.clearFocus();
binding.editContent.setText(searchAndColor(new SpannableString(getContent()), newText, requireContext(), current), TextView.BufferType.SPANNABLE);
}
}
}

View file

@ -183,10 +183,10 @@ public class NotePreviewFragment extends SearchableBaseNoteFragment implements O
}
@Override
protected void colorWithText(String newText) {
protected void colorWithText(@NonNull String newText, @Nullable Integer current) {
if (binding != null && ViewCompat.isAttachedToWindow(binding.singleNoteContent)) {
binding.singleNoteContent.setText(parseCompat(markdownProcessor, searchAndColor(getContent(), new SpannableString
(getContent()), newText, getResources().getColor(R.color.primary))),
binding.singleNoteContent.setText(
searchAndColor(new SpannableString(parseCompat(markdownProcessor, getContent())), newText, requireContext(), current),
TextView.BufferType.SPANNABLE);
}
}

View file

@ -19,7 +19,6 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.view.ViewCompat;
import androidx.preference.PreferenceManager;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
@ -31,10 +30,11 @@ import it.niedermann.owncloud.notes.android.activity.EditNoteActivity;
import it.niedermann.owncloud.notes.databinding.FragmentNotePreviewBinding;
import it.niedermann.owncloud.notes.model.ISyncCallback;
import it.niedermann.owncloud.notes.persistence.NotesDatabase;
import it.niedermann.owncloud.notes.util.DisplayUtils;
import it.niedermann.owncloud.notes.util.MarkDownUtil;
import it.niedermann.owncloud.notes.util.NoteLinksUtils;
import static androidx.core.view.ViewCompat.isAttachedToWindow;
import static it.niedermann.owncloud.notes.util.DisplayUtils.searchAndColor;
import static it.niedermann.owncloud.notes.util.MarkDownUtil.parseCompat;
public class NoteReadonlyFragment extends SearchableBaseNoteFragment {
@ -147,11 +147,9 @@ public class NoteReadonlyFragment extends SearchableBaseNoteFragment {
}
@Override
protected void colorWithText(String newText) {
if ((binding != null) && ViewCompat.isAttachedToWindow(binding.singleNoteContent)) {
binding.singleNoteContent.setText(parseCompat(markdownProcessor, DisplayUtils.searchAndColor(getContent(), new SpannableString
(getContent()), newText, getResources().getColor(R.color.primary))),
TextView.BufferType.SPANNABLE);
protected void colorWithText(@NonNull String newText, @Nullable Integer current) {
if ((binding != null) && isAttachedToWindow(binding.singleNoteContent)) {
binding.singleNoteContent.setText(searchAndColor(new SpannableString(parseCompat(markdownProcessor, getContent())), newText, requireContext(), current), TextView.BufferType.SPANNABLE);
}
}

View file

@ -68,12 +68,12 @@ public abstract class SearchableBaseNoteFragment extends BaseNoteFragment {
if (currentVisibility != oldVisibility) {
if (currentVisibility != View.VISIBLE) {
colorWithText("");
colorWithText("", null);
searchQuery = "";
hideSearchFabs();
} else {
jumpToOccurrence();
colorWithText(searchQuery);
colorWithText(searchQuery, null);
occurrenceCount = countOccurrences(getContent(), searchQuery);
showSearchFabs();
}
@ -90,6 +90,7 @@ public abstract class SearchableBaseNoteFragment extends BaseNoteFragment {
if (next != null) {
next.setOnClickListener(v -> {
currentOccurrence++;
colorWithText(searchView.getQuery().toString(), currentOccurrence);
jumpToOccurrence();
});
}
@ -97,6 +98,7 @@ public abstract class SearchableBaseNoteFragment extends BaseNoteFragment {
if (prev != null) {
prev.setOnClickListener(v -> {
currentOccurrence--;
colorWithText(searchView.getQuery().toString(), currentOccurrence);
jumpToOccurrence();
});
}
@ -120,7 +122,7 @@ public abstract class SearchableBaseNoteFragment extends BaseNoteFragment {
}
currentOccurrence = 1;
jumpToOccurrence();
colorWithText(searchQuery);
colorWithText(searchQuery, currentOccurrence);
return true;
}
});
@ -136,7 +138,7 @@ public abstract class SearchableBaseNoteFragment extends BaseNoteFragment {
}
}
protected abstract void colorWithText(String newText);
protected abstract void colorWithText(@NonNull String newText, @Nullable Integer current);
protected abstract ScrollView getScrollView();
@ -194,7 +196,7 @@ public abstract class SearchableBaseNoteFragment extends BaseNoteFragment {
int numberLine = layout.getLineForOffset(textUntilFirstOccurrence.length());
if (numberLine >= 0) {
getScrollView().smoothScrollTo(0, layout.getLineTop(numberLine));
getScrollView().post(() -> getScrollView().smoothScrollTo(0, layout.getLineTop(numberLine)));
}
}
}

View file

@ -19,29 +19,34 @@
*/
package it.niedermann.owncloud.notes.util;
import android.graphics.Typeface;
import android.content.Context;
import android.graphics.Color;
import android.text.Spannable;
import android.text.TextPaint;
import android.text.TextUtils;
import android.text.style.CharacterStyle;
import android.text.style.ForegroundColorSpan;
import android.text.style.StyleSpan;
import android.text.style.MetricAffectingSpan;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import it.niedermann.owncloud.notes.R;
public class DisplayUtils {
private DisplayUtils() {
}
public static Spannable searchAndColor(String text, Spannable spannable, String searchText, @ColorInt int color) {
public static Spannable searchAndColor(Spannable spannable, CharSequence searchText, Context context, @Nullable Integer current) {
CharSequence text = spannable.toString();
Object[] spansToRemove = spannable.getSpans(0, text.length(), Object.class);
for(Object span: spansToRemove){
if(span instanceof CharacterStyle)
for (Object span : spansToRemove) {
if (span instanceof SearchSpan)
spannable.removeSpan(span);
}
@ -49,18 +54,52 @@ public class DisplayUtils {
return spannable;
}
Matcher m = Pattern.compile(searchText, Pattern.CASE_INSENSITIVE | Pattern.LITERAL)
Matcher m = Pattern.compile(searchText.toString(), Pattern.CASE_INSENSITIVE | Pattern.LITERAL)
.matcher(text);
int i = 1;
while (m.find()) {
int start = m.start();
int end = m.end();
spannable.setSpan(new ForegroundColorSpan(color), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
spannable.setSpan(new StyleSpan(Typeface.BOLD), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
spannable.setSpan(new SearchSpan(context, (current != null && i == current)), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
i++;
}
return spannable;
}
static class SearchSpan extends MetricAffectingSpan {
private final boolean current;
private final int bgColorPrimary;
private final int bgColorSecondary;
SearchSpan(Context context, boolean current) {
this.current = current;
this.bgColorPrimary = context.getResources().getColor(R.color.bg_search_primary);
this.bgColorSecondary = context.getResources().getColor(R.color.bg_search_secondary);
}
@Override
public void updateDrawState(TextPaint tp) {
tp.bgColor = current ? bgColorPrimary : bgColorSecondary;
tp.setColor(current ? getForeground(Integer.toHexString(tp.bgColor)) : bgColorPrimary);
tp.setFakeBoldText(true);
}
@Override
public void updateMeasureState(@NonNull TextPaint tp) {
tp.setFakeBoldText(true);
}
private static @ColorInt
int getForeground(String backgroundColorHex) {
return ((float) (
0.2126 * Integer.valueOf(backgroundColorHex.substring(1, 3), 16)
+ 0.7152 * Integer.valueOf(backgroundColorHex.substring(3, 5), 16)
+ 0.0722 * Integer.valueOf(backgroundColorHex.substring(5, 7), 16)
) < 140) ? Color.WHITE : Color.BLACK;
}
}
}

View file

@ -17,6 +17,9 @@
<color name="icon_color_default">#757575</color>
<color name="bg_search_primary">@color/primary</color>
<color name="bg_search_secondary">#eee</color>
<color name="widget_background">#dfffffff</color>
<color name="widget_fg_default">#000000</color>
<color name="widget_fg_contrast">#ffffff</color>

View file

@ -8,6 +8,7 @@
<item name="windowActionModeOverlay">true</item>
<item name="android:colorBackground">@color/bg_normal</item>
</style>
<style name="toolbarStyle" parent="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
<item name="colorAccent">#fff</item>
</style>