Android 歌词显示

一.概述

  项目中设计到歌词显示的问题,这一块之前没有涉及过,只是套用过一个开源的项目,效果还行,于是想到拿来稍作修改,以适应项目需求.

二.歌词控件

  先来看下这个自定义控件写的歌词控件吧:

public class LrcView extends View implements ILrcView {
    /**
     * 所有的歌词
     ***/
    private List<LrcRow> mLrcRows;
    /**
     * 无歌词数据的时候 显示的默认文字
     **/
    private static final String DEFAULT_TEXT = "*暂未获取到歌词*";
    /**
     * 默认文字的字体大小
     **/
    private static final float SIZE_FOR_DEFAULT_TEXT = CommonUtils.dip2px(MyApplication.getContext(), 28);

    /**
     * 画高亮歌词的画笔
     ***/
    private Paint mPaintForHighLightLrc;
    /**
     * 高亮歌词的默认字体大小
     ***/
    private static final float DEFAULT_SIZE_FOR_HIGHT_LIGHT_LRC = CommonUtils.dip2px(MyApplication.getContext(), 32);
    /**
     * 高亮歌词当前的字体大小
     ***/
    private float mCurSizeForHightLightLrc = DEFAULT_SIZE_FOR_HIGHT_LIGHT_LRC;
    /**
     * 高亮歌词的默认字体颜色
     **/
    private static final int DEFAULT_COLOR_FOR_HIGHT_LIGHT_LRC = 0xffffffff;

    /**
     * 高亮歌词当前的字体颜色
     **/
    private int mCurColorForHightLightLrc = DEFAULT_COLOR_FOR_HIGHT_LIGHT_LRC;

    /**
     * 画其他歌词的画笔
     ***/
    private Paint mPaintForOtherLrc;
    /**
     * 其他歌词的默认字体大小
     ***/
    private static final float DEFAULT_SIZE_FOR_OTHER_LRC = CommonUtils.dip2px(MyApplication.getContext(), 28);
    /**
     * 其他歌词当前的字体大小
     ***/
    private float mCurSizeForOtherLrc = DEFAULT_SIZE_FOR_OTHER_LRC;
    /**
     * 其他歌词的默认字体颜色
     **/
    private static final int DEFAULT_COLOR_FOR_OTHER_LRC = 0x66ffffff;
    /**
     * 其他歌词当前的字体颜色
     **/
    private int mCurColorForOtherLrc = DEFAULT_COLOR_FOR_OTHER_LRC;

    /**
     * 画时间线的画笔
     ***/
    private Paint mPaintForTimeLine;
    /***
     * 时间线的颜色
     **/
    private static final int COLOR_FOR_TIME_LINE = 0xff999999;
    /**
     * 时间文字大小
     **/
    private static final int SIZE_FOR_TIME = CommonUtils.dip2px(MyApplication.getContext(), 12);
    /**
     * 是否画时间线
     **/
    private boolean mIsDrawTimeLine = false;

    /**
     * 歌词间默认的行距
     **/
    private static final float DEFAULT_PADDING = CommonUtils.dip2px(MyApplication.getContext(), 17);
    /**
     * 歌词当前的行距
     **/
    private float mCurPadding = DEFAULT_PADDING;

    /**
     * 歌词的最大缩放比例
     **/
    public static final float MAX_SCALING_FACTOR = 1.5f;
    /**
     * 歌词的最小缩放比例
     **/
    public static final float MIN_SCALING_FACTOR = 0.5f;
    /**
     * 默认缩放比例
     **/
    private static final float DEFAULT_SCALING_FACTOR = 1.0f;
    /**
     * 歌词的当前缩放比例
     **/
    private float mCurScalingFactor = DEFAULT_SCALING_FACTOR;

    /**
     * 实现歌词竖直方向平滑滚动的辅助对象
     **/
    private Scroller mScroller;
    /***
     * 移动一句歌词的持续时间
     **/
    private static final int DURATION_FOR_LRC_SCROLL = 500;
    /***
     * 停止触摸时 如果View需要滚动 时的持续时间
     **/
    private static final int DURATION_FOR_ACTION_UP = 400;

    /**
     * 控制文字缩放的因子
     **/
    private float mCurFraction = 0;
    private int mTouchSlop;

    private Bitmap arrowBitmap;

    public LrcView(Context context) {
        super(context);

        init(context);
    }

    public LrcView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    /**
     * 初始化画笔等
     */
    @Override
    public void init(Context context) {
        mScroller = new Scroller(getContext());
        mPaintForHighLightLrc = new Paint();
//        mPaintForHighLightLrc.setShadowLayer(5,0,0, Color.parseColor("#66ffffff"));
        mPaintForHighLightLrc.setColor(mCurColorForHightLightLrc);
        mPaintForHighLightLrc.setTextSize(mCurSizeForHightLightLrc);
        mPaintForHighLightLrc.setAntiAlias(true);

        mPaintForOtherLrc = new Paint();
        mPaintForOtherLrc.setColor(mCurColorForOtherLrc);
        mPaintForOtherLrc.setTextSize(mCurSizeForOtherLrc);
        mPaintForOtherLrc.setAntiAlias(true);

        mPaintForTimeLine = new Paint();
        mPaintForTimeLine.setColor(COLOR_FOR_TIME_LINE);
        mPaintForTimeLine.setTextSize(SIZE_FOR_TIME);

        mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();

        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inDensity = 30;
        options.inTargetDensity = 30;
        arrowBitmap = BitmapFactory.decodeResource(context.getResources(), R.raw.lrc_arrow, options);
    }

    private int mTotleDrawRow;

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (mLrcRows == null || mLrcRows.size() == 0) {
            //画默认的显示文字
            mPaintForOtherLrc.setTextSize(SIZE_FOR_DEFAULT_TEXT);
            float textWidth = mPaintForOtherLrc.measureText(DEFAULT_TEXT);
            float textX = (getWidth() - textWidth) / 2;
            canvas.drawText(DEFAULT_TEXT, textX, getHeight() / 2, mPaintForOtherLrc);
            return;
        }
        if (mTotleDrawRow == 0) {
            //初始化将要绘制的歌词行数
            mTotleDrawRow = (int) (getHeight() / (mCurSizeForOtherLrc + mCurPadding)) + 4;
        }
        //因为不需要将所有歌词画出来
        int minRaw = mCurRow - (mTotleDrawRow - 1) / 2;
        int maxRaw = mCurRow + (mTotleDrawRow - 1) / 2;
        minRaw = Math.max(minRaw, 0); //处理上边界
        maxRaw = Math.min(maxRaw, mLrcRows.size() - 1); //处理下边界
        //实现渐变的最大歌词行数
        int count = Math.max(maxRaw - mCurRow, mCurRow - minRaw);
        if (count == 0) {
            return;
        }
        //两行歌词间字体颜色变化的透明度
        int alpha = (0xFF - 0x11) / count;
        //画出来的第一行歌词的y坐标
        float rowY = getHeight() / 2 + minRaw * (mCurSizeForOtherLrc + mCurPadding);
        for (int i = minRaw; i <= maxRaw; i++) {

            if (i == mCurRow) {//画高亮歌词
                //因为有缩放效果,所有需要动态设置歌词的字体大小
                float textSize = mCurSizeForOtherLrc + (mCurSizeForHightLightLrc - mCurSizeForOtherLrc) * mCurFraction;
                mPaintForHighLightLrc.setTextSize(textSize);

                String text = mLrcRows.get(i).getContent();//获取到高亮歌词
                float textWidth = mPaintForHighLightLrc.measureText(text);//用画笔测量歌词的宽度
                if (textWidth > getWidth()) {
                    //如果歌词宽度大于view的宽,则需要动态设置歌词的起始x坐标,以实现水平滚动
                    canvas.drawText(text, mCurTextXForHighLightLrc, rowY, mPaintForHighLightLrc);
                } else {
                    //如果歌词宽度小于view的宽,则让歌词居中显示
                    float textX = (getWidth() - textWidth) / 2;
                    canvas.drawText(text, textX, rowY, mPaintForHighLightLrc);
                }
            } else {
                if (i == mLastRow) {//画高亮歌词的上一句
                    //因为有缩放效果,所有需要动态设置歌词的字体大小
                    float textSize = mCurSizeForHightLightLrc - (mCurSizeForHightLightLrc - mCurSizeForOtherLrc) * mCurFraction;
                    mPaintForOtherLrc.setTextSize(textSize);
                } else {//画其他的歌词
                    mPaintForOtherLrc.setTextSize(mCurSizeForOtherLrc);
                }
                String text = mLrcRows.get(i).getContent();
                float textWidth = mPaintForOtherLrc.measureText(text);
                float textX = (getWidth() - textWidth) / 2;
                //如果计算出的textX为负数,将textX置为0(实现:如果歌词宽大于view宽,则居左显示,否则居中显示)
                textX = Math.max(textX, 0);
                //实现颜色渐变  从0xFFFFFFFF 逐渐变为 0x11FFFFFF(颜色还是白色,只是透明度变化)
                int curAlpha = 255 - (Math.abs(i - mCurRow) - 1) * alpha; //求出当前歌词颜色的透明度
                //mPaintForOtherLrc.setColor(0x1000000*curAlpha+0xffffff);
                canvas.drawText(text, textX, rowY, mPaintForOtherLrc);
            }
            //计算出下一行歌词绘制的y坐标
            rowY += mCurSizeForOtherLrc + mCurPadding;
        }

        //画时间线和时间
        if (mIsDrawTimeLine) {
            float y = getHeight() / 2 + getScrollY();
            float x = getWidth();
            canvas.drawBitmap(arrowBitmap, -20, y - 41, null);
            canvas.drawText(mLrcRows.get(mCurRow).getTimeStr().substring(0, 5), x - 105, y + 13, mPaintForTimeLine);
            canvas.drawLine(60, y, getWidth() - 110, y, mPaintForTimeLine);
        }

    }

    /**
     * 是否可拖动歌词
     **/
    private boolean canDrag = false;
    /**
     * 事件的第一次的y坐标
     **/
    private float firstY;
    /**
     * 事件的上一次的y坐标
     **/
    private float lastY;
    private float lastX;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                firstY = event.getRawY();
                lastX = event.getRawX();
                break;
            case MotionEvent.ACTION_MOVE:
                if (mLrcRows == null || mLrcRows.size() == 0) {
                    return false;
                }
                if (!canDrag) {
                    if (Math.abs(event.getRawY() - firstY) > mTouchSlop && Math.abs(event.getRawY() - firstY) > Math.abs(event.getRawX() - lastX)) {
                        canDrag = true;
                        mIsDrawTimeLine = true;
                        mScroller.forceFinished(true);
                        stopScrollLrc();
                        mCurFraction = 1;
                    }
                    lastY = event.getRawY();
                }

                if (canDrag) {
                    float offset = event.getRawY() - lastY;//偏移量
                    if (getScrollY() - offset < 0) {
                        if (offset > 0) {
                            offset = offset / 3;
                        }
                    } else if (getScrollY() - offset > mLrcRows.size() * (mCurSizeForOtherLrc + mCurPadding) - mCurPadding) {
                        if (offset < 0) {
                            offset = offset / 3;
                        }
                    }
                    scrollBy(getScrollX(), -(int) offset);
                    lastY = event.getRawY();
                    int currentRow = (int) (getScrollY() / (mCurSizeForOtherLrc + mCurPadding));
                    currentRow = Math.min(currentRow, mLrcRows.size() - 1);
                    currentRow = Math.max(currentRow, 0);
                    seekTo(mLrcRows.get(currentRow).getTime(), false, false);
                    return true;
                }
                lastY = event.getRawY();
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                if (!canDrag) {
                    if (onLrcClickListener != null) {
                        onLrcClickListener.onClick();
                    }
                } else {
                    if (onSeekToListener != null && mCurRow != -1) {
                        onSeekToListener.onSeekTo(mLrcRows.get(mCurRow).getTime());
                    }
                    if (getScrollY() < 0) {
                        smoothScrollTo(0, DURATION_FOR_ACTION_UP);
                    } else if (getScrollY() > mLrcRows.size() * (mCurSizeForOtherLrc + mCurPadding) - mCurPadding) {
                        smoothScrollTo((int) (mLrcRows.size() * (mCurSizeForOtherLrc + mCurPadding) - mCurPadding), DURATION_FOR_ACTION_UP);
                    }

                    canDrag = false;
                    mIsDrawTimeLine = false;
                    invalidate();
                }
                break;
        }
        return true;
    }

    /**
     * 为LrcView设置歌词List集合数据
     */
    @Override
    public void setLrcRows(List<LrcRow> lrcRows) {
        reset();
        this.mLrcRows = lrcRows;
        invalidate();
    }

    /**
     * 当前高亮歌词的行号
     **/
    private int mCurRow = -1;
    /**
     * 上一次的高亮歌词的行号
     **/
    private int mLastRow = -1;

    @Override
    public void seekTo(int progress, boolean fromSeekBar, boolean fromSeekBarByUser) {
        if (mLrcRows == null || mLrcRows.size() == 0) {
            return;
        }
        //如果是由seekbar的进度改变触发 并且这时候处于拖动状态,则返回
        if (fromSeekBar && canDrag) {
            return;
        }
        for (int i = mLrcRows.size() - 1; i >= 0; i--) {

            if (progress >= mLrcRows.get(i).getTime()) {
                if (mCurRow != i) {
                    mLastRow = mCurRow;
                    mCurRow = i;
                    log("mCurRow=i=" + mCurRow);
                    if (fromSeekBarByUser) {
                        if (!mScroller.isFinished()) {
                            mScroller.forceFinished(true);
                        }
                        scrollTo(getScrollX(), (int) (mCurRow * (mCurSizeForOtherLrc + mCurPadding)));
                    } else {
                        smoothScrollTo((int) (mCurRow * (mCurSizeForOtherLrc + mCurPadding)), DURATION_FOR_LRC_SCROLL);
                    }
                    //如果高亮歌词的宽度大于View的宽,就需要开启属性动画,让它水平滚动
                    float textWidth = mPaintForHighLightLrc.measureText(mLrcRows.get(mCurRow).getContent());
                    log("textWidth=" + textWidth + "getWidth()=" + getWidth());
                    if (textWidth > getWidth()) {
                        if (fromSeekBarByUser) {
                            mScroller.forceFinished(true);
                        }
                        log("开始水平滚动歌词:" + mLrcRows.get(mCurRow).getContent());
                        startScrollLrc(getWidth() - textWidth, (long) (mLrcRows.get(mCurRow).getTotalTime() * 0.6));
                    }
                    invalidate();
                }
                break;
            }
        }

    }

    /**
     * 控制歌词水平滚动的属性动画
     ***/
    private ValueAnimator mAnimator;

    /**
     * 开始水平滚动歌词
     *
     * @param endX     歌词第一个字的最终的x坐标
     * @param duration 滚动的持续时间
     */

    private void startScrollLrc(float endX, long duration) {
        if (mAnimator == null) {
            mAnimator = ValueAnimator.ofFloat(0, endX);
            mAnimator.addUpdateListener(updateListener);
        } else {
            mCurTextXForHighLightLrc = 0;
            mAnimator.cancel();
            mAnimator.setFloatValues(0, endX);
        }
        mAnimator.setDuration(duration);
//        mAnimator.setStartDelay((long) (duration * 0.2)); //延迟执行属性动画
        mAnimator.start();
    }

    /**
     * 停止歌词的滚动
     */
    private void stopScrollLrc() {
        if (mAnimator != null) {
            mAnimator.cancel();
        }
        mCurTextXForHighLightLrc = 0;
    }

    /**
     * 高亮歌词当前的其实x轴绘制坐标
     **/
    private float mCurTextXForHighLightLrc;
    /***
     * 监听属性动画的数值值的改变
     */
    AnimatorUpdateListener updateListener = new AnimatorUpdateListener() {

        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            mCurTextXForHighLightLrc = (Float) animation.getAnimatedValue();
            log("mCurTextXForHighLightLrc=" + mCurTextXForHighLightLrc);
            invalidate();
        }
    };

    /**
     * 设置歌词的缩放比例
     */
    @Override
    public void setLrcScalingFactor(float scalingFactor) {
        mCurScalingFactor = scalingFactor;
        mCurSizeForHightLightLrc = DEFAULT_SIZE_FOR_HIGHT_LIGHT_LRC * mCurScalingFactor;
        mCurSizeForOtherLrc = DEFAULT_SIZE_FOR_OTHER_LRC * mCurScalingFactor;
        mCurPadding = DEFAULT_PADDING * mCurScalingFactor;
        mTotleDrawRow = (int) (getHeight() / (mCurSizeForOtherLrc + mCurPadding)) + 3;
        log("mRowTotal=" + mTotleDrawRow);
        scrollTo(getScrollX(), (int) (mCurRow * (mCurSizeForOtherLrc + mCurPadding)));
        invalidate();
        mScroller.forceFinished(true);
    }

    /**
     * 重置
     */
    @Override
    public void reset() {
        if (!mScroller.isFinished()) {
            mScroller.forceFinished(true);
        }
        mLrcRows = null;
        scrollTo(getScrollX(), 0);
        invalidate();
    }

    /**
     * 平滑的移动到某处
     *
     * @param dstY
     */
    private void smoothScrollTo(int dstY, int duration) {
        int oldScrollY = getScrollY();
        int offset = dstY - oldScrollY;
        mScroller.startScroll(getScrollX(), oldScrollY, getScrollX(), offset, duration);
        invalidate();
    }

    @Override
    public void computeScroll() {
        if (!mScroller.isFinished()) {
            if (mScroller.computeScrollOffset()) {
                int oldY = getScrollY();
                int y = mScroller.getCurrY();
                if (oldY != y && !canDrag) {
                    scrollTo(getScrollX(), y);
                }
                mCurFraction = mScroller.timePassed() * 3f / DURATION_FOR_LRC_SCROLL;
                mCurFraction = Math.min(mCurFraction, 1F);
                invalidate();
            }
        }
    }

    /**
     * 返回当前的歌词缩放比例
     *
     * @return
     */
    public float getmCurScalingFactor() {
        return mCurScalingFactor;
    }

    private OnSeekToListener onSeekToListener;

    public void setOnSeekToListener(OnSeekToListener onSeekToListener) {
        this.onSeekToListener = onSeekToListener;
    }

    public interface OnSeekToListener {
        void onSeekTo(int progress);
    }

    private OnLrcClickListener onLrcClickListener;

    public void setOnLrcClickListener(OnLrcClickListener onLrcClickListener) {
        this.onLrcClickListener = onLrcClickListener;
    }

    public interface OnLrcClickListener {
        void onClick();
    }

    public void log(Object o) {
        Log.d("LrcView", o + "");
    }
}
 * 在ViewGroup里面 scrollTo,scrollBy方法移动的是子View
 * 在View里面scrollTo,scrollBy方法移动的是View里面绘制的内容
 * 要点:
 * 1:歌词的上下平移用什么实现?
 * 用Scroller实现,Scroller只是一个工具而已,
 * 真正实现滚动效果的还是View的scrollTo方法
 * 2:歌词的水平滚动怎么实现?
 * 通过属性动画ValueAnimator控制高亮歌词绘制的x轴起始坐标

歌词与播放进度联动,只需要调用seekTo方法,原理是拿播放进度和歌词每一行前的时间作比较,只要播放进度超前,那么就滚动到歌词的相应时间上显示,理论上,只要歌词文本的时间是精确的,那么歌词就会随着播放进度一直滚动.

触摸滑动和点击事件都是在onTouchEvent中处理的,原理是记录手指按下和抬起这段时间内Y方向的偏移量,把这个偏移量与每行文字的高度作比较,看滚动到歌词的哪个部分,然后再把播放进度调到对应的时间位置,这样就实现了歌词进度与播放进度的完全绑定了.

当然,这些都是建立在歌词解析完成,并且获取到的前提之下,因此,歌词解析也是很重要的部分.

三.歌词解析

下载的歌词文件,看过的都知道是一种时间刻度+歌词字符串的形式;例如

这样的,既然如此,那么只需要按"["和"]"来截取字符串就可以了.来看下歌词解析类吧(歌词解析不少网友都分享过):

public class DefaultLrcParser implements ILrcParser {
    private static final DefaultLrcParser istance = new DefaultLrcParser();

    public static final DefaultLrcParser getIstance() {
        return istance;
    }

    private DefaultLrcParser() {
    }

    /***
     * 将歌词文件里面的字符串 解析成一个List<LrcRow>
     */
    @Override
    public List<LrcRow> getLrcRows(String str) {

        if (TextUtils.isEmpty(str)) {
            return null;
        }
        BufferedReader br = new BufferedReader(new StringReader(str));

        List<LrcRow> lrcRows = new ArrayList<>();
        String lrcLine;
        try {
            while ((lrcLine = br.readLine()) != null) {
                List<LrcRow> rows = LrcRow.createRows(lrcLine);
                if (rows != null && rows.size() > 0) {
                    lrcRows.addAll(rows);
                }
            }
            Collections.sort(lrcRows);
            int len = lrcRows.size();
            for (int i = 0; i < len - 1; i++) {
                lrcRows.get(i).setTotalTime(lrcRows.get(i + 1).getTime() - lrcRows.get(i).getTime());
            }
            lrcRows.get(len - 1).setTotalTime(5000);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        } finally {
            if (br != null) {
                try {
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        return lrcRows;
    }

歌词的实体类:

/**
 * 每行歌词的实体类,实现了Comparable接口,方便List<LrcRow>的sort排序
 *
 * @author Ligang  2014/8/19
 */
public class LrcRow implements Comparable<LrcRow> {

    /**
     * 开始时间 为00:10:00
     ***/
    private String timeStr;
    /**
     * 开始时间 毫米数  00:10:00  为10000
     **/
    private int time;
    /**
     * 歌词内容
     **/
    private String content;
    /**
     * 该行歌词显示的总时间
     **/
    private int totalTime;

    public long getTotalTime() {
        return totalTime;
    }

    public void setTotalTime(int totalTime) {
        this.totalTime = totalTime;
    }

    public String getTimeStr() {
        return timeStr;
    }

    public void setTimeStr(String timeStr) {
        this.timeStr = timeStr;
    }

    public int getTime() {
        return time;
    }

    public void setTime(int time) {
        this.time = time;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public LrcRow() {
        super();
    }

    public LrcRow(String timeStr, int time, String content) {
        super();
        this.timeStr = timeStr;
        this.time = time;
        this.content = content;
    }

    /**
     * 将歌词文件中的某一行 解析成一个List<LrcRow>
     * 因为一行中可能包含了多个LrcRow对象
     * 比如  [03:33.02][00:36.37]当鸽子不再象征和平  ,就包含了2个对象
     *
     * @param lrcLine
     * @return
     */
    public static final List<LrcRow> createRows(String lrcLine) {
        if (!lrcLine.startsWith("[")) {
            return null;
        }
        //最后一个"]"
        int lastIndexOfRightBracket = lrcLine.lastIndexOf("]");
        //歌词内容
        String content = lrcLine.substring(lastIndexOfRightBracket + 1, lrcLine.length());
        //截取出歌词时间,并将"[" 和"]" 替换为"-"   [offset:0]
       Log.e("歌词","lrcLine=" + lrcLine);
        // -03:33.02--00:36.37-
        String times = lrcLine.substring(0, lastIndexOfRightBracket + 1).replace("[", "-").replace("]", "-");
        String[] timesArray = times.split("-");
        List<LrcRow> lrcRows = new ArrayList<LrcRow>();
        for (String tem : timesArray) {
            if (TextUtils.isEmpty(tem.trim())) {
                continue;
            }
            //
            try {
                LrcRow lrcRow = new LrcRow(tem, formatTime(tem), content);
                lrcRows.add(lrcRow);
            } catch (Exception e) {
                Log.w("LrcRow", e.getMessage());
            }
        }
        return lrcRows;
    }

    /****
     * 把歌词时间转换为毫秒值  如 将00:10.00  转为10000
     *
     * @param timeStr
     * @return
     */
    private static int formatTime(String timeStr) {
        timeStr = timeStr.replace(‘.‘, ‘:‘);
        String[] times = timeStr.split(":");

        return Integer.parseInt(times[0]) * 60 * 1000
                + Integer.parseInt(times[1]) * 1000
                + Integer.parseInt(times[2]);
    }

    @Override
    public int compareTo(LrcRow anotherLrcRow) {
        return (int) (this.time - anotherLrcRow.time);
    }

    @Override
    public String toString() {
        return "LrcRow [timeStr=" + timeStr + ", time=" + time + ", content="
                + content + "]";
    }

}

解析成LrcRow后按照时间顺序排列,得到的集合作为一篇歌词的解析结果.

四.歌词显示

歌词显示的逻辑也有要注意的地方,下面画了个简图

   private class RequestLrc implements Runnable {

        private TrackEntity musicInfo;
        private boolean stop;

        RequestLrc(TrackEntity info) {
            this.musicInfo = info;
        }

        public void stop() {
            stop = true;
        }

        @Override
        public void run() {
            String url;
            if (musicInfo == null || musicInfo.getTrackId() == 0) {
                return;
            }
            String action = MUSIC_DETAIL + musicInfo.getTrackId();
            AscHttpHelper helper = new AscHttpHelper(getContext());
            url = helper.wrapUrl(action, null);
            Log.i(TAG, "请求url" + url);
            String resposeString = HttpUtil.getResposeString(url);
            Log.i(TAG, "请求结果" + resposeString);
            Gson gson = new Gson();
            SongDetailBean mDetailSong = gson.fromJson(resposeString, SongDetailBean.class);
            if (mDetailSong == null || mDetailSong.data == null || mDetailSong.data.musicInfoList == null) {
                return;
            }
            if (!stop) {
                File file = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + LRC_PATH + musicInfo.getTrackId());
                String lrc;
                try {
                    lrc = HttpUtil.getResposeString(mDetailSong.data.musicInfoList.get(0).lyricUrl);
                    if (!TextUtils.isEmpty(lrc)) {
                        if (!file.exists()) {
                            file.createNewFile();
                        }
                        writeToFile(file, lrc);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                    mTryGetLrc.post(new Runnable() {
                        @Override
                        public void run() {
                            mTryGetLrc.setVisibility(View.VISIBLE);
                            mLrcView.reset();
                        }
                    });
                }
            }

        }
    }

    private synchronized void writeToFile(File file, String lrc) {
        FileOutputStream outputStream = null;
        try {
            outputStream = new FileOutputStream(file);
            outputStream.write(lrc.getBytes());
        } catch (Exception e) {
            e.printStackTrace();
            mTryGetLrc.setVisibility(View.VISIBLE);
            mLrcView.reset();
        } finally {
            if (outputStream != null) {
                try {
                    outputStream.flush();
                    outputStream.close();
                    final List<LrcRow> list = getLrcRows();
                    mTryGetLrc.post(new Runnable() {
                        @Override
                        public void run() {
                            if (list != null && list.size() > 0) {
                                mTryGetLrc.setVisibility(View.INVISIBLE);
                                mLrcView.setLrcRows(list);
                            }
                        }
                    });
                } catch (IOException e) {
                    e.printStackTrace();
                    mTryGetLrc.setVisibility(View.VISIBLE);
                    mLrcView.reset();
                }
            }
        }
    }

五.体会与总结

千里之行,始于足下;任何看起来很棒的效果都是一行一行基础代码在发挥作用,他们相互关联,却又相互独立.

这两天来看,上面的歌词显示逻辑还是存在一些问题的,比如切换到下一首的时候,偶现显示了上一首歌曲的歌词.好的,我去修bug了.

时间: 2024-11-15 09:46:58

Android 歌词显示的相关文章

Android开发本地及网络Mp3音乐播放器(十六)歌词显示及滚动事件实现、ViewPager使用

实现功能: 歌词显示及滚动事件实现 ViewPager使用 后续将博文,将实现已下载音乐扫描功能和已存在歌曲歌词下载. 因为,没有自己的服务器,所以网络音乐所有相关功能(包含搜索音乐.下载音乐.下载歌词)均无法保证时效性,建议,尽快下载和练习:如果你下载时候,已经因为我采集的服务器更改规则,请给我留言,如果可以解决,我将在有空的时候献上新的源码. 截止到目前的源码下载: http://download.csdn.net/album/detail/3105 (最新的,请下载最后一个,本博文对应版本

浅谈动感歌词-歌词显示篇

1引言 经过分析篇.生成篇和解析篇之后,相信大家对动感歌词都已经不再陌生了,现在最重要的就是,动感歌词怎样显示的问题,这里就不再介绍java swing上面怎样显示了,因为在生成篇,已经做了一些简单的介绍,这一篇着重说一下动感歌词在android上面怎样显示. 2显示 关于歌词的平滑滚动,之前一直都是用android Scroller来滚动,发现在歌词滑动快进方面,一直都实现不了,能力有限.幸好,发现了一个帖子,这个帖子真是帮了大忙,这里先贴一下,他的博客,我强烈推荐大家看一下他的博客,他说得比

Android Studio显示行数

Android Studio在打开的文件左侧单击鼠标右键,也能像Eclipse一样设置显示代码行数,如图1.但是这边跟Eclipse有一个很大的区别,Eclipse设置后,其余的对应文件也跟着生效,即使文件关闭后重新打开行数也还是会显示,但是在Android Studio中这种设置只是针对本文件,而且也只是暂时的,文件关闭重新打开后行数又不显示了,感觉这功能很鸡肋啊. 那如何设置才能使行数永久显示呢,见图2. 图1: 图2: Android Studio显示行数,布布扣,bubuko.com

html5图片上传时IOS和Android均显示摄像头拍照和图片选择

最近在做信开发时,发现<input type="file" />在IOS中可以拍照或从照片图库选择,而Android系统则显示资源管理器,无拍照选项,网上查找资料,改为<input type="file" capture="camera">后,Android可显示相机和文档,但IOS则只有拍照选项了,最后通过判断设备类型使在IOS和Android下均可以显示拍照和图库选择,代码如下: var u = navigator.u

Android PopupWindow显示位置和显示大小

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:bac

Android Studio显示主题/样式设置

估计很多刚开始用Android Studio的DEV,都有经常看到网上关于Android Studio的贴图是灰色样式的,但是为啥自己刚安装的就是白色样式的呢. 这个其实只要改下显示主题就可以了. 如下图,选择Darcula就可以了,IntelliJ是默认风格,Windows这个风格其实颜色和IntelliJ是差不多的: Android Studio显示主题/样式设置,布布扣,bubuko.com

android竖向显示新特性界面

腾讯手机管家,初始界面有个小飞机动啊动啊,还挺好玩的,而且显示新特征为竖向展示,不知道这种东西该如何实现呢?给自己留下比较深的印象,然后楼主就是探索这种是如何实现的. 看着很不错,显示特征为竖向,增加小火箭的动态感,兼具金秀贤的帅气,简单.明确.有特点. 我得目的: 1.实现显示新特征的竖向. 2.增加动态箭头的动感. 3.颜色采用小清新 一个自定义的ViewPager可以搞定,引用自JakeWharton的一个开源项目:点击打开链接,同时借鉴了weidi1989的Android之仿网易V3.5

XE7 Android 文字显示有「锯齿」效果

说明:让 Android 文字显示有「锯齿」效果. 适用:Delphi XE7 修改:找出 FMX.FontGlyphs.Android.pas 档案,并复制到自己的 Project 路径里,找到 TAndroidFontGlyphManager.LoadResource 函数,修改成如下: 找到:FPaint.setAntiAlias(True); 改成:FPaint.setAntiAlias(False); 注意:此方法需要修改到 Delphi 源码,需自行承担可能的风险,如:与第三方控件不

(Android review)显示意图激活与隐式意图激活

一.基本知识点 1.<activity android:label="第一个activity" android:name=".Main2Activity"/> label属性:某个Acivity的标题 2.R文件不要引错了,引成Android底层的了 3.intent.setClass(this, Main2Activity.class);第一个参数:上下文第二个参数:要激活的组件的字节码文件 4.显示意图激活(明确指定了要激活的组件)1)intent.