先看下效果 canvas
在上篇文章中呢,分享了關於lrc歌詞文件的解析方法,根據歌詞文件格式,解析出對應實體類。可是,怎樣才能讓讓本身的音樂播放器的歌詞像網易雲音樂同樣,隨音樂(歌詞時間)滾動、當前歌詞高亮、其餘歌詞漸變等效果呢?bash
接下來我想和你們分享的就是如何經過自定義View實現炫酷的LyricView歌詞顯示控件。微信
咱們最重要的目的是將文字繪製在View中,並設置各類效果和動畫,而在繪製以前咱們須要計算出文字的位置,而歌詞又是以 行 爲單位來顯示,因此,計算行高和行間距,固然,還須要一些文字顏色等屬性;app
能夠想到,歌詞會隨着音樂或者說時間進行滾動,因此咱們須要對縱向偏移量進行計算和處理,並設置滾動動畫;ide
接下來就應該逐行進行繪製了,以後能夠給當前播放位置繪製指示器以提升逼格;oop
用戶能夠手勢滑動歌詞進行查看,以後還須要回滾,因此咱們須要對手勢,也就是onTouchEvent進行處理。post
好的,大體思路就是這樣,接下來咱們一步步地實現:字體
解析歌詞上一篇文章已經介紹很詳細,這裏再也不累贅。動畫
這裏講歌詞解析也封裝進LyricView中,因此解析與賦值一併進行。ui
注意賦值實體類時,View其實已經繪製過了,不過界面上什麼都沒有顯示(由於LyricInfo類爲null,在繪製時會返回再也不繼續),賦值實體類後,在刷新一下界面:
/**
* 刷新View
*/
private void invalidateView() {
if (Looper.getMainLooper() == Looper.myLooper()) {
// 當前線程是主UI線程,直接刷新。
invalidate();
} else {
// 當前線程是非UI線程,post刷新。
postInvalidate();
}
}
複製代碼
在這裏進行線程的判斷,主線程中直接調用invalidate,在子線程中調用postInvalidate,緣由想必你們都清楚,就再也不解釋了。
/**
* 設置高亮顯示文本的字體顏色
*
* @param color 顏色值
*/
public void setHighLightTextColor(int color) {
if (mHighLightColor != color) {
mHighLightColor = color;
invalidateView();
}
}
/**
* 設置歌詞內容行間距
*
* @param lineSpace 行間距大小
*/
public void setLineSpace(float lineSpace) {
if (mLineSpace != lineSpace) {
mLineSpace = getRawSize(TypedValue.COMPLEX_UNIT_SP, lineSpace);
measureLineHeight();
mScrollY = measureCurrentScrollY(mCurrentPlayLine);
invalidateView();
}
}
/**
* 設置歌詞文本內容字體大小
*
* @param unit
* @param size
*/
public void setTextSize(int unit, float size) {
setRawTextSize(getRawSize(unit, size));
}
/**
* 設置歌詞文本內容字體大小
*
* @param size
*/
public void setTextSize(float size) {
setTextSize(TypedValue.COMPLEX_UNIT_SP, size);
}
複製代碼
其中getRawSize方法只是根據設備將設置的數據轉換單位返回。
/**
* 設置字體大小,並從新計算,刷新
* @param size
*/
private void setRawTextSize(float size) {
if (size != mTextPaint.getTextSize()) {
mTextPaint.setTextSize(size);
measureLineHeight();
mScrollY = measureCurrentScrollY(mCurrentPlayLine);
invalidateView();
}
}
private float getRawSize(int unit, float size) {
Context context = getContext();
Resources resources;
if (context == null) {
resources = Resources.getSystem();
} else {
resources = context.getResources();
}
return TypedValue.applyDimension(unit, size, resources.getDisplayMetrics());
}
複製代碼
其中measureLineHeight是在計算行高,measureCurrentScrollY是在計算縱向偏移量,這兩個方法稍後就會介紹。
/**
* 初始化須要的尺寸
*/
private void initAllBounds() {
setTextSize(20);
setLineSpace(18);
mBtnWidth = (int) (getRawSize(TypedValue.COMPLEX_UNIT_SP, 20));
mTimerBound = new Rect();
mIndicatorPaint.getTextBounds(mDefaultTime, 0, mDefaultTime.length(), mTimerBound);
measureLineHeight();
}
/**
* 初始化畫筆
*/
private void initAllPaints() {
mTextPaint = new Paint();
mTextPaint.setDither(true); // 防抖動
mTextPaint.setAntiAlias(true); // 抗鋸齒
mTextPaint.setTextAlign(Paint.Align.CENTER); // 文字對其方式
mIndicatorPaint = new Paint();
mIndicatorPaint.setDither(true);
mIndicatorPaint.setAntiAlias(true);
mIndicatorPaint.setTextSize(getRawSize(TypedValue.COMPLEX_UNIT_SP, 12));
mIndicatorPaint.setTextAlign(Paint.Align.CENTER);
mBtnPaint = new Paint();
mBtnPaint.setDither(true); // 設置防抖動
mBtnPaint.setAntiAlias(true); // 設置抗鋸齒
mBtnPaint.setColor(mBtnColor);
mBtnPaint.setStrokeWidth(3.0f); // 設置空心線寬
mBtnPaint.setStyle(Paint.Style.STROKE);
}
複製代碼
這些簡單的初始化,就再也不詳細介紹了。
/**
* 計算行高度
*/
private void measureLineHeight() {
Rect lineBound = new Rect();
mTextPaint.getTextBounds(mDefaultHint, 0, mDefaultHint.length(), lineBound);
mLineHeight = lineBound.height() + mLineSpace;
}
複製代碼
以前行間距已經設置過了,固然,開發者不設置也是能夠的,我已經設了默認值。
咱們認爲一行,應該包括一行文字和一個行間距,因此 行高=文字高度+行間距。
計算文字高度,應該使用畫筆的getTextBounds方法,從文字區域的Rect中獲取文字所佔的高度。
/**
* 根據當前給定的時間戳滑動到指定位置
*
* @param time 時間戳
*/
private void scrollToCurrentTimeMillis(long time) {
int position = 0;
if (scrollable()) {
for (int i = 0, size = mLineCount; i < size; i++) {
LineInfo lineInfo = mLyricInfo.getLines().get(i);
if (lineInfo != null && lineInfo.getStartTime() > time) {
position = i;
break;
}
if (i == mLineCount - 1) {
position = mLineCount;
}
}
}
if (mCurrentPlayLine != position && !mUserTouch && !mSliding && !mIndicatorShow) {
mCurrentPlayLine = position;
smoothScrollTo(measureCurrentScrollY(position));
} else {
if (!mSliding && !mIndicatorShow) {
mCurrentPlayLine = mCurrentShowLine = position;
}
}
}
複製代碼
既然LyricView可以實現滑動功能,那麼引入scrollY值記錄滑動偏移量,並控制視圖繪製效果也就瓜熟蒂落。 須要明確一點,當偏移量scrollY的值爲零的時候,歌詞的首行將顯示在整個LyricView的正中間 。
在上篇中,咱們也知道每一句歌詞中都包含着開始時間,而咱們也就能夠經過當前歌曲播放進度匹配當前播放的行數 mCurrentPlayLine,並經過當前播放所在行,計算偏移量scrollY的值,控制歌詞播放滾動和當前播放位置的高亮顯示。
/**
* 根據行號計算偏移量
* @param line 當前指定行號
*/
private float measureCurrentScrollY(int line) {
return (line - 1) * mLineHeight;
}
複製代碼
這裏還需注意,第一行的時候偏移量爲0,因此計算對應偏移量的時候須要先減一。
@Override
protected void onDraw(Canvas canvas) {
if (mLyricInfo != null && mLyricInfo.getLines() != null && mLyricInfo.getLines().size() > 0) {
for (int i = 0, size = mLineCount; i < size; i++) {
float x = getMeasuredWidth() * 0.5f;
float y = getMeasuredHeight() * 0.5f + (i + 0.5f) * mLineHeight - 6 - mLineSpace * 0.5f - mScrollY;
// 已經繪製的再也不繪製
if (y + mLineHeight * 0.5f < 0) {
continue;
}
// 超出屏幕部分不繪製
if (y - mLineHeight * 0.5f > getMeasuredHeight()) {
break;
}
if (i == mCurrentPlayLine - 1) {
mTextPaint.setColor(mHighLightColor);
} else {
if (mIndicatorShow && i == mCurrentShowLine - 1) {
mTextPaint.setColor(mCurrentShowColor);
} else {
mTextPaint.setColor(mDefaultColor);
}
}
// 不在中心區域
if (y > getMeasuredHeight() - mShaderWidth || y < mShaderWidth) {
if (y < mShaderWidth) {
mTextPaint.setAlpha(26 + (int) (23000.0f * y / mShaderWidth * 0.01f));
} else {
mTextPaint.setAlpha(26 + (int) (23000.0f * (getMeasuredHeight() - y) / mShaderWidth * 0.01f));
}
// 在中心區
} else {
mTextPaint.setAlpha(255);
}
canvas.drawText(mLyricInfo.getLines().get(i).getContent(), x, y, mTextPaint);
}
} else {
mTextPaint.setColor(mHintColor);
canvas.drawText(mDefaultHint, getMeasuredWidth() * 0.5f, (getMeasuredHeight() + mLineHeight - 6) * 0.5f, mTextPaint);
}
}
複製代碼
這樣文字就能夠繪製在屏幕上了,同時你們可能也看出來了,我設置了透明度,也就是淡入淡出效果。
注意:
若是單純實現視圖滑動的功能的話,比較簡單:只須要記錄ACTION_DOWN時的y值,並比較ACTION_MOVE過程當中的y值計算二者的差值,生成新的偏移量scrollY,再刷新視圖,就能夠了 !
可是,這樣實現的話,用戶一直滑動,整個歌詞內容區域就會滑動出咱們的可視區域,也就是常說的overScroll,若是不加以限制將會是一種很是差的用戶體驗。
與正常滑動時有所區別,滑動應該有一種阻尼效果:也就是實際滑動距離和視圖的滾動距離並不相等,並且隨着overScroll的值越大,阻力越大,滑動越艱難,並在用戶手指離開屏幕後回到overScroll的值爲零的位置。
/**
* 計算阻尼效果的大小
*/
private final int mMaxDampingDistance = 360;
private float measureDampingDistance(float value02) {
return value02 > mMaxDampingDistance ? (mMaxDampingDistance * 0.6f + (value02 - mMaxDampingDistance) * 0.72f) : value02 * 0.6f;
}
複製代碼
/**
* 手勢移動執行事件
*
* @param event
*/
private void actionMove(MotionEvent event) {
if (scrollable()) {
final VelocityTracker tracker = mVelocityTracker;
tracker.computeCurrentVelocity(1000, maximumFlingVelocity);
float scrollY = mLastScrollY + mDownY - event.getY(); // 102 -2 58 42
float value01 = scrollY - (mLineCount * mLineHeight * 0.5f); // 52 -52 8 -8
float value02 = ((Math.abs(value01) - (mLineCount * mLineHeight * 0.5f))); // 2 2 -42 -42
mScrollY = value02 > 0 ? scrollY - (measureDampingDistance(value02) * value01 / Math.abs(value01)) : scrollY; // value01 / Math.abs(value01) 控制滑動方向
mVelocity = tracker.getYVelocity();
measureCurrentLine();
}
}
複製代碼
其中VelocityTracker主要用跟蹤觸摸屏事件(flinging事件和其餘gestures手勢事件)的速率。
經過一次一次對代碼的細化,只要這麼簡單的兩個方法,就完成了滑動時偏移量scrollY的計算,包括overScroll和非overScroll。
到了這一步,歌詞的顯示、滑動查看都已經完成。
/**
* 繪製指示器
*
* @param canvas
*/
private void drawIndicator(Canvas canvas) {
mIndicatorPaint.setColor(mIndicatorColor);
mIndicatorPaint.setAlpha(128);
mIndicatorPaint.setStyle(Paint.Style.FILL);
canvas.drawText(measureCurrentTime(), getMeasuredWidth() - mTimerBound.width(), (getMeasuredHeight() + mTimerBound.height() - 6) * 0.5f, mIndicatorPaint);
Path path = new Path();
mIndicatorPaint.setStrokeWidth(2.0f);
mIndicatorPaint.setStyle(Paint.Style.STROKE);
mIndicatorPaint.setPathEffect(new DashPathEffect(new float[]{20, 10}, 0));
path.moveTo(mPlayable ? mBtnBound.right + 24 : 24, getMeasuredHeight() * 0.5f);
path.lineTo(getMeasuredWidth() - mTimerBound.width() - mTimerBound.width() - 36, getMeasuredHeight() * 0.5f);
canvas.drawPath(path, mIndicatorPaint);
}
複製代碼
到這裏,歌詞顯示器就算完成了,有不對的地方還望你們指出。
獲取更多精彩內容,關注個人微信公衆號——Android機動車!