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

2019-07-02 IT学院 IT学院
浏览

这两天有个任务,说是要写一个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;
 }
}

效果图:

 

总结

以上所述是IT学院给大家介绍的Android实现音乐播放器歌词显示效果 ,希望对大家有所帮助,如果大家有任何疑问欢迎给我们留言,小编会及时回复大家的!