780 lines
23 KiB
Java
780 lines
23 KiB
Java
/*
|
||
* Copyright (C) 2017 wangchenyan
|
||
*
|
||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
|
||
* except in compliance with the License. You may obtain a copy of the License at
|
||
*
|
||
* http://www.apache.org/licenses/LICENSE-2.0
|
||
*
|
||
* Unless required by applicable law or agreed to in writing, software distributed under the
|
||
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||
* KIND, either express or implied. See the License for the specific language governing
|
||
* permissions and limitations under the License.
|
||
*/
|
||
|
||
package code.name.monkey.retromusic.lyrics;
|
||
|
||
import android.animation.ValueAnimator;
|
||
import android.annotation.SuppressLint;
|
||
import android.content.Context;
|
||
import android.content.res.TypedArray;
|
||
import android.graphics.Canvas;
|
||
import android.graphics.Paint;
|
||
import android.graphics.drawable.Drawable;
|
||
import android.os.AsyncTask;
|
||
import android.os.Looper;
|
||
import android.text.Layout;
|
||
import android.text.StaticLayout;
|
||
import android.text.TextPaint;
|
||
import android.text.TextUtils;
|
||
import android.text.format.DateUtils;
|
||
import android.util.AttributeSet;
|
||
import android.view.GestureDetector;
|
||
import android.view.MotionEvent;
|
||
import android.view.View;
|
||
import android.view.animation.LinearInterpolator;
|
||
import android.widget.Scroller;
|
||
|
||
import java.io.File;
|
||
import java.util.ArrayList;
|
||
import java.util.Collections;
|
||
import java.util.List;
|
||
|
||
import code.name.monkey.retromusic.BuildConfig;
|
||
import code.name.monkey.retromusic.R;
|
||
|
||
/**
|
||
* 歌词 Created by wcy on 2015/11/9.
|
||
*/
|
||
@SuppressLint("StaticFieldLeak")
|
||
public class LrcView extends View {
|
||
private static final long ADJUST_DURATION = 100;
|
||
private static final long TIMELINE_KEEP_TIME = 4 * DateUtils.SECOND_IN_MILLIS;
|
||
|
||
private final List<LrcEntry> mLrcEntryList = new ArrayList<>();
|
||
private final TextPaint mLrcPaint = new TextPaint();
|
||
private final 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 ValueAnimator mAnimator;
|
||
private GestureDetector mGestureDetector;
|
||
private Scroller mScroller;
|
||
private float mOffset;
|
||
private int mCurrentLine;
|
||
private Object mFlag;
|
||
private boolean isShowTimeline;
|
||
private boolean isTouching;
|
||
private boolean isFling;
|
||
private int mTextGravity; // 歌词显示位置,靠左/居中/靠右
|
||
private final Runnable hideTimelineRunnable =
|
||
new Runnable() {
|
||
@Override
|
||
public void run() {
|
||
if (hasLrc() && isShowTimeline) {
|
||
isShowTimeline = false;
|
||
smoothScrollTo(mCurrentLine);
|
||
}
|
||
}
|
||
};
|
||
/**
|
||
* 手势监听器
|
||
*/
|
||
private final GestureDetector.SimpleOnGestureListener mSimpleOnGestureListener =
|
||
new GestureDetector.SimpleOnGestureListener() {
|
||
@Override
|
||
public boolean onDown(MotionEvent e) {
|
||
if (hasLrc() && mOnPlayClickListener != null) {
|
||
mScroller.forceFinished(true);
|
||
removeCallbacks(hideTimelineRunnable);
|
||
isTouching = true;
|
||
isShowTimeline = true;
|
||
invalidate();
|
||
return true;
|
||
}
|
||
return super.onDown(e);
|
||
}
|
||
|
||
@Override
|
||
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
|
||
if (hasLrc()) {
|
||
mOffset += -distanceY;
|
||
mOffset = Math.min(mOffset, getOffset(0));
|
||
mOffset = Math.max(mOffset, getOffset(mLrcEntryList.size() - 1));
|
||
invalidate();
|
||
getParent().requestDisallowInterceptTouchEvent(true);
|
||
return true;
|
||
}
|
||
return super.onScroll(e1, e2, distanceX, distanceY);
|
||
}
|
||
|
||
@Override
|
||
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
|
||
if (hasLrc()) {
|
||
mScroller.fling(
|
||
0,
|
||
(int) mOffset,
|
||
0,
|
||
(int) velocityY,
|
||
0,
|
||
0,
|
||
(int) getOffset(mLrcEntryList.size() - 1),
|
||
(int) getOffset(0));
|
||
isFling = true;
|
||
return true;
|
||
}
|
||
return super.onFling(e1, e2, velocityX, velocityY);
|
||
}
|
||
|
||
@Override
|
||
public boolean onSingleTapConfirmed(MotionEvent e) {
|
||
if (hasLrc()
|
||
&& isShowTimeline
|
||
&& mPlayDrawable.getBounds().contains((int) e.getX(), (int) e.getY())) {
|
||
int centerLine = getCenterLine();
|
||
long centerLineTime = mLrcEntryList.get(centerLine).getTime();
|
||
// onPlayClick 消费了才更新 UI
|
||
if (mOnPlayClickListener != null && mOnPlayClickListener.onPlayClick(centerLineTime)) {
|
||
isShowTimeline = false;
|
||
removeCallbacks(hideTimelineRunnable);
|
||
mCurrentLine = centerLine;
|
||
invalidate();
|
||
return true;
|
||
}
|
||
}
|
||
return super.onSingleTapConfirmed(e);
|
||
}
|
||
};
|
||
|
||
public LrcView(Context context) {
|
||
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,
|
||
getResources().getColor(R.color.lrc_normal_text_color));
|
||
mCurrentTextColor =
|
||
ta.getColor(
|
||
R.styleable.LrcView_lrcCurrentTextColor,
|
||
getResources().getColor(R.color.lrc_current_text_color));
|
||
mTimelineTextColor =
|
||
ta.getColor(
|
||
R.styleable.LrcView_lrcTimelineTextColor,
|
||
getResources().getColor(R.color.lrc_timeline_text_color));
|
||
mDefaultLabel = ta.getString(R.styleable.LrcView_lrcLabel);
|
||
mDefaultLabel =
|
||
TextUtils.isEmpty(mDefaultLabel) ? getContext().getString(R.string.empty) : mDefaultLabel;
|
||
mLrcPadding = ta.getDimension(R.styleable.LrcView_lrcPadding, 0);
|
||
mTimelineColor =
|
||
ta.getColor(
|
||
R.styleable.LrcView_lrcTimelineColor,
|
||
getResources().getColor(R.color.lrc_timeline_color));
|
||
float timelineHeight =
|
||
ta.getDimension(
|
||
R.styleable.LrcView_lrcTimelineHeight,
|
||
getResources().getDimension(R.dimen.lrc_timeline_height));
|
||
mPlayDrawable = ta.getDrawable(R.styleable.LrcView_lrcPlayDrawable);
|
||
mPlayDrawable =
|
||
(mPlayDrawable == null)
|
||
? getResources().getDrawable(R.drawable.ic_play_arrow)
|
||
: mPlayDrawable;
|
||
mTimeTextColor =
|
||
ta.getColor(
|
||
R.styleable.LrcView_lrcTimeTextColor,
|
||
getResources().getColor(R.color.lrc_time_text_color));
|
||
float timeTextSize =
|
||
ta.getDimension(
|
||
R.styleable.LrcView_lrcTimeTextSize,
|
||
getResources().getDimension(R.dimen.lrc_time_text_size));
|
||
mTextGravity = ta.getInteger(R.styleable.LrcView_lrcTextGravity, LrcEntry.GRAVITY_CENTER);
|
||
|
||
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;
|
||
}
|
||
|
||
/** 设置歌词为空时屏幕中央显示的文字,如“暂无歌词” */
|
||
public void setLabel(String label) {
|
||
runOnUi(
|
||
() -> {
|
||
mDefaultLabel = label;
|
||
invalidate();
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 加载歌词文件
|
||
*
|
||
* @param lrcFile 歌词文件
|
||
*/
|
||
public void loadLrc(File lrcFile) {
|
||
loadLrc(lrcFile, null);
|
||
}
|
||
|
||
/**
|
||
* 加载双语歌词文件,两种语言的歌词时间戳需要一致
|
||
*
|
||
* @param mainLrcFile 第一种语言歌词文件
|
||
* @param secondLrcFile 第二种语言歌词文件
|
||
*/
|
||
public void loadLrc(File mainLrcFile, File secondLrcFile) {
|
||
runOnUi(
|
||
() -> {
|
||
reset();
|
||
|
||
StringBuilder sb = new StringBuilder("file://");
|
||
sb.append(mainLrcFile.getPath());
|
||
if (secondLrcFile != null) {
|
||
sb.append("#").append(secondLrcFile.getPath());
|
||
}
|
||
String flag = sb.toString();
|
||
setFlag(flag);
|
||
new AsyncTask<File, Integer, List<LrcEntry>>() {
|
||
@Override
|
||
protected List<LrcEntry> doInBackground(File... params) {
|
||
return LrcUtils.parseLrc(params);
|
||
}
|
||
|
||
@Override
|
||
protected void onPostExecute(List<LrcEntry> lrcEntries) {
|
||
if (getFlag() == flag) {
|
||
onLrcLoaded(lrcEntries);
|
||
setFlag(null);
|
||
}
|
||
}
|
||
}.execute(mainLrcFile, secondLrcFile);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 加载歌词文本
|
||
*
|
||
* @param lrcText 歌词文本
|
||
*/
|
||
public void loadLrc(String lrcText) {
|
||
loadLrc(lrcText, null);
|
||
}
|
||
|
||
/**
|
||
* 加载双语歌词文本,两种语言的歌词时间戳需要一致
|
||
*
|
||
* @param mainLrcText 第一种语言歌词文本
|
||
* @param secondLrcText 第二种语言歌词文本
|
||
*/
|
||
public void loadLrc(String mainLrcText, String secondLrcText) {
|
||
runOnUi(
|
||
() -> {
|
||
reset();
|
||
|
||
StringBuilder sb = new StringBuilder("file://");
|
||
sb.append(mainLrcText);
|
||
if (secondLrcText != null) {
|
||
sb.append("#").append(secondLrcText);
|
||
}
|
||
String flag = sb.toString();
|
||
setFlag(flag);
|
||
new AsyncTask<String, Integer, List<LrcEntry>>() {
|
||
@Override
|
||
protected List<LrcEntry> doInBackground(String... params) {
|
||
return LrcUtils.parseLrc(params);
|
||
}
|
||
|
||
@Override
|
||
protected void onPostExecute(List<LrcEntry> lrcEntries) {
|
||
if (getFlag() == flag) {
|
||
onLrcLoaded(lrcEntries);
|
||
setFlag(null);
|
||
}
|
||
}
|
||
}.execute(mainLrcText, secondLrcText);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 加载在线歌词,默认使用 utf-8 编码
|
||
*
|
||
* @param lrcUrl 歌词文件的网络地址
|
||
*/
|
||
public void loadLrcByUrl(String lrcUrl) {
|
||
loadLrcByUrl(lrcUrl, "utf-8");
|
||
}
|
||
|
||
/**
|
||
* 加载在线歌词
|
||
*
|
||
* @param lrcUrl 歌词文件的网络地址
|
||
* @param charset 编码格式
|
||
*/
|
||
public void loadLrcByUrl(String lrcUrl, String charset) {
|
||
String flag = "url://" + lrcUrl;
|
||
setFlag(flag);
|
||
new AsyncTask<String, Integer, String>() {
|
||
@Override
|
||
protected String doInBackground(String... params) {
|
||
return LrcUtils.getContentFromNetwork(params[0], params[1]);
|
||
}
|
||
|
||
@Override
|
||
protected void onPostExecute(String lrcText) {
|
||
if (getFlag() == flag) {
|
||
loadLrc(lrcText);
|
||
}
|
||
}
|
||
}.execute(lrcUrl, charset);
|
||
}
|
||
|
||
/**
|
||
* 歌词是否有效
|
||
*
|
||
* @return true,如果歌词有效,否则false
|
||
*/
|
||
public boolean hasLrc() {
|
||
return !mLrcEntryList.isEmpty();
|
||
}
|
||
|
||
/**
|
||
* 刷新歌词
|
||
*
|
||
* @param time 当前播放时间
|
||
*/
|
||
public void updateTime(long time) {
|
||
runOnUi(
|
||
() -> {
|
||
if (!hasLrc()) {
|
||
return;
|
||
}
|
||
|
||
int line = findShowLine(time);
|
||
if (line != mCurrentLine) {
|
||
mCurrentLine = line;
|
||
if (!isShowTimeline) {
|
||
smoothScrollTo(line);
|
||
} else {
|
||
invalidate();
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 将歌词滚动到指定时间
|
||
*
|
||
* @param time 指定的时间
|
||
* @deprecated 请使用 {@link #updateTime(long)} 代替
|
||
*/
|
||
@Deprecated
|
||
public void onDrag(long time) {
|
||
updateTime(time);
|
||
}
|
||
|
||
@Override
|
||
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||
super.onLayout(changed, left, top, right, bottom);
|
||
if (changed) {
|
||
initPlayDrawable();
|
||
initEntryList();
|
||
if (hasLrc()) {
|
||
smoothScrollTo(mCurrentLine, 0L);
|
||
}
|
||
}
|
||
}
|
||
|
||
@Override
|
||
protected void onDraw(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;
|
||
}
|
||
|
||
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() - 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 (BuildConfig.DEBUG) {
|
||
// mLrcPaint.setTypeface(ResourcesCompat.getFont(getContext(), R.font.sans));
|
||
}
|
||
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();
|
||
}
|
||
|
||
@Override
|
||
public boolean onTouchEvent(MotionEvent event) {
|
||
if (event.getAction() == MotionEvent.ACTION_UP
|
||
|| event.getAction() == MotionEvent.ACTION_CANCEL) {
|
||
isTouching = false;
|
||
if (hasLrc() && !isFling) {
|
||
adjustCenter();
|
||
postDelayed(hideTimelineRunnable, TIMELINE_KEEP_TIME);
|
||
}
|
||
}
|
||
return mGestureDetector.onTouchEvent(event);
|
||
}
|
||
|
||
@Override
|
||
public void computeScroll() {
|
||
if (mScroller.computeScrollOffset()) {
|
||
mOffset = mScroller.getCurrY();
|
||
invalidate();
|
||
}
|
||
|
||
if (isFling && mScroller.isFinished()) {
|
||
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 = 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 = 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;
|
||
}
|
||
|
||
/** 播放按钮点击监听器,点击后应该跳转到指定播放位置 */
|
||
public interface OnPlayClickListener {
|
||
/**
|
||
* 播放按钮被点击,应该跳转到指定播放位置
|
||
*
|
||
* @return 是否成功消费该事件,如果成功消费,则会更新UI
|
||
*/
|
||
boolean onPlayClick(long time);
|
||
}
|
||
}
|