歌詞顯示控件的實現下——自定義View

先看下效果 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機動車

相關文章
相關標籤/搜索