Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,20 @@ package org.cryptomator.presentation.ui.fragment

import android.os.Bundle
import android.text.Spannable
import android.text.TextWatcher
import android.text.style.BackgroundColorSpan
import android.view.View
import androidx.annotation.NonNull
import androidx.core.content.ContextCompat
import androidx.core.widget.doAfterTextChanged
import com.google.android.material.textfield.TextInputEditText
import org.cryptomator.generator.Fragment
import org.cryptomator.presentation.R
import org.cryptomator.presentation.databinding.FragmentTextEditorBinding
import org.cryptomator.presentation.presenter.TextEditorPresenter
import org.cryptomator.presentation.ui.layout.applySystemBarsMargins
import org.cryptomator.presentation.ui.layout.applySystemBarsPadding
import org.cryptomator.presentation.ui.layout.attachFastScrollThumb
import javax.inject.Inject

@Fragment
Expand All @@ -20,6 +24,9 @@ class TextEditorFragment : BaseFragment<FragmentTextEditorBinding>(FragmentTextE
@Inject
lateinit var textEditorPresenter: TextEditorPresenter

private var fastScrollCleanup: (() -> Unit)? = null
private var caretAutoScrollWatcher: TextWatcher? = null

val textFileContent: String
get() = binding.textEditor.text.toString()

Expand Down Expand Up @@ -103,7 +110,7 @@ class TextEditorFragment : BaseFragment<FragmentTextEditorBinding>(FragmentTextE
textEditorPresenter.lastFilterLocation = index

binding.textEditor.setSelection(index, index + it.length)
binding.textEditor.post { binding.textEditor.bringPointIntoView(index) }
binding.textEditor.post { scrollCaretIntoView() }
}
}

Expand All @@ -117,7 +124,39 @@ class TextEditorFragment : BaseFragment<FragmentTextEditorBinding>(FragmentTextE

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.textEditor.applySystemBarsPadding(left = true, right = true, bottom = true)
binding.textViewWrapper.applySystemBarsPadding(left = true, right = true, bottom = true)
binding.scrollThumb.applySystemBarsMargins(end = true, bottom = true)
binding.scrollTrack.applySystemBarsMargins(end = true, bottom = true)
fastScrollCleanup = binding.textViewWrapper.attachFastScrollThumb(binding.scrollThumb, binding.scrollTrack, binding.textEditor)
setupCaretAutoScroll()
}

override fun onDestroyView() {
fastScrollCleanup?.invoke()
fastScrollCleanup = null
caretAutoScrollWatcher?.let { binding.textEditor.removeTextChangedListener(it) }
caretAutoScrollWatcher = null
super.onDestroyView()
}

private fun setupCaretAutoScroll() {
caretAutoScrollWatcher = binding.textEditor.doAfterTextChanged {
binding.textEditor.post { scrollCaretIntoView() }
}
}
Comment thread
SailReal marked this conversation as resolved.

