日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > Android >内容正文

Android

android歌词效果,自定义View:Android歌词控件

發布時間:2025/3/12 Android 41 豆豆
生活随笔 收集整理的這篇文章主要介紹了 android歌词效果,自定义View:Android歌词控件 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

TicktockMusic 音樂播放器項目相關文章匯總:

簡介

之前做 TicktockMusic 音樂播放器,一個必要的需求肯定是歌詞,在 github 上找了幾個,發現或多或少都有點不滿足需求,所以就自己動手寫了一個,本篇文章主要介紹下實現的原理。

先附上項目地址和效果圖:

效果圖:

image

需求

歌詞的需求我想大家都很清楚,簡單的話,直接打開一個音樂播放器查看一下。我們打開后分析一下歌詞的功能:歌詞完整的顯示出來、當前歌詞變色、可以根據時間而進行定位、可以手動滑動、滑動后顯示一個指示器、點擊指示器播放進度跳轉、滑動時指示器變色等等。OK,我們自己寫歌詞控件,這些功能也是必不可少的,接下來就逐步分析下實現的過程。

實現

歌詞解析

歌詞顯示

滑動處理

指示器

基本實現就是這幾個過程,接下來一步步的分析。

歌詞解析

首先,我們在網上下載一個歌詞,即以 lrc 為后綴的文件。比如海闊天空這首歌的歌詞,我們用記事本或者其他工具打開后就可以看到具體的歌詞內容,如下:

[ti: 海闊天空]

[ar:黃家駒]

[al:樂與怒]

[by:mp3.50004.com]

[00:00.00]Beyond:海闊天空

[01:40.00][00:16.00]今天我寒夜里看雪飄過

[01:48.00][00:24.00]懷著冷卻了的心窩飄遠方

[01:53.00][00:29.00]風雨里追趕

...

[00:42.00]多少次迎著冷眼與嘲笑

[00:49.00]從沒有放棄過心中的理想

[00:54.00]一剎那恍惚

...

可以看到,歌詞主要包含歌名、歌手、專輯、作者等頭元素,以及歌詞的主體內容,我們需要處理的就是主體的歌詞內容。首先,歌詞是一行一行的文本,其次,每行的文本都包含時間標簽和具體的一行歌詞,我們首先將歌詞解析為一行行的數據。

InputStreamReader isr = null;

BufferedReader br = null;

try {

isr = new InputStreamReader(inputStream, CHARSET);

br = new BufferedReader(isr);

String line;

while ((line = br.readLine()) != null) {

//此處的 line 即為一行行的文本

//parseLrc 方法為解析單行

List lrcList = parseLrc(line);

if (lrcList != null && lrcList.size() != 0) {

lrcs.addAll(lrcList);

}

}

sortLrcs(lrcs);

return lrcs;

}catch ...

解析為一行行的文字后,就需要具體的處理單行的文字了,我們可以看到,大部分歌詞包含兩種格式,即單個時間標簽和多個時間標簽,這里可以采用正則表達式來匹配文字,正則表達式為 (([\d{2}:\d{2}.\d{2}])+)(.*)

[01:53.00][00:29.00]風雨里追趕 //多個時間標簽

[00:42.00]多少次迎著冷眼與嘲笑 //單個時間標簽

接下來根據正則表達式來解析單行歌詞

private static List parseLrc(String lrcLine) {

if (lrcLine.trim().isEmpty()) {

return null;

}

List lrcs = new ArrayList<>();

Matcher matcher = Pattern.compile(LINE_REGEX).matcher(lrcLine);

if (!matcher.matches()) {

return null;

}

String time = matcher.group(1);

String content = matcher.group(3);

Matcher timeMatcher = Pattern.compile(TIME_REGEX).matcher(time);

while (timeMatcher.find()) {

String min = timeMatcher.group(1);

String sec = timeMatcher.group(2);

String mil = timeMatcher.group(3);

Lrc lrc = new Lrc();

if (content != null && content.length() != 0) {

lrc.setTime(Long.parseLong(min) * 60 * 1000 + Long.parseLong(sec) * 1000

+ Long.parseLong(mil) * 10);

lrc.setText(content);

lrcs.add(lrc);

}

}

return lrcs;

}

這樣,第一步就完成了,歌詞解析完成后得到歌詞的數據集合,每個元素都包括時間和內容。

歌詞顯示

歌詞顯示的思路就是將歌詞一行行的畫出來,我們首先假設屏幕足夠大,那么只需要定位第一行歌詞的位置,畫出來第一行歌詞,然后逐行下移一個固定的距離,再畫出下一行歌詞,依次類推,整個歌詞內容就會全部畫在畫布上了。依照這個思路,我們可以先畫出來文字。

