Code refactor
This commit is contained in:
parent
3c7328f9c6
commit
10874744f3
28 changed files with 771 additions and 1205 deletions
|
@ -24,8 +24,8 @@ android {
|
|||
vectorDrawables.useSupportLibrary = true
|
||||
|
||||
applicationId "code.name.monkey.retromusic"
|
||||
versionCode 436
|
||||
versionName '3.5.300'
|
||||
versionCode 437
|
||||
versionName '3.5.600'
|
||||
|
||||
multiDexEnabled true
|
||||
|
||||
|
|
|
@ -29,7 +29,6 @@ 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
|
||||
|
@ -162,7 +161,6 @@ class LyricsActivity : AbsMusicServiceActivity(), View.OnClickListener,
|
|||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
/*val materialDialog = MaterialDialog(this)
|
||||
.show {
|
||||
title(R.string.add_time_framed_lryics)
|
||||
|
@ -381,11 +379,7 @@ class LyricsActivity : AbsMusicServiceActivity(), View.OnClickListener,
|
|||
)
|
||||
)
|
||||
setNoLrcTextColor(resolveColor(requireContext(), attr.textColorPrimary))
|
||||
setOnPlayIndicatorLineListener(object : LrcView.OnPlayIndicatorLineListener {
|
||||
override fun onPlay(time: Long, content: String) {
|
||||
MusicPlayerRemote.seekTo(time.toInt())
|
||||
}
|
||||
})
|
||||
setOnPlayIndicatorLineListener { time, _ -> MusicPlayerRemote.seekTo(time.toInt()) }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -60,7 +60,6 @@ import code.name.monkey.retromusic.helper.MusicPlayerRemote;
|
|||
import code.name.monkey.retromusic.helper.menu.SongMenuHelper;
|
||||
import code.name.monkey.retromusic.helper.menu.SongsMenuHelper;
|
||||
import code.name.monkey.retromusic.interfaces.CabHolder;
|
||||
import code.name.monkey.retromusic.interfaces.LoaderIds;
|
||||
import code.name.monkey.retromusic.interfaces.MainActivityFragmentCallbacks;
|
||||
import code.name.monkey.retromusic.misc.DialogAsyncTask;
|
||||
import code.name.monkey.retromusic.misc.UpdateToastMediaScannerCompletionListener;
|
||||
|
@ -87,7 +86,7 @@ public class FoldersFragment extends AbsMainActivityFragment implements
|
|||
FileUtil.fileIsMimeType(file, "application/ogg", MimeTypeMap.getSingleton()));
|
||||
private static final String PATH = "path";
|
||||
private static final String CRUMBS = "crumbs";
|
||||
private static final int LOADER_ID = LoaderIds.Companion.getFOLDERS_FRAGMENT();
|
||||
private static final int LOADER_ID = 5;
|
||||
private SongFileAdapter adapter;
|
||||
private BreadCrumbLayout breadCrumbs;
|
||||
private MaterialCab cab;
|
||||
|
|
|
@ -1,124 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2019 Hemanth Savarala.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*/
|
||||
|
||||
package code.name.monkey.retromusic.helper
|
||||
|
||||
import android.media.audiofx.BassBoost
|
||||
import android.media.audiofx.Equalizer
|
||||
import android.media.audiofx.Virtualizer
|
||||
import android.util.Log
|
||||
|
||||
import code.name.monkey.retromusic.interfaces.EqualizerInterface
|
||||
|
||||
/**
|
||||
* @author Hemanth S (h4h13).
|
||||
*/
|
||||
|
||||
class EqualizerHelper private constructor() : EqualizerInterface {
|
||||
override val equalizer: Equalizer
|
||||
override val bassBoost: BassBoost
|
||||
override val virtualizer: Virtualizer
|
||||
|
||||
override val bandLevelHigh: Int
|
||||
override val bandLevelLow: Int
|
||||
override var isRunning = false
|
||||
|
||||
override val numberOfBands: Int
|
||||
get() = equalizer.numberOfBands.toInt()
|
||||
|
||||
override var isBassBoostEnabled: Boolean
|
||||
get() = bassBoost.enabled
|
||||
set(isEnabled) {
|
||||
bassBoost.enabled = isEnabled
|
||||
}
|
||||
|
||||
override var bassBoostStrength: Int
|
||||
get() = bassBoost.roundedStrength.toInt()
|
||||
set(strength) = bassBoost.setStrength(strength.toShort())
|
||||
|
||||
override var isVirtualizerEnabled: Boolean
|
||||
get() = virtualizer.enabled
|
||||
set(isEnabled) {
|
||||
virtualizer.enabled = isEnabled
|
||||
}
|
||||
|
||||
override var virtualizerStrength: Int
|
||||
get() = virtualizer.roundedStrength.toInt()
|
||||
set(strength) = virtualizer.setStrength(strength.toShort())
|
||||
|
||||
init {
|
||||
|
||||
//Prevent form the reflection api.
|
||||
if (ourInstance != null) {
|
||||
throw RuntimeException("Use getInstance() method to get the single instance of this class.")
|
||||
}
|
||||
|
||||
val i = MusicPlayerRemote.audioSessionId
|
||||
|
||||
equalizer = Equalizer(100, i)
|
||||
|
||||
equalizer.enabled = true
|
||||
bassBoost = BassBoost(100, i)
|
||||
virtualizer = Virtualizer(100, i)
|
||||
|
||||
bandLevelHigh = equalizer.bandLevelRange[1].toInt()
|
||||
bandLevelLow = equalizer.bandLevelRange[0].toInt()
|
||||
|
||||
Log.i(TAG, "onCreate: $bandLevelHigh $bandLevelLow")
|
||||
isRunning = true
|
||||
}
|
||||
|
||||
//Make singleton from serialize and deserialize operation.
|
||||
protected fun readResolve(): EqualizerHelper? {
|
||||
return instance
|
||||
}
|
||||
|
||||
override fun getCenterFreq(band: Int): Int {
|
||||
return equalizer.getCenterFreq(band.toShort())
|
||||
}
|
||||
|
||||
|
||||
override fun getBandLevel(band: Int): Int {
|
||||
return equalizer.getBandLevel(band.toShort()).toInt()
|
||||
}
|
||||
|
||||
override fun setBandLevel(band: Int, level: Int) {
|
||||
equalizer.setBandLevel(band.toShort(), level.toShort())
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "EqualizerHelper"
|
||||
|
||||
@Volatile
|
||||
private var ourInstance: EqualizerHelper? = null
|
||||
|
||||
//Double check locking pattern
|
||||
//Check for the first time
|
||||
//Check for the second time.
|
||||
//if there is no instance available... create new one
|
||||
val instance: EqualizerHelper?
|
||||
get() {
|
||||
if (ourInstance == null) {
|
||||
|
||||
synchronized(EqualizerHelper::class.java) {
|
||||
if (ourInstance == null) {
|
||||
ourInstance = EqualizerHelper()
|
||||
}
|
||||
}
|
||||
}
|
||||
return ourInstance
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2019 Hemanth Savarala.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*/
|
||||
|
||||
package code.name.monkey.retromusic.helper;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import code.name.monkey.retromusic.model.Playlist;
|
||||
import code.name.monkey.retromusic.model.Song;
|
||||
|
||||
public class M3UWriter implements M3UConstants {
|
||||
|
||||
@Nullable
|
||||
public static File write(@NonNull Context context,
|
||||
@NonNull File dir,
|
||||
@NonNull Playlist playlist) throws IOException {
|
||||
if (!dir.exists()) //noinspection ResultOfMethodCallIgnored
|
||||
dir.mkdirs();
|
||||
File file = new File(dir, playlist.name.concat("." + EXTENSION));
|
||||
|
||||
ArrayList<Song> songs = playlist.getSongs(context);
|
||||
|
||||
if (songs.size() > 0) {
|
||||
BufferedWriter bw = new BufferedWriter(new FileWriter(file));
|
||||
|
||||
bw.write(HEADER);
|
||||
for (Song song : songs) {
|
||||
bw.newLine();
|
||||
bw.write(ENTRY + song.getDuration() + DURATION_SEPARATOR + song.getArtistName() + " - " + song.getTitle());
|
||||
bw.newLine();
|
||||
bw.write(song.getData());
|
||||
}
|
||||
|
||||
bw.close();
|
||||
}
|
||||
return file;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright (c) 2019 Hemanth Savarala.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*/
|
||||
package code.name.monkey.retromusic.helper
|
||||
|
||||
import android.content.Context
|
||||
import code.name.monkey.retromusic.model.Playlist
|
||||
import java.io.BufferedWriter
|
||||
import java.io.File
|
||||
import java.io.FileWriter
|
||||
import java.io.IOException
|
||||
|
||||
object M3UWriter : M3UConstants {
|
||||
@JvmStatic
|
||||
@Throws(IOException::class)
|
||||
fun write(
|
||||
context: Context,
|
||||
dir: File,
|
||||
playlist: Playlist
|
||||
): File? {
|
||||
if (!dir.exists()) dir.mkdirs()
|
||||
val file = File(dir, playlist.name + "." + M3UConstants.EXTENSION)
|
||||
val songs = playlist.getSongs(context)
|
||||
if (songs.size > 0) {
|
||||
val bw = BufferedWriter(FileWriter(file))
|
||||
bw.write(M3UConstants.HEADER)
|
||||
for (song in songs) {
|
||||
bw.newLine()
|
||||
bw.write(M3UConstants.ENTRY + song.duration + M3UConstants.DURATION_SEPARATOR + song.artistName + " - " + song.title)
|
||||
bw.newLine()
|
||||
bw.write(song.data)
|
||||
}
|
||||
bw.close()
|
||||
}
|
||||
return file
|
||||
}
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2019 Hemanth Savarala.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*/
|
||||
|
||||
package code.name.monkey.retromusic.interfaces
|
||||
|
||||
import android.media.audiofx.BassBoost
|
||||
import android.media.audiofx.Equalizer
|
||||
import android.media.audiofx.Virtualizer
|
||||
|
||||
/**
|
||||
* @author Hemanth S (h4h13).
|
||||
*/
|
||||
|
||||
interface EqualizerInterface {
|
||||
val bandLevelLow: Int
|
||||
|
||||
val bandLevelHigh: Int
|
||||
|
||||
val numberOfBands: Int
|
||||
|
||||
var isBassBoostEnabled: Boolean
|
||||
|
||||
var bassBoostStrength: Int
|
||||
|
||||
var isVirtualizerEnabled: Boolean
|
||||
|
||||
var virtualizerStrength: Int
|
||||
|
||||
val isRunning: Boolean
|
||||
|
||||
val equalizer: Equalizer
|
||||
|
||||
val bassBoost: BassBoost
|
||||
|
||||
val virtualizer: Virtualizer
|
||||
|
||||
fun getCenterFreq(band: Int): Int
|
||||
|
||||
fun getBandLevel(band: Int): Int
|
||||
|
||||
fun setBandLevel(band: Int, level: Int)
|
||||
|
||||
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2019 Hemanth Savarala.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*/
|
||||
|
||||
package code.name.monkey.retromusic.interfaces
|
||||
|
||||
|
||||
interface LoaderIds {
|
||||
companion object {
|
||||
val FOLDERS_FRAGMENT = 5
|
||||
}
|
||||
}
|
|
@ -14,13 +14,9 @@
|
|||
|
||||
package code.name.monkey.retromusic.interfaces
|
||||
|
||||
import androidx.annotation.ColorInt
|
||||
|
||||
/**
|
||||
* @author Aidan Follestad (afollestad)
|
||||
*/
|
||||
interface PaletteColorHolder {
|
||||
|
||||
@get:ColorInt
|
||||
val paletteColor: Int
|
||||
}
|
||||
|
|
|
@ -1,17 +1,3 @@
|
|||
/*
|
||||
* Copyright (c) 2019 Hemanth Savarala.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*/
|
||||
|
||||
package code.name.monkey.retromusic.lyrics;
|
||||
|
||||
/**
|
||||
|
@ -25,20 +11,20 @@ public class Lrc {
|
|||
private long time;
|
||||
private String text;
|
||||
|
||||
public long getTime() {
|
||||
return time;
|
||||
}
|
||||
|
||||
public void setTime(long time) {
|
||||
this.time = time;
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return text;
|
||||
}
|
||||
|
||||
public void setText(String text) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
public long getTime() {
|
||||
return time;
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return text;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,17 +1,3 @@
|
|||
/*
|
||||
* Copyright (c) 2019 Hemanth Savarala.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*/
|
||||
|
||||
package code.name.monkey.retromusic.lyrics;
|
||||
|
||||
import android.content.Context;
|
||||
|
@ -42,8 +28,8 @@ public class LrcHelper {
|
|||
|
||||
private static final String CHARSET = "utf-8";
|
||||
//[03:56.00][03:18.00][02:06.00][01:07.00]原谅我这一生不羁放纵爱自由
|
||||
private static final String LINE_REGEX = "((\\[\\d{2}:\\d{2}\\.\\d{2,3}])+)(.*)";
|
||||
private static final String TIME_REGEX = "\\[(\\d{2}):(\\d{2})\\.(\\d{2,3})]";
|
||||
private static final String LINE_REGEX = "((\\[\\d{2}:\\d{2}\\.\\d{2}])+)(.*)";
|
||||
private static final String TIME_REGEX = "\\[(\\d{2}):(\\d{2})\\.(\\d{2})]";
|
||||
|
||||
public static List<Lrc> parseLrcFromAssets(Context context, String fileName) {
|
||||
try {
|
||||
|
@ -127,14 +113,8 @@ public class LrcHelper {
|
|||
String mil = timeMatcher.group(3);
|
||||
Lrc lrc = new Lrc();
|
||||
if (content != null && content.length() != 0) {
|
||||
if (Integer.parseInt(mil) < 100) {
|
||||
lrc.setTime(Long.parseLong(min) * 60 * 1000 + Long.parseLong(sec) * 1000
|
||||
+ Long.parseLong(mil) * 10);
|
||||
} else {
|
||||
lrc.setTime(Long.parseLong(min) * 60 * 1000 + Long.parseLong(sec) * 1000
|
||||
+ Long.parseLong(mil)
|
||||
);
|
||||
}
|
||||
lrc.setText(content);
|
||||
lrcs.add(lrc);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,663 @@
|
|||
package code.name.monkey.retromusic.lyrics;
|
||||
|
||||
import android.animation.ValueAnimator;
|
||||
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.Looper;
|
||||
import android.text.Layout;
|
||||
import android.text.StaticLayout;
|
||||
import android.text.TextPaint;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.TypedValue;
|
||||
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 androidx.annotation.ColorInt;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.view.ViewCompat;
|
||||
|
||||
import java.util.HashMap;
|
||||
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
|
||||
*/
|
||||
public class LrcView extends View {
|
||||
|
||||
private static final String DEFAULT_CONTENT = "Empty";
|
||||
private List<Lrc> 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 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<String, StaticLayout> mLrcMap = new HashMap<>();
|
||||
private Runnable mHideIndicatorRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
isShowTimeIndicator = false;
|
||||
invalidateView();
|
||||
}
|
||||
};
|
||||
private HashMap<String, StaticLayout> 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) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public LrcView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
init(context, attrs);
|
||||
}
|
||||
|
||||
private void init(Context context, AttributeSet attrs) {
|
||||
|
||||
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();
|
||||
|
||||
setupConfigs(context);
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
mTextPaint = new TextPaint();
|
||||
mTextPaint.setAntiAlias(true);
|
||||
mTextPaint.setTextAlign(Paint.Align.CENTER);
|
||||
mTextPaint.setTextSize(mLrcTextSize);
|
||||
mDefaultContent = DEFAULT_CONTENT;
|
||||
|
||||
mIndicatorPaint = new Paint();
|
||||
mIndicatorPaint.setAntiAlias(true);
|
||||
mIndicatorPaint.setStrokeWidth(mIndicatorLineWidth);
|
||||
mIndicatorPaint.setColor(mIndicatorLineColor);
|
||||
mPlayRect = new Rect();
|
||||
mIndicatorPaint.setTextSize(mIndicatorTextSize);
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
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<Lrc> lrcData) {
|
||||
resetView(DEFAULT_CONTENT);
|
||||
mLrcData = lrcData;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
if (isLrcEmpty()) {
|
||||
drawEmptyText(canvas);
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
canvas.save();
|
||||
canvas.translate(x, y - staticLayout.getHeight() / 2f - mOffset);
|
||||
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 (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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void computeScroll() {
|
||||
super.computeScroll();
|
||||
if (mOverScroller.computeScrollOffset()) {
|
||||
mOffset = mOverScroller.getCurrY();
|
||||
invalidateView();
|
||||
}
|
||||
}
|
||||
|
||||
private void releaseVelocityTracker() {
|
||||
if (null != mVelocityTracker) {
|
||||
mVelocityTracker.clear();
|
||||
mVelocityTracker.recycle();
|
||||
mVelocityTracker = null;
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
public int dp2px(Context context, float dpVal) {
|
||||
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
|
||||
dpVal, context.getResources().getDisplayMetrics());
|
||||
}
|
||||
|
||||
public int sp2px(Context context, float spVal) {
|
||||
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
|
||||
spVal, context.getResources().getDisplayMetrics());
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂停(手动滑动歌词后,不再自动回滚至当前播放位置)
|
||||
*/
|
||||
public void pause() {
|
||||
isAutoAdjustPosition = false;
|
||||
invalidateView();
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复(继续自动回滚)
|
||||
*/
|
||||
public void resume() {
|
||||
isAutoAdjustPosition = true;
|
||||
ViewCompat.postOnAnimationDelayed(LrcView.this, mScrollRunnable, mTouchDelay);
|
||||
invalidateView();
|
||||
}
|
||||
|
||||
|
||||
/*------------------Config-------------------*/
|
||||
|
||||
private void invalidateView() {
|
||||
if (Looper.getMainLooper().getThread() == Thread.currentThread()) {
|
||||
invalidate();
|
||||
} else {
|
||||
postInvalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public void setOnPlayIndicatorLineListener(OnPlayIndicatorLineListener onPlayIndicatorLineListener) {
|
||||
mOnPlayIndicatorLineListener = onPlayIndicatorLineListener;
|
||||
}
|
||||
|
||||
public void setEmptyContent(String defaultContent) {
|
||||
mDefaultContent = defaultContent;
|
||||
invalidateView();
|
||||
}
|
||||
|
||||
public void setLrcTextSize(float lrcTextSize) {
|
||||
mLrcTextSize = lrcTextSize;
|
||||
invalidateView();
|
||||
}
|
||||
|
||||
public void setLrcLineSpaceHeight(float lrcLineSpaceHeight) {
|
||||
mLrcLineSpaceHeight = lrcLineSpaceHeight;
|
||||
invalidateView();
|
||||
}
|
||||
|
||||
public void setTouchDelay(int touchDelay) {
|
||||
mTouchDelay = touchDelay;
|
||||
invalidateView();
|
||||
}
|
||||
|
||||
public void setNormalColor(@ColorInt int normalColor) {
|
||||
mNormalColor = normalColor;
|
||||
invalidateView();
|
||||
}
|
||||
|
||||
public void setCurrentPlayLineColor(@ColorInt int currentPlayLineColor) {
|
||||
mCurrentPlayLineColor = currentPlayLineColor;
|
||||
invalidateView();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -1,713 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2019 Hemanth Savarala.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*/
|
||||
package code.name.monkey.retromusic.lyrics
|
||||
|
||||
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Looper
|
||||
import android.text.Layout
|
||||
import android.text.StaticLayout
|
||||
import android.text.TextPaint
|
||||
import android.util.AttributeSet
|
||||
import android.util.TypedValue
|
||||
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 androidx.annotation.ColorInt
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import code.name.monkey.retromusic.BuildConfig
|
||||
import code.name.monkey.retromusic.R
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Desc : 歌词
|
||||
* Author : Lauzy
|
||||
* Date : 2017/10/13
|
||||
* Blog : http://www.jianshu.com/u/e76853f863a9
|
||||
* Email : freedompaladin@gmail.com
|
||||
*/
|
||||
class LrcView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : View(context, attrs, defStyleAttr) {
|
||||
private var mLrcData: MutableList<Lrc>? = null
|
||||
private var mTextPaint: TextPaint? = null
|
||||
private var mDefaultContent: String? = null
|
||||
private var mCurrentLine: Int = 0
|
||||
private var mOffset: Float = 0.toFloat()
|
||||
private var mLastMotionX: Float = 0.toFloat()
|
||||
private var mLastMotionY: Float = 0.toFloat()
|
||||
private var mScaledTouchSlop: Int = 0
|
||||
private var mOverScroller: OverScroller? = null
|
||||
private var mVelocityTracker: VelocityTracker? = null
|
||||
private var mMaximumFlingVelocity: Int = 0
|
||||
private var mMinimumFlingVelocity: Int = 0
|
||||
private var mLrcTextSize: Float = 0.toFloat()
|
||||
private var mLrcLineSpaceHeight: Float = 0.toFloat()
|
||||
private var mTouchDelay: Int = 0
|
||||
private var mNormalColor: Int = 0
|
||||
private var mCurrentPlayLineColor: Int = 0
|
||||
private var mNoLrcTextSize: Float = 0.toFloat()
|
||||
private var mNoLrcTextColor: Int = 0
|
||||
|
||||
//是否拖拽中,否的话响应onClick事件
|
||||
private var isDragging: Boolean = false
|
||||
|
||||
//用户开始操作
|
||||
private var isUserScroll: Boolean = false
|
||||
private var isAutoAdjustPosition = true
|
||||
private var mPlayDrawable: Drawable? = null
|
||||
private var isShowTimeIndicator: Boolean = false
|
||||
private var mPlayRect: Rect? = null
|
||||
private var mIndicatorPaint: Paint? = null
|
||||
private var mIndicatorLineWidth: Float = 0.toFloat()
|
||||
private var mIndicatorTextSize: Float = 0.toFloat()
|
||||
private var mCurrentIndicateLineTextColor: Int = 0
|
||||
private var mIndicatorLineColor: Int = 0
|
||||
private var mIndicatorMargin: Float = 0.toFloat()
|
||||
private var mIconLineGap: Float = 0.toFloat()
|
||||
private var mIconWidth: Float = 0.toFloat()
|
||||
private var mIconHeight: Float = 0.toFloat()
|
||||
private var isEnableShowIndicator = true
|
||||
private var mIndicatorTextColor: Int = 0
|
||||
private var mIndicatorTouchDelay: Int = 0
|
||||
private val mLrcMap = HashMap<String, StaticLayout>()
|
||||
private val mHideIndicatorRunnable = Runnable {
|
||||
isShowTimeIndicator = false
|
||||
invalidateView()
|
||||
}
|
||||
private val mStaticLayoutHashMap = HashMap<String, StaticLayout>()
|
||||
private val mScrollRunnable = Runnable {
|
||||
isUserScroll = false
|
||||
scrollToPosition(mCurrentLine)
|
||||
}
|
||||
private var mOnPlayIndicatorLineListener: OnPlayIndicatorLineListener? = null
|
||||
|
||||
private val lrcWidth: Int
|
||||
get() = width - paddingLeft - paddingRight
|
||||
|
||||
private val lrcHeight: Int
|
||||
get() = height
|
||||
|
||||
private val isLrcEmpty: Boolean
|
||||
get() = mLrcData == null || lrcCount == 0
|
||||
|
||||
private val lrcCount: Int
|
||||
get() = mLrcData!!.size
|
||||
|
||||
//itemOffset 和 mOffset 最小即当前位置
|
||||
val indicatePosition: Int
|
||||
get() {
|
||||
var pos = 0
|
||||
var min = java.lang.Float.MAX_VALUE
|
||||
for (i in mLrcData!!.indices) {
|
||||
val offsetY = getItemOffsetY(i)
|
||||
val abs = Math.abs(offsetY - mOffset)
|
||||
if (abs < min) {
|
||||
min = abs
|
||||
pos = i
|
||||
}
|
||||
}
|
||||
return pos
|
||||
}
|
||||
|
||||
var playDrawable: Drawable?
|
||||
get() = mPlayDrawable
|
||||
set(playDrawable) {
|
||||
mPlayDrawable = playDrawable
|
||||
mPlayDrawable!!.bounds = mPlayRect!!
|
||||
invalidateView()
|
||||
}
|
||||
|
||||
init {
|
||||
init(context, attrs)
|
||||
}
|
||||
|
||||
private fun init(context: Context, attrs: AttributeSet?) {
|
||||
|
||||
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.LrcView)
|
||||
mLrcTextSize =
|
||||
typedArray.getDimension(R.styleable.LrcView_lrcTextSize, sp2px(context, 15f).toFloat())
|
||||
mLrcLineSpaceHeight = typedArray.getDimension(
|
||||
R.styleable.LrcView_lrcLineSpaceSize,
|
||||
dp2px(context, 20f).toFloat()
|
||||
)
|
||||
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, 20f).toFloat()
|
||||
)
|
||||
mNoLrcTextColor = typedArray.getColor(R.styleable.LrcView_noLrcTextColor, Color.BLACK)
|
||||
mIndicatorLineWidth = typedArray.getDimension(
|
||||
R.styleable.LrcView_indicatorLineHeight,
|
||||
dp2px(context, 0.5f).toFloat()
|
||||
)
|
||||
mIndicatorTextSize = typedArray.getDimension(
|
||||
R.styleable.LrcView_indicatorTextSize,
|
||||
sp2px(context, 13f).toFloat()
|
||||
)
|
||||
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, 5f).toFloat()
|
||||
)
|
||||
mIconLineGap =
|
||||
typedArray.getDimension(R.styleable.LrcView_iconLineGap, dp2px(context, 3f).toFloat())
|
||||
mIconWidth = typedArray.getDimension(
|
||||
R.styleable.LrcView_playIconWidth,
|
||||
dp2px(context, 20f).toFloat()
|
||||
)
|
||||
mIconHeight = typedArray.getDimension(
|
||||
R.styleable.LrcView_playIconHeight,
|
||||
dp2px(context, 20f).toFloat()
|
||||
)
|
||||
mPlayDrawable = typedArray.getDrawable(R.styleable.LrcView_playIcon)
|
||||
mPlayDrawable = if (mPlayDrawable == null) ContextCompat.getDrawable(
|
||||
context,
|
||||
R.drawable.ic_play_arrow_white_24dp
|
||||
) else mPlayDrawable
|
||||
typedArray.recycle()
|
||||
|
||||
setupConfigs(context)
|
||||
}
|
||||
|
||||
private fun setupConfigs(context: Context) {
|
||||
mScaledTouchSlop = ViewConfiguration.get(context).scaledTouchSlop
|
||||
mMaximumFlingVelocity = ViewConfiguration.get(context).scaledMaximumFlingVelocity
|
||||
mMinimumFlingVelocity = ViewConfiguration.get(context).scaledMinimumFlingVelocity
|
||||
mOverScroller = OverScroller(context, DecelerateInterpolator())
|
||||
mOverScroller!!.setFriction(0.1f)
|
||||
// ViewConfiguration.getScrollFriction(); 默认摩擦力 0.015f
|
||||
|
||||
mTextPaint = TextPaint()
|
||||
mTextPaint!!.apply {
|
||||
isAntiAlias = true
|
||||
textAlign = Paint.Align.LEFT
|
||||
textSize = mLrcTextSize
|
||||
/* if (BuildConfig.FLAVOR != "nofont") {
|
||||
typeface = ResourcesCompat.getFont(context, R.font.circular)
|
||||
}*/
|
||||
}
|
||||
mDefaultContent = DEFAULT_CONTENT
|
||||
|
||||
mIndicatorPaint = Paint()
|
||||
mIndicatorPaint!!.isAntiAlias = true
|
||||
mIndicatorPaint!!.strokeWidth = mIndicatorLineWidth
|
||||
mIndicatorPaint!!.color = mIndicatorLineColor
|
||||
mPlayRect = Rect()
|
||||
mIndicatorPaint!!.textSize = mIndicatorTextSize
|
||||
}
|
||||
|
||||
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
||||
super.onLayout(changed, left, top, right, bottom)
|
||||
if (changed) {
|
||||
mPlayRect!!.left = mIndicatorMargin.toInt()
|
||||
mPlayRect!!.top = (height / 2 - mIconHeight / 2).toInt()
|
||||
mPlayRect!!.right = (mPlayRect!!.left + mIconWidth).toInt()
|
||||
mPlayRect!!.bottom = (mPlayRect!!.top + mIconHeight).toInt()
|
||||
mPlayDrawable!!.bounds = mPlayRect!!
|
||||
}
|
||||
}
|
||||
|
||||
fun setLrcData(lrcData: MutableList<Lrc>) {
|
||||
resetView(DEFAULT_CONTENT)
|
||||
mLrcData = lrcData
|
||||
invalidate()
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
if (isLrcEmpty) {
|
||||
drawEmptyText(canvas)
|
||||
return
|
||||
}
|
||||
val indicatePosition = indicatePosition
|
||||
mTextPaint!!.textSize = mLrcTextSize
|
||||
mTextPaint!!.textAlign = Paint.Align.LEFT
|
||||
var y = (lrcHeight / 2).toFloat()
|
||||
val x = dip2px(context, 16f).toFloat()
|
||||
for (i in 0 until lrcCount) {
|
||||
if (i > 0) {
|
||||
y += (getTextHeight(i - 1) + getTextHeight(i)) / 2 + mLrcLineSpaceHeight
|
||||
}
|
||||
if (mCurrentLine == i) {
|
||||
mTextPaint!!.color = mCurrentPlayLineColor
|
||||
} else if (indicatePosition == i && isShowTimeIndicator) {
|
||||
mTextPaint!!.color = mCurrentIndicateLineTextColor
|
||||
} else {
|
||||
mTextPaint!!.color = mNormalColor
|
||||
}
|
||||
drawLrc(canvas, x, y, i)
|
||||
}
|
||||
|
||||
if (isShowTimeIndicator) {
|
||||
mPlayDrawable!!.draw(canvas)
|
||||
val time = mLrcData!![indicatePosition].time
|
||||
val timeWidth = mIndicatorPaint!!.measureText(LrcHelper.formatTime(time))
|
||||
mIndicatorPaint!!.color = mIndicatorLineColor
|
||||
canvas.drawLine(
|
||||
mPlayRect!!.right + mIconLineGap, (height / 2).toFloat(),
|
||||
width - timeWidth * 1.3f, (height / 2).toFloat(), mIndicatorPaint!!
|
||||
)
|
||||
val baseX = (width - timeWidth * 1.1f).toInt()
|
||||
val baseline =
|
||||
(height / 2).toFloat() - (mIndicatorPaint!!.descent() - mIndicatorPaint!!.ascent()) / 2 - mIndicatorPaint!!.ascent()
|
||||
mIndicatorPaint!!.color = mIndicatorTextColor
|
||||
canvas.drawText(
|
||||
LrcHelper.formatTime(time),
|
||||
baseX.toFloat(),
|
||||
baseline,
|
||||
mIndicatorPaint!!
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun dip2px(context: Context, dpVale: Float): Int {
|
||||
val scale = context.resources.displayMetrics.density
|
||||
return (dpVale * scale + 0.5f).toInt()
|
||||
}
|
||||
|
||||
private fun drawLrc(canvas: Canvas, x: Float, y: Float, i: Int) {
|
||||
val text = mLrcData!![i].text
|
||||
var staticLayout: StaticLayout? = mLrcMap[text]
|
||||
if (staticLayout == null) {
|
||||
mTextPaint!!.textSize = mLrcTextSize
|
||||
staticLayout = StaticLayout(
|
||||
text,
|
||||
mTextPaint,
|
||||
lrcWidth,
|
||||
Layout.Alignment.ALIGN_NORMAL,
|
||||
1f,
|
||||
0f,
|
||||
false
|
||||
)
|
||||
mLrcMap[text] = staticLayout
|
||||
}
|
||||
canvas.save()
|
||||
canvas.translate(x, y - (staticLayout.height / 2).toFloat() - mOffset)
|
||||
staticLayout.draw(canvas)
|
||||
canvas.restore()
|
||||
}
|
||||
|
||||
//中间空文字
|
||||
private fun drawEmptyText(canvas: Canvas) {
|
||||
mTextPaint!!.textAlign = Paint.Align.LEFT
|
||||
mTextPaint!!.color = mNoLrcTextColor
|
||||
mTextPaint!!.textSize = mNoLrcTextSize
|
||||
canvas.save()
|
||||
val staticLayout = StaticLayout(
|
||||
mDefaultContent, mTextPaint,
|
||||
lrcWidth, Layout.Alignment.ALIGN_NORMAL, 1f, 0f, false
|
||||
)
|
||||
val margin = dip2px(context, 16f).toFloat();
|
||||
canvas.translate(margin, margin)
|
||||
staticLayout.draw(canvas)
|
||||
canvas.restore()
|
||||
}
|
||||
|
||||
fun updateTime(time: Long) {
|
||||
if (isLrcEmpty) {
|
||||
return
|
||||
}
|
||||
val linePosition = getUpdateTimeLinePosition(time)
|
||||
if (mCurrentLine != linePosition) {
|
||||
mCurrentLine = linePosition
|
||||
if (isUserScroll) {
|
||||
invalidateView()
|
||||
return
|
||||
}
|
||||
ViewCompat.postOnAnimation(this@LrcView, mScrollRunnable)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getUpdateTimeLinePosition(time: Long): Int {
|
||||
var linePos = 0
|
||||
for (i in 0 until lrcCount) {
|
||||
val lrc = mLrcData!![i]
|
||||
if (time >= lrc.time) {
|
||||
if (i == lrcCount - 1) {
|
||||
linePos = lrcCount - 1
|
||||
} else if (time < mLrcData!![i + 1].time) {
|
||||
linePos = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return linePos
|
||||
}
|
||||
|
||||
private fun scrollToPosition(linePosition: Int) {
|
||||
val scrollY = getItemOffsetY(linePosition)
|
||||
val animator = ValueAnimator.ofFloat(mOffset, scrollY)
|
||||
animator.addUpdateListener { animation ->
|
||||
mOffset = animation.animatedValue as Float
|
||||
invalidateView()
|
||||
}
|
||||
animator.duration = 300
|
||||
animator.start()
|
||||
}
|
||||
|
||||
private fun getItemOffsetY(linePosition: Int): Float {
|
||||
var tempY = 0f
|
||||
for (i in 1..linePosition) {
|
||||
tempY += (getTextHeight(i - 1) + getTextHeight(i)) / 2 + mLrcLineSpaceHeight
|
||||
}
|
||||
return tempY
|
||||
}
|
||||
|
||||
private fun getTextHeight(linePosition: Int): Float {
|
||||
val text = mLrcData!![linePosition].text
|
||||
var staticLayout: StaticLayout? = mStaticLayoutHashMap[text]
|
||||
if (staticLayout == null) {
|
||||
mTextPaint!!.textSize = mLrcTextSize
|
||||
staticLayout = StaticLayout(
|
||||
text, mTextPaint,
|
||||
lrcWidth, Layout.Alignment.ALIGN_NORMAL, 1f, 0f, false
|
||||
)
|
||||
mStaticLayoutHashMap[text] = staticLayout
|
||||
}
|
||||
return staticLayout.height.toFloat()
|
||||
}
|
||||
|
||||
private fun overScrolled(): Boolean {
|
||||
return mOffset > getItemOffsetY(lrcCount - 1) || mOffset < 0
|
||||
}
|
||||
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
if (isLrcEmpty) {
|
||||
return super.onTouchEvent(event)
|
||||
}
|
||||
if (mVelocityTracker == null) {
|
||||
mVelocityTracker = VelocityTracker.obtain()
|
||||
}
|
||||
mVelocityTracker!!.addMovement(event)
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
removeCallbacks(mScrollRunnable)
|
||||
removeCallbacks(mHideIndicatorRunnable)
|
||||
if (!mOverScroller!!.isFinished) {
|
||||
mOverScroller!!.abortAnimation()
|
||||
}
|
||||
mLastMotionX = event.x
|
||||
mLastMotionY = event.y
|
||||
isUserScroll = true
|
||||
isDragging = false
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
var moveY = event.y - mLastMotionY
|
||||
if (Math.abs(moveY) > mScaledTouchSlop) {
|
||||
isDragging = true
|
||||
isShowTimeIndicator = isEnableShowIndicator
|
||||
}
|
||||
if (isDragging) {
|
||||
|
||||
// if (mOffset < 0) {
|
||||
// mOffset = Math.max(mOffset, -getTextHeight(0) - mLrcLineSpaceHeight);
|
||||
// }
|
||||
val maxHeight = getItemOffsetY(lrcCount - 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.y
|
||||
invalidateView()
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> {
|
||||
if (!isDragging && (!isShowTimeIndicator || !onClickPlayButton(event))) {
|
||||
isShowTimeIndicator = false
|
||||
invalidateView()
|
||||
performClick()
|
||||
}
|
||||
handleActionUp(event)
|
||||
}
|
||||
}
|
||||
// return isDragging || super.onTouchEvent(event);
|
||||
return true
|
||||
}
|
||||
|
||||
private fun handleActionUp(event: MotionEvent) {
|
||||
if (isEnableShowIndicator) {
|
||||
ViewCompat.postOnAnimationDelayed(
|
||||
this@LrcView,
|
||||
mHideIndicatorRunnable,
|
||||
mIndicatorTouchDelay.toLong()
|
||||
)
|
||||
}
|
||||
if (isShowTimeIndicator && mPlayRect != null && onClickPlayButton(event)) {
|
||||
isShowTimeIndicator = false
|
||||
invalidateView()
|
||||
if (mOnPlayIndicatorLineListener != null) {
|
||||
mOnPlayIndicatorLineListener!!.onPlay(
|
||||
mLrcData!![indicatePosition].time,
|
||||
mLrcData!![indicatePosition].text
|
||||
)
|
||||
}
|
||||
}
|
||||
if (overScrolled() && mOffset < 0) {
|
||||
scrollToPosition(0)
|
||||
if (isAutoAdjustPosition) {
|
||||
ViewCompat.postOnAnimationDelayed(
|
||||
this@LrcView,
|
||||
mScrollRunnable,
|
||||
mTouchDelay.toLong()
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (overScrolled() && mOffset > getItemOffsetY(lrcCount - 1)) {
|
||||
scrollToPosition(lrcCount - 1)
|
||||
if (isAutoAdjustPosition) {
|
||||
ViewCompat.postOnAnimationDelayed(
|
||||
this@LrcView,
|
||||
mScrollRunnable,
|
||||
mTouchDelay.toLong()
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
mVelocityTracker!!.computeCurrentVelocity(1000, mMaximumFlingVelocity.toFloat())
|
||||
val YVelocity = mVelocityTracker!!.yVelocity
|
||||
val absYVelocity = Math.abs(YVelocity)
|
||||
if (absYVelocity > mMinimumFlingVelocity) {
|
||||
mOverScroller!!.fling(
|
||||
0, mOffset.toInt(), 0, (-YVelocity).toInt(), 0,
|
||||
0, 0, getItemOffsetY(lrcCount - 1).toInt(),
|
||||
0, getTextHeight(0).toInt()
|
||||
)
|
||||
invalidateView()
|
||||
}
|
||||
releaseVelocityTracker()
|
||||
if (isAutoAdjustPosition) {
|
||||
ViewCompat.postOnAnimationDelayed(this@LrcView, mScrollRunnable, mTouchDelay.toLong())
|
||||
}
|
||||
}
|
||||
|
||||
private fun onClickPlayButton(event: MotionEvent): Boolean {
|
||||
val left = mPlayRect!!.left.toFloat()
|
||||
val right = mPlayRect!!.right.toFloat()
|
||||
val top = mPlayRect!!.top.toFloat()
|
||||
val bottom = mPlayRect!!.bottom.toFloat()
|
||||
val x = event.x
|
||||
val y = event.y
|
||||
return (mLastMotionX > left && mLastMotionX < right && mLastMotionY > top
|
||||
&& mLastMotionY < bottom && x > left && x < right && y > top && y < bottom)
|
||||
}
|
||||
|
||||
override fun computeScroll() {
|
||||
super.computeScroll()
|
||||
if (mOverScroller!!.computeScrollOffset()) {
|
||||
mOffset = mOverScroller!!.currY.toFloat()
|
||||
invalidateView()
|
||||
}
|
||||
}
|
||||
|
||||
private fun releaseVelocityTracker() {
|
||||
if (null != mVelocityTracker) {
|
||||
mVelocityTracker!!.clear()
|
||||
mVelocityTracker!!.recycle()
|
||||
mVelocityTracker = null
|
||||
}
|
||||
}
|
||||
|
||||
fun resetView(defaultContent: String) {
|
||||
if (mLrcData != null) {
|
||||
mLrcData!!.clear()
|
||||
}
|
||||
mLrcMap.clear()
|
||||
mStaticLayoutHashMap.clear()
|
||||
mCurrentLine = 0
|
||||
mOffset = 0f
|
||||
isUserScroll = false
|
||||
isDragging = false
|
||||
mDefaultContent = defaultContent
|
||||
removeCallbacks(mScrollRunnable)
|
||||
invalidate()
|
||||
}
|
||||
|
||||
override fun performClick(): Boolean {
|
||||
return super.performClick()
|
||||
}
|
||||
|
||||
fun dp2px(context: Context, dpVal: Float): Int {
|
||||
return TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP,
|
||||
dpVal, context.resources.displayMetrics
|
||||
).toInt()
|
||||
}
|
||||
|
||||
fun sp2px(context: Context, spVal: Float): Int {
|
||||
return TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_SP,
|
||||
spVal, context.resources.displayMetrics
|
||||
).toInt()
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂停(手动滑动歌词后,不再自动回滚至当前播放位置)
|
||||
*/
|
||||
fun pause() {
|
||||
isAutoAdjustPosition = false
|
||||
invalidateView()
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复(继续自动回滚)
|
||||
*/
|
||||
fun resume() {
|
||||
isAutoAdjustPosition = true
|
||||
ViewCompat.postOnAnimationDelayed(this@LrcView, mScrollRunnable, mTouchDelay.toLong())
|
||||
invalidateView()
|
||||
}
|
||||
|
||||
|
||||
/*------------------Config-------------------*/
|
||||
|
||||
private fun invalidateView() {
|
||||
if (Looper.getMainLooper().thread === Thread.currentThread()) {
|
||||
invalidate()
|
||||
} else {
|
||||
postInvalidate()
|
||||
}
|
||||
}
|
||||
|
||||
fun setOnPlayIndicatorLineListener(onPlayIndicatorLineListener: OnPlayIndicatorLineListener) {
|
||||
mOnPlayIndicatorLineListener = onPlayIndicatorLineListener
|
||||
}
|
||||
|
||||
fun setEmptyContent(defaultContent: String) {
|
||||
mDefaultContent = defaultContent
|
||||
invalidateView()
|
||||
}
|
||||
|
||||
fun setLrcTextSize(lrcTextSize: Float) {
|
||||
mLrcTextSize = lrcTextSize
|
||||
invalidateView()
|
||||
}
|
||||
|
||||
fun setLrcLineSpaceHeight(lrcLineSpaceHeight: Float) {
|
||||
mLrcLineSpaceHeight = lrcLineSpaceHeight
|
||||
invalidateView()
|
||||
}
|
||||
|
||||
fun setTouchDelay(touchDelay: Int) {
|
||||
mTouchDelay = touchDelay
|
||||
invalidateView()
|
||||
}
|
||||
|
||||
fun setNormalColor(@ColorInt normalColor: Int) {
|
||||
mNormalColor = normalColor
|
||||
invalidateView()
|
||||
}
|
||||
|
||||
fun setCurrentPlayLineColor(@ColorInt currentPlayLineColor: Int) {
|
||||
mCurrentPlayLineColor = currentPlayLineColor
|
||||
invalidateView()
|
||||
}
|
||||
|
||||
fun setNoLrcTextSize(noLrcTextSize: Float) {
|
||||
mNoLrcTextSize = noLrcTextSize
|
||||
invalidateView()
|
||||
}
|
||||
|
||||
fun setNoLrcTextColor(@ColorInt noLrcTextColor: Int) {
|
||||
mNoLrcTextColor = noLrcTextColor
|
||||
invalidateView()
|
||||
}
|
||||
|
||||
fun setIndicatorLineWidth(indicatorLineWidth: Float) {
|
||||
mIndicatorLineWidth = indicatorLineWidth
|
||||
invalidateView()
|
||||
}
|
||||
|
||||
fun setIndicatorTextSize(indicatorTextSize: Float) {
|
||||
// mIndicatorTextSize = indicatorTextSize;
|
||||
mIndicatorPaint!!.textSize = indicatorTextSize
|
||||
invalidateView()
|
||||
}
|
||||
|
||||
fun setCurrentIndicateLineTextColor(currentIndicateLineTextColor: Int) {
|
||||
mCurrentIndicateLineTextColor = currentIndicateLineTextColor
|
||||
invalidateView()
|
||||
}
|
||||
|
||||
fun setIndicatorLineColor(indicatorLineColor: Int) {
|
||||
mIndicatorLineColor = indicatorLineColor
|
||||
invalidateView()
|
||||
}
|
||||
|
||||
fun setIndicatorMargin(indicatorMargin: Float) {
|
||||
mIndicatorMargin = indicatorMargin
|
||||
invalidateView()
|
||||
}
|
||||
|
||||
fun setIconLineGap(iconLineGap: Float) {
|
||||
mIconLineGap = iconLineGap
|
||||
invalidateView()
|
||||
}
|
||||
|
||||
fun setIconWidth(iconWidth: Float) {
|
||||
mIconWidth = iconWidth
|
||||
invalidateView()
|
||||
}
|
||||
|
||||
fun setIconHeight(iconHeight: Float) {
|
||||
mIconHeight = iconHeight
|
||||
invalidateView()
|
||||
}
|
||||
|
||||
fun setEnableShowIndicator(enableShowIndicator: Boolean) {
|
||||
isEnableShowIndicator = enableShowIndicator
|
||||
invalidateView()
|
||||
}
|
||||
|
||||
fun setIndicatorTextColor(indicatorTextColor: Int) {
|
||||
mIndicatorTextColor = indicatorTextColor
|
||||
invalidateView()
|
||||
}
|
||||
|
||||
interface OnPlayIndicatorLineListener {
|
||||
fun onPlay(time: Long, content: String)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DEFAULT_CONTENT = "Empty"
|
||||
}
|
||||
}
|
|
@ -170,6 +170,8 @@
|
|||
<androidx.appcompat.widget.AppCompatSeekBar
|
||||
android:id="@+id/progressSlider"
|
||||
android:layout_width="0dp"
|
||||
android:maxHeight="2dp"
|
||||
android:progressDrawable="@drawable/color_progress_seek"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:layout_toLeftOf="@id/songTotalTime"
|
||||
|
|
|
@ -1,152 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/colorSurface"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appBarLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@android:color/transparent"
|
||||
android:elevation="0dp"
|
||||
app:elevation="0dp">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
style="@style/Toolbar"
|
||||
app:navigationIcon="@drawable/ic_keyboard_backspace_black_24dp"
|
||||
app:title="@string/profile"
|
||||
app:titleTextAppearance="@style/ToolbarTextAppearanceNormal" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="128dp"
|
||||
android:layout_marginEnd="128dp"
|
||||
android:orientation="vertical"
|
||||
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/imageContainer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_margin="24dp"
|
||||
app:cardCornerRadius="24dp"
|
||||
app:layout_constraintDimensionRatio="21:10"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/bannerImage"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintDimensionRatio="16:9"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/material_design_default" />
|
||||
|
||||
<View
|
||||
android:id="@+id/bannerSelect"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:background="@color/twenty_percent_black_overlay"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:background="?roundSelector"
|
||||
android:padding="16dp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/bannerImage"
|
||||
app:layout_constraintEnd_toEndOf="@id/bannerImage"
|
||||
app:layout_constraintStart_toStartOf="@id/bannerImage"
|
||||
app:layout_constraintTop_toTopOf="@id/bannerImage"
|
||||
app:srcCompat="@drawable/ic_add_photo_white_24dp"
|
||||
app:tint="@color/md_white_1000" />
|
||||
|
||||
<code.name.monkey.retromusic.views.RetroShapeableImageView
|
||||
android:id="@+id/userImage"
|
||||
android:layout_width="72dp"
|
||||
android:layout_height="72dp"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:retroCornerSize="36dp"
|
||||
app:srcCompat="@drawable/ic_account_white_24dp" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_margin="16dp"
|
||||
android:gravity="center"
|
||||
android:padding="12dp"
|
||||
android:src="@drawable/ic_add_photo_white_24dp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/userImage"
|
||||
app:layout_constraintEnd_toEndOf="@id/userImage"
|
||||
app:layout_constraintStart_toStartOf="@id/userImage"
|
||||
app:layout_constraintTop_toTopOf="@id/userImage"
|
||||
app:tint="?android:attr/colorControlNormal" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/nameContainer"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:hintEnabled="true"
|
||||
app:layout_constraintBottom_toBottomOf="@id/next"
|
||||
app:layout_constraintEnd_toStartOf="@id/next"
|
||||
app:layout_constraintStart_toStartOf="@id/imageContainer"
|
||||
app:layout_constraintTop_toTopOf="@id/next">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@null"
|
||||
android:hint="@string/my_name"
|
||||
android:inputType="textPersonName|textCapWords|text"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/next"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:gravity="center"
|
||||
android:padding="16dp"
|
||||
android:text="@string/save"
|
||||
app:icon="@drawable/ic_save_white_24dp"
|
||||
app:iconGravity="textStart"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="@id/imageContainer"
|
||||
app:layout_constraintStart_toEndOf="@id/nameContainer"
|
||||
app:layout_constraintTop_toBottomOf="@id/imageContainer" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
@ -57,6 +57,7 @@
|
|||
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/fab"
|
||||
android:visibility="gone"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
|
|
|
@ -26,6 +26,8 @@
|
|||
<androidx.appcompat.widget.AppCompatSeekBar
|
||||
android:id="@+id/progressSlider"
|
||||
android:layout_width="0dp"
|
||||
android:maxHeight="2dp"
|
||||
android:progressDrawable="@drawable/color_progress_seek"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toStartOf="@id/songTotalTime"
|
||||
app:layout_constraintStart_toEndOf="@id/songCurrentProgress"
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
android:id="@+id/imageContainer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:cardCornerRadius="24dp"
|
||||
app:cardCornerRadius="16dp"
|
||||
app:cardUseCompatPadding="true"
|
||||
app:layout_constraintDimensionRatio="21:10"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
|
|
|
@ -42,6 +42,8 @@
|
|||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:splitTrack="false"
|
||||
android:maxHeight="2dp"
|
||||
android:progressDrawable="@drawable/color_progress_seek"
|
||||
app:layout_constraintEnd_toStartOf="@id/songTotalTime"
|
||||
app:layout_constraintStart_toEndOf="@id/songCurrentProgress"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
|
|
@ -56,6 +56,8 @@
|
|||
<androidx.appcompat.widget.AppCompatSeekBar
|
||||
android:id="@+id/progressSlider"
|
||||
android:layout_width="match_parent"
|
||||
android:maxHeight="2dp"
|
||||
android:progressDrawable="@drawable/color_progress_seek"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingEnd="20dp"
|
||||
|
|
|
@ -28,6 +28,8 @@
|
|||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:maxHeight="2dp"
|
||||
android:progressDrawable="@drawable/color_progress_seek"
|
||||
app:layout_constraintEnd_toStartOf="@id/songTotalTime"
|
||||
app:layout_constraintStart_toEndOf="@id/songCurrentProgress"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
|
|
|
@ -92,6 +92,8 @@
|
|||
<androidx.appcompat.widget.AppCompatSeekBar
|
||||
android:id="@+id/progressSlider"
|
||||
android:layout_width="0dp"
|
||||
android:maxHeight="2dp"
|
||||
android:progressDrawable="@drawable/color_progress_seek"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
app:layout_constraintEnd_toStartOf="@id/songTotalTime"
|
||||
|
|
|
@ -28,6 +28,8 @@
|
|||
<androidx.appcompat.widget.AppCompatSeekBar
|
||||
android:id="@+id/progressSlider"
|
||||
android:layout_width="0dp"
|
||||
android:maxHeight="2dp"
|
||||
android:progressDrawable="@drawable/color_progress_seek"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toStartOf="@id/songTotalTime"
|
||||
app:layout_constraintStart_toEndOf="@id/songCurrentProgress"
|
||||
|
|
|
@ -29,6 +29,8 @@
|
|||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:maxHeight="2dp"
|
||||
android:progressDrawable="@drawable/color_progress_seek"
|
||||
app:layout_constraintEnd_toStartOf="@id/songTotalTime"
|
||||
app:layout_constraintStart_toEndOf="@id/songCurrentProgress"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
|
|
|
@ -39,6 +39,8 @@
|
|||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:maxHeight="2dp"
|
||||
android:progressDrawable="@drawable/color_progress_seek"
|
||||
app:layout_constraintEnd_toStartOf="@id/songTotalTime"
|
||||
app:layout_constraintStart_toEndOf="@id/songCurrentProgress"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
|
|
|
@ -26,6 +26,8 @@
|
|||
android:id="@+id/progressSlider"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxHeight="2dp"
|
||||
android:progressDrawable="@drawable/color_progress_seek"
|
||||
app:layout_constraintBottom_toTopOf="@+id/playPauseButton"
|
||||
app:layout_constraintEnd_toStartOf="@id/songTotalTime"
|
||||
app:layout_constraintStart_toEndOf="@id/songCurrentProgress"
|
||||
|
|
|
@ -27,6 +27,8 @@
|
|||
<androidx.appcompat.widget.AppCompatSeekBar
|
||||
android:id="@+id/progressSlider"
|
||||
android:layout_width="0dp"
|
||||
android:maxHeight="2dp"
|
||||
android:progressDrawable="@drawable/color_progress_seek"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toStartOf="@id/songTotalTime"
|
||||
app:layout_constraintStart_toEndOf="@id/songCurrentProgress"
|
||||
|
|
|
@ -32,5 +32,7 @@
|
|||
<attr name="playIconWidth" format="dimension"/>
|
||||
<attr name="playIconHeight" format="dimension"/>
|
||||
<attr name="playIcon" format="reference"/>
|
||||
<attr name="isLrcCurrentTextBold" format="boolean"/>
|
||||
<attr name="isLrcIndicatorTextBold" format="boolean"/>
|
||||
</declare-styleable>
|
||||
</resources>
|
Loading…
Reference in a new issue