From 02ec85e342de092e1fece2e94e4ab9482d4246f0 Mon Sep 17 00:00:00 2001 From: sffzh Date: Tue, 13 Jan 2026 17:19:15 +0800 Subject: [PATCH 1/4] Supports dual-line lyric display (for bilingual lyric files with translations). --- .../ui/fragment/PlayerLyricsFragment.java | 54 +++++++++++++------ 1 file changed, 39 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerLyricsFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerLyricsFragment.java index 24a1abcd9..8abac3902 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerLyricsFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerLyricsFragment.java @@ -37,6 +37,10 @@ import com.google.common.util.concurrent.MoreExecutors; import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; @OptIn(markerClass = UnstableApi.class) @@ -53,6 +57,8 @@ public class PlayerLyricsFragment extends Fragment { private LyricsList currentLyricsList; private String currentDescription; + private Map> lineMap; + @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { bind = InnerFragmentPlayerLyricsBinding.inflate(inflater, container, false); @@ -60,6 +66,8 @@ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, playerBottomSheetViewModel = new ViewModelProvider(requireActivity()).get(PlayerBottomSheetViewModel.class); + initLyricsLineMap(playerBottomSheetViewModel.getLiveLyricsList().getValue()); + initOverlay(); return view; @@ -111,6 +119,16 @@ public void onDestroyView() { currentDescription = null; } + private synchronized void initLyricsLineMap(LyricsList lyricsList){ + + if (hasStructuredLyrics(lyricsList)){ + lineMap = Objects.requireNonNull(Objects.requireNonNull(lyricsList.getStructuredLyrics()).get(0).getLine()) + .stream().filter(line -> Objects.nonNull(line) && Objects.nonNull(line.getStart())) + .collect(Collectors.groupingBy(line -> line.getStart() == null? 0 : line.getStart())); + } + + } + private void initOverlay() { bind.syncLyricsTapButton.setOnClickListener(view -> { playerBottomSheetViewModel.changeSyncLyricsState(); @@ -232,12 +250,11 @@ private boolean hasText(String value) { } private boolean hasStructuredLyrics(LyricsList lyricsList) { - return lyricsList != null - && lyricsList.getStructuredLyrics() != null - && !lyricsList.getStructuredLyrics().isEmpty() - && lyricsList.getStructuredLyrics().get(0) != null - && lyricsList.getStructuredLyrics().get(0).getLine() != null - && !lyricsList.getStructuredLyrics().get(0).getLine().isEmpty(); + if (lyricsList == null + || lyricsList.getStructuredLyrics() == null + || lyricsList.getStructuredLyrics().isEmpty()) return false; + lyricsList.getStructuredLyrics(); + return lyricsList.getStructuredLyrics().get(0).getLine() != null && !Objects.requireNonNull(lyricsList.getStructuredLyrics().get(0).getLine()).isEmpty(); } @SuppressLint("DefaultLocale") @@ -289,7 +306,7 @@ private void displaySyncedLyrics() { if (hasStructuredLyrics(lyricsList)) { StringBuilder lyricsBuilder = new StringBuilder(); - List lines = lyricsList.getStructuredLyrics().get(0).getLine(); + List lines = Objects.requireNonNull(lyricsList.getStructuredLyrics()).get(0).getLine(); if (lines == null || lines.isEmpty()) return; @@ -297,14 +314,20 @@ private void displaySyncedLyrics() { lyricsBuilder.append(line.getValue().trim()).append("\n"); } - Line toHighlight = lines.stream().filter(line -> line != null && line.getStart() != null && line.getStart() < timestamp).reduce((first, second) -> second).orElse(null); + Optional currentLineStart = lineMap.keySet().stream().filter(start -> start!=null && start < timestamp ).max(Integer::compareTo); + + if (currentLineStart.isEmpty()){ + return; + } + + List currentLines = lineMap.get(currentLineStart.get()); - if (toHighlight != null) { + if (currentLines != null && !currentLines.isEmpty()) { String lyrics = lyricsBuilder.toString(); Spannable spannableString = new SpannableString(lyrics); - int startingPosition = getStartPosition(lines, toHighlight); - int endingPosition = startingPosition + toHighlight.getValue().length(); + int startingPosition = getStartPosition(lines, currentLines.get(0)); + int endingPosition = startingPosition + currentLines.stream().mapToInt(line -> line.getValue().length() + 1).sum() -1; spannableString.setSpan(new ForegroundColorSpan(requireContext().getResources().getColor(R.color.shadowsLyricsTextColor, null)), 0, lyrics.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); spannableString.setSpan(new ForegroundColorSpan(requireContext().getResources().getColor(R.color.lyricsTextColor, null)), startingPosition, endingPosition, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); @@ -312,7 +335,7 @@ private void displaySyncedLyrics() { bind.nowPlayingSongLyricsTextView.setText(spannableString); if (playerBottomSheetViewModel.getSyncLyricsState()) { - bind.nowPlayingSongLyricsSrollView.smoothScrollTo(0, getScroll(lines, toHighlight)); + bind.nowPlayingSongLyricsSrollView.smoothScrollTo(0, getScroll(lines, currentLines)); } } } @@ -347,14 +370,15 @@ private int getLineCount(List lines, Line toHighlight) { return start; } - private int getScroll(List lines, Line toHighlight) { - int startIndex = getStartPosition(lines, toHighlight); + private int getScroll(List lines, List toHighlight) { + int startIndex = getStartPosition(lines, toHighlight.get(0)); Layout layout = bind.nowPlayingSongLyricsTextView.getLayout(); if (layout == null) return 0; int line = layout.getLineForOffset(startIndex); int lineTop = layout.getLineTop(line); - int lineBottom = layout.getLineBottom(line); + int lastLineNum = lines.size() ==1 ? line : layout.getLineForOffset(getStartPosition(lines, toHighlight.get(lines.size()-1))); + int lineBottom = layout.getLineBottom(lastLineNum); int lineCenter = (lineTop + lineBottom) / 2; int scrollViewHeight = bind.nowPlayingSongLyricsSrollView.getHeight(); From 1058c356aecceb848c460e7c72692f26602e34f7 Mon Sep 17 00:00:00 2001 From: sffzh Date: Wed, 14 Jan 2026 08:50:00 +0800 Subject: [PATCH 2/4] stash changes --- .../ui/fragment/PlayerLyricsFragment.java | 59 +++++++++++-------- 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerLyricsFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerLyricsFragment.java index 8abac3902..ce8f7f1af 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerLyricsFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerLyricsFragment.java @@ -36,6 +36,7 @@ import com.google.android.material.button.MaterialButton; import com.google.common.util.concurrent.MoreExecutors; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Objects; @@ -57,8 +58,6 @@ public class PlayerLyricsFragment extends Fragment { private LyricsList currentLyricsList; private String currentDescription; - private Map> lineMap; - @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { bind = InnerFragmentPlayerLyricsBinding.inflate(inflater, container, false); @@ -66,8 +65,6 @@ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, playerBottomSheetViewModel = new ViewModelProvider(requireActivity()).get(PlayerBottomSheetViewModel.class); - initLyricsLineMap(playerBottomSheetViewModel.getLiveLyricsList().getValue()); - initOverlay(); return view; @@ -119,16 +116,6 @@ public void onDestroyView() { currentDescription = null; } - private synchronized void initLyricsLineMap(LyricsList lyricsList){ - - if (hasStructuredLyrics(lyricsList)){ - lineMap = Objects.requireNonNull(Objects.requireNonNull(lyricsList.getStructuredLyrics()).get(0).getLine()) - .stream().filter(line -> Objects.nonNull(line) && Objects.nonNull(line.getStart())) - .collect(Collectors.groupingBy(line -> line.getStart() == null? 0 : line.getStart())); - } - - } - private void initOverlay() { bind.syncLyricsTapButton.setOnClickListener(view -> { playerBottomSheetViewModel.changeSyncLyricsState(); @@ -310,19 +297,9 @@ private void displaySyncedLyrics() { if (lines == null || lines.isEmpty()) return; - for (Line line : lines) { - lyricsBuilder.append(line.getValue().trim()).append("\n"); - } - - Optional currentLineStart = lineMap.keySet().stream().filter(start -> start!=null && start < timestamp ).max(Integer::compareTo); - - if (currentLineStart.isEmpty()){ - return; - } - - List currentLines = lineMap.get(currentLineStart.get()); + List currentLines = getLines(lines, lyricsBuilder, timestamp); - if (currentLines != null && !currentLines.isEmpty()) { + if (!currentLines.isEmpty()) { String lyrics = lyricsBuilder.toString(); Spannable spannableString = new SpannableString(lyrics); @@ -341,6 +318,36 @@ private void displaySyncedLyrics() { } } + @NonNull + private static List getLines(List lines, StringBuilder lyricsBuilder, int timestamp) { + int curTIme = 0; + int startIndex = 0, endIndex = 0; + + for (int i = 0; i < lines.size(); i++) { + Line line = lines.get(i); + lyricsBuilder.append(line.getValue().trim()).append("\n"); + if (line.getStart() == null){ + continue; + } + if ( line.getStart() < timestamp) { + if (curTIme < line.getStart()){ + curTIme = line.getStart(); + startIndex = i; + } + }else{ + if (endIndex == 0){ + endIndex = i; + } + } + } + + if (curTIme == 0 || endIndex == 0){ + return new ArrayList<>(); + } + + return lines.subList(startIndex, endIndex); + } + private int getStartPosition(List lines, Line toHighlight) { int start = 0; From 699588ca7febbafa8163a55b40ea0143f985939c Mon Sep 17 00:00:00 2001 From: sffzh Date: Wed, 14 Jan 2026 09:26:20 +0800 Subject: [PATCH 3/4] hightlight the first line only. --- .../ui/fragment/PlayerLyricsFragment.java | 72 +++++++++---------- 1 file changed, 35 insertions(+), 37 deletions(-) diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerLyricsFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerLyricsFragment.java index ce8f7f1af..5f1d7d0fa 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerLyricsFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerLyricsFragment.java @@ -38,10 +38,6 @@ import java.util.ArrayList; import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.stream.Collectors; @OptIn(markerClass = UnstableApi.class) @@ -237,11 +233,12 @@ private boolean hasText(String value) { } private boolean hasStructuredLyrics(LyricsList lyricsList) { - if (lyricsList == null - || lyricsList.getStructuredLyrics() == null - || lyricsList.getStructuredLyrics().isEmpty()) return false; - lyricsList.getStructuredLyrics(); - return lyricsList.getStructuredLyrics().get(0).getLine() != null && !Objects.requireNonNull(lyricsList.getStructuredLyrics().get(0).getLine()).isEmpty(); + return lyricsList != null + && lyricsList.getStructuredLyrics() != null + && !lyricsList.getStructuredLyrics().isEmpty() + && lyricsList.getStructuredLyrics().get(0) != null + && lyricsList.getStructuredLyrics().get(0).getLine() != null + && !lyricsList.getStructuredLyrics().get(0).getLine().isEmpty(); } @SuppressLint("DefaultLocale") @@ -293,18 +290,22 @@ private void displaySyncedLyrics() { if (hasStructuredLyrics(lyricsList)) { StringBuilder lyricsBuilder = new StringBuilder(); - List lines = Objects.requireNonNull(lyricsList.getStructuredLyrics()).get(0).getLine(); + List lines = lyricsList.getStructuredLyrics().get(0).getLine(); if (lines == null || lines.isEmpty()) return; - List currentLines = getLines(lines, lyricsBuilder, timestamp); + List curLines = getCurerntLyricsLine(lines, lyricsBuilder, timestamp); - if (!currentLines.isEmpty()) { + if (!curLines.isEmpty()) { + Line toHighlight = curLines.get(0); String lyrics = lyricsBuilder.toString(); Spannable spannableString = new SpannableString(lyrics); - int startingPosition = getStartPosition(lines, currentLines.get(0)); - int endingPosition = startingPosition + currentLines.stream().mapToInt(line -> line.getValue().length() + 1).sum() -1; + int startingPosition = getStartPosition(lines, toHighlight); + int endingPosition = startingPosition + toHighlight.getValue().length(); + if (curLines.size() >1){ + endingPosition = curLines.stream().mapToInt(line -> line.getValue().length() + 1).sum() + startingPosition -1; + } spannableString.setSpan(new ForegroundColorSpan(requireContext().getResources().getColor(R.color.shadowsLyricsTextColor, null)), 0, lyrics.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); spannableString.setSpan(new ForegroundColorSpan(requireContext().getResources().getColor(R.color.lyricsTextColor, null)), startingPosition, endingPosition, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); @@ -312,40 +313,38 @@ private void displaySyncedLyrics() { bind.nowPlayingSongLyricsTextView.setText(spannableString); if (playerBottomSheetViewModel.getSyncLyricsState()) { - bind.nowPlayingSongLyricsSrollView.smoothScrollTo(0, getScroll(lines, currentLines)); + bind.nowPlayingSongLyricsSrollView.smoothScrollTo(0, getScroll(lines, toHighlight)); } } } } @NonNull - private static List getLines(List lines, StringBuilder lyricsBuilder, int timestamp) { - int curTIme = 0; - int startIndex = 0, endIndex = 0; - + private List getCurerntLyricsLine(List lines, StringBuilder lyricsBuilder, int timestamp){ + lyricsBuilder.setLength(0); + int currentLineStart = 0; + int firstIndex = 0, lastIndex = 0; for (int i = 0; i < lines.size(); i++) { Line line = lines.get(i); lyricsBuilder.append(line.getValue().trim()).append("\n"); - if (line.getStart() == null){ + + if (line.getStart() == null || lastIndex > 0){ continue; } - if ( line.getStart() < timestamp) { - if (curTIme < line.getStart()){ - curTIme = line.getStart(); - startIndex = i; + + if (line.getStart() < timestamp) { + if (currentLineStart < line.getStart()){ + currentLineStart = line.getStart(); + firstIndex = i; } }else{ - if (endIndex == 0){ - endIndex = i; - } + lastIndex = i; } } - - if (curTIme == 0 || endIndex == 0){ - return new ArrayList<>(); + if (lastIndex == 0){ + return new ArrayList<>(0); } - - return lines.subList(startIndex, endIndex); + return lines.subList(firstIndex, lastIndex); } private int getStartPosition(List lines, Line toHighlight) { @@ -377,15 +376,14 @@ private int getLineCount(List lines, Line toHighlight) { return start; } - private int getScroll(List lines, List toHighlight) { - int startIndex = getStartPosition(lines, toHighlight.get(0)); + private int getScroll(List lines, Line toHighlight) { + int startIndex = getStartPosition(lines, toHighlight); Layout layout = bind.nowPlayingSongLyricsTextView.getLayout(); if (layout == null) return 0; int line = layout.getLineForOffset(startIndex); int lineTop = layout.getLineTop(line); - int lastLineNum = lines.size() ==1 ? line : layout.getLineForOffset(getStartPosition(lines, toHighlight.get(lines.size()-1))); - int lineBottom = layout.getLineBottom(lastLineNum); + int lineBottom = layout.getLineBottom(line); int lineCenter = (lineTop + lineBottom) / 2; int scrollViewHeight = bind.nowPlayingSongLyricsSrollView.getHeight(); @@ -393,4 +391,4 @@ private int getScroll(List lines, List toHighlight) { return Math.max(scroll, 0); } -} \ No newline at end of file +} From 0f18f80a4e34041aac0ea20207caeacb6ff5a370 Mon Sep 17 00:00:00 2001 From: sffzh Date: Wed, 14 Jan 2026 09:34:07 +0800 Subject: [PATCH 4/4] fix bug: lost the lastest lyrics line --- .../tempo/ui/fragment/PlayerLyricsFragment.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerLyricsFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerLyricsFragment.java index 5f1d7d0fa..1ef6f229e 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerLyricsFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerLyricsFragment.java @@ -342,7 +342,11 @@ private List getCurerntLyricsLine(List lines, StringBuilder lyricsBu } } if (lastIndex == 0){ - return new ArrayList<>(0); + if (firstIndex > 0){ + lastIndex = lines.size(); + }else{ + return new ArrayList<>(0); + } } return lines.subList(firstIndex, lastIndex); }