实现音乐播放器歌词显示效果

时间:2022-04-02 10:16:50

这两天有个任务,说是要写一个QQ音乐播放器歌词的那种效果,毕竟刚学自定义View,没有什么思路,然后就Google.写了一个歌词效果,效果图在后面,下面是我整理的代码。

首先实现这种效果有两种方式

    1.自定义View里重载onDraw方法,自己绘制歌词

    2.用ScrollView实现

   第一种方式比较精确,但要支持滑动之后跳转播放的话难度很大,所以我选择第二种,自定义ScrollView

我也不多说,直接上代码,代码中有注释

 一.自定义LycicView extends ScrollView

   里面包括一个空白布局,高度是LycicView的一半,再是一个布局存放歌词的,最后是一个空白布局高度是LycicView的一半

  这里动态的向第二个布局里面添加了显示歌词的TextView,并利用ViewTreeObserver得到每个textview的高度,方便知道每个textview歌词所要滑动到的高度

public class LycicView extends ScrollView {
    LinearLayout rootView;//父布局
    LinearLayout lycicList;//垂直布局
    ArrayList<TextView> lyricItems = new ArrayList<TextView>();//每项的歌词集合

    ArrayList<String> lyricTextList = new ArrayList<String>();//每行歌词文本集合,建议先去看看手机音乐里的歌词格式和内容
    ArrayList<Long> lyricTimeList = new ArrayList<Long>();//每行歌词所对应的时间集合
    ArrayList<Integer> lyricItemHeights;//每行歌词TextView所要显示的高度

    int height;//控件高度
    int width;//控件宽度
    int prevSelected = 0;//前一个选择的歌词所在的item


    public LycicView(Context context) {
        super(context);
        init();
    }

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

    public LycicView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }
    private void init(){
        rootView = new LinearLayout(getContext());
        rootView.setOrientation(LinearLayout.VERTICAL);
        //创建视图树,会在onLayout执行后立即得到正确的高度等参数
        ViewTreeObserver vto = rootView.getViewTreeObserver();
        vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                height = LycicView.this.getHeight();
                width = LycicView.this.getWidth();

                refreshRootView();

            }
        });
        addView(rootView);//把布局加进去
    }

    /**
     *
     */
    void refreshRootView(){
        rootView.removeAllViews();//刷新,先把之前包含的所有的view清除
        //创建两个空白view
        LinearLayout blank1 = new LinearLayout(getContext());
        LinearLayout blank2 = new LinearLayout(getContext());
        //高度平分
        LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(width,height/2);
        rootView.addView(blank1,params);
        if(lycicList !=null){
            rootView.addView(lycicList);//加入一个歌词显示布局
            rootView.addView(blank2,params);
        }

    }

    /**
     *设置歌词,
     */
    void refreshLyicList(){
        if(lycicList == null){
            lycicList = new LinearLayout(getContext());
            lycicList.setOrientation(LinearLayout.VERTICAL);
            //刷新,重新添加
            lycicList.removeAllViews();
            lyricItems.clear();
            lyricItemHeights = new ArrayList<Integer>();
            prevSelected = 0;
            //为每行歌词创建一个TextView
            for(int i = 0;i<lyricTextList.size();i++){
                final TextView textView = new TextView(getContext());

                textView.setText(lyricTextList.get(i));
                //居中显示
                LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
                params.gravity = Gravity.CENTER_HORIZONTAL;
                textView.setLayoutParams(params);
                //对高度进行测量
                ViewTreeObserver vto = textView.getViewTreeObserver();
                final int index = i;
                vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
                    @Override
                    public void onGlobalLayout() {
                                textView.getViewTreeObserver().removeOnGlobalLayoutListener(this);//api 要在16以上 >=16
                                lyricItemHeights.add(index,textView.getHeight());//将高度添加到对应的item位置
                    }
                });
                lycicList.addView(textView);
                lyricItems.add(index,textView);
            }
        }
    }
    /**
     *     滚动到index位置
     */
    public void scrollToIndex(int index){
        if(index < 0){
            scrollTo(0,0);
        }
        //计算index对应的textview的高度
        if(index < lyricTextList.size()){
            int sum = 0;
            for(int i = 0;i<=index-1;i++){
                sum+=lyricItemHeights.get(i);
            }
            //加上index这行高度的一半
            sum+=lyricItemHeights.get(index)/2;
            scrollTo(0,sum);
        }
    }

    /**
     * 歌词一直滑动,小于歌词总长度
     * @param length
     * @return
     */

    int getIndex(int length){
        int index = 0;
        int sum = 0;
        while(sum <= length){
            sum+=lyricItemHeights.get(index);
            index++;
        }
        //从1开始,所以得到的是总item,脚标就得减一
        return index - 1;
    }

    /**
     * 设置选择的index,选中的颜色
     * @param index
     */
    void setSelected(int index){
        //如果和之前选的一样就不变
        if(index == prevSelected){
            return;
        }
        for(int i = 0;i<lyricItems.size();i++){
            //设置选中和没选中的的颜色
            if(i == index){
                lyricItems.get(i).setTextColor(Color.BLUE);
            }else{
                lyricItems.get(i).setTextColor(Color.WHITE);
            }
            prevSelected = index;
        }
    }

    /**
     * 设置歌词,并调用之前写的refreshLyicList()方法设置view
     * @param textList
     * @param timeList
     */
    public void setLyricText(ArrayList<String> textList,ArrayList<Long> timeList){
        //因为你从歌词lrc里面可以看出,每行歌词前面都对应有时间,所以两者必须相等
        if(textList.size() != timeList.size()){
             throw  new IllegalArgumentException();
        }
        this.lyricTextList = textList;
        this.lyricTimeList = timeList;

        refreshLyicList();
    }

    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);
        //滑动时,不往回弹,滑到哪就定位到哪
        setSelected(getIndex(t));
        if(listener != null){
            listener.onLyricScrollChange(getIndex(t),getIndex(oldt));
        }
    }
    OnLyricScrollChangeListener listener;
    public void setOnLyricScrollChangeListener(OnLyricScrollChangeListener l){
        this.listener = l;
    }

    /**
     * 向外部提供接口
     */
    public interface  OnLyricScrollChangeListener{
        void onLyricScrollChange(int index,int oldindex);
    }
}

 二..MainActivity中的布局

 

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@mipmap/img01"
    tools:context=".MainActivity">

    <EditText
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:inputType="number"
        android:ems="10"
        android:id="@+id/editText"
        android:layout_alignParentBottom="true"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true" />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="scroll to"
        android:id="@+id/button"
        android:layout_alignTop="@+id/editText"
        android:layout_alignParentRight="true"
        android:layout_alignParentEnd="true" />

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_alignParentTop="true"
        android:layout_centerHorizontal="true"
        android:layout_above="@+id/editText">

        <custom.LycicView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:id="@+id/view"
            android:layout_centerVertical="true"
            android:layout_centerHorizontal="true" />

        <View
            android:layout_width="match_parent"
            android:layout_height="2dp"
            android:background="@null"
            android:id="@+id/imageView"
            android:layout_centerVertical="true"
            android:layout_centerHorizontal="true" />
        <View
            android:layout_below="@id/imageView"
            android:layout_width="match_parent"
            android:layout_height="1dp"
            android:layout_marginTop="6dp"
            android:background="#999999"
            android:id="@+id/imageView2"
            android:layout_centerVertical="true"
            android:layout_centerHorizontal="true" />
    </RelativeLayout>
</RelativeLayout>

   具体实现代码如下:

public class MainActivity extends AppCompatActivity {

    LycicView view;
    EditText editText;
    Button btn;
    Handler handler = new Handler(new Handler.Callback() {
        @Override
        public boolean handleMessage(Message msg) {
            if(msg.what == 1){

                if(lrc_index == list.size()){
                    handler.removeMessages(1);
                }
                lrc_index++;

                System.out.println("******"+lrc_index+"*******");
                view.scrollToIndex(lrc_index);
                handler.sendEmptyMessageDelayed(1,4000);
            }
            return false;
        }
    });
    private ArrayList<LrcMusic> lrcs;
    private ArrayList<String> list;
    private ArrayList<Long> list1;
    private int lrc_index;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        initViews();