private fun scrollCaretIntoView() {
val editor = binding.textEditor
val scroll = binding.textViewWrapper
val layout = editor.layout ?: return
val line = layout.getLineForOffset(editor.selectionEnd)
val lineTop = editor.paddingTop + layout.getLineTop(line)
val lineBottom = editor.paddingTop + layout.getLineBottom(line)
val visibleHeight = scroll.height - scroll.paddingTop - scroll.paddingBottom
when {
lineTop < scroll.scrollY -> scroll.smoothScrollTo(0, lineTop)
lineBottom > scroll.scrollY + visibleHeight -> scroll.smoothScrollTo(0, lineBottom - visibleHeight)
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

enum class Direction { PREVIOUS, NEXT }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package org.cryptomator.presentation.ui.layout

import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import android.widget.ScrollView

/**
* Wires [thumb] as a draggable fast-scroll handle over this [ScrollView] (whose scrolling content is [content]).
* Tapping anywhere on [track] jumps the thumb to that position.
* Returns a cleanup callback to be invoked from the host's `onDestroyView`.
*/
fun ScrollView.attachFastScrollThumb(thumb: View, track: View, content: View): () -> Unit {
val scroll = this

fun scrollableHeight(): Int =
(content.height + scroll.paddingTop + scroll.paddingBottom - scroll.height).coerceAtLeast(0)

fun trackHeight(): Int {
val bottomMargin = (thumb.layoutParams as? ViewGroup.MarginLayoutParams)?.bottomMargin ?: 0
return (scroll.height - thumb.height - bottomMargin).coerceAtLeast(0)
}

fun syncThumb() {
val total = scrollableHeight()
if (total == 0) {
thumb.visibility = View.GONE
track.visibility = View.GONE
return
}
thumb.visibility = View.VISIBLE
track.visibility = View.VISIBLE
thumb.translationY = scroll.scrollY.toFloat() / total * trackHeight()
}

fun jumpToTrackY(yOnTrack: Float) {
val trackPx = trackHeight().toFloat()
if (trackPx == 0f) return
val clamped = yOnTrack.coerceIn(0f, trackPx)
scroll.scrollTo(0, (clamped / trackPx * scrollableHeight()).toInt())
}

val scrollListener = ViewTreeObserver.OnScrollChangedListener { syncThumb() }
scroll.viewTreeObserver.addOnScrollChangedListener(scrollListener)

val layoutListener = View.OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> syncThumb() }
scroll.addOnLayoutChangeListener(layoutListener)
content.addOnLayoutChangeListener(layoutListener)

var thumbDragOffsetY = 0f
var thumbDragMoved = false
thumb.isClickable = true
thumb.setOnTouchListener { _, event ->
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
thumbDragOffsetY = event.rawY - thumb.translationY
thumbDragMoved = false
thumb.isPressed = true
true
}
MotionEvent.ACTION_MOVE -> {
thumbDragMoved = true
jumpToTrackY(event.rawY - thumbDragOffsetY)
true
}
MotionEvent.ACTION_UP -> {
thumb.isPressed = false
if (!thumbDragMoved) thumb.performClick()
true
}
MotionEvent.ACTION_CANCEL -> {
thumb.isPressed = false
true
}
else -> false
}
}

track.isClickable = true
track.setOnTouchListener { _, event ->
when (event.actionMasked) {
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> {
jumpToTrackY(event.y - thumb.height / 2f)
true
}
MotionEvent.ACTION_UP -> {
track.performClick()
true
}
else -> false
}
}

return {
scroll.viewTreeObserver.removeOnScrollChangedListener(scrollListener)
scroll.removeOnLayoutChangeListener(layoutListener)
content.removeOnLayoutChangeListener(layoutListener)
thumb.setOnTouchListener(null)
track.setOnTouchListener(null)
}
}
6 changes: 6 additions & 0 deletions presentation/src/main/res/drawable/scroll_thumb.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="3dp" />
<solid android:color="@color/colorPrimaryTransparent" />
</shape>
28 changes: 25 additions & 3 deletions presentation/src/main/res/layout/fragment_text_editor.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,36 @@
android:layout_width="match_parent"
android:layout_height="match_parent">

<ScrollView
android:id="@+id/text_view_wrapper"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:scrollbars="none">

<com.google.android.material.textfield.TextInputEditText
android:id="@+id/text_editor"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="top"
android:imeOptions="flagNoPersonalizedLearning"
android:inputType="textMultiLine"
android:scrollbars="vertical"
android:overScrollMode="ifContentScrolls" />
android:inputType="textMultiLine" />
</ScrollView>

<View
android:id="@+id/scroll_track"
android:layout_width="24dp"
android:layout_height="match_parent"
android:layout_gravity="end"
android:visibility="gone" />

<View
android:id="@+id/scroll_thumb"
android:layout_width="6dp"
android:layout_height="48dp"
android:layout_gravity="end"
android:layout_marginEnd="2dp"
android:background="@drawable/scroll_thumb"
android:visibility="gone" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>
Loading