关闭 x
IT技术网
    技 采 号
    ITJS.cn - 技术改变世界
    • 实用工具
    • 菜鸟教程
    IT采购网 中国存储网 科技号 CIO智库

    IT技术网

    IT采购网
    • 首页
    • 行业资讯
    • 系统运维
      • 操作系统
        • Windows
        • Linux
        • Mac OS
      • 数据库
        • MySQL
        • Oracle
        • SQL Server
      • 网站建设
    • 人工智能
    • 半导体芯片
    • 笔记本电脑
    • 智能手机
    • 智能汽车
    • 编程语言
    IT技术网 - ITJS.CN
    首页 » 安卓开发 »Android 实现平滑滚动的歌词控件

    Android 实现平滑滚动的歌词控件

    2015-11-15 00:00:00 出处:小鄧子的简书
    分享

    马上毕业了,前段时间一直忙自己的毕业设计和毕业论文(蛋疼连着菊花疼),做的是一个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

    上一篇返回首页 下一篇

    声明: 此文观点不代表本站立场;转载务必保留本文链接;版权疑问请联系我们。

    别人在看

    正版 Windows 11产品密钥怎么查找/查看?

    还有3个月,微软将停止 Windows 10 的更新

    Windows 10 终止支持后,企业为何要立即升级?

    Windows 10 将于 2025年10 月终止技术支持,建议迁移到 Windows 11

    Windows 12 发布推迟,微软正全力筹备Windows 11 25H2更新

    Linux 退出 mail的命令是什么

    Linux 提醒 No space left on device,但我的空间看起来还有不少空余呢

    hiberfil.sys文件可以删除吗?了解该文件并手把手教你删除C盘的hiberfil.sys文件

    Window 10和 Windows 11哪个好?答案是:看你自己的需求

    盗版软件成公司里的“隐形炸弹”?老板们的“法务噩梦” 有救了!

    IT头条

    公安部:我国在售汽车搭载的“智驾”系统都不具备“自动驾驶”功能

    02:03

    液冷服务器概念股走强,博汇、润泽等液冷概念股票大涨

    01:17

    亚太地区的 AI 驱动型医疗保健:2025 年及以后的下一步是什么?

    16:30

    智能手机市场风云:iPhone领跑销量榜,华为缺席引争议

    15:43

    大数据算法和“老师傅”经验叠加 智慧化收储粮食尽显“科技范”

    15:17

    技术热点

    SQL汉字转换为拼音的函数

    windows 7系统无法运行Photoshop CS3的解决方法

    巧用MySQL加密函数对Web网站敏感数据进行保护

    MySQL基础知识简介

    Windows7和WinXP下如何实现不输密码自动登录系统的设置方法介绍

    windows 7系统ip地址冲突怎么办?windows 7系统IP地址冲突问题的

      友情链接:
    • IT采购网
    • 科技号
    • 中国存储网
    • 存储网
    • 半导体联盟
    • 医疗软件网
    • 软件中国
    • ITbrand
    • 采购中国
    • CIO智库
    • 考研题库
    • 法务网
    • AI工具网
    • 电子芯片网
    • 安全库
    • 隐私保护
    • 版权申明
    • 联系我们
    IT技术网 版权所有 © 2020-2025,京ICP备14047533号-20,Power by OK设计网

    在上方输入关键词后,回车键 开始搜索。Esc键 取消该搜索窗口。