Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automatic Scroll on Search added #988

Merged
merged 10 commits into from
May 20, 2023
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Please add your entries according to this format.
* Added ability to export transactions to a file programmatically, LOG or HAR.
* GraphQL OperationName header to transaction title [#69], [#116]
* Added support for Android 13 and notifications permission handling
* Added scroll to highlighted text search in response screen [#988]eye
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* Added scroll to highlighted text search in response screen [#988]eye
* Added scroll to highlighted text search in response screen [#988]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My bad


### Fixed

Expand Down Expand Up @@ -527,3 +528,5 @@ Initial release.
[#593]: https://github.com/ChuckerTeam/chucker/issues/593
[#653]: https://github.com/ChuckerTeam/chucker/pull/653
[#737]: https://github.com/ChuckerTeam/chucker/issues/737
[#388]: https://github.com/ChuckerTeam/chucker/issues/388
[#988]: https://github.com/ChuckerTeam/chucker/pull/988
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.chuckerteam.chucker.internal.support

import android.view.View


internal fun View.visible() {
if (this.visibility != View.VISIBLE) {
this.visibility = View.VISIBLE
}
}

internal fun View.gone() {
if (this.visibility != View.GONE) {
this.visibility = View.GONE
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ internal class TransactionBodyAdapter : RecyclerView.Adapter<TransactionPayloadV
}
}

internal fun highlightQueryWithColors(newText: String, backgroundColor: Int, foregroundColor: Int) {
internal fun highlightQueryWithColors(newText: String, backgroundColor: Int, foregroundColor: Int): List<Int> {
val listOfFoundQueryIndices = mutableListOf<Int>()
items.filterIsInstance<TransactionPayloadItem.BodyLineItem>()
.withIndex()
.forEach { (index, item) ->
Expand All @@ -76,14 +77,34 @@ internal class TransactionBodyAdapter : RecyclerView.Adapter<TransactionPayloadV
item.line
.highlightWithDefinedColors(newText, backgroundColor, foregroundColor)
notifyItemChanged(index + 1)
listOfFoundQueryIndices.add(index + 1)
} else {
// Let's clear the spans if we haven't found the query string.
val removedSpansCount = item.line.clearHighlightSpans()
if (removedSpansCount > 0) {
notifyItemChanged(index + 1)
listOfFoundQueryIndices.remove(index + 1)
}
}
}
return listOfFoundQueryIndices
}


internal fun highlightItemWithColorOnPosition(
position: Int,
queryText: String,
backgroundColor: Int,
foregroundColor: Int,
) {
val item = items.getOrNull(position) as? TransactionPayloadItem.BodyLineItem
if (item != null) {
if (item.line.contains(queryText, ignoreCase = true)) {
item.line.clearHighlightSpans()
item.line = item.line.highlightWithDefinedColors(queryText, backgroundColor, foregroundColor)
notifyItemChanged(position)
}
}
}

internal fun resetHighlight() {
Expand Down Expand Up @@ -123,7 +144,7 @@ internal sealed class TransactionPayloadViewHolder(view: View) : RecyclerView.Vi
abstract fun bind(item: TransactionPayloadItem)

internal class HeaderViewHolder(
private val headerBinding: ChuckerTransactionItemHeadersBinding
private val headerBinding: ChuckerTransactionItemHeadersBinding,
) : TransactionPayloadViewHolder(headerBinding.root) {
override fun bind(item: TransactionPayloadItem) {
if (item is TransactionPayloadItem.HeaderItem) {
Expand All @@ -133,7 +154,7 @@ internal sealed class TransactionPayloadViewHolder(view: View) : RecyclerView.Vi
}

internal class BodyLineViewHolder(
private val bodyBinding: ChuckerTransactionItemBodyLineBinding
private val bodyBinding: ChuckerTransactionItemBodyLineBinding,
) : TransactionPayloadViewHolder(bodyBinding.root) {
override fun bind(item: TransactionPayloadItem) {
if (item is TransactionPayloadItem.BodyLineItem) {
Expand All @@ -143,7 +164,7 @@ internal sealed class TransactionPayloadViewHolder(view: View) : RecyclerView.Vi
}

internal class ImageViewHolder(
private val imageBinding: ChuckerTransactionItemImageBinding
private val imageBinding: ChuckerTransactionItemImageBinding,
) : TransactionPayloadViewHolder(imageBinding.root) {

override fun bind(item: TransactionPayloadItem) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
@file:Suppress("TooManyFunctions")

package com.chuckerteam.chucker.internal.ui.transaction

import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.graphics.Color
import android.net.Uri
Expand All @@ -11,26 +14,36 @@ import android.view.Menu
import android.view.MenuInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.ImageButton
import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.widget.SearchView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
import androidx.core.text.HtmlCompat
import androidx.core.text.bold
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.withResumed
import com.chuckerteam.chucker.R
import com.chuckerteam.chucker.databinding.ChuckerFragmentTransactionPayloadBinding
import com.chuckerteam.chucker.internal.data.entity.HttpTransaction
import com.chuckerteam.chucker.internal.support.Logger
import com.chuckerteam.chucker.internal.support.calculateLuminance
import com.chuckerteam.chucker.internal.support.combineLatest
import com.chuckerteam.chucker.internal.support.gone
import com.chuckerteam.chucker.internal.support.visible
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.FileOutputStream
import java.io.IOException
import kotlin.math.abs

internal class TransactionPayloadFragment :
Fragment(), SearchView.OnQueryTextListener {
Expand Down Expand Up @@ -69,6 +82,28 @@ internal class TransactionPayloadFragment :

private var backgroundSpanColor: Int = Color.YELLOW
private var foregroundSpanColor: Int = Color.RED
private var backgroundSpanColorSearchItem: Int = Color.GREEN

private val scrollableIndices by lazy { mutableListOf<Int>() }
private var currentSearchScrollIndex = -1
private var currentSearchQuery: String = ""

private val searchViewSummaryRootLayout by lazy {
requireActivity().findViewById<ConstraintLayout>(R.id.constraintToolbar)
}

private val searchTextViewSummary by lazy {
requireActivity().findViewById<TextView>(R.id.toolbarSearchSummary)
}

private val searchNavButton by lazy {
requireActivity().findViewById<ImageButton>(R.id.toolbarSearchNavButton)
}

private val searchNavButtonUp by lazy {
requireActivity().findViewById<ImageButton>(R.id.toolbarSearchNavButtonUp)
}


override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Expand All @@ -78,7 +113,7 @@ internal class TransactionPayloadFragment :
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
savedInstanceState: Bundle?,
): View {
payloadBinding = ChuckerFragmentTransactionPayloadBinding.inflate(
inflater,
Expand Down Expand Up @@ -117,6 +152,28 @@ internal class TransactionPayloadFragment :
}
}
)
searchNavButton.setOnClickListener {
onSearchScrollerButtonClick(true)
}
searchNavButtonUp.setOnClickListener {
onSearchScrollerButtonClick(false)
}
}

private fun onSearchScrollerButtonClick(goNext: Boolean) {
// hide the keyboard if visible
val inputMethodManager = activity?.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
if (inputMethodManager.isAcceptingText) {
activity?.currentFocus?.clearFocus()
inputMethodManager.hideSoftInputFromWindow(view?.windowToken, 0)
}

val scrollToIndex =
if (goNext) ((currentSearchScrollIndex + 1) % scrollableIndices.size)
else (abs(currentSearchScrollIndex - 1 + scrollableIndices.size) % scrollableIndices.size)

scrollToSearchedItemPosition(scrollToIndex)

}

private fun showEmptyState() {
Expand Down Expand Up @@ -200,22 +257,92 @@ internal class TransactionPayloadFragment :
override fun onQueryTextSubmit(query: String): Boolean = false

override fun onQueryTextChange(newText: String): Boolean {
var listOfScrollableIndex = listOf<Int>()
if (newText.isNotBlank() && newText.length > NUMBER_OF_IGNORED_SYMBOLS) {
payloadAdapter.highlightQueryWithColors(
listOfScrollableIndex = payloadAdapter.highlightQueryWithColors(
newText,
backgroundSpanColor,
foregroundSpanColor
)
} else {
payloadAdapter.resetHighlight()
}

scrollableIndices.clear()
4shutosh marked this conversation as resolved.
Show resolved Hide resolved
when {
listOfScrollableIndex.isEmpty() -> {
currentSearchScrollIndex = -1
makeToolbarSearchSummaryVisible(false)
}
else -> {
makeToolbarSearchSummaryVisible(true)
scrollableIndices.addAll(listOfScrollableIndex)
updateToolbarText(newText, listOfScrollableIndex.size, 1)
}
}

lifecycleScope.launch {
delay(DELAY_FOR_SEARCH_SCROLL)
lifecycle.withResumed {
if (scrollableIndices.isNotEmpty()) scrollToSearchedItemPosition(0)
else {
currentSearchScrollIndex = -1
scrollableIndices.clear()
}
}
}
return true
}

private fun makeToolbarSearchSummaryVisible(visible: Boolean = true) {
with(searchViewSummaryRootLayout) {
if (visible) visible() else gone()
}
}

private fun updateToolbarText(searchQuery: String, searchResultsCount: Int, currentIndex: Int = 1) {
currentSearchQuery = searchQuery
searchTextViewSummary.text = SpannableStringBuilder().apply {
append(getString(R.string.chucker_search_results_title))
bold {
append(" $searchQuery ")
append("$currentIndex/$searchResultsCount")
}
}
}

private fun scrollToSearchedItemPosition(positionOfScrollableIndices: Int) {
// reset the last searched item highlight if done
scrollableIndices.getOrNull(currentSearchScrollIndex)?.let {
payloadAdapter.highlightItemWithColorOnPosition(
it,
currentSearchQuery,
backgroundSpanColor,
foregroundSpanColor
)
}

currentSearchScrollIndex = positionOfScrollableIndices
val scrollTo = scrollableIndices.getOrNull(positionOfScrollableIndices)
if (scrollTo != null) {
// highlight the next navigated item and update toolbar summary text
payloadAdapter.highlightItemWithColorOnPosition(
scrollTo,
currentSearchQuery,
backgroundSpanColorSearchItem,
foregroundSpanColor
)
updateToolbarText(currentSearchQuery, scrollableIndices.size, positionOfScrollableIndices + 1)

payloadBinding.payloadRecyclerView.smoothScrollToPosition(scrollTo)
currentSearchScrollIndex = positionOfScrollableIndices
}
}

private suspend fun processPayload(
type: PayloadType,
transaction: HttpTransaction,
formatRequestBody: Boolean
formatRequestBody: Boolean,
): MutableList<TransactionPayloadItem> {
return withContext(Dispatchers.Default) {
val result = mutableListOf<TransactionPayloadItem>()
Expand Down Expand Up @@ -267,9 +394,12 @@ internal class TransactionPayloadFragment :
result.add(TransactionPayloadItem.BodyLineItem(SpannableStringBuilder.valueOf(text)))
}
else -> bodyString.lines().forEach {
result.add(TransactionPayloadItem.BodyLineItem(
result.add(
TransactionPayloadItem.BodyLineItem(
if (it is SpannableStringBuilder) it
else SpannableStringBuilder.valueOf(it)))
else SpannableStringBuilder.valueOf(it)
)
)
}
}
return@withContext result
Expand Down Expand Up @@ -304,6 +434,7 @@ internal class TransactionPayloadFragment :
companion object {
private const val ARG_TYPE = "type"
private const val TRANSACTION_EXCEPTION = "Transaction not ready"
private const val DELAY_FOR_SEARCH_SCROLL: Long = 600L

private const val NUMBER_OF_IGNORED_SYMBOLS = 1

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@android:color/darker_gray" android:state_enabled="false" />
<item android:color="@color/chucker_color_primary" />
</selector>
10 changes: 10 additions & 0 deletions library/src/main/res/drawable/chucker_ic_arrow_down.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/chucker_color_on_primary"
android:pathData="M7.41,8.59L12,13.17l4.59,-4.58L18,10l-6,6 -6,-6 1.41,-1.41z" />
</vector>
Loading