更新了歌词组件支持更多操作
This commit is contained in:
parent
f78e19db08
commit
595e887010
BIN
app/release/Cloud_music-release-v1.9.7.apk
Normal file
BIN
app/release/Cloud_music-release-v1.9.7.apk
Normal file
Binary file not shown.
20
app/release/output-metadata.json
Normal file
20
app/release/output-metadata.json
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"artifactType": {
|
||||||
|
"type": "APK",
|
||||||
|
"kind": "Directory"
|
||||||
|
},
|
||||||
|
"applicationId": "com.muqingbfq",
|
||||||
|
"variantName": "release",
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "SINGLE",
|
||||||
|
"filters": [],
|
||||||
|
"attributes": [],
|
||||||
|
"versionCode": 1,
|
||||||
|
"versionName": "1.9.7",
|
||||||
|
"outputFile": "Cloud_music-release-v1.9.7.apk"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"elementType": "File"
|
||||||
|
}
|
1
lrcview/.gitignore
vendored
Normal file
1
lrcview/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/build
|
22
lrcview/build.gradle
Normal file
22
lrcview/build.gradle
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
apply plugin: 'com.android.library'
|
||||||
|
apply plugin: 'maven-publish'
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace 'me.wcy.lrcview'
|
||||||
|
compileSdk 34
|
||||||
|
defaultConfig {
|
||||||
|
minSdkVersion 23
|
||||||
|
//noinspection ExpiredTargetSdkVersion
|
||||||
|
targetSdk 31
|
||||||
|
versionCode 1
|
||||||
|
versionName "1.0"
|
||||||
|
}
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = 1.8
|
||||||
|
targetCompatibility = 1.8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||||
|
implementation 'com.google.android.material:material:1.9.0'
|
||||||
|
}
|
5
lrcview/src/main/AndroidManifest.xml
Normal file
5
lrcview/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
|
</manifest>
|
117
lrcview/src/main/java/me/wcy/lrcview/LrcEntry.java
Normal file
117
lrcview/src/main/java/me/wcy/lrcview/LrcEntry.java
Normal file
|
@ -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 me.wcy.lrcview;
|
||||||
|
|
||||||
|
import android.text.Layout;
|
||||||
|
import android.text.StaticLayout;
|
||||||
|
import android.text.TextPaint;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 一行歌词实体
|
||||||
|
*/
|
||||||
|
public class LrcEntry implements Comparable<LrcEntry> {
|
||||||
|
public long time;
|
||||||
|
public String text, 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public 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());
|
||||||
|
}
|
||||||
|
}
|
244
lrcview/src/main/java/me/wcy/lrcview/LrcUtils.java
Normal file
244
lrcview/src/main/java/me/wcy/lrcview/LrcUtils.java
Normal file
|
@ -0,0 +1,244 @@
|
||||||
|
/*
|
||||||
|
* 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 me.wcy.lrcview;
|
||||||
|
|
||||||
|
import android.animation.ValueAnimator;
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.text.format.DateUtils;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
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.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
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<LrcEntry> parseLrc(File[] lrcFiles) {
|
||||||
|
if (lrcFiles == null || lrcFiles.length != 2 || lrcFiles[0] == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
File mainLrcFile = lrcFiles[0];
|
||||||
|
File secondLrcFile = lrcFiles[1];
|
||||||
|
List<LrcEntry> mainEntryList = parseLrc(mainLrcFile);
|
||||||
|
List<LrcEntry> 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<LrcEntry> parseLrc(File lrcFile) {
|
||||||
|
if (lrcFile == null || !lrcFile.exists()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<LrcEntry> entryList = new ArrayList<>();
|
||||||
|
try {
|
||||||
|
@SuppressLint({"NewApi", "LocalSuppress"})
|
||||||
|
BufferedReader br = new BufferedReader(new InputStreamReader(
|
||||||
|
Files.newInputStream(lrcFile.toPath()), StandardCharsets.UTF_8));
|
||||||
|
String line;
|
||||||
|
while ((line = br.readLine()) != null) {
|
||||||
|
List<LrcEntry> 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<LrcEntry> parseLrc(String[] lrcTexts) {
|
||||||
|
if (lrcTexts == null || lrcTexts.length != 2 || TextUtils.isEmpty(lrcTexts[0])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String mainLrcText = lrcTexts[0];
|
||||||
|
String secondLrcText = lrcTexts[1];
|
||||||
|
List<LrcEntry> mainEntryList = parseLrc(mainLrcText);
|
||||||
|
List<LrcEntry> 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<LrcEntry> parseLrc(String lrcText) {
|
||||||
|
if (TextUtils.isEmpty(lrcText)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lrcText.startsWith("\uFEFF")) {
|
||||||
|
lrcText = lrcText.replace("\uFEFF", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<LrcEntry> entryList = new ArrayList<>();
|
||||||
|
String[] array = lrcText.split("\\n");
|
||||||
|
for (String line : array) {
|
||||||
|
List<LrcEntry> 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<LrcEntry> 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<LrcEntry> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 强制开启动画
|
||||||
|
* Android 10 以后无法使用
|
||||||
|
*/
|
||||||
|
static void resetDurationScale() {
|
||||||
|
try {
|
||||||
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
|
||||||
|
@SuppressLint("DiscouragedPrivateApi")
|
||||||
|
Field mField = ValueAnimator.class.getDeclaredField("sDurationScale");
|
||||||
|
mField.setAccessible(true);
|
||||||
|
mField.setFloat(null, 1);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
811
lrcview/src/main/java/me/wcy/lrcview/LrcView.java
Normal file
811
lrcview/src/main/java/me/wcy/lrcview/LrcView.java
Normal file
|
@ -0,0 +1,811 @@
|
||||||
|
/*
|
||||||
|
* 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 me.wcy.lrcview;
|
||||||
|
|
||||||
|
import android.animation.ValueAnimator;
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.res.TypedArray;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
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.Log;
|
||||||
|
import android.view.GestureDetector;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.animation.LinearInterpolator;
|
||||||
|
import android.widget.Scroller;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 歌词
|
||||||
|
* Created by wcy on 2015/11/9.
|
||||||
|
*
|
||||||
|
* @noinspection unused
|
||||||
|
*/
|
||||||
|
@SuppressLint("StaticFieldLeak")
|
||||||
|
public class LrcView extends View {
|
||||||
|
private static final String TAG = "LrcView";
|
||||||
|
private static final long ADJUST_DURATION = 100;
|
||||||
|
private static final long TIMELINE_KEEP_TIME = 4 * DateUtils.SECOND_IN_MILLIS;
|
||||||
|
public static List<LrcEntry> mLrcEntryList = new ArrayList<>();
|
||||||
|
private TextPaint mLrcPaint = new TextPaint(), mTimePaint = new TextPaint();
|
||||||
|
private Paint.FontMetrics mTimeFontMetrics;
|
||||||
|
private Drawable mPlayDrawable;
|
||||||
|
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 OnTapListener mOnTapListener;
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 播放按钮点击监听器,点击后应该跳转到指定播放位置
|
||||||
|
*/
|
||||||
|
public interface OnPlayClickListener {
|
||||||
|
/**
|
||||||
|
* 播放按钮被点击,应该跳转到指定播放位置
|
||||||
|
*
|
||||||
|
* @param view 歌词控件
|
||||||
|
* @param time 选中播放进度
|
||||||
|
* @return 是否成功消费该事件,如果成功消费,则会更新UI
|
||||||
|
*/
|
||||||
|
boolean onPlayClick(LrcView view, long time);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 歌词控件点击监听器
|
||||||
|
*/
|
||||||
|
public interface OnTapListener {
|
||||||
|
/**
|
||||||
|
* 歌词控件被点击
|
||||||
|
*
|
||||||
|
* @param view 歌词控件
|
||||||
|
* @param x 点击坐标x,相对于控件
|
||||||
|
* @param y 点击坐标y,相对于控件
|
||||||
|
*/
|
||||||
|
void onTap(LrcView view, float x, float y);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LrcView(Context context) {
|
||||||
|
this(context, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LrcView(Context context, AttributeSet attrs) {
|
||||||
|
this(context, attrs, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LrcView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
init(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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
ContextCompat.getColor(getContext(), R.color.lrc_normal_text_color));
|
||||||
|
mCurrentTextColor = ta.getColor(R.styleable.LrcView_lrcCurrentTextColor,
|
||||||
|
ContextCompat.getColor(getContext(), R.color.lrc_current_text_color));
|
||||||
|
mTimelineTextColor = ta.getColor(R.styleable.LrcView_lrcTimelineTextColor,
|
||||||
|
ContextCompat.getColor(getContext(), R.color.lrc_timeline_text_color));
|
||||||
|
mDefaultLabel = ta.getString(R.styleable.LrcView_lrcLabel);
|
||||||
|
mDefaultLabel = TextUtils.isEmpty(mDefaultLabel) ? getContext().getString(R.string.lrc_label) : mDefaultLabel;
|
||||||
|
mLrcPadding = ta.getDimension(R.styleable.LrcView_lrcPadding, 0);
|
||||||
|
mTimelineColor = ta.getColor(R.styleable.LrcView_lrcTimelineColor,
|
||||||
|
ContextCompat.getColor(getContext(), 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) ?
|
||||||
|
ContextCompat.getDrawable(getContext(), R.drawable.lrc_play) : mPlayDrawable;
|
||||||
|
mTimeTextColor = ta.getColor(R.styleable.LrcView_lrcTimeTextColor,
|
||||||
|
ContextCompat.getColor(getContext(), 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);
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置非当前行歌词字体颜色
|
||||||
|
*/
|
||||||
|
public void setNormalColor(int normalColor) {
|
||||||
|
mNormalTextColor = normalColor;
|
||||||
|
postInvalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 普通歌词文本字体大小
|
||||||
|
*/
|
||||||
|
public void setNormalTextSize(float size) {
|
||||||
|
mNormalTextSize = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前歌词文本字体大小
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置歌词控件点击监听器
|
||||||
|
*
|
||||||
|
* @param onTapListener 歌词控件点击监听器
|
||||||
|
*/
|
||||||
|
public void setOnTapListener(OnTapListener onTapListener) {
|
||||||
|
mOnTapListener = onTapListener;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置歌词为空时屏幕中央显示的文字,如“暂无歌词”
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||||
|
Callable<List<LrcEntry>> callable = () -> LrcUtils.parseLrc(new File[]{mainLrcFile, secondLrcFile});
|
||||||
|
Future<List<LrcEntry>> future = executor.submit(callable);
|
||||||
|
try {
|
||||||
|
List<LrcEntry> lrcEntries = future.get();
|
||||||
|
if (getFlag() == flag) {
|
||||||
|
onLrcLoaded(lrcEntries);
|
||||||
|
setFlag(null);
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
// 处理中断异常
|
||||||
|
e.printStackTrace();
|
||||||
|
} catch (ExecutionException e) {
|
||||||
|
// 处理执行异常
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
// 关闭线程池
|
||||||
|
executor.shutdown();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载歌词文本
|
||||||
|
*
|
||||||
|
* @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);
|
||||||
|
ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||||
|
Callable<List<LrcEntry>> callable = () -> LrcUtils.parseLrc(
|
||||||
|
new String[]{mainLrcText, secondLrcText});
|
||||||
|
Future<List<LrcEntry>> future = executor.submit(callable);
|
||||||
|
try {
|
||||||
|
List<LrcEntry> lrcEntries = future.get();
|
||||||
|
if (getFlag() == flag) {
|
||||||
|
onLrcLoaded(lrcEntries);
|
||||||
|
setFlag(null);
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
// 处理中断异常
|
||||||
|
e.printStackTrace();
|
||||||
|
} catch (ExecutionException e) {
|
||||||
|
// 处理执行异常
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
// 关闭线程池
|
||||||
|
executor.shutdown();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 歌词是否有效
|
||||||
|
*
|
||||||
|
* @return true,如果歌词有效,否则false
|
||||||
|
*/
|
||||||
|
public boolean hasLrc() {
|
||||||
|
return !mLrcEntryList.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新歌词
|
||||||
|
* time 当前播放时间
|
||||||
|
*/
|
||||||
|
|
||||||
|
public int index = 0;
|
||||||
|
public void updateTime(long time) {
|
||||||
|
if (mOnPlayClickListener == null) {
|
||||||
|
runOnUi(() -> {
|
||||||
|
if (!hasLrc()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
index = findShowLine(time);
|
||||||
|
invalidate();
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
initPlayDrawable();
|
||||||
|
initEntryList();
|
||||||
|
if (hasLrc()) {
|
||||||
|
smoothScrollTo(mCurrentLine, 0L);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDraw(@NonNull Canvas canvas) {
|
||||||
|
super.onDraw(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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mOnPlayClickListener == null) {
|
||||||
|
mLrcPaint.setColor(mCurrentTextColor);
|
||||||
|
@SuppressLint("DrawAllocation")
|
||||||
|
StaticLayout staticLayout = new StaticLayout(mLrcEntryList.get(index).getShowText()
|
||||||
|
, mLrcPaint,
|
||||||
|
(int) getLrcWidth(), Layout.Alignment.ALIGN_CENTER, 1f,
|
||||||
|
0f, false);
|
||||||
|
drawText(canvas, staticLayout, centerY);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int centerLine = getCenterLine();
|
||||||
|
|
||||||
|
if (isShowTimeline) {
|
||||||
|
mPlayDrawable.draw(canvas);
|
||||||
|
|
||||||
|
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() - (float) 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 画一行歌词
|
||||||
|
*
|
||||||
|
* @param y 歌词中心 Y 坐标
|
||||||
|
*/
|
||||||
|
private void drawText(Canvas canvas, StaticLayout staticLayout, float y) {
|
||||||
|
canvas.save();
|
||||||
|
canvas.translate(mLrcPadding, y - (staticLayout.getHeight() >> 1));
|
||||||
|
staticLayout.draw(canvas);
|
||||||
|
canvas.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
|
@Override
|
||||||
|
public boolean onTouchEvent(MotionEvent event) {
|
||||||
|
if (event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL) {
|
||||||
|
isTouching = false;
|
||||||
|
// 手指离开屏幕,启动延时任务,恢复歌词位置
|
||||||
|
if (hasLrc() && isShowTimeline) {
|
||||||
|
adjustCenter();
|
||||||
|
postDelayed(hideTimelineRunnable, TIMELINE_KEEP_TIME);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mGestureDetector.onTouchEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手势监听器
|
||||||
|
*/
|
||||||
|
private GestureDetector.SimpleOnGestureListener mSimpleOnGestureListener =
|
||||||
|
new GestureDetector.SimpleOnGestureListener() {
|
||||||
|
// 本次点击仅仅为了停止歌词滚动,则不响应点击事件
|
||||||
|
private boolean isTouchForStopFling = false;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onDown(@NonNull MotionEvent e) {
|
||||||
|
if (!hasLrc()) {
|
||||||
|
return mOnTapListener != null;
|
||||||
|
}
|
||||||
|
isTouching = true;
|
||||||
|
removeCallbacks(hideTimelineRunnable);
|
||||||
|
if (isFling) {
|
||||||
|
isTouchForStopFling = true;
|
||||||
|
mScroller.forceFinished(true);
|
||||||
|
} else {
|
||||||
|
isTouchForStopFling = false;
|
||||||
|
}
|
||||||
|
return mOnPlayClickListener != null || mOnTapListener != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onScroll(MotionEvent e1, @NonNull MotionEvent e2, float distanceX, float distanceY) {
|
||||||
|
if (!hasLrc() || mOnPlayClickListener == null) {
|
||||||
|
return super.onScroll(e1, e2, distanceX, distanceY);
|
||||||
|
}
|
||||||
|
if (!isShowTimeline) {
|
||||||
|
isShowTimeline = true;
|
||||||
|
} else {
|
||||||
|
mOffset -= distanceY;
|
||||||
|
mOffset = Math.min(mOffset, getOffset(0));
|
||||||
|
mOffset = Math.max(mOffset, getOffset(mLrcEntryList.size() - 1));
|
||||||
|
}
|
||||||
|
invalidate();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onFling(MotionEvent e1, @NonNull MotionEvent e2, float velocityX, float velocityY) {
|
||||||
|
if (!hasLrc() || mOnPlayClickListener == null) {
|
||||||
|
return super.onFling(e1, e2, velocityX, velocityY);
|
||||||
|
}
|
||||||
|
if (isShowTimeline) {
|
||||||
|
isFling = true;
|
||||||
|
removeCallbacks(hideTimelineRunnable);
|
||||||
|
mScroller.fling(0, (int) mOffset, 0, (int) velocityY, 0, 0, (int) getOffset(mLrcEntryList.size() - 1), (int) getOffset(0));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return super.onFling(e1, e2, velocityX, velocityY);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onSingleTapConfirmed(@NonNull MotionEvent e) {
|
||||||
|
if (hasLrc() && mOnPlayClickListener != null && 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(LrcView.this, centerLineTime)) {
|
||||||
|
isShowTimeline = false;
|
||||||
|
removeCallbacks(hideTimelineRunnable);
|
||||||
|
mCurrentLine = centerLine;
|
||||||
|
invalidate();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else if (mOnTapListener != null && !isTouchForStopFling) {
|
||||||
|
mOnTapListener.onTap(LrcView.this, e.getX(), e.getY());
|
||||||
|
}
|
||||||
|
return super.onSingleTapConfirmed(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private Runnable hideTimelineRunnable = new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Log.d(TAG, "hideTimelineRunnable run");
|
||||||
|
if (hasLrc() && isShowTimeline) {
|
||||||
|
isShowTimeline = false;
|
||||||
|
smoothScrollTo(mCurrentLine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void computeScroll() {
|
||||||
|
if (mScroller.computeScrollOffset()) {
|
||||||
|
mOffset = mScroller.getCurrY();
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFling && mScroller.isFinished()) {
|
||||||
|
Log.d(TAG, "fling finish");
|
||||||
|
isFling = false;
|
||||||
|
if (hasLrc() && !isTouching) {
|
||||||
|
adjustCenter();
|
||||||
|
postDelayed(hideTimelineRunnable, TIMELINE_KEEP_TIME);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDetachedFromWindow() {
|
||||||
|
removeCallbacks(hideTimelineRunnable);
|
||||||
|
super.onDetachedFromWindow();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onLrcLoaded(List<LrcEntry> entryList) {
|
||||||
|
if (entryList != null && !entryList.isEmpty()) {
|
||||||
|
mLrcEntryList.addAll(entryList);
|
||||||
|
}
|
||||||
|
|
||||||
|
Collections.sort(mLrcEntryList);
|
||||||
|
|
||||||
|
initEntryList();
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = (float) getHeight() / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reset() {
|
||||||
|
endAnimation();
|
||||||
|
mScroller.forceFinished(true);
|
||||||
|
isShowTimeline = false;
|
||||||
|
isTouching = false;
|
||||||
|
isFling = false;
|
||||||
|
removeCallbacks(hideTimelineRunnable);
|
||||||
|
mLrcEntryList.clear();
|
||||||
|
mOffset = 0;
|
||||||
|
mCurrentLine = 0;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将中心行微调至正中心
|
||||||
|
*/
|
||||||
|
private void adjustCenter() {
|
||||||
|
smoothScrollTo(getCenterLine(), ADJUST_DURATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 滚动到某一行
|
||||||
|
*/
|
||||||
|
private void smoothScrollTo(int line) {
|
||||||
|
smoothScrollTo(line, mAnimationDuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 滚动到某一行
|
||||||
|
*/
|
||||||
|
private void smoothScrollTo(int line, long duration) {
|
||||||
|
float offset = getOffset(line);
|
||||||
|
endAnimation();
|
||||||
|
|
||||||
|
mAnimator = ValueAnimator.ofFloat(mOffset, offset);
|
||||||
|
mAnimator.setDuration(duration);
|
||||||
|
mAnimator.setInterpolator(new LinearInterpolator());
|
||||||
|
mAnimator.addUpdateListener(animation -> {
|
||||||
|
mOffset = (float) animation.getAnimatedValue();
|
||||||
|
invalidate();
|
||||||
|
});
|
||||||
|
LrcUtils.resetDurationScale();
|
||||||
|
mAnimator.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 结束滚动动画
|
||||||
|
*/
|
||||||
|
private void endAnimation() {
|
||||||
|
if (mAnimator != null && mAnimator.isRunning()) {
|
||||||
|
mAnimator.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 二分法查找当前时间应该显示的行数(最后一个 <= 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前在视图中央的行数
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取歌词距离视图顶部的距离
|
||||||
|
* 采用懒加载方式
|
||||||
|
*/
|
||||||
|
private float getOffset(int line) {
|
||||||
|
if (mLrcEntryList.get(line).getOffset() == Float.MIN_VALUE) {
|
||||||
|
float offset = (float) 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取歌词宽度
|
||||||
|
*/
|
||||||
|
private float getLrcWidth() {
|
||||||
|
return getWidth() - mLrcPadding * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在主线程中运行
|
||||||
|
*/
|
||||||
|
private void runOnUi(Runnable r) {
|
||||||
|
if (Looper.myLooper() == Looper.getMainLooper()) {
|
||||||
|
r.run();
|
||||||
|
} else {
|
||||||
|
post(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object getFlag() {
|
||||||
|
return mFlag;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setFlag(Object flag) {
|
||||||
|
this.mFlag = flag;
|
||||||
|
}
|
||||||
|
}
|
BIN
lrcview/src/main/res/drawable-xxhdpi/lrc_play.png
Normal file
BIN
lrcview/src/main/res/drawable-xxhdpi/lrc_play.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 438 B |
24
lrcview/src/main/res/values/attrs.xml
Normal file
24
lrcview/src/main/res/values/attrs.xml
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<declare-styleable name="LrcView">
|
||||||
|
<attr name="lrcTextSize" format="dimension" />
|
||||||
|
<attr name="lrcNormalTextSize" format="dimension" />
|
||||||
|
<attr name="lrcDividerHeight" format="dimension" />
|
||||||
|
<attr name="lrcNormalTextColor" format="reference|color" />
|
||||||
|
<attr name="lrcCurrentTextColor" format="reference|color" />
|
||||||
|
<attr name="lrcTimelineTextColor" format="reference|color" />
|
||||||
|
<attr name="lrcAnimationDuration" format="integer" />
|
||||||
|
<attr name="lrcLabel" format="string" />
|
||||||
|
<attr name="lrcPadding" format="dimension" />
|
||||||
|
<attr name="lrcTimelineColor" format="reference|color" />
|
||||||
|
<attr name="lrcTimelineHeight" format="dimension" />
|
||||||
|
<attr name="lrcPlayDrawable" format="reference" />
|
||||||
|
<attr name="lrcTimeTextColor" format="reference|color" />
|
||||||
|
<attr name="lrcTimeTextSize" format="dimension" />
|
||||||
|
<attr name="lrcTextGravity">
|
||||||
|
<enum name="center" value="0" />
|
||||||
|
<enum name="left" value="1" />
|
||||||
|
<enum name="right" value="2" />
|
||||||
|
</attr>
|
||||||
|
</declare-styleable>
|
||||||
|
</resources>
|
8
lrcview/src/main/res/values/colors.xml
Normal file
8
lrcview/src/main/res/values/colors.xml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="lrc_normal_text_color">#9E9E9E</color>
|
||||||
|
<color name="lrc_current_text_color">#FF4081</color>
|
||||||
|
<color name="lrc_timeline_text_color">#F8BBD0</color>
|
||||||
|
<color name="lrc_timeline_color">#809E9E9E</color>
|
||||||
|
<color name="lrc_time_text_color">#809E9E9E</color>
|
||||||
|
</resources>
|
10
lrcview/src/main/res/values/dimens.xml
Normal file
10
lrcview/src/main/res/values/dimens.xml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<integer name="lrc_animation_duration">1000</integer>
|
||||||
|
<dimen name="lrc_text_size">16sp</dimen>
|
||||||
|
<dimen name="lrc_time_text_size">12sp</dimen>
|
||||||
|
<dimen name="lrc_divider_height">16dp</dimen>
|
||||||
|
<dimen name="lrc_timeline_height">1dp</dimen>
|
||||||
|
<dimen name="lrc_drawable_width">30dp</dimen>
|
||||||
|
<dimen name="lrc_time_width">40dp</dimen>
|
||||||
|
</resources>
|
3
lrcview/src/main/res/values/strings.xml
Normal file
3
lrcview/src/main/res/values/strings.xml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<resources>
|
||||||
|
<string name="lrc_label">暂无歌词</string>
|
||||||
|
</resources>
|
Loading…
Reference in New Issue
Block a user