//此處為偽代碼

float y = getLrcHeight() / 2;

float x = getLrcWidth() / 2 + getPaddingLeft();

for (int i = 0; i < getLrcCount(); i++) {

if (i > 0) {

y += textHeight + mLrcLineSpaceHeight;

}

...

canvas.drawText(text, x, y, mPaint);

}

畫出來文字的思路就是這樣,首先從屏幕的中間開始,然后縱坐標每次增加文字的高度與距離之和,依次畫出來每行文字。這樣,假如屏幕足夠大的話,那么所有的歌詞就會從屏幕中間開始,依次向下一行行的顯示出來。但是,我們的屏幕不可能是無限大的。首先,假如一行歌詞很長的話,canvas.drawText() 的效果會是屏幕覆蓋掉多余的 text 文字,所以當一行文字超過我們設置的 View 最大寬度時,最理想的方法就是多余的部分換行,就像 TextView 一樣。所幸的是,Android 中給我們提供了方法,那就是 StaticLayout ,StaticLayout 用法很簡單,我們使用它來替代 canvas.drawText(),下面是基本用法。

private void drawLrc(Canvas canvas, float x, float y, int i) {

mTextPaint.setTextSize(mLrcTextSize);

String text = mLrcData.get(i).getText();

StaticLayout staticLayout = new StaticLayout(text, mTextPaint, getLrcWidth(),

Layout.Alignment.ALIGN_NORMAL, 1f, 0f, false);

canvas.save();

canvas.translate(x, y - staticLayout.getHeight() / 2 - mOffset);

staticLayout.draw(canvas);

canvas.restore();

}

這樣我們就能獲取想要的效果了,文字一行行的排列,文字比較長的話,會自動換行到下一行。但是,這樣僅僅是實現效果,在 onDraw() 方法中,我們應該盡量的避免新建對象,以免造成界面的卡頓,而 StaticLayout 需要實例化對象,所以這邊需要我們手動優化一下。

因為使用 StaticLayout 后,一行文字的高度不再固定,所以 y 坐標不再累加固定的文字高度,而是上一行和下一行文字之和的一半+文字間距。代碼如下:

for (int i = 0; i < getLrcCount(); i++) {

if (i > 0) {

y += (getTextHeight(i - 1) + getTextHeight(i)) / 2 + mLrcLineSpaceHeight;

}

drawLrc(canvas, x, y, i);

}

為了避免過多的實例化,在使用 StaticLayout 時,這里采用 map 進行緩存,創建過對象后緩存起來,后邊就不需要再繼續創建。

private void drawLrc(Canvas canvas, float x, float y, int i) {

String text = mLrcData.get(i).getText();

StaticLayout staticLayout = mLrcMap.get(text);

if (staticLayout == null) {

mTextPaint.setTextSize(mLrcTextSize);

staticLayout = new StaticLayout(text, mTextPaint, getLrcWidth(),

Layout.Alignment.ALIGN_NORMAL, 1f, 0f, false);

mLrcMap.put(text, staticLayout);

}

canvas.save();

canvas.translate(x, y - staticLayout.getHeight() / 2 - mOffset);

staticLayout.draw(canvas);

canvas.restore();

}

到這里,我們已經解決了水平方向的顯示,但是垂直方向呢,垂直方向則利用滑動來解決,這也是歌詞的基本需求之一。

滑動處理

歌詞的滑動是做歌詞控件的必然要求,包括根據音樂播放的進度進行自動的滑動,以及用戶主動拖動的滑動,我們來逐個分析。

1、根據播放進度滾動

音樂的播放時間進度可以根據 MediaPlayer 來獲取,在一首音樂播放的過程中,播放的進度是不斷更新的,所以就需要我們根據這個不斷更新的時間,來決定歌詞滾動的位置。

我們需要比較不斷更新的時間和每行歌詞的時間,最接近或者相等時,就可以視作音樂播放的進度對應當前這一行歌詞,所以需要獲取播放時間對應的歌詞行數。

private int getUpdateTimeLinePosition(long time) {

int linePos = 0;

for (int i = 0; i < getLrcCount(); i++) {

Lrc lrc = mLrcData.get(i);

if (time >= lrc.getTime()) {

if (i == getLrcCount() - 1) {假如時間大于最后一行歌詞的時間,則行數為最后一行

linePos = getLrcCount() - 1;

} else if (time < mLrcData.get(i + 1).getTime()) {//否則若同時小于下一行,則行數為 i

linePos = i;

break;

}

}

}

return linePos;

}

