Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> Android資訊 >> Android 實現平滑滾動的歌詞控件

Android 實現平滑滾動的歌詞控件

編輯:Android資訊

馬上畢業了,前段時間一直忙自己的畢業設計和畢業論文(蛋疼連著菊花疼),做的是一個android音樂播放器,今天特意抽出裡面的一塊功能來湊這篇博客–歌詞的顯示。

看看QQ音樂,歌詞顯示略屌,可惜我們的LRC文件並不能做到詞的同步,只能做到行的同步,所以,退而求之,今天的歌詞空間只是同步行,那他有什麼功能呢? 歌詞同步就不說了,切換滑動效果是我後加上的,因為我看著一行行的切換太過生硬。

下面開始進入主題。

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" />

這裡我們來看看幾個以lrc為命名空間的配置項。

textSize不用多說,肯定是文本的大小了;normalTextColor是普通文本的顏色,因為歌詞分為普通的行和當前高亮行,那currentTextColor肯定是高亮行的顏色了;dividerHeight是行間距;rows是顯示多少行歌詞,在該配置文件中是顯示9行的歌詞。配置好了,我們需要在activity或者fragment中來使用它。

...
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);  
    }  
...  
}

這麼多變量!到底是干嘛用的!只是為了裝B嗎? NO NO NO, 我們定義它,肯定是需要啦,一個個的來解釋一下吧吧。

常量SCROLL_TIME定義了當歌詞切換時滑動的時間,這裡是500ms。

常量DEFAULT_TEXT定義的是當沒有歌詞的時候顯示的默認文本。

兩個ArrayList,mLrcs保存的是一行行的歌詞,mTimes保存的是歌詞對應的時間。

mNextTime表示的是下一行開始的時間。

其他的一些變量,可以看看代碼裡的注釋,這裡就不一一貼出來了。

再來看看構造方法,除了初始化Scroller外,我們調用了inflateAttributes(),那我們跟進inflateAttributes():

// 初始化操作  
    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);  
    }

5~12行,解析出屬性值,沒有什麼好說的,無非就是獲取用戶配置的顏色啦,字體大小啦,多少行啦。

16行,通過獲取到的屬性,計算出Lrc能顯示下需要多少高度。

然後接下來的一系列動作就是初始化兩個Paint,並獲取Scroller最大滾動的距離,為什麼要計算這個呢? 因為我們需要知道歌詞每次要滾動多大距離。(廢話!)

初始化完了,就是測量了,我們的測量也是很簡單的。

@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();  
    }

測量,我們只是重新定義了高度,然後在onSizeChanged中獲取了該view的寬度。

按照,進度呢,我們接下來應該看draw了,但是現在我們先不去看onDraw,而是去看看setLrcPath這個方法。

// 外部提供方法  
    // 設置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();  
                }  
            }  
        }  
    }

雖然長了點,但是都是基本的java io,按行去讀取文件,然後正則匹配。

需要注意的是22~26行,這裡需要說明一下,我們只是匹配了形如:

[05:20.59] 我從天上來

這樣的歌詞。繼續看看parseLine()方法吧。用正則去匹配歌詞。

// 解析每行  
    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;  
    }

只是簡單的正則,沒看懂的可以腦補正則了,這裡我們只匹配[開頭是數字的],如果不是數字,例如:[title:可惜沒如果],這樣的我們直接拋棄掉。在這個方法中,我們保存了每一行歌詞,但是時間還需要調用parseTime()方法來處理一下。繼續跟進parseTime()。

// 解析時間  
    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;  
    }

也是很簡單的,通過分割形如“03:02.12”的時間,並且在最後以毫秒的形式返回。

到目前為止,所有的歌詞和歌詞對應的時間已經保存起來了,接下來,就是要調用changeCurrent()方法來切換歌詞了。

// 外部提供方法  
    // 傳入當前播放時間  
    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;  
            }  
        }  
    }

6~8行判斷一下現在傳進來的時間是不是大於下一行的時間,如果不是,直接返回,避免過度重繪。

接下來,去遍歷所有的時間,如果發現該時間大於傳進來的時間,那麼證明現在要顯示上一行了,保存這個時間,並開始一個Scroller。startScroll方法的參數我們是這樣設置的。x值在scroll中沒有用,所以我們用來保存當前key,並且讓他的變化度為0,y的值是從0到mMaxScroll.

接著來看看computeScroll()中怎麼處理的。

@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();  
        }  
    }

y的變化值我們作為滑動的偏移量,而x呢 當然就是當前行了。

接下來,我們就要開始進入onDraw方法了。

@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);  
        }  
    }

首先第4行,我們計算出了該view的中間位置,因為我們的歌詞是從中間往兩邊畫的。

5~10行,如果歌詞為空,則顯示默認的文本”暫無歌詞“

12~15行是去繪制當前正在歌唱的行,drawText的第三個參數,我們減去了mOffetY,效果就是一個滑動的過程。

繪制完當前行,我們就需要繪制出當前行上面的和下面的。

17行,是每一行占領的高度。
18、19行,獲取的是當前需要顯示的第一行(並不是歌詞的第一行)。
20、21行,獲取需要顯示的最後一行。
24~28行,去繪制當前行上面的的需要顯示的歌詞,需要注意的drawText的第三個參數,我們是通過中間那行的繪制位置去偏移的。
31~35行是去繪制當前行下面的,原理和繪制上面的一樣。
這樣,一個帶有平滑滾動效果的歌詞控件就完成了。

最後我們來看看最終的效果:

最後,是關於代碼的問題,有人說我的博客沒有demo下載,這個以後會注意哈, 這次的代碼,等我這個月月底畢業答辯完了,會把音樂播放器一塊開源了。

LitePlayer源碼下載:https://git.oschina.net/qibin/LitePlayer

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved