commit dd1d36137139fd501997f8f986eaa9cd8eb2f4b0 equwal <truex@equwal.com> 2026-09-22 22:47:59 -0700 Look up a word when the finger lifts from it A touch on a word pauses the player at once. Before, the player ran on until the user pressed "Look up", and the line could go past. When the finger lifts, the selection goes to the dictionary. A drag before the lift selects more words. The "Look up" button is removed. A touch beside the words does not pause the player.
README.md | 6 +-- .../kotlin/space/subread/overlay/OverlayTest.kt | 52 +++++++++++++++++++--- .../kotlin/space/subread/overlay/MediaListener.kt | 4 ++ .../kotlin/space/subread/overlay/OverlayView.kt | 17 ++++--- .../metadata/android/en-US/full_description.txt | 2 +- 5 files changed, 67 insertions(+), 14 deletions(-)
diff --git a/README.md b/README.md index e7b8f46..ac0c3d1 100644 --- a/README.md +++ b/README.md @@ -31,9 +31,9 @@ whether it shows the line before and the line after the line of now. On the panel: - `≡` moves the panel. -- A tap selects the word under the finger. A drag selects more words. - "Look up" pauses the player and sends the selection to the dictionary. - "Share" opens the share sheet of Android, for an app without an entry in +- A tap on a word pauses the player at once and selects the word. A drag + selects more words. When the finger lifts, the selection goes to the + dictionary. "Share" opens the share sheet of Android, for an app without an entry in the text selection menu. "Copy" copies it. - `▶` starts the player again after a lookup. `⏸` pauses it. - `⋯` opens the timing row. `−0.5 s` and `+0.5 s` shift the subtitles. diff --git a/app/src/androidTest/kotlin/space/subread/overlay/OverlayTest.kt b/app/src/androidTest/kotlin/space/subread/overlay/OverlayTest.kt index 27bfb31..2b6647b 100644 --- a/app/src/androidTest/kotlin/space/subread/overlay/OverlayTest.kt +++ b/app/src/androidTest/kotlin/space/subread/overlay/OverlayTest.kt @@ -81,11 +81,13 @@ class WordsTest { class OverlayViewTest { private val lookedUp = CopyOnWriteArrayList<String>() + private var touches = 0 private val shared = CopyOnWriteArrayList<String>() private val events = object : OverlayView.Events { override fun onDrag(dx: Float, dy: Float) = Unit override fun onDragEnd() = Unit override fun onClose() = Unit + override fun onTouchWord() { touches++ } override fun onLookUp(word: String) { lookedUp += word } override fun onShare(word: String) { shared += word } override fun onTogglePlay() = Unit @@ -103,8 +105,52 @@ class OverlayViewTest { private fun OverlayView.rowOf(offset: Int): Float = text.totalPaddingTop + text.layout.getLineBaseline(text.layout.getLineForOffset(offset)) - 5f + /** Gives a touch event at [x], [y] of the text view to the text view. */ + private fun OverlayView.touch(action: Int, x: Float, y: Float) { + val now = SystemClock.uptimeMillis() + val event = android.view.MotionEvent.obtain(now, now, action, x, y, 0) + text.dispatchTouchEvent(event) + event.recycle() + } + @Test - fun aTapSelectsTheWordUnderTheFingerAndLookUpSendsIt() { + fun aTouchPausesAtOnceAndTheLiftLooksUpTheSelection() { + ActivityScenario.launch(MainActivity::class.java).use { scenario -> + lateinit var panel: OverlayView + scenario.onActivity { + panel = OverlayView(it, events) + panel.setTextSize(30f) + it.addContentView(panel, ViewGroup.LayoutParams(-1, -2)) + panel.showLine("reading along is easy") + } + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + scenario.onActivity { + val start = panel.line.indexOf("along") + val last = panel.line.lastIndex + panel.touch(android.view.MotionEvent.ACTION_DOWN, panel.middleOf(start + 2), panel.rowOf(start + 2)) + assertEquals("the finger on a word pauses the player", 1, touches) + assertEquals("no lookup before the finger lifts", emptyList<String>(), lookedUp) + panel.touch(android.view.MotionEvent.ACTION_MOVE, panel.middleOf(last), panel.rowOf(last)) + panel.touch(android.view.MotionEvent.ACTION_UP, panel.middleOf(last), panel.rowOf(last)) + assertEquals(listOf("along is easy"), lookedUp) + + val found = arrayListOf<android.view.View>() + panel.findViewsWithText(found, "Look up", android.view.View.FIND_VIEWS_WITH_TEXT) + assertEquals("no \"Look up\" button", 0, found.size) + + // A tap beside the words neither pauses nor looks up. + val right = panel.text.width - 1f + panel.touch(android.view.MotionEvent.ACTION_DOWN, right, panel.rowOf(last)) + panel.touch(android.view.MotionEvent.ACTION_UP, right, panel.rowOf(last)) + assertNull(panel.selection) + assertEquals(1, touches) + assertEquals(listOf("along is easy"), lookedUp) + } + } + } + + @Test + fun aTapSelectsTheWordUnderTheFingerAndADragSelectsMore() { ActivityScenario.launch(MainActivity::class.java).use { scenario -> lateinit var panel: OverlayView scenario.onActivity { @@ -126,10 +172,6 @@ class OverlayViewTest { assertEquals(start..last, panel.selection) val found = arrayListOf<android.view.View>() - panel.findViewsWithText(found, "Look up", android.view.View.FIND_VIEWS_WITH_TEXT) - found.single().performClick() - assertEquals(listOf("along is easy"), lookedUp) - found.clear() panel.findViewsWithText(found, "Share", android.view.View.FIND_VIEWS_WITH_TEXT) found.single().performClick() assertEquals(listOf("along is easy"), shared) diff --git a/app/src/main/kotlin/space/subread/overlay/MediaListener.kt b/app/src/main/kotlin/space/subread/overlay/MediaListener.kt index 252eb80..6759612 100644 --- a/app/src/main/kotlin/space/subread/overlay/MediaListener.kt +++ b/app/src/main/kotlin/space/subread/overlay/MediaListener.kt @@ -206,6 +206,10 @@ class MediaListener : NotificationListenerService(), OverlayView.Events { } /** The player pauses for the lookup. The play button of the panel starts it again. */ + override fun onTouchWord() { + follower.pause() + } + override fun onLookUp(word: String) { follower.pause() runCatching { startActivity(Lookup.intent(word, store.dictionary)) }.onFailure { diff --git a/app/src/main/kotlin/space/subread/overlay/OverlayView.kt b/app/src/main/kotlin/space/subread/overlay/OverlayView.kt index 540d811..62fd35f 100644 --- a/app/src/main/kotlin/space/subread/overlay/OverlayView.kt +++ b/app/src/main/kotlin/space/subread/overlay/OverlayView.kt @@ -23,9 +23,10 @@ import androidx.core.view.isVisible /** * The panel over the media player: the subtitle line, and a few buttons. * - * A tap on the line selects the word under the finger. A drag makes the selection longer, word - * by word. The selection goes to a dictionary with "Look up". The system text selection is not - * used: it needs a window with the input focus, and then the player below gets no keys. + * A tap on the line selects the word under the finger and pauses the player at once. A drag + * makes the selection longer, word by word. When the finger lifts, the selection goes to the + * dictionary. The system text selection is not used: it needs a window with the input focus, + * and then the player below gets no keys. * * Black on white and nothing that moves, so that the panel is usable on an e-ink screen. */ @@ -36,6 +37,9 @@ class OverlayView(context: Context, private val events: Events) : LinearLayout(c fun onDrag(dx: Float, dy: Float) fun onDragEnd() fun onClose() + /** A finger is on a word: the player pauses now, before the lookup. */ + fun onTouchWord() + /** The finger lifted from a selection: the selection goes to the dictionary. */ fun onLookUp(word: String) /** "Share": the selection goes to an app that the user picks in the share sheet. */ fun onShare(word: String) @@ -90,7 +94,6 @@ class OverlayView(context: Context, private val events: Events) : LinearLayout(c lookUpRow.gravity = Gravity.END lookUpRow.addView(button("Copy", null) { copy() }, wrap()) lookUpRow.addView(button("Share", null) { share() }, wrap()) - lookUpRow.addView(button("Look up", null) { lookUp() }, wrap()) lookUpRow.visibility = View.GONE addView(lookUpRow, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)) @@ -212,8 +215,12 @@ class OverlayView(context: Context, private val events: Events) : LinearLayout(c private fun selectWith(view: TextView) { view.setOnTouchListener { _, event -> when (event.actionMasked) { - MotionEvent.ACTION_DOWN -> selectAt(event.x, event.y) + MotionEvent.ACTION_DOWN -> { + selectAt(event.x, event.y) + if (selection != null) events.onTouchWord() + } MotionEvent.ACTION_MOVE -> selectAt(event.x, event.y, extend = true) + MotionEvent.ACTION_UP -> lookUp() } true } diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt index 1147b32..59bb9c0 100644 --- a/fastlane/metadata/android/en-US/full_description.txt +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -3,7 +3,7 @@ SubRead Overlay shows the lines of a subtitle file (.srt) over the app that play <b>What it does</b> * Works with each player that publishes an Android media session: Voice, VLC, mpv-android, podcast apps and more. No player needs a change. -* Tap a word to select it. Drag to select more words. "Look up" pauses the player and sends the selection to a dictionary, a translator or a flash card app. Each app with an entry in the text selection menu can be the target. "Share" opens the share sheet of Android for the other apps. A button on the panel starts the player again. +* Tap a word: the player pauses at once, and the word goes to a dictionary, a translator or a flash card app. Drag to look up more words. Each app with an entry in the text selection menu can be the target. "Share" opens the share sheet of Android for the other apps. A button on the panel starts the player again. * On request the panel shows the line before and the line after too, and lets the player show through. * The line stays on the panel until the next line starts, so there is time to look a word up. * A timing row shifts the subtitles by half a second, or makes the line before or the next line the line of now.