獲取行數之后,行數變化時,就可以利用動畫,來讓歌詞進行滾動。

private void scrollToPosition(int linePosition) {

float scrollY = getItemOffsetY(linePosition);//將要滾動的一行的偏移量

final ValueAnimator animator = ValueAnimator.ofFloat(mOffset, scrollY);

animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

@Override

public void onAnimationUpdate(ValueAnimator animation) {

mOffset = (float) animation.getAnimatedValue();

invalidateView();

}

});

animator.setDuration(300);

animator.start();

}

此處最重要的屬性就是 mOffset ,mOffset 是為了決定歌詞偏移量而定義的一個屬性, mOffset 的取值是在原有值和目標行的偏移量之間,由動畫控制其變化。假如向下滑動,初始為0,則滾動到第二行歌詞,mOffset 就是從 0 到 getItemOffsetY(1) 的過程。 getItemOffsetY(i) 就是第 i 行的偏移量。

private float getItemOffsetY(int linePosition) {

float tempY = 0;

for (int i = 1; i <= linePosition; i++) {

tempY += (getTextHeight(i - 1) + getTextHeight(i)) / 2 + mLrcLineSpaceHeight;

}

return tempY;

}

然后,再根據播放進度,進行不斷的更新。

public void updateTime(long time) {

if (isLrcEmpty()) {

return;

}

int linePosition = getUpdateTimeLinePosition(time);

if (mCurrentLine != linePosition) {

mCurrentLine = linePosition;

ViewCompat.postOnAnimation(LrcView.this, mScrollRunnable);

}

}

private Runnable mScrollRunnable = new Runnable() {

@Override

public void run() {

scrollToPosition(mCurrentLine);

}

};

到此為止,我們已經完成了歌詞的自動滾動功能。

2、滑動事件處理

僅僅有自動滾動是無法滿足歌詞的需求的,所以我們還需要控制歌詞的滑動事件,讓用戶可以手動滑動歌詞到某個位置。既然是手勢的事件,那么就需要我們重寫 onTouch 方法,處理不同的手勢。

@Override

public boolean onTouchEvent(MotionEvent event) {

if (isLrcEmpty()) { //歌詞為空,則默認事件

return super.onTouchEvent(event);

}

//速度跟蹤

if (mVelocityTracker == null) {

mVelocityTracker = VelocityTracker.obtain();

}

mVelocityTracker.addMovement(event);

switch (event.getAction()) {

case MotionEvent.ACTION_DOWN:

removeCallbacks(mScrollRunnable);

if (!mOverScroller.isFinished()) {

mOverScroller.abortAnimation();

}

mLastMotionX = event.getX();

mLastMotionY = event.getY();

isUserScroll = true;

isDragging = false;

break;

case MotionEvent.ACTION_MOVE:

float moveY = event.getY() - mLastMotionY;

if (Math.abs(moveY) > mScaledTouchSlop) {

isDragging = true;

isShowTimeIndicator = isEnableShowIndicator;

}

if (isDragging) {

float maxHeight = getItemOffsetY(getLrcCount() - 1);

if (mOffset < 0 || mOffset > maxHeight) {

moveY /= 3.5f;

}

mOffset -= moveY;

mLastMotionY = event.getY();

invalidateView();

}

break;

case MotionEvent.ACTION_CANCEL:

case MotionEvent.ACTION_UP:

handleActionUp(event);

break;

}

return true;

}

簡單解釋下上述代碼,先忽略掉 VelocityTracker 和 OverScroller。在 ACTION_DOWN 時,記錄下 x 和 y 的坐標;然后在 ACTION_MOVE 時,若拖動的距離大于觸發滑動的最小值,則改變 mOffset 的值,然后刷新 View。當 mOffset < 0 或者 mOffset > maxHeight 即歌詞已經滾動到頂部或者底部時,為了回彈的阻尼效果,將 moveY 的值大幅減小。

接下來介紹下手勢抬起的事件,VelocityTracker 和 OverScroller 就是用于此處,在手勢滑動抬起時,我們希望有一個 fling 的效果,Android 中的 OverScroller 可以簡單的實現這種效果。

private void handleActionUp(MotionEvent event) {

//越界的處理

if (overScrolled() && mOffset < 0) {

scrollToPosition(0);

ViewCompat.postOnAnimationDelayed(LrcView.this, mScrollRunnable, mTouchDelay);

return;

}

if (overScrolled() && mOffset > getItemOffsetY(getLrcCount() - 1)) {

scrollToPosition(getLrcCount() - 1);

ViewCompat.postOnAnimationDelayed(LrcView.this, mScrollRunnable, mTouchDelay);

return;

}

mVelocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);

