更新了歌词组件支持更多操作
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