一个卡拉OK效果的自定义歌词控件



上图就是效果。
背景和字体颜色可以自己调,都定义成了常量并有说明,直接更改即可。

可以自适应不同分别率。

可以自己将其改成根据屏幕高度动态决定显示几行。只需要调整ondraw就好了。

package com.godyde.musicplayer;

/**
 * 显示歌词的自定义控件。
 * 核心思想就是根据Mediaplayer.getCurrentPosition()返回的当前进度
 * 从歌词中查找当前进度对应的行mCount。以及当前行进行的百分比mPercent
 * 然后根据mCount 来决定打印哪些行[mCount-6,mCount+6
 * 每行歌词打印的基线会随着mPercent的增加而上移(mPercent/100)*mLineHeight.
 * 第一行的透明度会随着mPercent的增加而增加形成淡出效果。
 * 第十三行的透明度会随着mPercent的增加而减少形成淡入效果。
 * 当前进度对应的行为第7行。第7行的效果来自于渐变效果。只是将渐变设置成了类似突变的效果。
 * 突变的位置会随着mPercent的增加而右移。形成卡拉OK效果。
 * <p/>
 * 几个未完成的事情。
 * 2.当歌词单行过长时未分行。会导致部分歌词行显示不完全。(TODO 根据字符串长度和控件宽度来分行显示)
 * 3.未实现从文件加载。直接加载的项目内资源。(TODO 使用Uri来加载歌词)
 * 4.是否考虑使用surfaceview
 *
 * V0.2 :已解决1,2,
 * @author 易登
 * @version V0.2 06/12/2016
 */

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Shader;
import android.media.MediaPlayer;
import android.util.AttributeSet;
import android.view.View;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;


public class LrcTextView extends View {
    //歌词显示的基本颜色
    static final int BASECOLOR = 0xFFEEEEEE;
    //歌词显示的卡拉OK效果颜色
    static final int COVERCOLOR = 0xFF39DF7C;
    static final int BACKGROUNDCOLOR=0xFF201633;
    //字体大小
    static final float MAXTEXTSIZE = 48.0f;
    static final float MINTEXTSIZE = 8f;



    //歌词刷新频率 每秒25帧
    static final long REFRESHRATE = 25;


    //歌词字体大小
    private float mTextSize;

    //每行歌词占用的高度
    private int mLineHeight = 100;

    //歌词最多的一行。所包含的字符数。
    private int mMaxLrcLine;
    //画笔
    private Paint mPaint;
    //歌词字符串数组。从lrc文件提取
    private String mLrc[];
    //显示歌词的时间索引。从lrc文件提取
    private int mLrcTime[];
    //播放歌曲的播放器(由start函数传入)
    private MediaPlayer mMediaPlayer;
    //当前播放进度对应的歌词行
    private int mCount;
    //当前播放进度对应的歌词当前行的百分比 0-100
    private float mPercent;


    public LrcTextView(Context context) {
        super(context);
        initPaint();
    }

