fixed some bugs
This commit is contained in:
@@ -37,6 +37,7 @@ kotlin {
|
||||
dependencies {
|
||||
implementation(project(":oral-trainer-sdk"))
|
||||
implementation("androidx.core:core-ktx:1.13.1")
|
||||
implementation("androidx.recyclerview:recyclerview:1.3.2")
|
||||
implementation("androidx.media3:media3-common:1.11.0")
|
||||
implementation("androidx.media3:media3-exoplayer:1.11.0")
|
||||
}
|
||||
|
||||
@@ -263,6 +263,9 @@ class MainActivity : Activity() {
|
||||
if (screenState.activeModule == Module.MINE) {
|
||||
screenFactory.refreshMyContent(sdk.userApi, authSessionStore.session?.token.orEmpty())
|
||||
}
|
||||
if (screenState.activeModule == Module.READING) {
|
||||
screenFactory.scrollToReadingCurrentSentence()
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshCurrentUi() {
|
||||
@@ -552,6 +555,17 @@ class MainActivity : Activity() {
|
||||
return sentenceFavoriteStore.toggle(favoriteUserKey(), item.id, sentence.index)
|
||||
}
|
||||
|
||||
override fun onSentenceDoubleTapped(sentence: SentenceBoundary) {
|
||||
screenState.continuousPlaybackEnabled = false
|
||||
switchModule(Module.TRAIN)
|
||||
controller.setContinuousPlayback(false)
|
||||
controller.seekTo(sentence.startMs)
|
||||
controller.play()
|
||||
if (bindings.hasPlayerChrome) {
|
||||
bindings.statusText.text = "已定位到第 ${sentence.index + 1} 句,单句循环播放中"
|
||||
}
|
||||
}
|
||||
|
||||
override fun onExplainCurrentSentence() {
|
||||
val sentence = controller.currentSentence() ?: return
|
||||
if (sentence.text.isNullOrBlank()) return
|
||||
|
||||
@@ -29,6 +29,7 @@ internal interface ReadingScreenActions {
|
||||
fun onReadingTranslationVisible(visible: Boolean)
|
||||
fun isSentenceFavorite(sentence: SentenceBoundary): Boolean
|
||||
fun toggleSentenceFavorite(sentence: SentenceBoundary): Boolean
|
||||
fun onSentenceDoubleTapped(sentence: SentenceBoundary)
|
||||
}
|
||||
|
||||
internal interface TestScreenActions :
|
||||
|
||||
@@ -85,6 +85,12 @@ internal class SampleScreenFactory(
|
||||
}
|
||||
}
|
||||
|
||||
fun scrollToReadingCurrentSentence() {
|
||||
if (screenState.activeModule == Module.READING) {
|
||||
readingScreen.scrollToCurrentSentence()
|
||||
}
|
||||
}
|
||||
|
||||
fun renderCatalog() {
|
||||
if (screenState.activeModule == Module.COURSE) {
|
||||
courseScreen.renderCatalog()
|
||||
|
||||
@@ -19,6 +19,7 @@ internal object SampleTheme {
|
||||
val textDark = Color.rgb(18, 24, 31)
|
||||
val textMuted = Color.rgb(151, 162, 174)
|
||||
val textSubtle = Color.rgb(88, 98, 108)
|
||||
val readingTranslation = Color.rgb(147, 197, 253)
|
||||
val danger = Color.rgb(220, 38, 38)
|
||||
val textOverlay = Color.rgb(163, 172, 182)
|
||||
}
|
||||
|
||||
@@ -3,14 +3,16 @@ package cn.learningpad.oraltrainer.sample.ui
|
||||
import android.app.Activity
|
||||
import android.graphics.Color
|
||||
import android.graphics.Typeface
|
||||
import android.os.SystemClock
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.ScrollView
|
||||
import android.widget.Switch
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import cn.learningpad.oraltrainer.sample.ReadingScreenActions
|
||||
import cn.learningpad.oraltrainer.sample.SampleTheme
|
||||
import cn.learningpad.oraltrainer.sample.TrainingScreenState
|
||||
@@ -18,7 +20,7 @@ import cn.learningpad.oraltrainer.sample.UiStyled
|
||||
import cn.learningpad.oraltrainer.sdk.OralTrainerController
|
||||
import cn.learningpad.oraltrainer.sdk.SentenceBoundary
|
||||
|
||||
/** Scrollable full-text reading view for the selected course. */
|
||||
/** Lazy-loading full-text reading view for the selected course. */
|
||||
internal class ReadingScreen(
|
||||
private val activity: Activity,
|
||||
private val screenState: TrainingScreenState,
|
||||
@@ -28,90 +30,81 @@ internal class ReadingScreen(
|
||||
) : UiStyled {
|
||||
override val uiContext get() = activity
|
||||
|
||||
private data class SentenceRow(
|
||||
val translation: TextView,
|
||||
val favoriteButton: Button,
|
||||
)
|
||||
|
||||
private val sentenceRows = mutableMapOf<Int, SentenceRow>()
|
||||
private var sentenceList: LinearLayout? = null
|
||||
private var readingAdapter: ReadingSentenceAdapter? = null
|
||||
private var titleText: TextView? = null
|
||||
private var sentenceListView: RecyclerView? = null
|
||||
private var lastClickedSentenceIndex = Int.MIN_VALUE
|
||||
private var lastClickedAtMs = 0L
|
||||
|
||||
fun createSection(): View {
|
||||
val content = LinearLayout(activity).apply {
|
||||
val root = LinearLayout(activity).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
setPadding(16.dp, 12.dp, 16.dp, 16.dp)
|
||||
}
|
||||
val list = LinearLayout(activity).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
}
|
||||
sentenceList = list
|
||||
|
||||
val title = TextView(activity).apply {
|
||||
setTextColor(Color.WHITE)
|
||||
textSize = 24f
|
||||
typeface = Typeface.DEFAULT_BOLD
|
||||
setPadding(0, 0, 0, 6.dp)
|
||||
}
|
||||
titleText = title
|
||||
content.addView(title)
|
||||
|
||||
content.addView(createTranslationToggle())
|
||||
content.addView(list)
|
||||
renderSentences()
|
||||
|
||||
return ScrollView(activity).apply {
|
||||
val adapter = ReadingSentenceAdapter()
|
||||
readingAdapter = adapter
|
||||
val list = RecyclerView(activity).apply {
|
||||
layoutManager = LinearLayoutManager(activity)
|
||||
this.adapter = adapter
|
||||
itemAnimator = null
|
||||
setItemViewCacheSize(12)
|
||||
clipToPadding = false
|
||||
setPadding(0, 0, 0, 16.dp)
|
||||
layoutParams = LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
0,
|
||||
1f,
|
||||
)
|
||||
addView(content)
|
||||
}
|
||||
sentenceListView = list
|
||||
|
||||
val title = TextView(activity).apply {
|
||||
setTextColor(Color.WHITE)
|
||||
textSize = 15f
|
||||
typeface = Typeface.DEFAULT_BOLD
|
||||
setPadding(0, 0, 0, 6.dp)
|
||||
}
|
||||
titleText = title
|
||||
root.addView(title)
|
||||
root.addView(createTranslationToggle())
|
||||
root.addView(list)
|
||||
renderSentences()
|
||||
|
||||
return root
|
||||
}
|
||||
|
||||
fun renderSentences() {
|
||||
val list = sentenceList ?: return
|
||||
val item = controller.currentTrainingItem()
|
||||
titleText?.text = screenState.currentCourse?.title.orEmpty()
|
||||
val baseTitle = screenState.currentCourse?.title.orEmpty()
|
||||
.ifBlank { item?.title ?: "阅读模式" }
|
||||
sentenceRows.clear()
|
||||
list.removeAllViews()
|
||||
if (item == null || item.sentences.isEmpty()) {
|
||||
list.addView(widgets.infoCard("当前课程还没有可阅读的句子文本"))
|
||||
return
|
||||
}
|
||||
val currentTitle = titleText?.text?.toString().orEmpty()
|
||||
titleText?.text = "$currentTitle · ${item.sentences.size} 句"
|
||||
item.sentences.forEach { sentence ->
|
||||
list.addView(sentenceRow(sentence))
|
||||
val sentences = item?.sentences.orEmpty()
|
||||
titleText?.text = if (sentences.isEmpty()) {
|
||||
baseTitle
|
||||
} else {
|
||||
"$baseTitle · ${sentences.size} 句"
|
||||
}
|
||||
readingAdapter?.submitSentences(sentences)
|
||||
}
|
||||
|
||||
fun refreshTranslations() {
|
||||
val item = controller.currentTrainingItem() ?: return
|
||||
item.sentences.forEach { sentence ->
|
||||
val row = sentenceRows[sentence.index] ?: return@forEach
|
||||
row.translation.visibility = if (screenState.readingTranslationVisible) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
row.translation.text = translationText(sentence)
|
||||
}
|
||||
readingAdapter?.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun refreshFavorites() {
|
||||
readingAdapter?.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun scrollToCurrentSentence() {
|
||||
val list = sentenceListView ?: return
|
||||
val currentSentence = controller.currentSentence() ?: return
|
||||
val item = controller.currentTrainingItem() ?: return
|
||||
item.sentences.forEach { sentence ->
|
||||
sentenceRows[sentence.index]?.favoriteButton?.apply {
|
||||
val isFavorite = actions.isSentenceFavorite(sentence)
|
||||
setText(if (isFavorite) "已收藏" else "收藏")
|
||||
background = rounded(
|
||||
if (isFavorite) SampleTheme.accent else SampleTheme.button,
|
||||
8f,
|
||||
)
|
||||
}
|
||||
val position = item.sentences.indexOfFirst { it.index == currentSentence.index }
|
||||
if (position < 0) return
|
||||
list.post {
|
||||
if (sentenceListView !== list) return@post
|
||||
val layoutManager = list.layoutManager as? LinearLayoutManager ?: return@post
|
||||
val offset = (list.height * 0.25f).toInt().coerceAtLeast(0)
|
||||
layoutManager.scrollToPositionWithOffset(position, offset)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,75 +129,178 @@ internal class ReadingScreen(
|
||||
})
|
||||
}
|
||||
|
||||
private fun sentenceRow(sentence: SentenceBoundary): View {
|
||||
val translation = TextView(activity).apply {
|
||||
text = translationText(sentence)
|
||||
setTextColor(SampleTheme.textSubtle)
|
||||
textSize = 14f
|
||||
setPadding(0, 6.dp, 0, 0)
|
||||
visibility = if (screenState.readingTranslationVisible) View.VISIBLE else View.GONE
|
||||
}
|
||||
val favoriteButton = Button(activity).apply {
|
||||
setText("收藏")
|
||||
isAllCaps = false
|
||||
setTextColor(Color.WHITE)
|
||||
textSize = 12f
|
||||
typeface = Typeface.DEFAULT_BOLD
|
||||
minWidth = 0
|
||||
minHeight = 0
|
||||
minimumWidth = 0
|
||||
minimumHeight = 0
|
||||
setPadding(12.dp, 0, 12.dp, 0)
|
||||
background = rounded(SampleTheme.button, 8f)
|
||||
layoutParams = LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
32.dp,
|
||||
).withMargins(10.dp, 0, 0, 0)
|
||||
setOnClickListener {
|
||||
val isFavorite = actions.toggleSentenceFavorite(sentence)
|
||||
setText(if (isFavorite) "已收藏" else "收藏")
|
||||
background = rounded(
|
||||
if (isFavorite) SampleTheme.accent else SampleTheme.button,
|
||||
8f,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val isFavorite = actions.isSentenceFavorite(sentence)
|
||||
favoriteButton.setText(if (isFavorite) "已收藏" else "收藏")
|
||||
favoriteButton.background = rounded(
|
||||
if (isFavorite) SampleTheme.accent else SampleTheme.button,
|
||||
8f,
|
||||
)
|
||||
val textColumn = LinearLayout(activity).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
layoutParams = LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f)
|
||||
addView(TextView(activity).apply {
|
||||
text = "${sentence.index + 1}. ${sentence.text?.takeIf { it.isNotBlank() } ?: "未提供文本"}"
|
||||
setTextColor(Color.WHITE)
|
||||
textSize = 16f
|
||||
})
|
||||
addView(translation)
|
||||
}
|
||||
|
||||
return LinearLayout(activity).apply {
|
||||
orientation = LinearLayout.HORIZONTAL
|
||||
gravity = Gravity.CENTER_VERTICAL
|
||||
background = rounded(SampleTheme.surface, 10f, SampleTheme.border)
|
||||
setPadding(14.dp, 12.dp, 10.dp, 12.dp)
|
||||
layoutParams = LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
).withMargins(0, 0, 0, 10.dp)
|
||||
addView(textColumn)
|
||||
addView(favoriteButton)
|
||||
}
|
||||
}
|
||||
|
||||
private fun translationText(sentence: SentenceBoundary): String = when {
|
||||
!sentence.zhText.isNullOrBlank() -> sentence.zhText.orEmpty()
|
||||
screenState.chineseTranslationLoading -> "正在翻译中文…"
|
||||
screenState.chineseTranslationError.isNotBlank() -> screenState.chineseTranslationError
|
||||
else -> "暂无中文翻译"
|
||||
}
|
||||
|
||||
private inner class ReadingSentenceAdapter :
|
||||
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
private var sentences: List<SentenceBoundary> = emptyList()
|
||||
|
||||
init {
|
||||
setHasStableIds(true)
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
return if (sentences.isEmpty()) EMPTY_ID else sentences[position].index.toLong()
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return if (sentences.isEmpty()) TYPE_EMPTY else TYPE_SENTENCE
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return if (viewType == TYPE_EMPTY) {
|
||||
val empty = TextView(activity).apply {
|
||||
text = "当前课程还没有可阅读的句子文本"
|
||||
setTextColor(SampleTheme.textDark)
|
||||
textSize = 13f
|
||||
background = rounded(SampleTheme.selected, 8f)
|
||||
setPadding(12.dp, 10.dp, 12.dp, 10.dp)
|
||||
layoutParams = RecyclerView.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
)
|
||||
}
|
||||
EmptyViewHolder(empty)
|
||||
} else {
|
||||
SentenceViewHolder(createSentenceView())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
if (holder !is SentenceViewHolder || sentences.isEmpty()) return
|
||||
val sentence = sentences[position]
|
||||
attachDoubleTap(holder.itemView, sentence)
|
||||
holder.text.text =
|
||||
"${sentence.index + 1}. ${sentence.text?.takeIf { it.isNotBlank() } ?: "未提供文本"}"
|
||||
holder.translation.visibility = if (screenState.readingTranslationVisible) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
holder.translation.text = translationText(sentence)
|
||||
bindFavoriteButton(holder.favoriteButton, sentence)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return if (sentences.isEmpty()) 1 else sentences.size
|
||||
}
|
||||
|
||||
fun submitSentences(updated: List<SentenceBoundary>) {
|
||||
sentences = updated
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
private fun createSentenceView(): View {
|
||||
val text = TextView(activity).apply {
|
||||
setTextColor(Color.WHITE)
|
||||
textSize = 16f
|
||||
}
|
||||
val translation = TextView(activity).apply {
|
||||
setTextColor(SampleTheme.readingTranslation)
|
||||
textSize = 14f
|
||||
setPadding(0, 6.dp, 0, 0)
|
||||
}
|
||||
val favoriteButton = Button(activity).apply {
|
||||
isAllCaps = false
|
||||
setTextColor(Color.WHITE)
|
||||
textSize = 12f
|
||||
typeface = Typeface.DEFAULT_BOLD
|
||||
minWidth = 0
|
||||
minHeight = 0
|
||||
minimumWidth = 0
|
||||
minimumHeight = 0
|
||||
setPadding(12.dp, 0, 12.dp, 0)
|
||||
layoutParams = LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
32.dp,
|
||||
).withMargins(10.dp, 0, 0, 0)
|
||||
}
|
||||
val textColumn = LinearLayout(activity).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
layoutParams = LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f)
|
||||
addView(text)
|
||||
addView(translation)
|
||||
}
|
||||
return LinearLayout(activity).apply {
|
||||
orientation = LinearLayout.HORIZONTAL
|
||||
gravity = Gravity.CENTER_VERTICAL
|
||||
background = rounded(SampleTheme.surface, 10f, SampleTheme.border)
|
||||
setPadding(14.dp, 12.dp, 10.dp, 12.dp)
|
||||
layoutParams = RecyclerView.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
).apply {
|
||||
bottomMargin = 10.dp
|
||||
}
|
||||
addView(textColumn)
|
||||
addView(favoriteButton)
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindFavoriteButton(button: Button, sentence: SentenceBoundary) {
|
||||
val isFavorite = actions.isSentenceFavorite(sentence)
|
||||
button.setText(if (isFavorite) "已收藏" else "收藏")
|
||||
button.background = rounded(
|
||||
if (isFavorite) SampleTheme.accent else SampleTheme.button,
|
||||
8f,
|
||||
)
|
||||
button.setOnClickListener {
|
||||
val favorite = actions.toggleSentenceFavorite(sentence)
|
||||
button.setText(if (favorite) "已收藏" else "收藏")
|
||||
button.background = rounded(
|
||||
if (favorite) SampleTheme.accent else SampleTheme.button,
|
||||
8f,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun attachDoubleTap(view: View, sentence: SentenceBoundary) {
|
||||
view.isClickable = true
|
||||
view.setOnClickListener {
|
||||
val now = SystemClock.elapsedRealtime()
|
||||
val isDoubleTap = lastClickedSentenceIndex == sentence.index &&
|
||||
now - lastClickedAtMs <= DOUBLE_TAP_INTERVAL_MS
|
||||
if (isDoubleTap) {
|
||||
lastClickedSentenceIndex = Int.MIN_VALUE
|
||||
lastClickedAtMs = 0L
|
||||
actions.onSentenceDoubleTapped(sentence)
|
||||
} else {
|
||||
lastClickedSentenceIndex = sentence.index
|
||||
lastClickedAtMs = now
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inner class EmptyViewHolder(root: View) : RecyclerView.ViewHolder(root)
|
||||
|
||||
private inner class SentenceViewHolder(
|
||||
root: View,
|
||||
) : RecyclerView.ViewHolder(root) {
|
||||
val text: TextView
|
||||
val translation: TextView
|
||||
val favoriteButton: Button
|
||||
|
||||
init {
|
||||
val textColumn = root as ViewGroup
|
||||
val content = textColumn.getChildAt(0) as ViewGroup
|
||||
text = content.getChildAt(0) as TextView
|
||||
translation = content.getChildAt(1) as TextView
|
||||
favoriteButton = textColumn.getChildAt(1) as Button
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val TYPE_EMPTY = 0
|
||||
private const val TYPE_SENTENCE = 1
|
||||
private const val EMPTY_ID = Long.MIN_VALUE
|
||||
private const val DOUBLE_TAP_INTERVAL_MS = 300L
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user