Android -- [SelfView] 自定义多行歌词滚动显示器
import android.animation.Animator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.LinearInterpolator;
import androidx.annotation.Nullable;
import androidx.annotation.RawRes;
import com.nepalese.harinetest.R;
import com.nepalese.harinetest.utils.CommonUtil;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Created by Administrator on 2024/11/26.
* Usage:更流畅、丝滑的滚动歌词控件
* 1. 背景透明;
* 2. 外部可控制进度变化;
* 3. 支持屏幕拖动调节进度(回调给外部);
*/
public class VirgoLrcView extends View {
private static final String TAG = "VirgoLrcView";
private static final float PADD_VALUE = 25f;//时间线两边缩进值
private static final float TEXT_RATE = 1.25f;//当前行字体放大比例
private static final long INTERVAL_ANIMATION = 400L;//动画时长
private static final String DEFAULT_TEXT = "暂无歌词,快去下载吧!";
private final Context context;
private Paint paint;//画笔, 仅一个
private ValueAnimator animator;//动画
private List<LrcBean> lineList;//歌词行
private LrcCallback callback;//手动滑动进度刷新回调
//可设置变量
private int textColorMain;//选中字体颜色
private int textColorSec;//其他字体颜色
private float textSize;//字体大小
private float lineSpace;//行间距
private float selectTextSize;//当前选中行字体大小
private int width, height;//控件宽高
private int curLine;//当前行数
private int locateLine;//滑动时居中行数
private int underRows;//中分下需显示行数
private float itemHeight;//一行字+行间距
private float centerY;//居中y
private float startY;//首行y
private float oldY;//划屏时起始按压点y
private float offsetY;//动画已偏移量
private float offsetY2;//每次手动滑动偏移量
private long maxTime;//歌词显示最大时间
private boolean isDown;//按压界面
private boolean isReverse;//往回滚动?
public VirgoLrcView(Context context) {
this(context, null);
}
public VirgoLrcView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public VirgoLrcView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.context = context;
init(attrs);
}
private void init(AttributeSet attrs) {
TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.VirgoLrcView);
textColorMain = ta.getColor(R.styleable.VirgoLrcView_vlTextColorM, Color.CYAN);
textColorSec = ta.getColor(R.styleable.VirgoLrcView_vlTextColorS, Color.GRAY);
textSize = ta.getDimension(R.styleable.VirgoLrcView_vlTextSize, 45f);
lineSpace = ta.getDimension(R.styleable.VirgoLrcView_vlLineSpace, 28f);
ta.recycle();
selectTextSize = textSize * TEXT_RATE;
curLine = 0;
maxTime = 0;
isDown = false;
isReverse = false;
lineList = new ArrayList<>();
paint = new Paint();
paint.setTextSize(textSize);
paint.setAntiAlias(true);
calculateItem();
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (width == 0 || height == 0) {
initLayout();
}
}
//控件大小变化时需重置计算
private void initLayout() {
width = getWidth();
height = getHeight();
centerY = (height - itemHeight) / 2.0f;
startY = centerY;
underRows = (int) Math.ceil(height / itemHeight / 3);
Log.d(TAG, "itemHeight: " + itemHeight + ", underRows: " + underRows);
}
@Override
protected void onDraw(Canvas canvas) {
//提示无歌词
if (lineList.isEmpty()) {
paint.setColor(textColorMain);
paint.setTextSize(selectTextSize);
canvas.drawText(DEFAULT_TEXT, getStartX(DEFAULT_TEXT, paint), centerY, paint);
return;
}
if (isDown) {
paint.setTextSize(textSize);
paint.setColor(textColorSec);
//画时间
if (locateLine >= 0) {
canvas.drawText(lineList.get(locateLine).getStrTime(), PADD_VALUE, centerY, paint);
}
//画选择线
canvas.drawLine(PADD_VALUE, centerY, width - PADD_VALUE, centerY, paint);
//手动滑动
drawTexts(canvas, startY - offsetY2);
} else {
//自动滚动
if (isReverse) {
drawTexts(canvas, startY + offsetY);
} else {
drawTexts(canvas, startY - offsetY);
}
}
}
private void drawTexts(Canvas canvas, float tempY) {
for (int i = 0; i < lineList.size(); i++) {
float y = tempY + i * itemHeight;
if (y < 0 || y > height) {
continue;
}
if (curLine == i) {
paint.setTextSize(selectTextSize);
paint.setColor(textColorMain);
} else {
paint.setTextSize(textSize);
paint.setColor(textColorSec);
}
canvas.drawText(lineList.get(i).getLrc(), getStartX(lineList.get(i).getLrc(), paint), y, paint);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
isDown = true;
if (animator != null) {
if (animator.isRunning()) {
//停止动画
animator.end();
}
}
locateLine = -1;
oldY = event.getY();
break;
case MotionEvent.ACTION_MOVE:
offsetY2 = oldY - event.getY();
calculateCurLine(oldY - event.getY());//定位时间啊
invalidate();
break;
case MotionEvent.ACTION_UP:
isDown = false;
postNewLine();
break;
}
return true;
}
//计算滑动后当前居中的行
private void calculateCurLine(float y) {
int offLine = (int) Math.floor(y / itemHeight);
if (offLine == 0) {
return;
}
locateLine = curLine + offLine;
if (locateLine > lineList.size() - 1) {
//最后一行
locateLine = lineList.size() - 1;
} else if (locateLine < 0) {
//第一行
locateLine = 0;
}
}
//回调通知,自身不跳转进度
private void postNewLine() {
//返回当前行对应的时间线
if (callback == null) {
return;
}
if (locateLine >= 0) {
callback.onUpdateTime(lineList.get(locateLine).getTime());
}
}
@Override
protected void onDetachedFromWindow() {
releaseBase();
super.onDetachedFromWindow();
}
/**
* 移除控件,注销资源
*/
private void releaseBase() {
cancelAnim();
if (lineList != null) {
lineList.clear();
lineList = null;
}
if (callback != null) {
callback = null;
}
}
private void calculateItem() {
itemHeight = getTextHeight() + lineSpace;
}
//计算使文字水平居中
private float getStartX(String str, Paint paint) {
return (width - paint.measureText(str)) / 2.0f;
}
//获取文字高度
private float getTextHeight() {
Paint.FontMetrics fm = paint.getFontMetrics();
return fm.descent - fm.ascent;
}
//解析歌词
private void parseLrc(InputStreamReader inputStreamReader) {
BufferedReader reader = new BufferedReader(inputStreamReader);
String line;
try {
while ((line = reader.readLine()) != null) {
parseLine(line);
}
} catch (IOException e) {
e.printStackTrace();
}
try {
inputStreamReader.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
maxTime = lineList.get(lineList.size() - 1).getTime() + 1000;//多加一秒
}
private long parseTime(String time) {
// 00:01.10
String[] min = time.split(":");
String[] sec = min[1].split("\\.");
long minInt = Long.parseLong(min[0].replaceAll("\\D+", "")
.replaceAll("\r", "").replaceAll("\n", "").trim());
long secInt = Long.parseLong(sec[0].replaceAll("\\D+", "")
.replaceAll("\r", "").replaceAll("\n", "").trim());
long milInt = Long.parseLong(sec[1].replaceAll("\\D+", "")
.replaceAll("\r", "").replaceAll("\n", "").trim());
return minInt * 60 * 1000 + secInt * 1000 + milInt;// * 10;
}
private void parseLine(String line) {
Matcher matcher = Pattern.compile("\\[\\d.+].+").matcher(line