/* * 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 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>() { @Override protected List doInBackground(File... params) { return LrcUtils.parseLrc(params); } @Override protected void onPostExecute(List 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>() { @Override protected List doInBackground(String... params) { return LrcUtils.parseLrc(params); } @Override protected void onPostExecute(List 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() { @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 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); } }