    public LrcTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initPaint();
    }

    public LrcTextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initPaint();
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //偷懒的做法。控件总是从宽度上铺满父控件。高度取800和父控件高度中的较小值。
        setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), Math.min(widthMeasureSpec, MeasureSpec.getSize(heightMeasureSpec)));
    }

    @Override
    protected void onDraw(Canvas canvas) {
        if (mMediaPlayer != null) {
            //获取当前的进度
            getCountAndPrecent();
            //循环打印七行歌词
            setTextSize(canvas);
            for (int i = 0; i < 13; i++) {
                //计算需要打印的歌词所在行
                int line = mCount - 6 + i;
                //这个判断是在歌曲开始和结束时,防止越界。小于0 和大于等于mLrc.length无需打印
                if (line >= 0 && line < mLrc.length) {
                    //最上面一行歌词淡出效果
                    if (i == 0) {
                        //根据mPercent设置透明度
                        mPaint.setAlpha((int) ((1 - mPercent / 100) * 255));
                        //打印歌词
                        canvas.drawText(mLrc[line], mTextSize, (i + 1 - mPercent / 100) * mLineHeight, mPaint);
                        //恢复成不透明。
                        mPaint.setAlpha(255);
                    } else if (i == 6) { //当前正在播放的歌词 实现卡拉OK效果
                        //获取当前行歌词的显示宽度
                        float mTextWidth = mPaint.measureText(mLrc[mCount]);
                        //创建一个类似突变的渐变效果 。变化节点根据 mPercent设置。
                        LinearGradient shader = new LinearGradient(0, 0,mTextSize*2+mTextWidth * mPercent / 100, 0,
                                new int[]{COVERCOLOR, BASECOLOR}, new float[]{0.99f, 0.01f},
                                Shader.TileMode.CLAMP);
                        //将突变效果设置给画笔。
                        mPaint.setShader(shader);
                        //使用该画笔画出当前歌词。
                        canvas.drawText(mLrc[line], mTextSize, (i + 1 - mPercent / 100) * mLineHeight, mPaint);
                        //恢复画笔属性。
                        //mPaint.setColor(BASECOLOR);
                        mPaint.setShader(null);

                    } else if (i == 12) {//最下面一行歌词实现淡入效果
                        //根据mPercent设置透明度
                        mPaint.setAlpha((int) ((mPercent / 100) * 255));
                        //打印歌词
                        canvas.drawText(mLrc[line],mTextSize, (i + 1 - mPercent / 100) * mLineHeight, mPaint);
                        //恢复画笔效果
                        mPaint.setAlpha(255);
                    } else {
                        //打印其他行歌词。
                        canvas.drawText(mLrc[line], mTextSize, (i + 1 - mPercent / 100) * mLineHeight, mPaint);
                    }
                }
            }
            //根据刷新频率设置下一次刷新时间
            postInvalidateDelayed(1000 / REFRESHRATE);
        } else
            super.onDraw(canvas);

    }

    /**
     * 初始化画笔属性
     */
    private void initPaint() {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(BASECOLOR);
        //mPaint.setTextSize(mTextSize);
        mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        mPaint.setShader(null);
        mPaint.setTextAlign(Paint.Align.LEFT);
        setBackgroundColor(BACKGROUNDCOLOR);
    }

    /**
     * 计算当前的歌词显示参数
     * 当前行,以及当前行进行的百分比
     */
    private void getCountAndPrecent() {
        if (mMediaPlayer != null) {
            int mPosition = mMediaPlayer.getCurrentPosition();
            //计算当前对应的歌词行。
            for (int i = mLrc.length - 1; i >= 0; i--) {
                if (mPosition > mLrcTime[i]) {
                    mCount = i;
                    break;
                }
            }
            //计算当前行进行的百分比
            if (mCount == mLrc.length) {
                //如果已经到了最后一行,表示播放完毕。
                mPercent = 100;
            } else {
                mPercent = ((float) (mPosition - mLrcTime[mCount]) * 100) / (mLrcTime[mCount + 1] - mLrcTime[mCount]);
            }
        }
    }

    /**
     * 根据最长一行歌词的字符数来确认歌词的字体大小。
     * @param canvas 显示歌词的画布
     */
    private void setTextSize(Canvas canvas) {
        if (mTextSize == 0) {
            float textWidth;
            mTextSize = MAXTEXTSIZE;
            //逐步缩小字体让最长一行的歌词也能显示完整。
            do {
                mPaint.setTextSize(mTextSize);
                textWidth = mPaint.measureText(mLrc[mMaxLrcLine]);
                if (textWidth > canvas.getWidth()-mTextSize && mTextSize > MINTEXTSIZE)
                    mTextSize -= 0.5f;
                else
                    break;
            } while (true);
            System.out.println("字体大小:" + mTextSize);
            //调整行高。
            mLineHeight = canvas.getHeight()/13;
            System.out.println("行高:" + mLineHeight);
        }
    }

    /**
     * 开始显示歌词
     * @param lrcSource 歌词文件
     * @param mediaPlayer  播放器
     */
    public void start(int lrcSource, MediaPlayer mediaPlayer) {
        this.mMediaPlayer = mediaPlayer;

        //加载和分解歌词
        InputStream inputStream = getResources().openRawResource(lrcSource);
        BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);
        StringBuilder builder = new StringBuilder("");
        byte buffer[] = new byte[1024];
        try {
            int count;
            while ((count = bufferedInputStream.read(buffer)) != -1) {
                builder.append(new String(buffer, 0, count));
            }
            mLrc = builder.toString().split("\n");
            //歌词时间索引数组比歌词数组长度要大1.用来标识最后一行歌词时间(这里默认5秒)
            mLrcTime = new int[mLrc.length + 1];
            //去掉第一行初始的一个字符 百度歌词下载下来,第一行的第一个字节未特殊字符。
            while (mLrc[0].charAt(0) != '[')
                mLrc[0] = mLrc[0].substring(1);
            int mMaxLrcLineChar = 0;
            mMaxLrcLine = 0;
            for (int i = 0; i < mLrc.length; i++) {
                String item[] = mLrc[i].split("]");
                mLrcTime[i] = 0;
                //本行的开始时间
                mLrcTime[i] += Integer.parseInt(item[0].substring(1, 3)) * 60 * 1000;
                mLrcTime[i] += Integer.parseInt(item[0].substring(4, 6)) * 1000;
                mLrcTime[i] += Integer.parseInt(item[0].substring(7, 9)) * 10;
                //本行显示的歌词
                if (item.length == 2)
                    mLrc[i] = item[1];
                else
                    mLrc[i] = "";
                if (mMaxLrcLineChar < mLrc[i].length()) {
                    mMaxLrcLineChar = mLrc[i].length();
                    mMaxLrcLine = i;
                }
            }
            //最后一行歌词默认耗时5秒
            mLrcTime[mLrc.length] = mLrcTime[mLrc.length - 1] + 5000;

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                bufferedInputStream.close();
                inputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            mTextSize = 0;//置0让onDraw重新计算字体大小和行高。
            invalidate();//触发onDraw 开始刷新歌词
        }
    }

    /**
     * 停止显示歌词
     */
    public void stop() {
        mMediaPlayer = null;
        invalidate();
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值