From 8a2b8032863ed98895e9ccad8e9516cf56241ef7 Mon Sep 17 00:00:00 2001 From: Hemanth S Date: Tue, 30 Jun 2020 23:15:47 +0530 Subject: [PATCH] Code refactor Added new lyrics view and code refactor --- .../code/name/monkey/retromusic/Constants.kt | 2 +- .../retromusic/activities/LyricsActivity.kt | 402 +----- .../activities/SupportDevelopmentActivity.kt | 2 +- .../retromusic/activities/UserInfoActivity.kt | 2 +- .../monkey/retromusic/adapter/HomeAdapter.kt | 3 +- .../adapter/album/AlbumCoverPagerAdapter.kt | 36 +- .../player/PlayerAlbumCoverFragment.kt | 1 - .../player/adaptive/AdaptiveFragment.kt | 104 +- .../monkey/retromusic/lyrics/LrcEntry.java | 117 ++ .../monkey/retromusic/lyrics/LrcUtils.java | 232 ++++ .../monkey/retromusic/lyrics/LrcView.java | 1175 +++++++++-------- .../monkey/retromusic/util/LyricUtil.java | 19 + .../retromusic/util/NavigationUtil.java | 4 +- .../monkey/retromusic/util/PreferenceUtil.kt | 7 +- app/src/main/res/layout/activity_lyrics.xml | 40 +- ...ment_adaptive_player_playback_controls.xml | 1 + app/src/main/res/menu/menu_search.xml | 9 + app/src/main/res/values/arrays.xml | 2 +- app/src/main/res/values/lrc_colors.xml | 8 + app/src/main/res/values/lrc_dimens.xml | 10 + app/src/main/res/values/lrc_view_attrs.xml | 39 +- app/src/main/res/values/strings.xml | 1 + 22 files changed, 1151 insertions(+), 1065 deletions(-) create mode 100644 app/src/main/java/code/name/monkey/retromusic/lyrics/LrcEntry.java create mode 100644 app/src/main/java/code/name/monkey/retromusic/lyrics/LrcUtils.java create mode 100644 app/src/main/res/menu/menu_search.xml create mode 100644 app/src/main/res/values/lrc_colors.xml create mode 100644 app/src/main/res/values/lrc_dimens.xml diff --git a/app/src/main/java/code/name/monkey/retromusic/Constants.kt b/app/src/main/java/code/name/monkey/retromusic/Constants.kt index 79e76a37..8dfe5112 100644 --- a/app/src/main/java/code/name/monkey/retromusic/Constants.kt +++ b/app/src/main/java/code/name/monkey/retromusic/Constants.kt @@ -21,7 +21,7 @@ object Constants { const val RATE_ON_GOOGLE_PLAY = "https://play.google.com/store/apps/details?id=code.name.monkey.retromusic" - const val TRANSLATE = "http://monkeycodeapp.oneskyapp.com/collaboration/project?id=238534" + const val TRANSLATE = "https://github.com/h4h13/RetroMusicPlayer" const val GITHUB_PROJECT = "https://github.com/h4h13/RetroMusicPlayer" const val TELEGRAM_CHANGE_LOG = "https://t.me/retromusiclog" const val USER_PROFILE = "profile.jpg" diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/LyricsActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/LyricsActivity.kt index 12e45b46..76bc7665 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/LyricsActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/LyricsActivity.kt @@ -1,72 +1,27 @@ package code.name.monkey.retromusic.activities -import android.R.attr -import android.annotation.SuppressLint -import android.content.res.ColorStateList -import android.os.AsyncTask -import android.os.Build import android.os.Bundle -import android.text.TextUtils -import android.view.* -import androidx.annotation.StringRes -import androidx.core.content.ContextCompat -import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentManager -import androidx.fragment.app.FragmentStatePagerAdapter -import androidx.viewpager.widget.ViewPager +import android.view.Menu +import android.view.MenuItem +import android.view.WindowManager import code.name.monkey.appthemehelper.ThemeStore -import code.name.monkey.appthemehelper.util.ATHUtil.resolveColor -import code.name.monkey.appthemehelper.util.ColorUtil -import code.name.monkey.appthemehelper.util.MaterialValueHelper -import code.name.monkey.appthemehelper.util.TintHelper import code.name.monkey.appthemehelper.util.ToolbarContentTintHelper -import code.name.monkey.retromusic.App import code.name.monkey.retromusic.R import code.name.monkey.retromusic.activities.base.AbsMusicServiceActivity import code.name.monkey.retromusic.extensions.surfaceColor -import code.name.monkey.retromusic.extensions.textColorSecondary -import code.name.monkey.retromusic.fragments.base.AbsMusicServiceFragment import code.name.monkey.retromusic.helper.MusicPlayerRemote import code.name.monkey.retromusic.helper.MusicProgressViewUpdateHelper -import code.name.monkey.retromusic.lyrics.LrcHelper +import code.name.monkey.retromusic.lyrics.LrcView import code.name.monkey.retromusic.model.Song -import code.name.monkey.retromusic.model.lyrics.Lyrics import code.name.monkey.retromusic.util.LyricUtil -import code.name.monkey.retromusic.util.MusicUtil -import code.name.monkey.retromusic.util.PreferenceUtil +import code.name.monkey.retromusic.util.RetroUtil import kotlinx.android.synthetic.main.activity_lyrics.* -import kotlinx.android.synthetic.main.fragment_lyrics.* -import kotlinx.android.synthetic.main.fragment_synced.* -import java.io.File -class LyricsActivity : AbsMusicServiceActivity(), View.OnClickListener, - ViewPager.OnPageChangeListener { - override fun onPageScrollStateChanged(state: Int) { - when (state) { - ViewPager.SCROLL_STATE_IDLE -> fab.show() - ViewPager.SCROLL_STATE_DRAGGING, - ViewPager.SCROLL_STATE_SETTLING -> fab.hide() - } - } +class LyricsActivity : AbsMusicServiceActivity(), MusicProgressViewUpdateHelper.Callback { + private lateinit var updateHelper: MusicProgressViewUpdateHelper - override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) { - } - - override fun onPageSelected(position: Int) { - PreferenceUtil.lyricsOption = position - if (position == 0) fab.text = getString(R.string.synced_lyrics) - else if (position == 1) fab.text = getString(R.string.lyrics) - } - - override fun onClick(v: View?) { - when (viewPager.currentItem) { - 0 -> showSyncedLyrics() - 1 -> showLyricsSaveDialog() - } - } private lateinit var song: Song - private var lyricsString: String? = null private val googleSearchLrcUrl: String get() { @@ -84,56 +39,63 @@ class LyricsActivity : AbsMusicServiceActivity(), View.OnClickListener, setTaskDescriptionColorAuto() setNavigationbarColorAuto() - fab.backgroundTintList = ColorStateList.valueOf(ThemeStore.accentColor(this)) - ColorStateList.valueOf( - MaterialValueHelper.getPrimaryTextColor( - this, - ColorUtil.isColorLight(ThemeStore.accentColor(this)) - ) - ) - .apply { - fab.setTextColor(this) - fab.iconTint = this - } setupWakelock() - viewPager.apply { - adapter = PagerAdapter(supportFragmentManager) - currentItem = PreferenceUtil.lyricsOption - addOnPageChangeListener(this@LyricsActivity) - } - - toolbar.setBackgroundColor(surfaceColor()) - tabs.setBackgroundColor(surfaceColor()) ToolbarContentTintHelper.colorBackButton(toolbar) setSupportActionBar(toolbar) - tabs.setupWithViewPager(viewPager) - tabs.setSelectedTabIndicator( - TintHelper.createTintedDrawable( - ContextCompat.getDrawable( - this, - R.drawable.tab_indicator - ), ThemeStore.accentColor(this) - ) - ) - tabs.setTabTextColors( - textColorSecondary(), - ThemeStore.accentColor(this) - ) - tabs.setSelectedTabIndicatorColor(ThemeStore.accentColor(this)) - fab.setOnClickListener(this) + updateHelper = MusicProgressViewUpdateHelper(this, 500, 1000) + setupLyricsView() + } + + private fun setupLyricsView() { + lyricsView.apply { + setCurrentColor(ThemeStore.accentColor(context)) + setTimeTextColor(ThemeStore.accentColor(context)) + setTimelineColor(ThemeStore.accentColor(context)) + setTimelineTextColor(ThemeStore.accentColor(context)) + setDraggable(true, LrcView.OnPlayClickListener { + MusicPlayerRemote.seekTo(it.toInt()) + return@OnPlayClickListener true + }) + } + } + + override fun onResume() { + super.onResume() + updateHelper.start() + } + + override fun onPause() { + super.onPause() + updateHelper.stop() + } + + override fun onUpdateProgressViews(progress: Int, total: Int) { + lyricsView.updateTime(progress.toLong()) + } + + private fun loadLRCLyrics() { + lyricsView.setLabel("Empty") + val song = MusicPlayerRemote.currentSong + if (LyricUtil.isLrcOriginalFileExist(song.data)) { + lyricsView.loadLrc(LyricUtil.getLocalLyricOriginalFile(song.data)) + } else if (LyricUtil.isLrcFileExist(song.title, song.artistName)) { + lyricsView.loadLrc(LyricUtil.getLocalLyricFile(song.title, song.artistName)) + } } override fun onPlayingMetaChanged() { super.onPlayingMetaChanged() updateTitleSong() + loadLRCLyrics() } override fun onServiceConnected() { super.onServiceConnected() updateTitleSong() + loadLRCLyrics() } private fun updateTitleSong() { @@ -146,269 +108,19 @@ class LyricsActivity : AbsMusicServiceActivity(), View.OnClickListener, window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) } + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.menu_search, menu) + return super.onCreateOptionsMenu(menu) + } + override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == android.R.id.home) { finish() return true } + if (item.itemId == R.id.action_search) { + RetroUtil.openUrl(this, googleSearchLrcUrl) + } return super.onOptionsItemSelected(item) } - - private fun showSyncedLyrics() { - var content = "" - try { - content = LyricUtil.getStringFromFile(song.title, song.artistName) - } catch (e: Exception) { - e.printStackTrace() - } - /*val materialDialog = MaterialDialog(this) - .show { - title(R.string.add_time_framed_lryics) - negativeButton(R.string.action_search) { - RetroUtil.openUrl(this@LyricsActivity, googleSearchLrcUrl) - } - input( - hint = getString(R.string.paste_lyrics_here), - prefill = content, - inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE - ) { _, input -> - LyricUtil.writeLrcToLoc(song.title, song.artistName, input.toString()) - } - positiveButton(android.R.string.ok) { - updateSong() - } - } - - MaterialUtil.setTint(materialDialog.getInputLayout(), false)*/ - } - - private fun updateSong() { - val page = - supportFragmentManager.findFragmentByTag("android:switcher:" + R.id.viewPager + ":" + viewPager.currentItem) - if (viewPager.currentItem == 0 && page != null) { - (page as BaseLyricsFragment).upDateSong() - } - } - - private fun showLyricsSaveDialog() { - val content: String = if (lyricsString == null) { - "" - } else { - lyricsString!! - } - - /*val materialDialog = MaterialDialog( - this - ).show { - - title(R.string.add_lyrics) - negativeButton(R.string.action_search) { - RetroUtil.openUrl(this@LyricsActivity, getGoogleSearchUrl()) - } - input( - hint = getString(R.string.paste_lyrics_here), - prefill = content, - inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE - ) { _, input -> - val fieldKeyValueMap = EnumMap(FieldKey::class.java) - fieldKeyValueMap[FieldKey.LYRICS] = input.toString() - WriteTagsAsyncTask(this@LyricsActivity).execute( - WriteTagsAsyncTask.LoadingInfo( - getSongPaths(song), fieldKeyValueMap, null - ) - ) - } - positiveButton(android.R.string.ok) { - updateSong() - } - } - MaterialUtil.setTint(materialDialog.getInputLayout(), false)*/ - } - - private fun getSongPaths(song: Song): ArrayList { - val paths = ArrayList(1) - paths.add(song.data) - return paths - } - - private fun getGoogleSearchUrl(): String { - var baseUrl = "http://www.google.com/search?" - var query = song.title + "+" + song.artistName - query = "q=" + query.replace(" ", "+") + " lyrics" - baseUrl += query - return baseUrl - } - - class PagerAdapter(fm: FragmentManager) : FragmentStatePagerAdapter( - fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT - ) { - - class Tabs( - @StringRes val title: Int, val fragment: Fragment - ) - - private var tabs = ArrayList() - - init { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { - tabs.add(Tabs(R.string.synced_lyrics, SyncedLyricsFragment())) - } - tabs.add(Tabs(R.string.normal_lyrics, OfflineLyricsFragment())) - } - - override fun getItem(position: Int): Fragment { - return tabs[position].fragment - } - - override fun getPageTitle(position: Int): CharSequence? { - return App.getContext().getString(tabs[position].title) - } - - override fun getCount(): Int { - return tabs.size - } - } - - abstract class BaseLyricsFragment : AbsMusicServiceFragment() { - abstract fun upDateSong() - - override fun onPlayingMetaChanged() { - super.onPlayingMetaChanged() - upDateSong() - } - - override fun onServiceConnected() { - super.onServiceConnected() - upDateSong() - } - } - - class OfflineLyricsFragment : BaseLyricsFragment() { - override fun upDateSong() { - loadSongLyrics() - } - - private var updateLyricsAsyncTask: AsyncTask<*, *, *>? = null - private var lyrics: Lyrics? = null - - @SuppressLint("StaticFieldLeak") - private fun loadSongLyrics() { - if (updateLyricsAsyncTask != null) { - updateLyricsAsyncTask!!.cancel(false) - } - val song = MusicPlayerRemote.currentSong - updateLyricsAsyncTask = object : AsyncTask() { - override fun doInBackground(vararg params: Void?): Lyrics? { - val data = MusicUtil.getLyrics(song) - return if (TextUtils.isEmpty(data)) { - null - } else Lyrics.parse(song, data!!) - } - - override fun onPreExecute() { - super.onPreExecute() - lyrics = null - } - - override fun onPostExecute(l: Lyrics?) { - lyrics = l - offlineLyrics?.visibility = View.VISIBLE - if (l == null) { - offlineLyrics?.setText(R.string.no_lyrics_found) - return - } - (activity as LyricsActivity).lyricsString = l.text - offlineLyrics?.text = l.text - } - - override fun onCancelled(s: Lyrics?) { - onPostExecute(null) - } - }.execute() - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - loadSongLyrics() - } - - override fun onDestroyView() { - super.onDestroyView() - if (updateLyricsAsyncTask != null && !updateLyricsAsyncTask!!.isCancelled) { - updateLyricsAsyncTask?.cancel(true) - } - } - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { - return inflater.inflate(R.layout.fragment_lyrics, container, false) - } - } - - class SyncedLyricsFragment : BaseLyricsFragment(), MusicProgressViewUpdateHelper.Callback { - override fun upDateSong() { - loadLRCLyrics() - } - - private lateinit var updateHelper: MusicProgressViewUpdateHelper - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { - return inflater.inflate(R.layout.fragment_synced, container, false) - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - updateHelper = MusicProgressViewUpdateHelper(this, 500, 1000) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - setupLyricsView() - } - - private fun setupLyricsView() { - lyricsView.apply { - setCurrentPlayLineColor(ThemeStore.accentColor(requireContext())) - setIndicatorTextColor(ThemeStore.accentColor(requireContext())) - setCurrentIndicateLineTextColor( - resolveColor( - requireContext(), - attr.textColorPrimary - ) - ) - setNoLrcTextColor(resolveColor(requireContext(), attr.textColorPrimary)) - setOnPlayIndicatorLineListener { time, _ -> MusicPlayerRemote.seekTo(time.toInt()) } - } - } - - override fun onResume() { - super.onResume() - updateHelper.start() - } - - override fun onPause() { - super.onPause() - updateHelper.stop() - } - - override fun onUpdateProgressViews(progress: Int, total: Int) { - lyricsView.updateTime(progress.toLong()) - } - - private fun loadLRCLyrics() { - lyricsView.resetView("Empty") - val song = MusicPlayerRemote.currentSong - if (LyricUtil.isLrcFileExist(song.title, song.artistName)) { - showLyricsLocal(LyricUtil.getLocalLyricFile(song.title, song.artistName)) - } - } - - private fun showLyricsLocal(file: File?) { - if (file != null) { - lyricsView.setLrcData(LrcHelper.parseLrcFromFile(file)) - } - } - } -} +} \ No newline at end of file diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/SupportDevelopmentActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/SupportDevelopmentActivity.kt index 9dcec81b..7cea20fa 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/SupportDevelopmentActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/SupportDevelopmentActivity.kt @@ -201,7 +201,7 @@ class SkuDetailsAdapter( override fun onBindViewHolder(viewHolder: ViewHolder, i: Int) { val skuDetails = skuDetailsList[i] - viewHolder.title.text = skuDetails.title.replace("(Retro Music Player \uD83C\uDFB5)", "") + viewHolder.title.text = skuDetails.title.replace("Music Player - MP3 Player - Retro", "") .trim { it <= ' ' } viewHolder.text.text = skuDetails.description viewHolder.text.visibility = View.GONE diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/UserInfoActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/UserInfoActivity.kt index 5e1e8bba..1bcd78ef 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/UserInfoActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/UserInfoActivity.kt @@ -63,7 +63,7 @@ class UserInfoActivity : AbsBaseActivity() { next.setOnClickListener { val nameString = name.text.toString().trim { it <= ' ' } if (TextUtils.isEmpty(nameString)) { - Toast.makeText(this, "Umm name is empty", Toast.LENGTH_SHORT).show() + Toast.makeText(this, "Umm you're name can't be empty!", Toast.LENGTH_SHORT).show() return@setOnClickListener } PreferenceUtil.userName = nameString diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/HomeAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/HomeAdapter.kt index 0dcfb0a0..e881b1a9 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/HomeAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/HomeAdapter.kt @@ -178,5 +178,4 @@ private fun ArrayList.toPlaylist(): ArrayList { arrayList.add(x as Playlist) } return arrayList -} - +} \ No newline at end of file diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/album/AlbumCoverPagerAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/album/AlbumCoverPagerAdapter.kt index a504cc81..b2c844e6 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/album/AlbumCoverPagerAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/album/AlbumCoverPagerAdapter.kt @@ -14,11 +14,16 @@ import code.name.monkey.retromusic.glide.RetroMusicColoredTarget import code.name.monkey.retromusic.glide.SongGlideRequest import code.name.monkey.retromusic.misc.CustomFragmentStatePagerAdapter import code.name.monkey.retromusic.model.Song +import code.name.monkey.retromusic.util.MusicUtil import code.name.monkey.retromusic.util.NavigationUtil - import code.name.monkey.retromusic.util.PreferenceUtil import code.name.monkey.retromusic.util.color.MediaNotificationProcessor import com.bumptech.glide.Glide +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext class AlbumCoverPagerAdapter( fragmentManager: FragmentManager, @@ -85,20 +90,33 @@ class AlbumCoverPagerAdapter( val view = inflater.inflate(getLayoutWithPlayerTheme(), container, false) albumCover = view.findViewById(R.id.player_image) albumCover.setOnClickListener { - NavigationUtil.goToLyrics(requireActivity()) + showLyricsDialog() } return view } + private fun showLyricsDialog() { + GlobalScope.launch(Dispatchers.IO) { + val data = MusicUtil.getLyrics(song) + withContext(Dispatchers.Main) { + MaterialAlertDialogBuilder( + requireContext(), + R.style.ThemeOverlay_MaterialComponents_Dialog_Alert + ).apply { + setTitle(song.title) + setMessage(data) + setNegativeButton(R.string.synced_lyrics) { _, _ -> + NavigationUtil.goToLyrics(requireActivity()) + } + show() + } + } + } + } + private fun getLayoutWithPlayerTheme(): Int { return when (PreferenceUtil.nowPlayingScreen) { - Card, - Fit, - Tiny, - Classic, - Peak, - Gradient, - Full -> R.layout.fragment_album_full_cover + Card, Fit, Tiny, Classic, Peak, Gradient, Full -> R.layout.fragment_album_full_cover else -> { if (PreferenceUtil.isCarouselEffect) { R.layout.fragment_album_carousel_cover diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/player/PlayerAlbumCoverFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/player/PlayerAlbumCoverFragment.kt index cdab3015..7a2c4249 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/player/PlayerAlbumCoverFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/player/PlayerAlbumCoverFragment.kt @@ -33,7 +33,6 @@ class PlayerAlbumCoverFragment : AbsMusicServiceFragment(), ViewPager.OnPageChan fun removeSlideEffect() { val transformer = ParallaxPagerTransformer(R.id.player_image) transformer.setSpeed(0.3f) - //viewPager.setPageTransformer(true, transformer) } override fun onCreateView( diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/player/adaptive/AdaptiveFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/player/adaptive/AdaptiveFragment.kt index eb73da8a..b50d34f7 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/player/adaptive/AdaptiveFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/player/adaptive/AdaptiveFragment.kt @@ -4,8 +4,6 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.FrameLayout -import android.widget.TextView import androidx.appcompat.widget.Toolbar import code.name.monkey.appthemehelper.util.ATHUtil import code.name.monkey.appthemehelper.util.ToolbarContentTintHelper @@ -16,104 +14,11 @@ import code.name.monkey.retromusic.extensions.textColorSecondary import code.name.monkey.retromusic.fragments.base.AbsPlayerFragment import code.name.monkey.retromusic.fragments.player.PlayerAlbumCoverFragment import code.name.monkey.retromusic.helper.MusicPlayerRemote -import code.name.monkey.retromusic.helper.MusicProgressViewUpdateHelper import code.name.monkey.retromusic.model.Song -import code.name.monkey.retromusic.model.lyrics.AbsSynchronizedLyrics -import code.name.monkey.retromusic.model.lyrics.Lyrics import code.name.monkey.retromusic.util.color.MediaNotificationProcessor import kotlinx.android.synthetic.main.fragment_adaptive_player.* -class AdaptiveFragment : AbsPlayerFragment(), MusicProgressViewUpdateHelper.Callback { - - private lateinit var lyricsLayout: FrameLayout - private lateinit var lyricsLine1: TextView - private lateinit var lyricsLine2: TextView - - private var lyrics: Lyrics? = null - private lateinit var progressViewUpdateHelper: MusicProgressViewUpdateHelper - - override fun onUpdateProgressViews(progress: Int, total: Int) { - if (!isLyricsLayoutBound()) return - - if (!isLyricsLayoutVisible()) { - hideLyricsLayout() - return - } - - if (lyrics !is AbsSynchronizedLyrics) return - val synchronizedLyrics = lyrics as AbsSynchronizedLyrics - - lyricsLayout.visibility = View.VISIBLE - lyricsLayout.alpha = 1f - - val oldLine = lyricsLine2.text.toString() - val line = synchronizedLyrics.getLine(progress) - - if (oldLine != line || oldLine.isEmpty()) { - lyricsLine1.text = oldLine - lyricsLine2.text = line - - lyricsLine1.visibility = View.VISIBLE - lyricsLine2.visibility = View.VISIBLE - - lyricsLine2.measure( - View.MeasureSpec.makeMeasureSpec( - lyricsLine2.measuredWidth, - View.MeasureSpec.EXACTLY - ), - View.MeasureSpec.UNSPECIFIED - ) - val h: Float = lyricsLine2.measuredHeight.toFloat() - - lyricsLine1.alpha = 1f - lyricsLine1.translationY = 0f - lyricsLine1.animate().alpha(0f).translationY(-h).duration = VISIBILITY_ANIM_DURATION - - lyricsLine2.alpha = 0f - lyricsLine2.translationY = h - lyricsLine2.animate().alpha(1f).translationY(0f).duration = VISIBILITY_ANIM_DURATION - } - } - - private fun isLyricsLayoutVisible(): Boolean { - return lyrics != null && lyrics!!.isSynchronized && lyrics!!.isValid - } - - private fun isLyricsLayoutBound(): Boolean { - return lyricsLayout != null && lyricsLine1 != null && lyricsLine2 != null - } - - private fun hideLyricsLayout() { - lyricsLayout.animate().alpha(0f).setDuration(VISIBILITY_ANIM_DURATION) - .withEndAction(Runnable { - if (!isLyricsLayoutBound()) return@Runnable - lyricsLayout.visibility = View.GONE - lyricsLine1.text = null - lyricsLine2.text = null - }) - } - - override fun setLyrics(l: Lyrics?) { - lyrics = l - - if (!isLyricsLayoutBound()) return - - if (!isLyricsLayoutVisible()) { - hideLyricsLayout() - return - } - - lyricsLine1.text = null - lyricsLine2.text = null - - lyricsLayout.visibility = View.VISIBLE - lyricsLayout.animate().alpha(1f).duration = VISIBILITY_ANIM_DURATION - } - - override fun onDestroyView() { - super.onDestroyView() - progressViewUpdateHelper.stop() - } +class AdaptiveFragment : AbsPlayerFragment() { override fun playerToolbar(): Toolbar { return playerToolbar @@ -132,15 +37,8 @@ class AdaptiveFragment : AbsPlayerFragment(), MusicProgressViewUpdateHelper.Call override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - lyricsLayout = view.findViewById(R.id.player_lyrics) - lyricsLine1 = view.findViewById(R.id.player_lyrics_line1) - lyricsLine2 = view.findViewById(R.id.player_lyrics_line2) - setUpSubFragments() setUpPlayerToolbar() - - progressViewUpdateHelper = MusicProgressViewUpdateHelper(this, 500, 1000) - progressViewUpdateHelper.start() } private fun setUpSubFragments() { diff --git a/app/src/main/java/code/name/monkey/retromusic/lyrics/LrcEntry.java b/app/src/main/java/code/name/monkey/retromusic/lyrics/LrcEntry.java new file mode 100644 index 00000000..6b5991e1 --- /dev/null +++ b/app/src/main/java/code/name/monkey/retromusic/lyrics/LrcEntry.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2017 wangchenyan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package code.name.monkey.retromusic.lyrics; + +import android.text.Layout; +import android.text.StaticLayout; +import android.text.TextPaint; +import android.text.TextUtils; + +/** + * 一行歌词实体 + */ +class LrcEntry implements Comparable { + private long time; + private String text; + private String secondText; + private StaticLayout staticLayout; + /** + * 歌词距离视图顶部的距离 + */ + private float offset = Float.MIN_VALUE; + public static final int GRAVITY_CENTER = 0; + public static final int GRAVITY_LEFT = 1; + public static final int GRAVITY_RIGHT = 2; + + LrcEntry(long time, String text) { + this.time = time; + this.text = text; + } + + LrcEntry(long time, String text, String secondText) { + this.time = time; + this.text = text; + this.secondText = secondText; + } + + void init(TextPaint paint, int width, int gravity) { + Layout.Alignment align; + switch (gravity) { + case GRAVITY_LEFT: + align = Layout.Alignment.ALIGN_NORMAL; + break; + + default: + case GRAVITY_CENTER: + align = Layout.Alignment.ALIGN_CENTER; + break; + + case GRAVITY_RIGHT: + align = Layout.Alignment.ALIGN_OPPOSITE; + break; + } + staticLayout = new StaticLayout(getShowText(), paint, width, align, 1f, 0f, false); + + offset = Float.MIN_VALUE; + } + + long getTime() { + return time; + } + + StaticLayout getStaticLayout() { + return staticLayout; + } + + int getHeight() { + if (staticLayout == null) { + return 0; + } + return staticLayout.getHeight(); + } + + public float getOffset() { + return offset; + } + + public void setOffset(float offset) { + this.offset = offset; + } + + String getText() { + return text; + } + + + void setSecondText(String secondText) { + this.secondText = secondText; + } + + private String getShowText() { + if (!TextUtils.isEmpty(secondText)) { + return text + "\n" + secondText; + } else { + return text; + } + } + + @Override + public int compareTo(LrcEntry entry) { + if (entry == null) { + return -1; + } + return (int) (time - entry.getTime()); + } +} \ No newline at end of file diff --git a/app/src/main/java/code/name/monkey/retromusic/lyrics/LrcUtils.java b/app/src/main/java/code/name/monkey/retromusic/lyrics/LrcUtils.java new file mode 100644 index 00000000..20f147a7 --- /dev/null +++ b/app/src/main/java/code/name/monkey/retromusic/lyrics/LrcUtils.java @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2017 wangchenyan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package code.name.monkey.retromusic.lyrics; + +import android.animation.ValueAnimator; +import android.text.TextUtils; +import android.text.format.DateUtils; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.reflect.Field; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 工具类 + */ +class LrcUtils { + private static final Pattern PATTERN_LINE = Pattern.compile("((\\[\\d\\d:\\d\\d\\.\\d{2,3}\\])+)(.+)"); + private static final Pattern PATTERN_TIME = Pattern.compile("\\[(\\d\\d):(\\d\\d)\\.(\\d{2,3})\\]"); + + /** + * 从文件解析双语歌词 + */ + static List parseLrc(File[] lrcFiles) { + if (lrcFiles == null || lrcFiles.length != 2 || lrcFiles[0] == null) { + return null; + } + + File mainLrcFile = lrcFiles[0]; + File secondLrcFile = lrcFiles[1]; + List mainEntryList = parseLrc(mainLrcFile); + List secondEntryList = parseLrc(secondLrcFile); + + if (mainEntryList != null && secondEntryList != null) { + for (LrcEntry mainEntry : mainEntryList) { + for (LrcEntry secondEntry : secondEntryList) { + if (mainEntry.getTime() == secondEntry.getTime()) { + mainEntry.setSecondText(secondEntry.getText()); + } + } + } + } + return mainEntryList; + } + + /** + * 从文件解析歌词 + */ + private static List parseLrc(File lrcFile) { + if (lrcFile == null || !lrcFile.exists()) { + return null; + } + + List entryList = new ArrayList<>(); + try { + BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(lrcFile), "utf-8")); + String line; + while ((line = br.readLine()) != null) { + List list = parseLine(line); + if (list != null && !list.isEmpty()) { + entryList.addAll(list); + } + } + br.close(); + } catch (IOException e) { + e.printStackTrace(); + } + + Collections.sort(entryList); + return entryList; + } + + /** + * 从文本解析双语歌词 + */ + static List parseLrc(String[] lrcTexts) { + if (lrcTexts == null || lrcTexts.length != 2 || TextUtils.isEmpty(lrcTexts[0])) { + return null; + } + + String mainLrcText = lrcTexts[0]; + String secondLrcText = lrcTexts[1]; + List mainEntryList = parseLrc(mainLrcText); + List secondEntryList = parseLrc(secondLrcText); + + if (mainEntryList != null && secondEntryList != null) { + for (LrcEntry mainEntry : mainEntryList) { + for (LrcEntry secondEntry : secondEntryList) { + if (mainEntry.getTime() == secondEntry.getTime()) { + mainEntry.setSecondText(secondEntry.getText()); + } + } + } + } + return mainEntryList; + } + + /** + * 从文本解析歌词 + */ + private static List parseLrc(String lrcText) { + if (TextUtils.isEmpty(lrcText)) { + return null; + } + + if (lrcText.startsWith("\uFEFF")) { + lrcText = lrcText.replace("\uFEFF", ""); + } + + List entryList = new ArrayList<>(); + String[] array = lrcText.split("\\n"); + for (String line : array) { + List list = parseLine(line); + if (list != null && !list.isEmpty()) { + entryList.addAll(list); + } + } + + Collections.sort(entryList); + return entryList; + } + + /** + * 获取网络文本,需要在工作线程中执行 + */ + static String getContentFromNetwork(String url, String charset) { + String lrcText = null; + try { + URL _url = new URL(url); + HttpURLConnection conn = (HttpURLConnection) _url.openConnection(); + conn.setRequestMethod("GET"); + conn.setConnectTimeout(10000); + conn.setReadTimeout(10000); + if (conn.getResponseCode() == 200) { + InputStream is = conn.getInputStream(); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int len; + while ((len = is.read(buffer)) != -1) { + bos.write(buffer, 0, len); + } + is.close(); + bos.close(); + lrcText = bos.toString(charset); + } + } catch (Exception e) { + e.printStackTrace(); + } + return lrcText; + } + + /** + * 解析一行歌词 + */ + private static List parseLine(String line) { + if (TextUtils.isEmpty(line)) { + return null; + } + + line = line.trim(); + // [00:17.65]让我掉下眼泪的 + Matcher lineMatcher = PATTERN_LINE.matcher(line); + if (!lineMatcher.matches()) { + return null; + } + + String times = lineMatcher.group(1); + String text = lineMatcher.group(3); + List entryList = new ArrayList<>(); + + // [00:17.65] + Matcher timeMatcher = PATTERN_TIME.matcher(times); + while (timeMatcher.find()) { + long min = Long.parseLong(timeMatcher.group(1)); + long sec = Long.parseLong(timeMatcher.group(2)); + String milString = timeMatcher.group(3); + long mil = Long.parseLong(milString); + // 如果毫秒是两位数,需要乘以10 + if (milString.length() == 2) { + mil = mil * 10; + } + long time = min * DateUtils.MINUTE_IN_MILLIS + sec * DateUtils.SECOND_IN_MILLIS + mil; + entryList.add(new LrcEntry(time, text)); + } + return entryList; + } + + /** + * 转为[分:秒] + */ + static String formatTime(long milli) { + int m = (int) (milli / DateUtils.MINUTE_IN_MILLIS); + int s = (int) ((milli / DateUtils.SECOND_IN_MILLIS) % 60); + String mm = String.format(Locale.getDefault(), "%02d", m); + String ss = String.format(Locale.getDefault(), "%02d", s); + return mm + ":" + ss; + } + + static void resetDurationScale() { + try { + Field mField = ValueAnimator.class.getDeclaredField("sDurationScale"); + mField.setAccessible(true); + mField.setFloat(null, 1); + } catch (Exception e) { + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/code/name/monkey/retromusic/lyrics/LrcView.java b/app/src/main/java/code/name/monkey/retromusic/lyrics/LrcView.java index 9da2ce30..319512c1 100644 --- a/app/src/main/java/code/name/monkey/retromusic/lyrics/LrcView.java +++ b/app/src/main/java/code/name/monkey/retromusic/lyrics/LrcView.java @@ -1,663 +1,750 @@ +/* + * Copyright (C) 2017 wangchenyan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + package code.name.monkey.retromusic.lyrics; import android.animation.ValueAnimator; +import android.annotation.SuppressLint; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; -import android.graphics.Color; import android.graphics.Paint; -import android.graphics.Rect; import android.graphics.drawable.Drawable; +import android.os.AsyncTask; import android.os.Looper; import android.text.Layout; import android.text.StaticLayout; import android.text.TextPaint; +import android.text.TextUtils; +import android.text.format.DateUtils; import android.util.AttributeSet; -import android.util.TypedValue; +import android.view.GestureDetector; import android.view.MotionEvent; -import android.view.VelocityTracker; import android.view.View; -import android.view.ViewConfiguration; -import android.view.animation.DecelerateInterpolator; -import android.widget.OverScroller; +import android.view.animation.LinearInterpolator; +import android.widget.Scroller; -import androidx.annotation.ColorInt; -import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; -import androidx.core.view.ViewCompat; - -import java.util.HashMap; +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import code.name.monkey.retromusic.R; /** - * Desc : 歌词 - * Author : Lauzy - * Date : 2017/10/13 - * Blog : http://www.jianshu.com/u/e76853f863a9 - * Email : freedompaladin@gmail.com + * 歌词 + * Created by wcy on 2015/11/9. */ +@SuppressLint("StaticFieldLeak") public class LrcView extends View { + private static final long ADJUST_DURATION = 100; + private static final long TIMELINE_KEEP_TIME = 4 * DateUtils.SECOND_IN_MILLIS; - private static final String DEFAULT_CONTENT = "Empty"; - private List mLrcData; - private TextPaint mTextPaint; - private String mDefaultContent; - private int mCurrentLine; - private float mOffset; - private float mLastMotionX; - private float mLastMotionY; - private int mScaledTouchSlop; - private OverScroller mOverScroller; - private VelocityTracker mVelocityTracker; - private int mMaximumFlingVelocity; - private int mMinimumFlingVelocity; - private float mLrcTextSize; - private float mLrcLineSpaceHeight; - private int mTouchDelay; - private int mNormalColor; - private int mCurrentPlayLineColor; - private float mNoLrcTextSize; - private int mNoLrcTextColor; - //是否拖拽中,否的话响应onClick事件 - private boolean isDragging; - //用户开始操作 - private boolean isUserScroll; - private boolean isAutoAdjustPosition = true; + private List mLrcEntryList = new ArrayList<>(); + private TextPaint mLrcPaint = new TextPaint(); + private TextPaint mTimePaint = new TextPaint(); + private Paint.FontMetrics mTimeFontMetrics; private Drawable mPlayDrawable; - private boolean isShowTimeIndicator; - private Rect mPlayRect; - private Paint mIndicatorPaint; - private float mIndicatorLineWidth; - private float mIndicatorTextSize; - private int mCurrentIndicateLineTextColor; - private int mIndicatorLineColor; - private float mIndicatorMargin; - private float mIconLineGap; - private float mIconWidth; - private float mIconHeight; - private boolean isEnableShowIndicator = true; - private int mIndicatorTextColor; - private int mIndicatorTouchDelay; - private boolean isCurrentTextBold; - private boolean isLrcIndicatorTextBold; - private HashMap mLrcMap = new HashMap<>(); - private Runnable mHideIndicatorRunnable = new Runnable() { + private float mDividerHeight; + private long mAnimationDuration; + private int mNormalTextColor; + private float mNormalTextSize; + private int mCurrentTextColor; + private float mCurrentTextSize; + private int mTimelineTextColor; + private int mTimelineColor; + private int mTimeTextColor; + private int mDrawableWidth; + private int mTimeTextWidth; + private String mDefaultLabel; + private float mLrcPadding; + private OnPlayClickListener mOnPlayClickListener; + private ValueAnimator mAnimator; + private GestureDetector mGestureDetector; + private Scroller mScroller; + private float mOffset; + private int mCurrentLine; + private Object mFlag; + private boolean isShowTimeline; + private boolean isTouching; + private boolean isFling; + private int mTextGravity;//歌词显示位置,靠左/居中/靠右 + private Runnable hideTimelineRunnable = new Runnable() { @Override public void run() { - isShowTimeIndicator = false; - invalidateView(); + if (hasLrc() && isShowTimeline) { + isShowTimeline = false; + smoothScrollTo(mCurrentLine); + } } }; - private HashMap mStaticLayoutHashMap = new HashMap<>(); - private Runnable mScrollRunnable = new Runnable() { - @Override - public void run() { - isUserScroll = false; - scrollToPosition(mCurrentLine); - } - }; - private OnPlayIndicatorLineListener mOnPlayIndicatorLineListener; public LrcView(Context context) { this(context, null); } - public LrcView(Context context, @Nullable AttributeSet attrs) { + /** + * 手势监听器 + */ + private GestureDetector.SimpleOnGestureListener mSimpleOnGestureListener = new GestureDetector.SimpleOnGestureListener() { + @Override + public boolean onDown(MotionEvent e) { + if (hasLrc() && mOnPlayClickListener != null) { + mScroller.forceFinished(true); + removeCallbacks(hideTimelineRunnable); + isTouching = true; + isShowTimeline = true; + invalidate(); + return true; + } + return super.onDown(e); + } + + @Override + public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { + if (hasLrc()) { + mOffset += -distanceY; + mOffset = Math.min(mOffset, getOffset(0)); + mOffset = Math.max(mOffset, getOffset(mLrcEntryList.size() - 1)); + invalidate(); + return true; + } + return super.onScroll(e1, e2, distanceX, distanceY); + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + if (hasLrc()) { + mScroller.fling(0, (int) mOffset, 0, (int) velocityY, 0, 0, (int) getOffset(mLrcEntryList.size() - 1), (int) getOffset(0)); + isFling = true; + return true; + } + return super.onFling(e1, e2, velocityX, velocityY); + } + + @Override + public boolean onSingleTapConfirmed(MotionEvent e) { + if (hasLrc() && isShowTimeline && mPlayDrawable.getBounds().contains((int) e.getX(), (int) e.getY())) { + int centerLine = getCenterLine(); + long centerLineTime = mLrcEntryList.get(centerLine).getTime(); + // onPlayClick 消费了才更新 UI + if (mOnPlayClickListener != null && mOnPlayClickListener.onPlayClick(centerLineTime)) { + isShowTimeline = false; + removeCallbacks(hideTimelineRunnable); + mCurrentLine = centerLine; + invalidate(); + return true; + } + } + return super.onSingleTapConfirmed(e); + } + }; + + public LrcView(Context context, AttributeSet attrs) { this(context, attrs, 0); } - public LrcView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + public LrcView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); - init(context, attrs); + init(attrs); } - private void init(Context context, AttributeSet attrs) { + private void init(AttributeSet attrs) { + TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.LrcView); + mCurrentTextSize = ta.getDimension(R.styleable.LrcView_lrcTextSize, getResources().getDimension(R.dimen.lrc_text_size)); + mNormalTextSize = ta.getDimension(R.styleable.LrcView_lrcNormalTextSize, getResources().getDimension(R.dimen.lrc_text_size)); + if (mNormalTextSize == 0) { + mNormalTextSize = mCurrentTextSize; + } - TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.LrcView); - mLrcTextSize = typedArray.getDimension(R.styleable.LrcView_lrcTextSize, sp2px(context, 15)); - mLrcLineSpaceHeight = typedArray.getDimension(R.styleable.LrcView_lrcLineSpaceSize, dp2px(context, 20)); - mTouchDelay = typedArray.getInt(R.styleable.LrcView_lrcTouchDelay, 3500); - mIndicatorTouchDelay = typedArray.getInt(R.styleable.LrcView_indicatorTouchDelay, 2500); - mNormalColor = typedArray.getColor(R.styleable.LrcView_lrcNormalTextColor, Color.GRAY); - mCurrentPlayLineColor = typedArray.getColor(R.styleable.LrcView_lrcCurrentTextColor, Color.BLUE); - mNoLrcTextSize = typedArray.getDimension(R.styleable.LrcView_noLrcTextSize, dp2px(context, 20)); - mNoLrcTextColor = typedArray.getColor(R.styleable.LrcView_noLrcTextColor, Color.BLACK); - mIndicatorLineWidth = typedArray.getDimension(R.styleable.LrcView_indicatorLineHeight, dp2px(context, 0.5f)); - mIndicatorTextSize = typedArray.getDimension(R.styleable.LrcView_indicatorTextSize, sp2px(context, 13)); - mIndicatorTextColor = typedArray.getColor(R.styleable.LrcView_indicatorTextColor, Color.GRAY); - mCurrentIndicateLineTextColor = typedArray.getColor(R.styleable.LrcView_currentIndicateLrcColor, Color.GRAY); - mIndicatorLineColor = typedArray.getColor(R.styleable.LrcView_indicatorLineColor, Color.GRAY); - mIndicatorMargin = typedArray.getDimension(R.styleable.LrcView_indicatorStartEndMargin, dp2px(context, 5)); - mIconLineGap = typedArray.getDimension(R.styleable.LrcView_iconLineGap, dp2px(context, 3)); - mIconWidth = typedArray.getDimension(R.styleable.LrcView_playIconWidth, dp2px(context, 20)); - mIconHeight = typedArray.getDimension(R.styleable.LrcView_playIconHeight, dp2px(context, 20)); - mPlayDrawable = typedArray.getDrawable(R.styleable.LrcView_playIcon); - mPlayDrawable = mPlayDrawable == null ? ContextCompat.getDrawable(context, R.drawable.ic_play_arrow_white_24dp) : mPlayDrawable; - isCurrentTextBold = typedArray.getBoolean(R.styleable.LrcView_isLrcCurrentTextBold, false); - isLrcIndicatorTextBold = typedArray.getBoolean(R.styleable.LrcView_isLrcIndicatorTextBold, false); - typedArray.recycle(); + mDividerHeight = ta.getDimension(R.styleable.LrcView_lrcDividerHeight, getResources().getDimension(R.dimen.lrc_divider_height)); + int defDuration = getResources().getInteger(R.integer.lrc_animation_duration); + mAnimationDuration = ta.getInt(R.styleable.LrcView_lrcAnimationDuration, defDuration); + mAnimationDuration = (mAnimationDuration < 0) ? defDuration : mAnimationDuration; + mNormalTextColor = ta.getColor(R.styleable.LrcView_lrcNormalTextColor, getResources().getColor(R.color.lrc_normal_text_color)); + mCurrentTextColor = ta.getColor(R.styleable.LrcView_lrcCurrentTextColor, getResources().getColor(R.color.lrc_current_text_color)); + mTimelineTextColor = ta.getColor(R.styleable.LrcView_lrcTimelineTextColor, getResources().getColor(R.color.lrc_timeline_text_color)); + mDefaultLabel = ta.getString(R.styleable.LrcView_lrcLabel); + mDefaultLabel = TextUtils.isEmpty(mDefaultLabel) ? getContext().getString(R.string.empty) : mDefaultLabel; + mLrcPadding = ta.getDimension(R.styleable.LrcView_lrcPadding, 0); + mTimelineColor = ta.getColor(R.styleable.LrcView_lrcTimelineColor, getResources().getColor(R.color.lrc_timeline_color)); + float timelineHeight = ta.getDimension(R.styleable.LrcView_lrcTimelineHeight, getResources().getDimension(R.dimen.lrc_timeline_height)); + mPlayDrawable = ta.getDrawable(R.styleable.LrcView_lrcPlayDrawable); + mPlayDrawable = (mPlayDrawable == null) ? getResources().getDrawable(R.drawable.ic_play_arrow_white_24dp) : mPlayDrawable; + mTimeTextColor = ta.getColor(R.styleable.LrcView_lrcTimeTextColor, getResources().getColor(R.color.lrc_time_text_color)); + float timeTextSize = ta.getDimension(R.styleable.LrcView_lrcTimeTextSize, getResources().getDimension(R.dimen.lrc_time_text_size)); + mTextGravity = ta.getInteger(R.styleable.LrcView_lrcTextGravity, LrcEntry.GRAVITY_CENTER); - setupConfigs(context); + ta.recycle(); + + mDrawableWidth = (int) getResources().getDimension(R.dimen.lrc_drawable_width); + mTimeTextWidth = (int) getResources().getDimension(R.dimen.lrc_time_width); + + mLrcPaint.setAntiAlias(true); + mLrcPaint.setTextSize(mCurrentTextSize); + mLrcPaint.setTextAlign(Paint.Align.LEFT); + mTimePaint.setAntiAlias(true); + mTimePaint.setTextSize(timeTextSize); + mTimePaint.setTextAlign(Paint.Align.CENTER); + //noinspection SuspiciousNameCombination + mTimePaint.setStrokeWidth(timelineHeight); + mTimePaint.setStrokeCap(Paint.Cap.ROUND); + mTimeFontMetrics = mTimePaint.getFontMetrics(); + + mGestureDetector = new GestureDetector(getContext(), mSimpleOnGestureListener); + mGestureDetector.setIsLongpressEnabled(false); + mScroller = new Scroller(getContext()); } - private void setupConfigs(Context context) { - mScaledTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); - mMaximumFlingVelocity = ViewConfiguration.get(context).getScaledMaximumFlingVelocity(); - mMinimumFlingVelocity = ViewConfiguration.get(context).getScaledMinimumFlingVelocity(); - mOverScroller = new OverScroller(context, new DecelerateInterpolator()); - mOverScroller.setFriction(0.1f); -// ViewConfiguration.getScrollFriction(); 默认摩擦力 0.015f + /** + * 设置非当前行歌词字体颜色 + */ + public void setNormalColor(int normalColor) { + mNormalTextColor = normalColor; + postInvalidate(); + } - mTextPaint = new TextPaint(); - mTextPaint.setAntiAlias(true); - mTextPaint.setTextAlign(Paint.Align.CENTER); - mTextPaint.setTextSize(mLrcTextSize); - mDefaultContent = DEFAULT_CONTENT; + /** + * 普通歌词文本字体大小 + */ + public void setNormalTextSize(float size) { + mNormalTextSize = size; + } - mIndicatorPaint = new Paint(); - mIndicatorPaint.setAntiAlias(true); - mIndicatorPaint.setStrokeWidth(mIndicatorLineWidth); - mIndicatorPaint.setColor(mIndicatorLineColor); - mPlayRect = new Rect(); - mIndicatorPaint.setTextSize(mIndicatorTextSize); + /** + * 当前歌词文本字体大小 + */ + public void setCurrentTextSize(float size) { + mCurrentTextSize = size; + } + + /** + * 设置当前行歌词的字体颜色 + */ + public void setCurrentColor(int currentColor) { + mCurrentTextColor = currentColor; + postInvalidate(); + } + + /** + * 设置拖动歌词时选中歌词的字体颜色 + */ + public void setTimelineTextColor(int timelineTextColor) { + mTimelineTextColor = timelineTextColor; + postInvalidate(); + } + + /** + * 设置拖动歌词时时间线的颜色 + */ + public void setTimelineColor(int timelineColor) { + mTimelineColor = timelineColor; + postInvalidate(); + } + + /** + * 设置拖动歌词时右侧时间字体颜色 + */ + public void setTimeTextColor(int timeTextColor) { + mTimeTextColor = timeTextColor; + postInvalidate(); + } + + /** + * 设置歌词是否允许拖动 + * + * @param draggable 是否允许拖动 + * @param onPlayClickListener 设置歌词拖动后播放按钮点击监听器,如果允许拖动,则不能为 null + */ + public void setDraggable(boolean draggable, OnPlayClickListener onPlayClickListener) { + if (draggable) { + if (onPlayClickListener == null) { + throw new IllegalArgumentException("if draggable == true, onPlayClickListener must not be null"); + } + mOnPlayClickListener = onPlayClickListener; + } else { + mOnPlayClickListener = null; + } + } + + /** + * 设置播放按钮点击监听器 + * + * @param onPlayClickListener 如果为非 null ,则激活歌词拖动功能,否则将将禁用歌词拖动功能 + * @deprecated use {@link #setDraggable(boolean, OnPlayClickListener)} instead + */ + @Deprecated + public void setOnPlayClickListener(OnPlayClickListener onPlayClickListener) { + mOnPlayClickListener = onPlayClickListener; + } + + /** + * 设置歌词为空时屏幕中央显示的文字,如“暂无歌词” + */ + public void setLabel(String label) { + runOnUi(() -> { + mDefaultLabel = label; + invalidate(); + }); + } + + /** + * 加载歌词文件 + * + * @param lrcFile 歌词文件 + */ + public void loadLrc(File lrcFile) { + loadLrc(lrcFile, null); + } + + /** + * 加载双语歌词文件,两种语言的歌词时间戳需要一致 + * + * @param mainLrcFile 第一种语言歌词文件 + * @param secondLrcFile 第二种语言歌词文件 + */ + public void loadLrc(File mainLrcFile, File secondLrcFile) { + runOnUi(() -> { + reset(); + + StringBuilder sb = new StringBuilder("file://"); + sb.append(mainLrcFile.getPath()); + if (secondLrcFile != null) { + sb.append("#").append(secondLrcFile.getPath()); + } + String flag = sb.toString(); + setFlag(flag); + new AsyncTask>() { + @Override + protected List doInBackground(File... params) { + return LrcUtils.parseLrc(params); + } + + @Override + protected void onPostExecute(List lrcEntries) { + if (getFlag() == flag) { + onLrcLoaded(lrcEntries); + setFlag(null); + } + } + }.execute(mainLrcFile, secondLrcFile); + }); + } + + /** + * 加载歌词文本 + * + * @param lrcText 歌词文本 + */ + public void loadLrc(String lrcText) { + loadLrc(lrcText, null); + } + + /** + * 加载双语歌词文本,两种语言的歌词时间戳需要一致 + * + * @param mainLrcText 第一种语言歌词文本 + * @param secondLrcText 第二种语言歌词文本 + */ + public void loadLrc(String mainLrcText, String secondLrcText) { + runOnUi(() -> { + reset(); + + StringBuilder sb = new StringBuilder("file://"); + sb.append(mainLrcText); + if (secondLrcText != null) { + sb.append("#").append(secondLrcText); + } + String flag = sb.toString(); + setFlag(flag); + new AsyncTask>() { + @Override + protected List doInBackground(String... params) { + return LrcUtils.parseLrc(params); + } + + @Override + protected void onPostExecute(List lrcEntries) { + if (getFlag() == flag) { + onLrcLoaded(lrcEntries); + setFlag(null); + } + } + }.execute(mainLrcText, secondLrcText); + }); + } + + /** + * 加载在线歌词,默认使用 utf-8 编码 + * + * @param lrcUrl 歌词文件的网络地址 + */ + public void loadLrcByUrl(String lrcUrl) { + loadLrcByUrl(lrcUrl, "utf-8"); + } + + /** + * 加载在线歌词 + * + * @param lrcUrl 歌词文件的网络地址 + * @param charset 编码格式 + */ + public void loadLrcByUrl(String lrcUrl, String charset) { + String flag = "url://" + lrcUrl; + setFlag(flag); + new AsyncTask() { + @Override + protected String doInBackground(String... params) { + return LrcUtils.getContentFromNetwork(params[0], params[1]); + } + + @Override + protected void onPostExecute(String lrcText) { + if (getFlag() == flag) { + loadLrc(lrcText); + } + } + }.execute(lrcUrl, charset); + } + + /** + * 歌词是否有效 + * + * @return true,如果歌词有效,否则false + */ + public boolean hasLrc() { + return !mLrcEntryList.isEmpty(); + } + + /** + * 刷新歌词 + * + * @param time 当前播放时间 + */ + public void updateTime(long time) { + runOnUi(() -> { + if (!hasLrc()) { + return; + } + + int line = findShowLine(time); + if (line != mCurrentLine) { + mCurrentLine = line; + if (!isShowTimeline) { + smoothScrollTo(line); + } else { + invalidate(); + } + } + }); + } + + /** + * 将歌词滚动到指定时间 + * + * @param time 指定的时间 + * @deprecated 请使用 {@link #updateTime(long)} 代替 + */ + @Deprecated + public void onDrag(long time) { + updateTime(time); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); if (changed) { - mPlayRect.left = (int) mIndicatorMargin; - mPlayRect.top = (int) (getHeight() / 2 - mIconHeight / 2); - mPlayRect.right = (int) (mPlayRect.left + mIconWidth); - mPlayRect.bottom = (int) (mPlayRect.top + mIconHeight); - mPlayDrawable.setBounds(mPlayRect); + initPlayDrawable(); + initEntryList(); + if (hasLrc()) { + smoothScrollTo(mCurrentLine, 0L); + } } } - private int getLrcWidth() { - return getWidth() - getPaddingLeft() - getPaddingRight(); - } - - private int getLrcHeight() { - return getHeight(); - } - - private boolean isLrcEmpty() { - return mLrcData == null || getLrcCount() == 0; - } - - private int getLrcCount() { - return mLrcData.size(); - } - - public void setLrcData(List lrcData) { - resetView(DEFAULT_CONTENT); - mLrcData = lrcData; - invalidate(); - } - @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); - if (isLrcEmpty()) { - drawEmptyText(canvas); + + int centerY = getHeight() / 2; + + // 无歌词文件 + if (!hasLrc()) { + mLrcPaint.setColor(mCurrentTextColor); + @SuppressLint("DrawAllocation") + StaticLayout staticLayout = new StaticLayout(mDefaultLabel, mLrcPaint, + (int) getLrcWidth(), Layout.Alignment.ALIGN_CENTER, 1f, 0f, false); + drawText(canvas, staticLayout, centerY); return; } - int indicatePosition = getIndicatePosition(); - mTextPaint.setTextSize(mLrcTextSize); - mTextPaint.setTextAlign(Paint.Align.CENTER); - float y = getLrcHeight() / 2f; - float x = getLrcWidth() / 2f + getPaddingLeft(); - for (int i = 0; i < getLrcCount(); i++) { - if (i > 0) { - y += (getTextHeight(i - 1) + getTextHeight(i)) / 2f + mLrcLineSpaceHeight; - } - if (mCurrentLine == i) { - mTextPaint.setColor(mCurrentPlayLineColor); - mTextPaint.setFakeBoldText(isCurrentTextBold); - } else if (indicatePosition == i && isShowTimeIndicator) { - mTextPaint.setFakeBoldText(isLrcIndicatorTextBold); - mTextPaint.setColor(mCurrentIndicateLineTextColor); - } else { - mTextPaint.setFakeBoldText(false); - mTextPaint.setColor(mNormalColor); - } - drawLrc(canvas, x, y, i); - } - if (isShowTimeIndicator) { + int centerLine = getCenterLine(); + + if (isShowTimeline) { mPlayDrawable.draw(canvas); - long time = mLrcData.get(indicatePosition).getTime(); - float timeWidth = mIndicatorPaint.measureText(LrcHelper.formatTime(time)); - mIndicatorPaint.setColor(mIndicatorLineColor); - canvas.drawLine(mPlayRect.right + mIconLineGap, getHeight() / 2f, - getWidth() - timeWidth * 1.3f, getHeight() / 2f, mIndicatorPaint); - int baseX = (int) (getWidth() - timeWidth * 1.1f); - float baseline = getHeight() / 2f - (mIndicatorPaint.descent() - mIndicatorPaint.ascent()) / 2 - mIndicatorPaint.ascent(); - mIndicatorPaint.setColor(mIndicatorTextColor); - canvas.drawText(LrcHelper.formatTime(time), baseX, baseline, mIndicatorPaint); + + mTimePaint.setColor(mTimelineColor); + canvas.drawLine(mTimeTextWidth, centerY, getWidth() - mTimeTextWidth, centerY, mTimePaint); + + mTimePaint.setColor(mTimeTextColor); + String timeText = LrcUtils.formatTime(mLrcEntryList.get(centerLine).getTime()); + float timeX = getWidth() - mTimeTextWidth / 2; + float timeY = centerY - (mTimeFontMetrics.descent + mTimeFontMetrics.ascent) / 2; + canvas.drawText(timeText, timeX, timeY, mTimePaint); + } + + canvas.translate(0, mOffset); + + float y = 0; + for (int i = 0; i < mLrcEntryList.size(); i++) { + if (i > 0) { + y += ((mLrcEntryList.get(i - 1).getHeight() + mLrcEntryList.get(i).getHeight()) >> 1) + mDividerHeight; + } + if (i == mCurrentLine) { + mLrcPaint.setTextSize(mCurrentTextSize); + mLrcPaint.setColor(mCurrentTextColor); + } else if (isShowTimeline && i == centerLine) { + mLrcPaint.setColor(mTimelineTextColor); + } else { + mLrcPaint.setTextSize(mNormalTextSize); + mLrcPaint.setColor(mNormalTextColor); + } + drawText(canvas, mLrcEntryList.get(i).getStaticLayout(), y); } } - private void drawLrc(Canvas canvas, float x, float y, int i) { - String text = mLrcData.get(i).getText(); - StaticLayout staticLayout = mLrcMap.get(text); - if (staticLayout == null) { - mTextPaint.setTextSize(mLrcTextSize); - staticLayout = new StaticLayout(text, mTextPaint, getLrcWidth(), - Layout.Alignment.ALIGN_NORMAL, 1f, 0f, false); - mLrcMap.put(text, staticLayout); - } + /** + * 画一行歌词 + * + * @param y 歌词中心 Y 坐标 + */ + private void drawText(Canvas canvas, StaticLayout staticLayout, float y) { canvas.save(); - canvas.translate(x, y - staticLayout.getHeight() / 2f - mOffset); + canvas.translate(mLrcPadding, y - (staticLayout.getHeight() >> 1)); staticLayout.draw(canvas); canvas.restore(); } - //中间空文字 - private void drawEmptyText(Canvas canvas) { - mTextPaint.setTextAlign(Paint.Align.CENTER); - mTextPaint.setColor(mNoLrcTextColor); - mTextPaint.setTextSize(mNoLrcTextSize); - canvas.save(); - StaticLayout staticLayout = new StaticLayout(mDefaultContent, mTextPaint, - getLrcWidth(), Layout.Alignment.ALIGN_NORMAL, 1f, 0f, false); - canvas.translate(getLrcWidth() / 2f + getPaddingLeft(), getLrcHeight() / 2f); - staticLayout.draw(canvas); - canvas.restore(); - } - - public void updateTime(long time) { - if (isLrcEmpty()) { - return; - } - int linePosition = getUpdateTimeLinePosition(time); - if (mCurrentLine != linePosition) { - mCurrentLine = linePosition; - if (isUserScroll) { - invalidateView(); - return; - } - ViewCompat.postOnAnimation(LrcView.this, mScrollRunnable); - } - } - - private int getUpdateTimeLinePosition(long time) { - int linePos = 0; - for (int i = 0; i < getLrcCount(); i++) { - Lrc lrc = mLrcData.get(i); - if (time >= lrc.getTime()) { - if (i == getLrcCount() - 1) { - linePos = getLrcCount() - 1; - } else if (time < mLrcData.get(i + 1).getTime()) { - linePos = i; - break; - } - } - } - return linePos; - } - - private void scrollToPosition(int linePosition) { - float scrollY = getItemOffsetY(linePosition); - final ValueAnimator animator = ValueAnimator.ofFloat(mOffset, scrollY); - animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { - - @Override - public void onAnimationUpdate(ValueAnimator animation) { - mOffset = (float) animation.getAnimatedValue(); - invalidateView(); - } - }); - animator.setDuration(300); - animator.start(); - } - - public int getIndicatePosition() { - int pos = 0; - float min = Float.MAX_VALUE; - //itemOffset 和 mOffset 最小即当前位置 - for (int i = 0; i < mLrcData.size(); i++) { - float offsetY = getItemOffsetY(i); - float abs = Math.abs(offsetY - mOffset); - if (abs < min) { - min = abs; - pos = i; - } - } - return pos; - } - - private float getItemOffsetY(int linePosition) { - float tempY = 0; - for (int i = 1; i <= linePosition; i++) { - tempY += (getTextHeight(i - 1) + getTextHeight(i)) / 2 + mLrcLineSpaceHeight; - } - return tempY; - } - - private float getTextHeight(int linePosition) { - String text = mLrcData.get(linePosition).getText(); - StaticLayout staticLayout = mStaticLayoutHashMap.get(text); - if (staticLayout == null) { - mTextPaint.setTextSize(mLrcTextSize); - staticLayout = new StaticLayout(text, mTextPaint, - getLrcWidth(), Layout.Alignment.ALIGN_NORMAL, 1f, 0f, false); - mStaticLayoutHashMap.put(text, staticLayout); - } - return staticLayout.getHeight(); - } - - private boolean overScrolled() { - return mOffset > getItemOffsetY(getLrcCount() - 1) || mOffset < 0; - } - @Override public boolean onTouchEvent(MotionEvent event) { - if (isLrcEmpty()) { - return super.onTouchEvent(event); - } - if (mVelocityTracker == null) { - mVelocityTracker = VelocityTracker.obtain(); - } - mVelocityTracker.addMovement(event); - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - removeCallbacks(mScrollRunnable); - removeCallbacks(mHideIndicatorRunnable); - if (!mOverScroller.isFinished()) { - mOverScroller.abortAnimation(); - } - mLastMotionX = event.getX(); - mLastMotionY = event.getY(); - isUserScroll = true; - isDragging = false; - break; - - case MotionEvent.ACTION_MOVE: - float moveY = event.getY() - mLastMotionY; - if (Math.abs(moveY) > mScaledTouchSlop) { - isDragging = true; - isShowTimeIndicator = isEnableShowIndicator; - } - if (isDragging) { - -// if (mOffset < 0) { -// mOffset = Math.max(mOffset, -getTextHeight(0) - mLrcLineSpaceHeight); -// } - float maxHeight = getItemOffsetY(getLrcCount() - 1); -// if (mOffset > maxHeight) { -// mOffset = Math.min(mOffset, maxHeight + getTextHeight(getLrcCount() - 1) + mLrcLineSpaceHeight); -// } - if (mOffset < 0 || mOffset > maxHeight) { - moveY /= 3.5f; - } - mOffset -= moveY; - mLastMotionY = event.getY(); - invalidateView(); - } - break; - case MotionEvent.ACTION_CANCEL: - case MotionEvent.ACTION_UP: - if (!isDragging && (!isShowTimeIndicator || !onClickPlayButton(event))) { - isShowTimeIndicator = false; - invalidateView(); - performClick(); - } - handleActionUp(event); - break; - } -// return isDragging || super.onTouchEvent(event); - return true; - } - - private void handleActionUp(MotionEvent event) { - if (isEnableShowIndicator) { - ViewCompat.postOnAnimationDelayed(LrcView.this, mHideIndicatorRunnable, mIndicatorTouchDelay); - } - if (isShowTimeIndicator && mPlayRect != null && onClickPlayButton(event)) { - isShowTimeIndicator = false; - invalidateView(); - if (mOnPlayIndicatorLineListener != null) { - mOnPlayIndicatorLineListener.onPlay(mLrcData.get(getIndicatePosition()).getTime(), - mLrcData.get(getIndicatePosition()).getText()); + if (event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL) { + isTouching = false; + if (hasLrc() && !isFling) { + adjustCenter(); + postDelayed(hideTimelineRunnable, TIMELINE_KEEP_TIME); } } - if (overScrolled() && mOffset < 0) { - scrollToPosition(0); - if (isAutoAdjustPosition) { - ViewCompat.postOnAnimationDelayed(LrcView.this, mScrollRunnable, mTouchDelay); - } - return; - } - - if (overScrolled() && mOffset > getItemOffsetY(getLrcCount() - 1)) { - scrollToPosition(getLrcCount() - 1); - if (isAutoAdjustPosition) { - ViewCompat.postOnAnimationDelayed(LrcView.this, mScrollRunnable, mTouchDelay); - } - return; - } - - mVelocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity); - float yVelocity = mVelocityTracker.getYVelocity(); - float absYVelocity = Math.abs(yVelocity); - if (absYVelocity > mMinimumFlingVelocity) { - mOverScroller.fling(0, (int) mOffset, 0, (int) (-yVelocity), 0, - 0, 0, (int) getItemOffsetY(getLrcCount() - 1), - 0, (int) getTextHeight(0)); - invalidateView(); - } - releaseVelocityTracker(); - if (isAutoAdjustPosition) { - ViewCompat.postOnAnimationDelayed(LrcView.this, mScrollRunnable, mTouchDelay); - } - } - - private boolean onClickPlayButton(MotionEvent event) { - float left = mPlayRect.left; - float right = mPlayRect.right; - float top = mPlayRect.top; - float bottom = mPlayRect.bottom; - float x = event.getX(); - float y = event.getY(); - return mLastMotionX > left && mLastMotionX < right && mLastMotionY > top - && mLastMotionY < bottom && x > left && x < right && y > top && y < bottom; + return mGestureDetector.onTouchEvent(event); } @Override public void computeScroll() { - super.computeScroll(); - if (mOverScroller.computeScrollOffset()) { - mOffset = mOverScroller.getCurrY(); - invalidateView(); + if (mScroller.computeScrollOffset()) { + mOffset = mScroller.getCurrY(); + invalidate(); } - } - private void releaseVelocityTracker() { - if (null != mVelocityTracker) { - mVelocityTracker.clear(); - mVelocityTracker.recycle(); - mVelocityTracker = null; + if (isFling && mScroller.isFinished()) { + isFling = false; + if (hasLrc() && !isTouching) { + adjustCenter(); + postDelayed(hideTimelineRunnable, TIMELINE_KEEP_TIME); + } } } - public void resetView(String defaultContent) { - if (mLrcData != null) { - mLrcData.clear(); - } - mLrcMap.clear(); - mStaticLayoutHashMap.clear(); - mCurrentLine = 0; - mOffset = 0; - isUserScroll = false; - isDragging = false; - mDefaultContent = defaultContent; - removeCallbacks(mScrollRunnable); - invalidate(); - } - @Override - public boolean performClick() { - return super.performClick(); + protected void onDetachedFromWindow() { + removeCallbacks(hideTimelineRunnable); + super.onDetachedFromWindow(); } - public int dp2px(Context context, float dpVal) { - return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, - dpVal, context.getResources().getDisplayMetrics()); + private void onLrcLoaded(List entryList) { + if (entryList != null && !entryList.isEmpty()) { + mLrcEntryList.addAll(entryList); + } + + Collections.sort(mLrcEntryList); + + initEntryList(); + invalidate(); } - public int sp2px(Context context, float spVal) { - return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, - spVal, context.getResources().getDisplayMetrics()); + private void initPlayDrawable() { + int l = (mTimeTextWidth - mDrawableWidth) / 2; + int t = getHeight() / 2 - mDrawableWidth / 2; + int r = l + mDrawableWidth; + int b = t + mDrawableWidth; + mPlayDrawable.setBounds(l, t, r, b); + } + + private void initEntryList() { + if (!hasLrc() || getWidth() == 0) { + return; + } + + for (LrcEntry lrcEntry : mLrcEntryList) { + lrcEntry.init(mLrcPaint, (int) getLrcWidth(), mTextGravity); + } + + mOffset = getHeight() / 2; + } + + private void reset() { + endAnimation(); + mScroller.forceFinished(true); + isShowTimeline = false; + isTouching = false; + isFling = false; + removeCallbacks(hideTimelineRunnable); + mLrcEntryList.clear(); + mOffset = 0; + mCurrentLine = 0; + invalidate(); } /** - * 暂停(手动滑动歌词后,不再自动回滚至当前播放位置) + * 将中心行微调至正中心 */ - public void pause() { - isAutoAdjustPosition = false; - invalidateView(); + private void adjustCenter() { + smoothScrollTo(getCenterLine(), ADJUST_DURATION); } /** - * 恢复(继续自动回滚) + * 滚动到某一行 */ - public void resume() { - isAutoAdjustPosition = true; - ViewCompat.postOnAnimationDelayed(LrcView.this, mScrollRunnable, mTouchDelay); - invalidateView(); + private void smoothScrollTo(int line) { + smoothScrollTo(line, mAnimationDuration); } + /** + * 滚动到某一行 + */ + private void smoothScrollTo(int line, long duration) { + float offset = getOffset(line); + endAnimation(); - /*------------------Config-------------------*/ - - private void invalidateView() { - if (Looper.getMainLooper().getThread() == Thread.currentThread()) { + mAnimator = ValueAnimator.ofFloat(mOffset, offset); + mAnimator.setDuration(duration); + mAnimator.setInterpolator(new LinearInterpolator()); + mAnimator.addUpdateListener(animation -> { + mOffset = (float) animation.getAnimatedValue(); invalidate(); - } else { - postInvalidate(); + }); + LrcUtils.resetDurationScale(); + mAnimator.start(); + } + + /** + * 结束滚动动画 + */ + private void endAnimation() { + if (mAnimator != null && mAnimator.isRunning()) { + mAnimator.end(); } } - public void setOnPlayIndicatorLineListener(OnPlayIndicatorLineListener onPlayIndicatorLineListener) { - mOnPlayIndicatorLineListener = onPlayIndicatorLineListener; + /** + * 二分法查找当前时间应该显示的行数(最后一个 <= time 的行数) + */ + private int findShowLine(long time) { + int left = 0; + int right = mLrcEntryList.size(); + while (left <= right) { + int middle = (left + right) / 2; + long middleTime = mLrcEntryList.get(middle).getTime(); + + if (time < middleTime) { + right = middle - 1; + } else { + if (middle + 1 >= mLrcEntryList.size() || time < mLrcEntryList.get(middle + 1).getTime()) { + return middle; + } + + left = middle + 1; + } + } + + return 0; } - public void setEmptyContent(String defaultContent) { - mDefaultContent = defaultContent; - invalidateView(); + /** + * 获取当前在视图中央的行数 + */ + private int getCenterLine() { + int centerLine = 0; + float minDistance = Float.MAX_VALUE; + for (int i = 0; i < mLrcEntryList.size(); i++) { + if (Math.abs(mOffset - getOffset(i)) < minDistance) { + minDistance = Math.abs(mOffset - getOffset(i)); + centerLine = i; + } + } + return centerLine; } - public void setLrcTextSize(float lrcTextSize) { - mLrcTextSize = lrcTextSize; - invalidateView(); + /** + * 获取歌词距离视图顶部的距离 + * 采用懒加载方式 + */ + private float getOffset(int line) { + if (mLrcEntryList.get(line).getOffset() == Float.MIN_VALUE) { + float offset = getHeight() / 2; + for (int i = 1; i <= line; i++) { + offset -= ((mLrcEntryList.get(i - 1).getHeight() + mLrcEntryList.get(i).getHeight()) >> 1) + mDividerHeight; + } + mLrcEntryList.get(line).setOffset(offset); + } + + return mLrcEntryList.get(line).getOffset(); } - public void setLrcLineSpaceHeight(float lrcLineSpaceHeight) { - mLrcLineSpaceHeight = lrcLineSpaceHeight; - invalidateView(); + /** + * 获取歌词宽度 + */ + private float getLrcWidth() { + return getWidth() - mLrcPadding * 2; } - public void setTouchDelay(int touchDelay) { - mTouchDelay = touchDelay; - invalidateView(); + /** + * 在主线程中运行 + */ + private void runOnUi(Runnable r) { + if (Looper.myLooper() == Looper.getMainLooper()) { + r.run(); + } else { + post(r); + } } - public void setNormalColor(@ColorInt int normalColor) { - mNormalColor = normalColor; - invalidateView(); + private Object getFlag() { + return mFlag; } - public void setCurrentPlayLineColor(@ColorInt int currentPlayLineColor) { - mCurrentPlayLineColor = currentPlayLineColor; - invalidateView(); + private void setFlag(Object flag) { + this.mFlag = flag; } - public void setNoLrcTextSize(float noLrcTextSize) { - mNoLrcTextSize = noLrcTextSize; - invalidateView(); - } - - public void setNoLrcTextColor(@ColorInt int noLrcTextColor) { - mNoLrcTextColor = noLrcTextColor; - invalidateView(); - } - - public void setIndicatorLineWidth(float indicatorLineWidth) { - mIndicatorLineWidth = indicatorLineWidth; - invalidateView(); - } - - public void setIndicatorTextSize(float indicatorTextSize) { -// mIndicatorTextSize = indicatorTextSize; - mIndicatorPaint.setTextSize(indicatorTextSize); - invalidateView(); - } - - public void setCurrentIndicateLineTextColor(int currentIndicateLineTextColor) { - mCurrentIndicateLineTextColor = currentIndicateLineTextColor; - invalidateView(); - } - - public void setIndicatorLineColor(int indicatorLineColor) { - mIndicatorLineColor = indicatorLineColor; - invalidateView(); - } - - public void setIndicatorMargin(float indicatorMargin) { - mIndicatorMargin = indicatorMargin; - invalidateView(); - } - - public void setIconLineGap(float iconLineGap) { - mIconLineGap = iconLineGap; - invalidateView(); - } - - public void setIconWidth(float iconWidth) { - mIconWidth = iconWidth; - invalidateView(); - } - - public void setIconHeight(float iconHeight) { - mIconHeight = iconHeight; - invalidateView(); - } - - public void setEnableShowIndicator(boolean enableShowIndicator) { - isEnableShowIndicator = enableShowIndicator; - invalidateView(); - } - - public Drawable getPlayDrawable() { - return mPlayDrawable; - } - - public void setPlayDrawable(Drawable playDrawable) { - mPlayDrawable = playDrawable; - mPlayDrawable.setBounds(mPlayRect); - invalidateView(); - } - - public void setIndicatorTextColor(int indicatorTextColor) { - mIndicatorTextColor = indicatorTextColor; - invalidateView(); - } - - public void setLrcCurrentTextBold(boolean bold) { - isCurrentTextBold = bold; - invalidateView(); - } - - public void setLrcIndicatorTextBold(boolean bold) { - isLrcIndicatorTextBold = bold; - invalidateView(); - } - - public interface OnPlayIndicatorLineListener { - void onPlay(long time, String content); + /** + * 播放按钮点击监听器,点击后应该跳转到指定播放位置 + */ + public interface OnPlayClickListener { + /** + * 播放按钮被点击,应该跳转到指定播放位置 + * + * @return 是否成功消费该事件,如果成功消费,则会更新UI + */ + boolean onPlayClick(long time); } } \ No newline at end of file diff --git a/app/src/main/java/code/name/monkey/retromusic/util/LyricUtil.java b/app/src/main/java/code/name/monkey/retromusic/util/LyricUtil.java index 9418f108..0de666ef 100644 --- a/app/src/main/java/code/name/monkey/retromusic/util/LyricUtil.java +++ b/app/src/main/java/code/name/monkey/retromusic/util/LyricUtil.java @@ -72,6 +72,11 @@ public class LyricUtil { return file.exists(); } + public static boolean isLrcOriginalFileExist(@NonNull String path) { + File file = new File(getLrcOriginalPath(path)); + return file.exists(); + } + @Nullable public static File getLocalLyricFile(@NonNull String title, @NonNull String artist) { File file = new File(getLrcPath(title, artist)); @@ -82,10 +87,24 @@ public class LyricUtil { } } + @Nullable + public static File getLocalLyricOriginalFile(@NonNull String path) { + File file = new File(getLrcOriginalPath(path)); + if (file.exists()) { + return file; + } else { + return null; + } + } + private static String getLrcPath(String title, String artist) { return lrcRootPath + title + " - " + artist + ".lrc"; } + private static String getLrcOriginalPath(String filePath) { + return filePath.replace(filePath.substring(filePath.lastIndexOf(".") + 1, filePath.length()), "lrc"); + } + @NonNull public static String decryptBASE64(@NonNull String str) { if (str == null || str.length() == 0) { diff --git a/app/src/main/java/code/name/monkey/retromusic/util/NavigationUtil.java b/app/src/main/java/code/name/monkey/retromusic/util/NavigationUtil.java index 3c834172..c67d0c92 100755 --- a/app/src/main/java/code/name/monkey/retromusic/util/NavigationUtil.java +++ b/app/src/main/java/code/name/monkey/retromusic/util/NavigationUtil.java @@ -29,8 +29,6 @@ import org.jetbrains.annotations.NotNull; import code.name.monkey.retromusic.R; import code.name.monkey.retromusic.activities.AboutActivity; -import code.name.monkey.retromusic.activities.albums.AlbumDetailsActivity; -import code.name.monkey.retromusic.activities.artists.ArtistDetailActivity; import code.name.monkey.retromusic.activities.DriveModeActivity; import code.name.monkey.retromusic.activities.GenreDetailsActivity; import code.name.monkey.retromusic.activities.LicenseActivity; @@ -43,6 +41,8 @@ import code.name.monkey.retromusic.activities.SettingsActivity; import code.name.monkey.retromusic.activities.SupportDevelopmentActivity; import code.name.monkey.retromusic.activities.UserInfoActivity; import code.name.monkey.retromusic.activities.WhatsNewActivity; +import code.name.monkey.retromusic.activities.albums.AlbumDetailsActivity; +import code.name.monkey.retromusic.activities.artists.ArtistDetailActivity; import code.name.monkey.retromusic.activities.bugreport.BugReportActivity; import code.name.monkey.retromusic.helper.MusicPlayerRemote; import code.name.monkey.retromusic.model.Genre; diff --git a/app/src/main/java/code/name/monkey/retromusic/util/PreferenceUtil.kt b/app/src/main/java/code/name/monkey/retromusic/util/PreferenceUtil.kt index 119918fd..292f01aa 100644 --- a/app/src/main/java/code/name/monkey/retromusic/util/PreferenceUtil.kt +++ b/app/src/main/java/code/name/monkey/retromusic/util/PreferenceUtil.kt @@ -96,7 +96,10 @@ object PreferenceUtil { val languageCode get() = sharedPreferences.getString(LANGUAGE_NAME, "auto") var userName - get() = sharedPreferences.getString(USER_NAME, "User Name") + get() = sharedPreferences.getString( + USER_NAME, + App.getContext().getString(R.string.user_name) + ) set(value) = sharedPreferences.edit { putString(USER_NAME, value) } @@ -420,7 +423,7 @@ object PreferenceUtil { var songGridSize get() = sharedPreferences.getInt( SONG_GRID_SIZE, - App.getContext().getIntRes(R.integer.default_grid_columns) + App.getContext().getIntRes(R.integer.default_list_columns) ) set(value) = sharedPreferences.edit { putInt(SONG_GRID_SIZE, value) diff --git a/app/src/main/res/layout/activity_lyrics.xml b/app/src/main/res/layout/activity_lyrics.xml index cb314357..92398c24 100644 --- a/app/src/main/res/layout/activity_lyrics.xml +++ b/app/src/main/res/layout/activity_lyrics.xml @@ -8,11 +8,8 @@ + android:layout_width="match_parent"> - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_adaptive_player_playback_controls.xml b/app/src/main/res/layout/fragment_adaptive_player_playback_controls.xml index b0aacd84..20bf7e04 100644 --- a/app/src/main/res/layout/fragment_adaptive_player_playback_controls.xml +++ b/app/src/main/res/layout/fragment_adaptive_player_playback_controls.xml @@ -84,6 +84,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="16dp" + android:layout_marginBottom="16dp" android:background="?attr/roundSelector" app:layout_constraintBottom_toTopOf="@+id/volumeFragmentContainer" app:layout_constraintEnd_toStartOf="@+id/nextButton" diff --git a/app/src/main/res/menu/menu_search.xml b/app/src/main/res/menu/menu_search.xml new file mode 100644 index 00000000..2c415de9 --- /dev/null +++ b/app/src/main/res/menu/menu_search.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 4f82f6e5..07161152 100755 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -212,7 +212,7 @@ sr sk es - sw + sv ta te tr diff --git a/app/src/main/res/values/lrc_colors.xml b/app/src/main/res/values/lrc_colors.xml new file mode 100644 index 00000000..e2e3dda8 --- /dev/null +++ b/app/src/main/res/values/lrc_colors.xml @@ -0,0 +1,8 @@ + + + #9E9E9E + #FF4081 + #F8BBD0 + #809E9E9E + #809E9E9E + \ No newline at end of file diff --git a/app/src/main/res/values/lrc_dimens.xml b/app/src/main/res/values/lrc_dimens.xml new file mode 100644 index 00000000..4a5c994f --- /dev/null +++ b/app/src/main/res/values/lrc_dimens.xml @@ -0,0 +1,10 @@ + + + 1000 + 16sp + 12sp + 16dp + 1dp + 30dp + 40dp + \ No newline at end of file diff --git a/app/src/main/res/values/lrc_view_attrs.xml b/app/src/main/res/values/lrc_view_attrs.xml index a7e24a14..ff3cc693 100644 --- a/app/src/main/res/values/lrc_view_attrs.xml +++ b/app/src/main/res/values/lrc_view_attrs.xml @@ -14,25 +14,24 @@ - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 78c92051..bef72a12 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -856,6 +856,7 @@ Share the app with your friends and family Need more help? Gradient + User Name Song