float YVelocity = mVelocityTracker.getYVelocity();

float absYVelocity = Math.abs(YVelocity);

if (absYVelocity > mMinimumFlingVelocity) {

mOverScroller.fling(0, (int) mOffset, 0, (int) (-YVelocity), 0,

0, 0, (int) getItemOffsetY(getLrcCount() - 1),

0, (int) getTextHeight(0));

invalidateView();

}

releaseVelocityTracker();

if (isAutoAdjustPosition) {

ViewCompat.postOnAnimationDelayed(LrcView.this, mScrollRunnable, mTouchDelay);

}

}

當手勢抬起時,計算下當前的手勢速度,然后利用 mOverScroller.fling() 方法,在 computeScroll() 中改變 mOffset 的值即可。

@Override

public void computeScroll() {

super.computeScroll();

if (mOverScroller.computeScrollOffset()) {

mOffset = mOverScroller.getCurrY();

invalidateView();

}

}

這樣,主動的手勢功能也已經實現了。

指示器

用戶手動滑動歌詞的目的,很大一部分是為了滑動后能根據歌詞來控制播放的進度,所以指示器也是一個不可或缺的功能。當用戶滑動歌詞時,顯示指示器,歌詞經過指示器的位置時變色,用戶點擊指示器按鈕后,歌詞跳轉到這個位置,播放進度也到了這里。

首先要做的就是顯示指示器以及歌詞變色,這里就需要我們獲取歌詞在指示器的位置時,歌詞的行數,因為指示器畫在歌詞的中間位置,所以某一行歌詞的偏移量和 mOffset 的差值最小時,就可以看作這一行歌詞經過了指示器。

public int getIndicatePosition() {

int pos = 0;

float min = Float.MAX_VALUE;

//itemOffset 和 mOffset 最小時,當前的位置

for (int i = 0; i < mLrcData.size(); i++) {

float offsetY = getItemOffsetY(i);

float abs = Math.abs(offsetY - mOffset);

if (abs < min) {

min = abs;

pos = i;

}

}

return pos;

}

然后在 onDraw() 中,畫出來具體的特性。

if (isShowTimeIndicator) {

mPlayDrawable.draw(canvas); // 畫出指示器的播放按鈕

long time = mLrcData.get(indicatePosition).getTime();

float timeWidth = mIndicatorPaint.measureText(LrcHelper.formatTime(time)); //獲取指示時間的文字長度

mIndicatorPaint.setColor(mIndicatorLineColor);

// 畫出指示線

canvas.drawLine(mPlayRect.right + mIconLineGap, getHeight() / 2,

getWidth() - timeWidth * 1.3f, getHeight() / 2, mIndicatorPaint);

int baseX = (int) (getWidth() - timeWidth * 1.1f);

float baseline = getHeight() / 2 - (mIndicatorPaint.descent() - mIndicatorPaint.ascent()) / 2 - mIndicatorPaint.ascent();

mIndicatorPaint.setColor(mIndicatorTextColor);

//畫出指示時間文字

canvas.drawText(LrcHelper.formatTime(time), baseX, baseline, mIndicatorPaint);

}

最后,處理用戶點擊事件,并且將當前行的歌詞及時間進行回調,來控制播放進度。

if (isShowTimeIndicator && mPlayRect != null && onClickPlayButton(event)) {

isShowTimeIndicator = false;

invalidateView();

if (mOnPlayIndicatorLineListener != null) {

mOnPlayIndicatorLineListener.onPlay(mLrcData.get(getIndicatePosition()).getTime(),

mLrcData.get(getIndicatePosition()).getText());

}

}

//點擊在按鈕范圍才響應

private boolean onClickPlayButton(MotionEvent event) {

float left = mPlayRect.left;

float right = mPlayRect.right;

float top = mPlayRect.top;

float bottom = mPlayRect.bottom;

float x = event.getX();

float y = event.getY();

return mLastMotionX > left && mLastMotionX < right && mLastMotionY > top

&& mLastMotionY < bottom && x > left && x < right && y > top && y < bottom;

}

這樣,指示器的功能也就完成了。

總結

上述就是整個歌詞控件繪制的流程,還有一些顏色變化等細節功能就不一一說明了,有興趣可以看一看源碼。這個控件我也已經封裝成了一個自定義 View 的庫,可以在 https://github.com/Lauzy/LyricView 這里看下具體的使用。歡迎討論、歡迎 star。

參考:

總結

以上是生活随笔為你收集整理的android歌词效果,自定义View:Android歌词控件的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。