Skip to content

Commit 3f79828

Browse files
committed
Add support for search occurrences count and search navigation
1 parent 42998df commit 3f79828

File tree

6 files changed

+133
-11
lines changed

6 files changed

+133
-11
lines changed

pluto-plugins/base/lib/src/main/java/com/pluto/utilities/spannable/SpanUtil.kt

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,15 @@ class Builder(val context: Context) {
5555
is String -> SpannableString(s).apply {
5656
setSpan(o, 0, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
5757
}
58+
5859
is SpannableStringBuilder -> s.apply {
5960
setSpan(o, 0, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
6061
}
62+
6163
is SpannableString -> s.apply {
6264
setSpan(o, 0, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
6365
}
66+
6467
else -> throw IllegalArgumentException("unhandled type $o")
6568
}
6669

@@ -92,14 +95,20 @@ class Builder(val context: Context) {
9295

9396
fun highlight(span: CharSequence, search: String?): CharSequence {
9497
if (search.isNullOrEmpty()) return span
95-
val normalizedText = Normalizer.normalize(span, Normalizer.Form.NFD)
96-
.replace("\\p{InCombiningDiacriticalMarks}+".toRegex(), "")
97-
.lowercase()
98+
val normalizedText = span.normalise().lowercase()
9899

99100
val startIndexes = normalizedText.allOccurrences(search)
100101
if (startIndexes.isNotEmpty()) {
102+
return highlight(span, search, startIndexes)
103+
}
104+
return span
105+
}
106+
107+
fun highlight(span: CharSequence, search: String?, indexes: List<Int>): CharSequence {
108+
if (search.isNullOrEmpty()) return span
109+
if (indexes.isNotEmpty()) {
101110
val highlighted: Spannable = SpannableString(span)
102-
startIndexes.forEach {
111+
indexes.forEach {
103112
highlighted.setSpan(
104113
BackgroundColorSpan(context.color(R.color.pluto___text_highlight)),
105114
it,
@@ -112,6 +121,12 @@ class Builder(val context: Context) {
112121
return span
113122
}
114123

124+
fun occurrences(span: CharSequence, search: String?): List<Int> {
125+
if (search.isNullOrEmpty()) return emptyList()
126+
val normalizedText = span.normalise().lowercase()
127+
return normalizedText.allOccurrences(search)
128+
}
129+
115130
fun clickable(span: CharSequence, listener: ClickableSpan): CharSequence {
116131
return span(span, listener)
117132
}
@@ -124,6 +139,11 @@ class Builder(val context: Context) {
124139
return span(span, StyleSpan(Typeface.ITALIC))
125140
}
126141

142+
private fun CharSequence.normalise(): String {
143+
return Normalizer.normalize(this, Normalizer.Form.NFD)
144+
.replace("[^\\p{ASCII}]".toRegex(), "")
145+
}
146+
127147
fun build(): CharSequence {
128148
return spanBuilder
129149
}

pluto-plugins/plugins/network/core/lib/src/main/java/com/pluto/plugins/network/internal/interceptor/ui/ContentFragment.kt

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ internal class ContentFragment : Fragment(R.layout.pluto_network___fragment_cont
2929
private val argumentData: ContentFormatterData?
3030
get() = arguments?.getParcelable(DATA)
3131

32+
private var currentHighlightIndex = 0
33+
private var occurrences = emptyList<Int>()
34+
3235
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
3336
super.onViewCreated(view, savedInstanceState)
3437
onBackPressed { handleBackPress() }
@@ -50,20 +53,47 @@ internal class ContentFragment : Fragment(R.layout.pluto_network___fragment_cont
5053
binding.editSearch.doOnTextChanged { text, _, _, _ ->
5154
viewLifecycleOwner.lifecycleScope.launchWhenResumed {
5255
text?.toString()?.let { search ->
56+
currentHighlightIndex = 0
5357
argumentData?.let {
5458
binding.content.setSpan {
55-
append(highlight(it.content, search.trim()))
59+
occurrences = occurrences(it.content, search.trim())
60+
append(highlight(it.content, search.trim(), occurrences))
5661
append("\n")
62+
binding.searchCount.visibility = if (search.isEmpty()) View.GONE else VISIBLE
63+
binding.searchCount.text = occurrences.size.toString()
64+
val highlightsVisibility = if (occurrences.size < 2) View.GONE else VISIBLE
65+
binding.previousHighlight.visibility = highlightsVisibility
66+
binding.nextHighlight.visibility = highlightsVisibility
5767
}
5868
}
5969

60-
scrollToText(search.trim())
70+
scrollToText(currentHighlightIndex, search.trim())
6171
}
6272
}
6373
}
74+
75+
binding.previousHighlight.setOnDebounceClickListener {
76+
if (occurrences.isNotEmpty()) {
77+
currentHighlightIndex = (currentHighlightIndex - 1 + occurrences.size) % occurrences.size
78+
scrollToText(occurrences[currentHighlightIndex], binding.editSearch.text.toString())
79+
}
80+
}
81+
82+
binding.nextHighlight.setOnDebounceClickListener {
83+
if (occurrences.isNotEmpty()) {
84+
currentHighlightIndex = (currentHighlightIndex + 1) % occurrences.size
85+
scrollToText(occurrences[currentHighlightIndex], binding.editSearch.text.toString())
86+
}
87+
}
88+
6489
binding.share.setOnDebounceClickListener {
6590
argumentData?.let {
66-
contentSharer.share(Shareable(title = "Share content", content = it.content.toString()))
91+
contentSharer.share(
92+
Shareable(
93+
title = "Share content",
94+
content = it.content.toString()
95+
)
96+
)
6797
}
6898
}
6999
argumentData?.let {
@@ -91,13 +121,13 @@ internal class ContentFragment : Fragment(R.layout.pluto_network___fragment_cont
91121
/**
92122
* helps to auto scroll to target search
93123
*/
94-
private fun scrollToText(targetText: String) {
124+
private fun scrollToText(startIndex: Int, targetText: String) {
95125
if (targetText.isEmpty()) {
96126
return
97127
}
98128

99129
val contentText = binding.content.getText().toString().lowercase()
100-
val index = contentText.indexOf(targetText.lowercase())
130+
val index = contentText.indexOf(targetText.lowercase(), startIndex)
101131

102132
if (index != -1) {
103133
binding.content.post {
@@ -108,7 +138,7 @@ internal class ContentFragment : Fragment(R.layout.pluto_network___fragment_cont
108138
val y = layout.getLineTop(lineNumber)
109139

110140
binding.horizontalScroll.smoothScrollTo(x / 2, 0)
111-
binding.contentNestedScrollView.smoothScrollTo(0, y / 2)
141+
binding.contentNestedScrollView.smoothScrollTo(0, y)
112142
}
113143
}
114144
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
2+
android:width="24dp"
3+
android:height="24dp"
4+
android:viewportWidth="24"
5+
android:viewportHeight="24">
6+
7+
<path
8+
android:fillColor="@color/pluto___dark_80"
9+
android:pathData="M7.41,8.59L12,13.17l4.59,-4.58L18,10l-6,6 -6,-6 1.41,-1.41z" />
10+
11+
</vector>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
2+
android:width="24dp"
3+
android:height="24dp"
4+
android:viewportWidth="24"
5+
android:viewportHeight="24">
6+
7+
<path
8+
android:fillColor="@color/pluto___dark_80"
9+
android:pathData="M7.41,15.41L12,10.83l4.59,4.58L18,14l-6,-6 -6,6z" />
10+
11+
</vector>

pluto-plugins/plugins/network/core/lib/src/main/res/layout/pluto_network___fragment_content.xml

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,10 +154,57 @@
154154
android:textColorHint="@color/pluto___text_dark_40"
155155
android:textSize="@dimen/pluto___text_small"
156156
app:layout_constraintBottom_toBottomOf="parent"
157-
app:layout_constraintEnd_toStartOf="@+id/clearSearch"
157+
app:layout_constraintEnd_toStartOf="@+id/nextHighlight"
158158
app:layout_constraintStart_toEndOf="@+id/closeSearch"
159159
app:layout_constraintTop_toTopOf="parent" />
160160

161+
<ImageView
162+
android:id="@+id/nextHighlight"
163+
android:layout_width="wrap_content"
164+
android:layout_height="0dp"
165+
android:contentDescription="@string/pluto_network___search_next_highlight"
166+
android:foreground="?android:attr/selectableItemBackground"
167+
android:paddingHorizontal="@dimen/pluto___margin_mini"
168+
android:src="@drawable/pluto_network___ic_arrow_down"
169+
android:visibility="gone"
170+
app:layout_constraintBottom_toBottomOf="parent"
171+
app:layout_constraintEnd_toStartOf="@id/previousHighlight"
172+
app:layout_constraintStart_toEndOf="@id/editSearch"
173+
app:layout_constraintTop_toTopOf="parent"
174+
tools:visibility="visible" />
175+
176+
<ImageView
177+
android:id="@+id/previousHighlight"
178+
android:layout_width="wrap_content"
179+
android:layout_height="0dp"
180+
android:contentDescription="@string/pluto_network___search_previous_highlight"
181+
android:foreground="?android:attr/selectableItemBackground"
182+
android:paddingHorizontal="@dimen/pluto___margin_mini"
183+
android:src="@drawable/pluto_network___ic_arrow_up"
184+
android:visibility="gone"
185+
app:layout_constraintBottom_toBottomOf="parent"
186+
app:layout_constraintEnd_toStartOf="@id/searchCount"
187+
app:layout_constraintStart_toEndOf="@id/nextHighlight"
188+
app:layout_constraintTop_toTopOf="parent"
189+
tools:visibility="visible" />
190+
191+
<TextView
192+
android:id="@+id/searchCount"
193+
android:layout_width="wrap_content"
194+
android:layout_height="0dp"
195+
android:fontFamily="@font/muli_semibold"
196+
android:gravity="center"
197+
android:paddingHorizontal="@dimen/pluto___margin_xsmall"
198+
android:textColor="@color/pluto___text_dark_40"
199+
android:textSize="@dimen/pluto___text_small"
200+
android:visibility="gone"
201+
app:layout_constraintBottom_toBottomOf="parent"
202+
app:layout_constraintEnd_toStartOf="@id/clearSearch"
203+
app:layout_constraintStart_toEndOf="@id/previousHighlight"
204+
app:layout_constraintTop_toTopOf="parent"
205+
tools:text="10"
206+
tools:visibility="visible" />
207+
161208
<ImageView
162209
android:id="@+id/clearSearch"
163210
android:layout_width="wrap_content"
@@ -167,6 +214,7 @@
167214
android:paddingHorizontal="@dimen/pluto___margin_xsmall"
168215
android:src="@drawable/pluto_network___ic_close_gray"
169216
app:layout_constraintBottom_toBottomOf="parent"
217+
app:layout_constraintStart_toEndOf="@id/searchCount"
170218
app:layout_constraintEnd_toEndOf="parent"
171219
app:layout_constraintTop_toTopOf="parent" />
172220

pluto-plugins/plugins/network/core/lib/src/main/res/values/strings.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,4 +94,6 @@
9494
<item quantity="one">1 items</item>
9595
<item quantity="other">%d items</item>
9696
</plurals>
97+
<string name="pluto_network___search_next_highlight">Next highlight</string>
98+
<string name="pluto_network___search_previous_highlight">Previous highlight</string>
9799
</resources>

0 commit comments

Comments
 (0)