PlayerAndroid/app/src/main/java/code/name/monkey/retromusic/lyrics/LrcView.java

753 lines
24 KiB
Java
Raw Normal View History

/*
* 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.
*/
2020-06-09 18:04:22 +00:00
package code.name.monkey.retromusic.lyrics;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
2020-06-09 18:04:22 +00:00
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;
2020-06-09 18:04:22 +00:00
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;
2020-06-09 18:04:22 +00:00
import android.util.AttributeSet;
import android.view.GestureDetector;
2020-06-09 18:04:22 +00:00
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.LinearInterpolator;
import android.widget.Scroller;
2020-06-09 18:04:22 +00:00
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
2020-06-09 18:04:22 +00:00
import java.util.List;
import code.name.monkey.retromusic.BuildConfig;
2020-06-09 18:04:22 +00:00
import code.name.monkey.retromusic.R;
/**
*
* Created by wcy on 2015/11/9.
2020-06-09 18:04:22 +00:00
*/
@SuppressLint("StaticFieldLeak")
2020-06-09 18:04:22 +00:00
public class LrcView extends View {
private static final long ADJUST_DURATION = 100;
private static final long TIMELINE_KEEP_TIME = 4 * DateUtils.SECOND_IN_MILLIS;
2020-06-09 18:04:22 +00:00
private List<LrcEntry> mLrcEntryList = new ArrayList<>();
private TextPaint mLrcPaint = new TextPaint();
private TextPaint mTimePaint = new TextPaint();
private Paint.FontMetrics mTimeFontMetrics;
2020-06-09 18:04:22 +00:00
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 Runnable hideTimelineRunnable = new Runnable() {
2020-06-09 18:04:22 +00:00
@Override
public void run() {
if (hasLrc() && isShowTimeline) {
isShowTimeline = false;
smoothScrollTo(mCurrentLine);
}
2020-06-09 18:04:22 +00:00
}
};
/**
*
*/
private GestureDetector.SimpleOnGestureListener mSimpleOnGestureListener = new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {
if (hasLrc() && mOnPlayClickListener != null) {
mScroller.forceFinished(true);
removeCallbacks(hideTimelineRunnable);
isTouching = true;
isShowTimeline = true;
invalidate();
return true;
}
return super.onDown(e);
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
if (hasLrc()) {
mOffset += -distanceY;
mOffset = Math.min(mOffset, getOffset(0));
mOffset = Math.max(mOffset, getOffset(mLrcEntryList.size() - 1));
invalidate();
return true;
}
return super.onScroll(e1, e2, distanceX, distanceY);
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
if (hasLrc()) {
mScroller.fling(0, (int) mOffset, 0, (int) velocityY, 0, 0, (int) getOffset(mLrcEntryList.size() - 1), (int) getOffset(0));
isFling = true;
return true;
}
return super.onFling(e1, e2, velocityX, velocityY);
}
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
if (hasLrc() && isShowTimeline && mPlayDrawable.getBounds().contains((int) e.getX(), (int) e.getY())) {
int centerLine = getCenterLine();
long centerLineTime = mLrcEntryList.get(centerLine).getTime();
// onPlayClick 消费了才更新 UI
if (mOnPlayClickListener != null && mOnPlayClickListener.onPlayClick(centerLineTime)) {
isShowTimeline = false;
removeCallbacks(hideTimelineRunnable);
mCurrentLine = centerLine;
invalidate();
return true;
}
}
return super.onSingleTapConfirmed(e);
}
};
2020-08-11 22:20:22 +00:00
public LrcView(Context context) {
this(context, null);
}
public LrcView(Context context, AttributeSet attrs) {
2020-06-09 18:04:22 +00:00
this(context, attrs, 0);
}
public LrcView(Context context, AttributeSet attrs, int defStyleAttr) {
2020-06-09 18:04:22 +00:00
super(context, attrs, defStyleAttr);
init(attrs);
2020-06-09 18:04:22 +00:00
}
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;
2020-06-09 18:04:22 +00:00
}
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);
2020-07-19 21:00:30 +00:00
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());
2020-06-09 18:04:22 +00:00
}
/**
*
*/
public void setNormalColor(int normalColor) {
mNormalTextColor = normalColor;
postInvalidate();
2020-06-09 18:04:22 +00:00
}
/**
*
*/
public void setNormalTextSize(float size) {
mNormalTextSize = size;
2020-06-09 18:04:22 +00:00
}
/**
*
*/
public void setCurrentTextSize(float size) {
mCurrentTextSize = size;
2020-06-09 18:04:22 +00:00
}
/**
*
*/
public void setCurrentColor(int currentColor) {
mCurrentTextColor = currentColor;
postInvalidate();
2020-06-09 18:04:22 +00:00
}
/**
*
*/
public void setTimelineTextColor(int timelineTextColor) {
mTimelineTextColor = timelineTextColor;
postInvalidate();
2020-06-09 18:04:22 +00:00
}
/**
* 线
*/
public void setTimelineColor(int timelineColor) {
mTimelineColor = timelineColor;
postInvalidate();
2020-06-09 18:04:22 +00:00
}
/**
*
*/
public void setTimeTextColor(int timeTextColor) {
mTimeTextColor = timeTextColor;
postInvalidate();
2020-06-09 18:04:22 +00:00
}
/**
*
*
* @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");
2020-06-09 18:04:22 +00:00
}
mOnPlayClickListener = onPlayClickListener;
} else {
mOnPlayClickListener = null;
2020-06-09 18:04:22 +00:00
}
}
/**
*
*
* @param onPlayClickListener null
* @deprecated use {@link #setDraggable(boolean, OnPlayClickListener)} instead
*/
@Deprecated
public void setOnPlayClickListener(OnPlayClickListener onPlayClickListener) {
mOnPlayClickListener = onPlayClickListener;
2020-06-09 18:04:22 +00:00
}
/**
*
*/
public void setLabel(String label) {
runOnUi(() -> {
mDefaultLabel = label;
invalidate();
2020-06-09 18:04:22 +00:00
});
}
/**
*
*
* @param lrcFile
*/
public void loadLrc(File lrcFile) {
loadLrc(lrcFile, null);
2020-06-09 18:04:22 +00:00
}
/**
*
*
* @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);
});
2020-06-09 18:04:22 +00:00
}
/**
*
*
* @param lrcText
*/
public void loadLrc(String lrcText) {
loadLrc(lrcText, null);
2020-06-09 18:04:22 +00:00
}
/**
*
*
* @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);
2020-06-09 18:04:22 +00:00
}
@Override
protected void onPostExecute(List<LrcEntry> lrcEntries) {
if (getFlag() == flag) {
onLrcLoaded(lrcEntries);
setFlag(null);
2020-06-09 18:04:22 +00:00
}
}
}.execute(mainLrcText, secondLrcText);
});
2020-06-09 18:04:22 +00:00
}
/**
* 线使 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]);
2020-06-09 18:04:22 +00:00
}
@Override
protected void onPostExecute(String lrcText) {
if (getFlag() == flag) {
loadLrc(lrcText);
}
2020-06-09 18:04:22 +00:00
}
}.execute(lrcUrl, charset);
}
/**
*
*
* @return truefalse
*/
public boolean hasLrc() {
return !mLrcEntryList.isEmpty();
}
2020-06-09 18:04:22 +00:00
/**
*
*
* @param time
*/
public void updateTime(long time) {
runOnUi(() -> {
if (!hasLrc()) {
return;
2020-06-09 18:04:22 +00:00
}
int line = findShowLine(time);
if (line != mCurrentLine) {
mCurrentLine = line;
if (!isShowTimeline) {
smoothScrollTo(line);
} else {
invalidate();
}
}
});
2020-06-09 18:04:22 +00:00
}
/**
*
*
* @param time
* @deprecated 使 {@link #updateTime(long)}
*/
@Deprecated
public void onDrag(long time) {
updateTime(time);
2020-06-09 18:04:22 +00:00
}
@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);
}
2020-06-09 18:04:22 +00:00
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
2020-06-09 18:04:22 +00:00
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;
2020-06-09 18:04:22 +00:00
}
int centerLine = getCenterLine();
2020-06-09 18:04:22 +00:00
if (isShowTimeline) {
mPlayDrawable.draw(canvas);
2020-06-09 18:04:22 +00:00
mTimePaint.setColor(mTimelineColor);
canvas.drawLine(mTimeTextWidth, centerY, getWidth() - mTimeTextWidth, centerY, mTimePaint);
2020-06-09 18:04:22 +00:00
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);
}
2020-06-09 18:04:22 +00:00
}
/**
*
*
* @param y Y
2020-06-09 18:04:22 +00:00
*/
private void drawText(Canvas canvas, StaticLayout staticLayout, float y) {
canvas.save();
canvas.translate(mLrcPadding, y - (staticLayout.getHeight() >> 1));
staticLayout.draw(canvas);
canvas.restore();
2020-06-09 18:04:22 +00:00
}
@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);
}
2020-06-09 18:04:22 +00:00
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
mOffset = mScroller.getCurrY();
2020-06-09 18:04:22 +00:00
invalidate();
}
if (isFling && mScroller.isFinished()) {
isFling = false;
if (hasLrc() && !isTouching) {
adjustCenter();
postDelayed(hideTimelineRunnable, TIMELINE_KEEP_TIME);
}
}
2020-06-09 18:04:22 +00:00
}
@Override
protected void onDetachedFromWindow() {
removeCallbacks(hideTimelineRunnable);
super.onDetachedFromWindow();
2020-06-09 18:04:22 +00:00
}
private void onLrcLoaded(List<LrcEntry> entryList) {
if (entryList != null && !entryList.isEmpty()) {
mLrcEntryList.addAll(entryList);
}
2020-06-09 18:04:22 +00:00
Collections.sort(mLrcEntryList);
2020-06-09 18:04:22 +00:00
initEntryList();
invalidate();
2020-06-09 18:04:22 +00:00
}
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);
2020-06-09 18:04:22 +00:00
}
private void initEntryList() {
if (!hasLrc() || getWidth() == 0) {
return;
}
2020-06-09 18:04:22 +00:00
for (LrcEntry lrcEntry : mLrcEntryList) {
lrcEntry.init(mLrcPaint, (int) getLrcWidth(), mTextGravity);
}
2020-06-09 18:04:22 +00:00
mOffset = getHeight() / 2;
2020-06-09 18:04:22 +00:00
}
private void reset() {
endAnimation();
mScroller.forceFinished(true);
isShowTimeline = false;
isTouching = false;
isFling = false;
removeCallbacks(hideTimelineRunnable);
mLrcEntryList.clear();
mOffset = 0;
mCurrentLine = 0;
invalidate();
2020-06-09 18:04:22 +00:00
}
/**
*
*/
private void adjustCenter() {
smoothScrollTo(getCenterLine(), ADJUST_DURATION);
2020-06-09 18:04:22 +00:00
}
/**
*
*/
private void smoothScrollTo(int line) {
smoothScrollTo(line, mAnimationDuration);
2020-06-09 18:04:22 +00:00
}
/**
*
*/
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();
2020-06-09 18:04:22 +00:00
}
/**
*
*/
private void endAnimation() {
if (mAnimator != null && mAnimator.isRunning()) {
mAnimator.end();
}
2020-06-09 18:04:22 +00:00
}
/**
* <= 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;
}
2020-06-09 18:04:22 +00:00
left = middle + 1;
}
}
2020-06-09 18:04:22 +00:00
return 0;
2020-06-09 18:04:22 +00:00
}
/**
*
*/
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;
2020-06-09 18:04:22 +00:00
}
/**
*
*
*/
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();
2020-06-09 18:04:22 +00:00
}
/**
*
*/
private float getLrcWidth() {
return getWidth() - mLrcPadding * 2;
2020-06-09 18:04:22 +00:00
}
/**
* 线
*/
private void runOnUi(Runnable r) {
if (Looper.myLooper() == Looper.getMainLooper()) {
r.run();
} else {
post(r);
}
2020-06-09 18:04:22 +00:00
}
private Object getFlag() {
return mFlag;
2020-06-09 18:04:22 +00:00
}
private void setFlag(Object flag) {
this.mFlag = flag;
2020-06-09 18:04:22 +00:00
}
/**
*
*/
public interface OnPlayClickListener {
/**
*
*
* @return UI
*/
boolean onPlayClick(long time);
2020-06-09 18:04:22 +00:00
}
}