        initEvents();
    }
    private void initViews(){
        view = (LycicView) findViewById(R.id.view);
        editText = (EditText) findViewById(R.id.editText);
        btn = (Button) findViewById(R.id.button);
    }
    private void initEvents(){
        InputStream is = getResources().openRawResource(R.raw.eason_tenyears);

       // BufferedReader br = new BufferedReader(new InputStreamReader(is));
        list = new ArrayList<String>();
        list1 = new ArrayList<>();
        lrcs = Utils.redLrc(is);
        for(int i = 0; i< lrcs.size(); i++){
             list.add(lrcs.get(i).getLrc());
            System.out.println(lrcs.get(i).getLrc()+"=====");
            list1.add(0l);//lrcs.get(i).getTime()
        }
        view.setLyricText(list, list1);
        view.postDelayed(new Runnable() {
            @Override
            public void run() {
                view.scrollToIndex(0);
            }
        },1000);

        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String text = editText.getText().toString();
                int index = 0;
                index = Integer.parseInt(text);
                view.scrollToIndex(index);
            }
        });
        view.setOnLyricScrollChangeListener(new LycicView.OnLyricScrollChangeListener() {
            @Override
            public void onLyricScrollChange(final int index, int oldindex) {
                editText.setText(""+index);
                lrc_index = index;
                System.out.println("===="+index+"======");
                //滚动handle不能放在这,因为,这是滚动监听事件,滚动到下一次,handle又会发送一次消息,出现意想不到的效果
            }
        });
        handler.sendEmptyMessageDelayed(1,4000);
        view.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()){
                    case MotionEvent.ACTION_DOWN:
                        handler.removeCallbacksAndMessages(null);

                        System.out.println("取消了");
                        break;
                    case MotionEvent.ACTION_UP:
                        System.out.println("开始了");
                        handler.sendEmptyMessageDelayed(1,2000);
                        break;
                    case MotionEvent.ACTION_CANCEL://时间别消耗了
                        break;
                }
                return false;
            }
        });

        getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE|WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN);
    }

}

 

其中utils类和LycicMusic是一个工具类和存放Music信息实体类
   Utils类
public class Utils {
    public static ArrayList<LrcMusic> redLrc(InputStream in) {
        ArrayList<LrcMusic> alist = new ArrayList<LrcMusic>();
        //File f = new File(path.replace(".mp3", ".lrc"));
        try {
            //FileInputStream fs = new FileInputStream(f);
            InputStreamReader input = new InputStreamReader(in, "utf-8");
            BufferedReader br = new BufferedReader(input);
            String s = "";

            while ((s = br.readLine()) != null) {
                if (!TextUtils.isEmpty(s)) {
                    String lyLrc = s.replace("[", "");
                    String[] data_ly = lyLrc.split("]");
                    if (data_ly.length > 1) {
                        String time = data_ly[0];
                        String lrc = data_ly[1];
                        LrcMusic lrcMusic = new LrcMusic(lrcData(time), lrc);
                        alist.add(lrcMusic);
                    }
                }
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return alist;
    }
    public static int lrcData(String time) {
        time = time.replace(":", "#");
        time = time.replace(".", "#");

        String[] mTime = time.split("#");

        //[03:31.42]
        int mtime = Integer.parseInt(mTime[0]);
        int stime = Integer.parseInt(mTime[1]);
        int mitime = Integer.parseInt(mTime[2]);

        int ctime = (mtime*60+stime)*1000+mitime*10;

        return ctime;
    }
}

  LrcMusic实体类

  

public class LrcMusic {
    private int time;
    private String lrc;

    public LrcMusic() {
    }

    public LrcMusic(int time, String lrc) {
        this.time = time;
        this.lrc = lrc;
    }

    public int getTime() {
        return time;
    }

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

    public String getLrc() {
        return lrc;
    }

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

效果图:

实现音乐播放器歌词显示效果

大体就这样,如有无情纠正,附上源码地址: 点击打开链接