金沙注册网站-新金沙官网 新金沙官网 金沙注册网站高仿QQ音乐歌词滚动控件,实现平滑滚动的歌词控件

金沙注册网站高仿QQ音乐歌词滚动控件,实现平滑滚动的歌词控件



马上毕业了,前段时间一直忙自己的毕业设计和毕业论文(蛋疼连着菊花疼),做的是一个android音乐播放器,今天特意抽出里面的一块功能来凑这篇博客–歌词的显示。

最近在以QQ音乐为样板做一个手机音乐播放器,源码下篇博文放出。今天我想聊的是这个QQ音乐播放器中歌词显示控件的问题,和小伙伴们一起来探讨怎么实现这个歌词滚动的效果。OK,废话不多说,先来看看效果图:

看看QQ音乐,歌词显示略屌,可惜我们的LRC文件并不能做到词的同步,只能做到行的同步,所以,退而求之,今天的歌词空间只是同步行,那他有什么功能呢?
歌词同步就不说了,切换滑动效果是我后加上的,因为我看着一行行的切换太过生硬。

新金沙官网 1

下面开始进入主题。

好,接下来我们就来看看怎么实现这样一个效果。本文主要包括如下几方面内容:

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

1.歌词文件格式分析及解析

这里我们来看看几个以lrc为命名空间的配置项。

2.歌词显示控件绘制

新金沙官网,textSize不用多说,肯定是文本的大小了;normalTextColor是普通文本的颜色,因为歌词分为普通的行和当前高亮行,那currentTextColor肯定是高亮行的颜色了;dividerHeight是行间距;rows是显示多少行歌词,在该配置文件中是显示9行的歌词。配置好了,我们需要在activity或者fragment中来使用它。

金沙注册网站,3.关于卡拉OK模式

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

4.使用方式

第一行代码去获取该控件,接着调用setLrcPath将歌词文件加载到内存中,在onPushlish方法中不断调用changeCurrent来更新歌词,那changeCurrent的参数哪来的呢?这个是音乐播放回调的进度,到这里,可能会有大神出疑问了,
这样做是不是会不断的更新歌词控件?就算当前没有切换歌词也回去更新?
这里先给出回答:当然不是了,我们在changeCurrent方法中做了判断,所以这里尽管调用,放心调用!

 

那接下来,我们开始进入今天的主题:LrcView。

好,那就开始吧。

在进入代码之前,先来看看我的设计思路吧:

1.歌词文件格式分析及解析

首先,小伙伴们需要明白歌词文件的格式都是固定的,是什么样子的呢,我们来看看下图:

新金沙官网 2

 

我们一个歌词文件打开都是这种格式,前面  [] 
中的是该行歌词显示的时间,后面一行是歌词,只有时间没有歌词的行就是伴奏时间。了解了这固定的歌词格式,剩下的就简单了,解析这段文本就行了。我创建一个LrcBean用来存放每一行的数据,这个LrcBean中包括三个属性,分别是一句歌词,该歌词开始唱的时间,该歌词唱完的时间,咦,有的小伙伴可能有疑问,唱完是什麽时候呢?就是下一句的开始时间呗。OK,那我们来看看实体类:

public class LrcBean {
    private String lrc;
    private long start;
    private long end;

    public LrcBean() {
    }

    public LrcBean(String text, long start, long end) {
        this.lrc = text;
        this.start = start;
        this.end = end;
    }

    public String getLrc() {
        return lrc;
    }

    public void setLrc(String lrc) {
        this.lrc = lrc;
    }

    public long getStart() {
        return start;
    }

    public void setStart(long start) {
        this.start = start;
    }

    public long getEnd() {
        return end;
    }

    public void setEnd(long end) {
        this.end = end;
    }
}

OK,实体类有了,接下来我们来看看实体类怎么解析歌词文本,解析过程分为两步:

1.考虑到歌词文本中可能有转义字符,我们需要先把转义字符还原

2.然后按照换行符将文本拆分,再通过字符串截取将每一行的数据提取出来。代码如下(由于转义字符显示不出来,所以我这里贴一张代码图,源码文末可以下载):

新金沙官网 3

OK,通过以上方式我们就把歌词文件解析成了一个List集合,该集合中的每一项就是一句歌词,另外,在伴奏的时间段,我显示一句music。

当我们传进一个lrc文件的path,首先按照行去read文件,并且利用正则解析出时间和歌词分别存放。设置完歌词后,我们通过不断调用changeCurrent()方法来切换歌词,那么changeCurrent又负责了什么工作呢?
在changeCurrent中首先判断下一行开始的时间是不是大于当前传进来的时间,如果是,直接返回,否则,遍历所有的时间,找到大于当前时间的上一行的key,
再次通过key找到歌词,咔咔咔, 显示出来就ok了。

2.歌词显示控件绘制

歌词解析完了,接下来我们就可以绘制歌词View了。绘制的整体思路是这样:

1.首先获取当前播放的时间

2.根据当前播放时间,遍历歌词的List集合,判断出当前正在播放的是List集合中的哪一句,找到该句的下标

3.遍历歌词List集合,绘制所有歌词,绘制的过程中,如果该句是正在播放的歌词,则使用高亮的画笔来绘制,否则使用普通画笔绘制。

4.判断当前是否已经换行了,如果是,则调用setScrollY方法让屏幕滚动一行。关于setScrollY方法如果小伙伴们还不太了解可以参考这篇文章View绘制详解(五),draw方法细节详解之View的滚动/滑动问题。

5.每隔100毫秒重绘View。

OK,整个流程就是这样,接下来我们来看看代码实现:

    @Override
    protected void onDraw(Canvas canvas) {
        if (width == 0 || height == 0) {
            width = getMeasuredWidth();
            height = getMeasuredHeight();
        }
        if (list == null || list.size() == 0) {
            canvas.drawText("暂无歌词", width / 2, height / 2, gPaint);
            return;
        }

        getCurrentPosition();

        int currentMillis = player.getCurrentPosition();
        drawLrc2(canvas);
        long start = list.get(currentPosition).getStart();
        float v = (currentMillis - start) > 500 ? currentPosition * 80 : lastPosition * 80 + (currentPosition - lastPosition) * 80 * ((currentMillis - start) / 500f);
        setScrollY((int) v);
        if (getScrollY() == currentPosition * 80) {
            lastPosition = currentPosition;
        }
        postInvalidateDelayed(100);
    }

    private void drawLrc2(Canvas canvas) {
        for (int i = 0; i < list.size(); i++) {
            if (i == currentPosition) {
                canvas.drawText(list.get(i).getText(), width / 2, height / 2 + 80 * i, hPaint);
            } else {
                canvas.drawText(list.get(i).getText(), width / 2, height / 2 + 80 * i, gPaint);
            }
        }
    }

    private void getCurrentPosition() {
        try {
            int currentMillis = player.getCurrentPosition();
            if (currentMillis < list.get(0).getStart()) {
                currentPosition = 0;
                return;
            }
            if (currentMillis > list.get(list.size() - 1).getStart()) {
                currentPosition = list.size() - 1;
                return;
            }
            for (int i = 0; i < list.size(); i++) {
                if (currentMillis >= list.get(i).getStart() && currentMillis < list.get(i).getEnd()) {
                    currentPosition = i;
                    return;
                }
            }
        } catch (Exception e) {
//            e.printStackTrace();
            postInvalidateDelayed(100);
        }
    }

OK,这里给出一个核心代码,完整代码小伙伴们在文末可以自行下载。

look code:

3.关于卡拉OK模式

OK,经过第二个步骤之后,我们这个歌词控件已经可以根据当前播放的时间来显示高亮的歌词,同时进行歌词的滚动。有的小伙伴可能还想实现一种类似于KTV里边的那种播放效果,我们也来看一看怎么实现。还是先来说说思路吧。

1.把所有的歌词都用普通的画笔画出来

2.为当前正在播放的歌词生成一个Bitmap

3.根据当前播放时间,计算出该句歌词播放的比例,然后根据这个比例绘制第二步生成的Bitmap。

OK,根据上述的思路,我贴出核心代码如下:

for (int i = 0; i < list.size(); i++) {
                canvas.drawText(list.get(i).getLrc(), width / 2, height / 2 + 80 * i, gPaint);
            }
            String highLineLrc = list.get(currentPosition).getLrc();
            int highLineWidth = (int) gPaint.measureText(highLineLrc);
            int leftOffset = (width - highLineWidth) / 2;
            LrcBean lrcBean = list.get(currentPosition);
            long start = lrcBean.getStart();
            long end = lrcBean.getEnd();
            int i = (int) ((currentMillis - start) * 1.0f / (end - start) * highLineWidth);
            if (i > 0) {
                Bitmap textBitmap = Bitmap.createBitmap(i, 80, Bitmap.Config.ARGB_8888);
                Canvas textCanvas = new Canvas(textBitmap);
                textCanvas.drawText(highLineLrc, highLineWidth / 2, 80, hPaint);
                canvas.drawBitmap(textBitmap, leftOffset, height / 2 + 80 * (currentPosition - 1), null);
            }
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);  
    }  
...  
}

4.使用方式

OK,控件做好了,最后我们再来看看使用方式。很简单,引入这个View
的类库(文末会给出下载地址),然后传入歌词的文本,开启绘制即可,如下:

lrcView.setLrc(lrcStr);
        lrcView.setPlayer(PlayUtil.player);
        lrcView.init();

简单三行代码,就可以开始使用了。

 

项目地址

 

这么多变量!到底是干嘛用的!只是为了装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的宽度。

相关文章

发表评论

电子邮件地址不会被公开。 必填项已用*标注

网站地图xml地图