1、首先我们来看看如何使用,控件的使用很简单,可以在xml中配置使用:
<org.loader.liteplayer.ui.LrcView xmlns:lrc="http://schemas.android.com/apk/res/org.loader.liteplayer" android:id="@+id/play_first_lrc_2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="5dp" android:layout_marginBottom="5dp" lrc:textSize="18sp" lrc:normalTextColor="@android:color/white" lrc:currentTextColor="@color/main" lrc:dividerHeight="20dp" lrc:rows="9" />
... mLrcViewOnSecondPage = (LrcView) lrcView.findViewById(R.id.play_first_lrc_2); ... mLrcViewOnSecondPage.setLrcPath(lrcPath); ... @Override public void onPublish(int progress) { if(mLrcViewOnSecondPage.hasLrc()) mLrcViewOnSecondPage.changeCurrent(progress); }
第一行代码去获取该控件,接着调用setLrcPath将歌词文件加载到内存中,在onPushlish方法中不断调用changeCurrent来更新歌词,那changeCurrent的参数哪来的呢?这个是音乐播放回调的进度,到这里,可能会有大神出疑问了, 这样做是不是会不断的更新歌词控件?就算当前没有切换歌词也回去更新? 这里先给出回答:当然不是了,我们在changeCurrent方法中做了判断,所以这里尽管调用,放心调用!
那接下来,我们开始进入今天的主题:LrcView。
在进入代码之前,先来看看我的设计思路吧:
当我们传进一个lrc文件的path,首先按照行去read文件,并且利用正则解析出时间和歌词分别存放。设置完歌词后,我们通过不断调用changeCurrent()方法来切换歌词,那么changeCurrent又负责了什么工作呢? 在changeCurrent中首先判断下一行开始的时间是不是大于当前传进来的时间,如果是,直接返回,否则,遍历所有的时间,找到大于当前时间的上一行的key, 再次通过key找到歌词,咔咔咔, 显示出来就ok了。
look code:
public class LrcView extends View { private static final int SCROLL_TIME = 500; private static final String DEFAULT_TEXT = "暂无歌词"; private List<String> mLrcs = new ArrayList<String>(); // 存放歌词 private List<Long> mTimes = new ArrayList<Long>(); // 存放时间 private long mNextTime = 0l; // 保存下一句开始的时间 private int mViewWidth; // view的宽度 private int mLrcHeight; // lrc界面的高度 private int mRows; // 多少行 private int mCurrentLine = 0; // 当前行 private int mOffsetY; // y上的偏移 private int mMaxScroll; // 最大滑动距离=一行歌词高度+歌词间距 private float mTextSize; // 字体 private float mDividerHeight; // 行间距 private Rect mTextBounds; private Paint mNormalPaint; // 常规的字体 private Paint mCurrentPaint; // 当前歌词的大小 private Bitmap mBackground; private Scroller mScroller; public LrcView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public LrcView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mScroller = new Scroller(context, new LinearInterpolator()); inflateAttributes(attrs); } ... }
// 初始化操作 private void inflateAttributes(AttributeSet attrs) { // <begin> // 解析自定义属性 TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.Lrc); mTextSize = ta.getDimension(R.styleable.Lrc_textSize, 50.0f); mRows = ta.getInteger(R.styleable.Lrc_rows, 5); mDividerHeight = ta.getDimension(R.styleable.Lrc_dividerHeight, 0.0f); int normalTextColor = ta.getColor(R.styleable.Lrc_normalTextColor, 0xffffffff); int currentTextColor = ta.getColor(R.styleable.Lrc_currentTextColor, 0xff00ffde); ta.recycle(); // </end> // 计算lrc面板的高度 mLrcHeight = (int) (mTextSize + mDividerHeight) * mRows + 5; mNormalPaint = new Paint(); mCurrentPaint = new Paint(); // 初始化paint mNormalPaint.setTextSize(mTextSize); mNormalPaint.setColor(normalTextColor); mNormalPaint.setAntiAlias(true); mCurrentPaint.setTextSize(mTextSize); mCurrentPaint.setColor(currentTextColor); mCurrentPaint.setAntiAlias(true); mTextBounds = new Rect(); mCurrentPaint.getTextBounds(DEFAULT_TEXT, 0, DEFAULT_TEXT.length(), mTextBounds); mMaxScroll = (int) (mTextBounds.height() + mDividerHeight); }
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); // 重新设置view的高度 int measuredHeightSpec = MeasureSpec.makeMeasureSpec(mLrcHeight, MeasureSpec.AT_MOST); super.onMeasure(widthMeasureSpec, measuredHeightSpec); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); // 获取view宽度 mViewWidth = getMeasuredWidth(); }
// 外部提供方法 // 设置lrc的路径 public void setLrcPath(String path) { reset(); File file = new File(path); if (!file.exists()) { postInvalidate(); return; } BufferedReader reader = null; try { reader = new BufferedReader(new InputStreamReader(new FileInputStream(file))); String line = ""; String[] arr; while (null != (line = reader.readLine())) { arr = parseLine(line); if (arr == null) continue; // 如果解析出来只有一个 if (arr.length == 1) { String last = mLrcs.remove(mLrcs.size() - 1); mLrcs.add(last + arr[0]); continue; } mTimes.add(Long.parseLong(arr[0])); mLrcs.add(arr[1]); } } catch (Exception e) { e.printStackTrace(); } finally { if(reader != null) { try { reader.close(); } catch (IOException e) { e.printStackTrace(); } } } }
[05:20.59] 我从天上来
// 解析每行 private String[] parseLine(String line) { Matcher matcher = Pattern.compile("\\[\\d.+\\].+").matcher(line); // 如果形如:[xxx]后面啥也没有的,则return空 if (!matcher.matches()) { System.out.println("throws " + line); return null; } line = line.replaceAll("\\[", ""); String[] result = line.split("\\]"); result[0] = String.valueOf(parseTime(result[0])); return result; }
// 解析时间 private Long parseTime(String time) { // 03:02.12 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; }
// 外部提供方法 // 传入当前播放时间 public synchronized void changeCurrent(long time) { // 如果当前时间小于下一句开始的时间 // 直接return if (mNextTime > time) { return; } // 每次进来都遍历存放的时间 for (int i = 0; i < mTimes.size(); i++) { // 发现这个时间大于传进来的时间 // 那么现在就应该显示这个时间前面的对应的那一行 // 每次都重新显示,是不是要判断:现在正在显示就不刷新了 if (mTimes.get(i) > time) { mNextTime = mTimes.get(i); mScroller.abortAnimation(); mScroller.startScroll(i, 0, 0, mMaxScroll, SCROLL_TIME); // mNextTime = mTimes.get(i); // mCurrentLine = i <= 1 ? 0 : i - 1; postInvalidate(); return; } } }
@Override public void computeScroll() { if(mScroller.computeScrollOffset()) { mOffsetY = mScroller.getCurrY(); if(mScroller.isFinished()) { int cur = mScroller.getCurrX(); mCurrentLine = cur <= 1 ? 0 : cur - 1; mOffsetY = 0; } postInvalidate(); } }
@Override protected void onDraw(Canvas canvas) { // float centerY = (getMeasuredHeight() + mTextBounds.height() - mDividerHeight) / 2; float centerY = (getMeasuredHeight() + mTextBounds.height()) / 2; if (mLrcs.isEmpty() || mTimes.isEmpty()) { canvas.drawText(DEFAULT_TEXT, (mViewWidth - mCurrentPaint.measureText(DEFAULT_TEXT)) / 2, centerY, mCurrentPaint); return; } String currentLrc = mLrcs.get(mCurrentLine); float currentX = (mViewWidth - mCurrentPaint.measureText(currentLrc)) / 2; // 画当前行 canvas.drawText(currentLrc, currentX, centerY - mOffsetY, mCurrentPaint); float offsetY = mTextBounds.height() + mDividerHeight; int firstLine = mCurrentLine - mRows / 2; firstLine = firstLine <= 0 ? 0 : firstLine; int lastLine = mCurrentLine + mRows / 2 + 2; lastLine = lastLine >= mLrcs.size() - 1 ? mLrcs.size() - 1 : lastLine; // 画当前行上面的 for (int i = mCurrentLine - 1,j=1; i >= firstLine; i--,j++) { String lrc = mLrcs.get(i); float x = (mViewWidth - mNormalPaint.measureText(lrc)) / 2; canvas.drawText(lrc, x, centerY - j * offsetY - mOffsetY, mNormalPaint); } // 画当前行下面的 for (int i = mCurrentLine + 1,j=1; i <= lastLine; i++,j++) { String lrc = mLrcs.get(i); float x = (mViewWidth - mNormalPaint.measureText(lrc)) / 2; canvas.drawText(lrc, x, centerY + j * offsetY - mOffsetY, mNormalPaint); } }
最后,是关于代码的问题,有人说我的博客没有demo下载,这个以后会注意哈, 这次的代码,等我这个月月底毕业答辩完了,会把音乐播放器一块开源了。