@@ -2,6 +2,7 @@ package remix.myplayer.ui.widget
22
33import android.annotation.SuppressLint
44import android.content.Context
5+ import android.os.Build
56import android.util.AttributeSet
67import android.view.LayoutInflater
78import android.view.MotionEvent
@@ -11,19 +12,24 @@ import android.widget.LinearLayout
1112import android.widget.TextView
1213import androidx.annotation.ColorInt
1314import androidx.annotation.UiThread
14- import remix.myplayer.R
15+ import androidx.core.view.updatePadding
16+ import remix.myplayer.databinding.LayoutLyricsLineBinding
1517import remix.myplayer.databinding.LayoutLyricsViewBinding
1618import remix.myplayer.lyrics.LyricsLine
1719import remix.myplayer.lyrics.PerWordLyricsLine
1820import remix.myplayer.theme.ThemeStore
21+ import timber.log.Timber
1922import kotlin.math.roundToInt
2023import kotlin.time.Duration.Companion.milliseconds
2124
2225class LyricsView @JvmOverloads constructor(
2326 context : Context , attrs : AttributeSet ? = null
2427) : FrameLayout(context, attrs), View.OnTouchListener {
2528 companion object {
29+ private const val TAG = " LyricsView"
30+
2631 private val DEACTIVATE_DELAY = 5000 .milliseconds
32+ private val AUTO_SCROLL_DELAY = 200 .milliseconds
2733
2834 private val normalTextColor
2935 @ColorInt get() = ThemeStore .textColorSecondary
@@ -41,40 +47,29 @@ class LyricsView @JvmOverloads constructor(
4147
4248 override fun onSizeChanged (w : Int , h : Int , oldw : Int , oldh : Int ) {
4349 super .onSizeChanged(w, h, oldw, oldh)
50+ Timber .tag(TAG ).v(" onSizeChanged, h=$h " )
4451 if (h != oldh) {
4552 // 给 container 上下加空白,确保第一行和最后一行歌词可以滚动到 view 中间
46- binding.innerContainer.setPadding(0 , h / 2 , 0 , h / 2 )
53+ val padding = (h + 1 ) / 2
54+ handler.post {
55+ binding.innerContainer.setPadding(0 , padding, 0 , padding)
56+ }
4757 }
4858 }
4959
50- private fun newLayoutForLine (line : LyricsLine ): LinearLayout {
51- val params = LayoutParams (LayoutParams .MATCH_PARENT , LayoutParams .WRAP_CONTENT )
52- val padding = resources.getDimensionPixelSize(R .dimen.lyrics_view_lrc_block_vertical_padding)
53- val textColor = normalTextColor
54-
55- val layout = LinearLayout (context)
56- layout.layoutParams = params
57- layout.setPadding(0 , padding, 0 , padding)
58- layout.orientation = LinearLayout .VERTICAL
60+ private fun addLayoutForLine (line : LyricsLine ) {
61+ val layout =
62+ LayoutLyricsLineBinding .inflate(LayoutInflater .from(context), binding.innerContainer, true )
5963 if (line.content.isNotBlank()) {
60- val view = TextView (context)
61- view.layoutParams = params
62- view.text = if (line is PerWordLyricsLine ) {
63- line.getSpannedString(0f , textColor)
64+ layout.content.text = if (line is PerWordLyricsLine ) {
65+ line.getSpannedString(0f , normalTextColor)
6466 } else {
6567 line.content
6668 }
67- view.setTextColor(textColor)
68- layout.addView(view)
6969 }
70- if (line.translation?.isNotBlank() == true ) {
71- val view = TextView (context)
72- view.layoutParams = params
73- view.text = line.translation
74- view.setTextColor(textColor)
75- layout.addView(view)
70+ if (! line.translation.isNullOrBlank()) {
71+ layout.translation.text = line.translation
7672 }
77- return layout
7873 }
7974
8075 /* *
@@ -89,7 +84,7 @@ class LyricsView @JvmOverloads constructor(
8984 binding.innerContainer.removeAllViews()
9085 isClickable = lyrics.isNotEmpty()
9186 value.forEach {
92- binding.innerContainer.addView(newLayoutForLine(it) )
87+ addLayoutForLine(it )
9388 }
9489 rawProgressAndDuration = null
9590 lastHighlightLine = null
@@ -107,7 +102,7 @@ class LyricsView @JvmOverloads constructor(
107102 }
108103 field = value
109104 if (isActive) {
110- showTimeIndicator ()
105+ updateTimeIndicator ()
111106 }
112107 rawProgressAndDuration?.run {
113108 updateProgress(first, second)
@@ -201,7 +196,8 @@ class LyricsView @JvmOverloads constructor(
201196 set(value) {
202197 field = value
203198 if (value) {
204- showTimeIndicator()
199+ updateTimeIndicator()
200+ binding.timeIndicator.visibility = View .VISIBLE
205201 handler.removeCallbacks(deactivateRunnable)
206202 handler.postDelayed(deactivateRunnable, DEACTIVATE_DELAY .inWholeMilliseconds)
207203 } else {
@@ -215,34 +211,69 @@ class LyricsView @JvmOverloads constructor(
215211 private val deactivateRunnable = Runnable {
216212 isActive = false
217213 }
214+ private val scrollToNearestLineRunnable = Runnable {
215+ scrollToLine(getNearestLine())
216+ }
218217
219218 @SuppressLint(" SetTextI18n" )
220- private fun showTimeIndicator () {
219+ private fun updateTimeIndicator () {
221220 (lyrics[getNearestLine()].time - offset).coerceAtLeast(0 ).let {
222221 binding.time.text =
223222 " %02d:%02d.%02d" .format(it / 1000 / 60 , it / 1000 % 60 , (it % 1000 / 10f ).roundToInt())
223+ // TODO: don't set every time
224224 binding.playButton.setOnClickListener { _ ->
225225 onSeekToListener?.onSeekTo(it)
226226 }
227227 }
228- binding.timeIndicator.visibility = View .VISIBLE
229228 }
230229
230+ private var isTouching: Boolean = false
231+
231232 override fun onTouch (v : View , event : MotionEvent ): Boolean {
233+ check(v == binding.outerContainer)
234+
232235 isActive = true
236+
237+ when (event.action) {
238+ MotionEvent .ACTION_DOWN -> {
239+ isTouching = true
240+ handler.removeCallbacks(scrollToNearestLineRunnable)
241+ }
242+
243+ MotionEvent .ACTION_UP -> {
244+ isTouching = false
245+ handler.postDelayed(scrollToNearestLineRunnable, AUTO_SCROLL_DELAY .inWholeMilliseconds)
246+ }
247+ }
248+
233249 return false
234250 }
235251
252+ private fun onScrollChange () {
253+ updateTimeIndicator()
254+
255+ handler.removeCallbacks(scrollToNearestLineRunnable)
256+ if (! isTouching) {
257+ handler.postDelayed(scrollToNearestLineRunnable, AUTO_SCROLL_DELAY .inWholeMilliseconds)
258+ }
259+ }
260+
236261 // 在单独函数以忽略警告
237262 @SuppressLint(" ClickableViewAccessibility" )
238- private fun setupOnTouchListener () {
263+ private fun init () {
239264 binding.outerContainer.setOnTouchListener(this )
265+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .M ) {
266+ binding.outerContainer.setOnScrollChangeListener { _, _, _, _, _ ->
267+ onScrollChange()
268+ }
269+ } else {
270+ binding.outerContainer.viewTreeObserver.addOnScrollChangedListener {
271+ onScrollChange()
272+ }
273+ }
240274 }
241275
242276 init {
243- binding.outerContainer.onFlingEndListener = ResponsiveScrollView .OnFlingEndListener {
244- scrollToLine(getNearestLine())
245- }
246- setupOnTouchListener()
277+ init ()
247278 }
248279}
0 commit comments