`
superonion
  • 浏览: 128168 次
  • 性别: Icon_minigender_1
  • 来自: 武汉
社区版块
存档分类
最新评论

Android 歌词同步滚动效果

阅读更多

歌词是播放器类App必不可少的组件,而一般的歌词组件都需要做到歌词的显示与播放进度同步。我们知道,歌词是如下所示的文件:

 

lrc 
[ti:原来爱情这么伤]
[ar:梁咏琪]
[al:给自己的情歌]


[00:00.55]梁咏琪 - 原来爱情这么伤
[00:05.43]作词:彭学斌
[00:06.68]作曲:彭学斌
[00:09.63]
[00:22.27]我睁开眼睛 却感觉不到天亮
[00:29.74]东西吃一半 莫名其妙哭一场
[00:37.06]我忍住不想 时间变得更漫长
[00:44.09]也与你有关 否则又开始胡思乱想
[00:53.81]我日月无光 忙得不知所以然
[00:59.96]找朋友交谈 其实全帮不上忙
[01:07.49]以为会习惯 有你在才是习惯
[01:14.62]你曾住在我心上 现在空了一个地方
[01:21.89]原来爱情这么伤 比想象中还难
[01:29.90]泪水总是不听话 幸福躲起来不声不响
[01:37.43]太多道理太牵强 道理全是一样
[01:44.34]说的时候很简单 爱上后却正巧打乱
[02:00.00]我日月无光 忙得不知所以然
[02:07.41]找朋友交谈 其实全帮不上忙
[02:15.07]以为会习惯 有你在才是习惯
[02:21.88]你曾住在我心上 现在空了一个地方
[02:29.38]原来爱情这么伤 比想象中还难
[02:36.60]泪水总是不听话 幸福躲起来不声不响
[02:44.22]太多道理太牵强 道理全是一样
[02:50.78]说的时候很简单 爱上后却正巧打乱
[03:00.32]只想变的坚强 强到能够去忘
[03:07.29]无所谓悲伤 只要学会抵抗
[03:14.19]原来爱情这么伤
[03:20.78]原来爱情是这样 这样峰回路转
[03:28.12]泪水明明流不干 瞎了眼还要再爱一趟
[03:35.83]有一天终于打完 思念的一场战
[03:43.45]回过头再看一看 原来爱情那么伤
[03:54.76]下次还会不会这样
[88:88.88]

 

我们需要读取以上歌词文件的每一行转换成成一个个歌词实体:

 

package com.music.lyricsync;

public class LyricObject {
	public int begintime; // 开始时间
	public int endtime; // 结束时间
	public int timeline; // 单句歌词用时
	public String lrc; // 单句歌词
}

 

可根据当前播放器的播放进度与每句歌词的开始时间,得到当前屏幕中央高亮显示的那句歌词。在UI线程中另起线程,通过回调函数 onDraw() 每隔100ms重新绘制屏幕,实现歌词平滑滚动的动画效果。MainActivity代码如下:

 

package com.music.lyricsync;

import java.io.IOException;
import android.app.Activity;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.SeekBar;
import android.widget.SeekBar.OnSeekBarChangeListener;

public class MainActivity extends Activity {
	/** Called when the activity is first created. */
	private LyricView lyricView;
	private MediaPlayer mediaPlayer;
	private Button button;
	private SeekBar seekBar;
	private String mp3Path;
	private int INTERVAL=45;//歌词每行的间隔

	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		// this.requestWindowFeature(Window.FEATURE_NO_TITLE);
		// getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
		setContentView(R.layout.main);

		mp3Path = Environment.getExternalStorageDirectory().getAbsolutePath() + "/LyricSync/1.mp3";

		lyricView = (LyricView) findViewById(R.id.mylrc);
		mediaPlayer = new MediaPlayer();
		// this.requestWindowFeature(Window.FEATURE_NO_TITLE);

		ResetMusic(mp3Path);
		SerchLrc();
		lyricView.SetTextSize();

		button = (Button) findViewById(R.id.button);
		button.setText("播放");

		seekBar = (SeekBar) findViewById(R.id.seekbarmusic);
		seekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {

			@Override
			public void onStopTrackingTouch(SeekBar seekBar) {
				// TODO Auto-generated method stub

			}

			@Override
			public void onStartTrackingTouch(SeekBar seekBar) {
				// TODO Auto-generated method stub

			}

			@Override
			public void onProgressChanged(SeekBar seekBar, int progress,
					boolean fromUser) {
				// TODO Auto-generated method stub
				if (fromUser) {
					mediaPlayer.seekTo(progress);
					lyricView.setOffsetY(220 - lyricView.SelectIndex(progress) 
							* (lyricView.getSIZEWORD() + INTERVAL-1));

				}
			}
		});

		button.setOnClickListener(new OnClickListener() {

			@Override
			public void onClick(View v) {
				// TODO Auto-generated method stub
				if (mediaPlayer.isPlaying()) {
					button.setText("播放");
					mediaPlayer.pause();
				} else {
					button.setText("暂停");
					mediaPlayer.start();
					lyricView.setOffsetY(220 - lyricView.SelectIndex(mediaPlayer.getCurrentPosition())
							* (lyricView.getSIZEWORD() + INTERVAL-1));

				}
			}
		});

		mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
			@Override
			public void onCompletion(MediaPlayer mp) {
				ResetMusic(mp3Path);
				lyricView.SetTextSize();
				lyricView.setOffsetY(200);
				mediaPlayer.start();
			}
		});
		seekBar.setMax(mediaPlayer.getDuration());
		new Thread(new runable()).start();
	}

	public void SerchLrc() {
		String lrc = mp3Path;
		lrc = lrc.substring(0, lrc.length() - 4).trim() + ".lrc".trim();
		LyricView.read(lrc);
		lyricView.SetTextSize();
		lyricView.setOffsetY(350);
	}

	public void ResetMusic(String path) {

		mediaPlayer.reset();
		try {

			mediaPlayer.setDataSource(mp3Path);
			mediaPlayer.prepare();
		} catch (IllegalArgumentException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IllegalStateException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	class runable implements Runnable {

		@Override
		public void run() {
			// TODO Auto-generated method stub
			while (true) {

				try {
					Thread.sleep(100);
					if (mediaPlayer.isPlaying()) {
						lyricView.setOffsetY(lyricView.getOffsetY() - lyricView.SpeedLrc());
						lyricView.SelectIndex(mediaPlayer.getCurrentPosition());
						seekBar.setProgress(mediaPlayer.getCurrentPosition());
						mHandler.post(mUpdateResults);
					}
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		}
	}

	Handler mHandler = new Handler();
	Runnable mUpdateResults = new Runnable() {
		public void run() {
			lyricView.invalidate(); // 更新视图
		}
	};
}

 

歌词View的代码如下:

 

package com.music.lyricsync;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Iterator;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;

public class LyricView extends View{
	
	private static TreeMap<Integer, LyricObject> lrc_map;
	private float mX;		//屏幕X轴的中点,此值固定,保持歌词在X中间显示
	private float offsetY;		//歌词在Y轴上的偏移量,此值会根据歌词的滚动变小
	private static boolean blLrc=false;
	private float touchY;	//当触摸歌词View时,保存为当前触点的Y轴坐标
	private float touchX;
	private boolean blScrollView=false;
	private int lrcIndex=0;	//保存歌词TreeMap的下标
	private  int SIZEWORD=0;//显示歌词文字的大小值
	private  int INTERVAL=45;//歌词每行的间隔
	Paint paint=new Paint();//画笔,用于画不是高亮的歌词
	Paint paintHL=new Paint();	//画笔,用于画高亮的歌词,即当前唱到这句歌词
	
	public LyricView(Context context){
		super(context);
		init();
	}
	
	public LyricView(Context context, AttributeSet attrs) {
		super(context, attrs);
		init();
	}
	
	/* (non-Javadoc)
	 * @see android.view.View#onDraw(android.graphics.Canvas)
	 */
	@Override
	protected void onDraw(Canvas canvas) {
		if(blLrc){
			paintHL.setTextSize(SIZEWORD);
			paint.setTextSize(SIZEWORD);
			LyricObject temp=lrc_map.get(lrcIndex);
			canvas.drawText(temp.lrc, mX, offsetY+(SIZEWORD+INTERVAL)*lrcIndex, paintHL);
			// 画当前歌词之前的歌词
			for(int i=lrcIndex-1;i>=0;i--){
				temp=lrc_map.get(i);
				if(offsetY+(SIZEWORD+INTERVAL)*i<0){
					break;
				}
				canvas.drawText(temp.lrc, mX, offsetY+(SIZEWORD+INTERVAL)*i, paint);
			}
			// 画当前歌词之后的歌词
			for(int i=lrcIndex+1;i<lrc_map.size();i++){
				temp=lrc_map.get(i);
				if(offsetY+(SIZEWORD+INTERVAL)*i>600){
					break;
				}
				canvas.drawText(temp.lrc, mX, offsetY+(SIZEWORD+INTERVAL)*i, paint);
			}
		}
		else{
			paint.setTextSize(25);
			canvas.drawText("找不到歌词", mX, 310, paint);
		}
		super.onDraw(canvas);
	}

	/* (non-Javadoc)
	 * @see android.view.View#onTouchEvent(android.view.MotionEvent)
	 */
	@Override
	public boolean onTouchEvent(MotionEvent event) {
		// TODO Auto-generated method stub
		System.out.println("bllll==="+blScrollView);
		float tt=event.getY();
		if(!blLrc){
			//return super.onTouchEvent(event);

			return super.onTouchEvent(event);
		}
		switch(event.getAction()){
		case MotionEvent.ACTION_DOWN:
			touchX=event.getX();
			break;
		case MotionEvent.ACTION_MOVE:
			touchY=tt-touchY;			
			offsetY=offsetY+touchY;
			break;
		case MotionEvent.ACTION_UP:
			blScrollView=false;
			break;		
		}
		touchY=tt;
		return true;
	}

	public void init(){
		lrc_map = new TreeMap<Integer, LyricObject>();
		offsetY=320;	
		
		paint=new Paint();
		paint.setTextAlign(Paint.Align.CENTER);
		paint.setColor(Color.GREEN);
		paint.setAntiAlias(true);
		paint.setDither(true);
		paint.setAlpha(180);
		
		
		paintHL=new Paint();
		paintHL.setTextAlign(Paint.Align.CENTER);
		
		paintHL.setColor(Color.RED);
		paintHL.setAntiAlias(true);
		paintHL.setAlpha(255);
	}
	
	/**
	 * 根据歌词里面最长的那句来确定歌词字体的大小
	 */
	
	public void SetTextSize(){
		if(!blLrc){
			return;
		}
		int max=lrc_map.get(0).lrc.length();
		for(int i=1;i<lrc_map.size();i++){
			LyricObject lrcStrLength=lrc_map.get(i);
			if(max<lrcStrLength.lrc.length()){
				max=lrcStrLength.lrc.length();
			}
		}
		SIZEWORD=320/max;
	
	}
	
	protected void onSizeChanged(int w, int h, int oldw, int oldh) {
		mX = w * 0.5f;
		super.onSizeChanged(w, h, oldw, oldh);
	}
	
	/**
	 *  歌词滚动的速度
	 * 
	 * @return 返回歌词滚动的速度
	 */
	public Float SpeedLrc(){
		float speed=0;
		if(offsetY+(SIZEWORD+INTERVAL)*lrcIndex>220){
			speed=((offsetY+(SIZEWORD+INTERVAL)*lrcIndex-220)/20);

		} else if(offsetY+(SIZEWORD+INTERVAL)*lrcIndex < 120){
			Log.i("speed", "speed is too fast!!!");
			speed = 0;
		}
//		if(speed<0.2){
//			speed=0.2f;
//		}
		return speed;
	}
	
	/**
	 * 按当前的歌曲的播放时间,从歌词里面获得那一句
	 * @param time 当前歌曲的播放时间
	 * @return 返回当前歌词的索引值
	 */
	public int SelectIndex(int time){
		if(!blLrc){
			return 0;
		}
		int index=0;
		for(int i=0;i<lrc_map.size();i++){
			LyricObject temp=lrc_map.get(i);
			if(temp.begintime<time){
				++index;
			}
		}
		lrcIndex=index-1;
		if(lrcIndex<0){
			lrcIndex=0;
		}
		return lrcIndex;
	
	}
	
	/**
	 * 读取歌词文件
	 * @param file 歌词的路径
	 * 
	 */
	public static void read(String file) {
		TreeMap<Integer, LyricObject> lrc_read =new TreeMap<Integer, LyricObject>();
		String data = "";
		try {
		  File saveFile=new File(file);
		 // System.out.println("是否有歌词文件"+saveFile.isFile());
		  if(!saveFile.isFile()){
			  blLrc=false;
			  return;
		  }
		  blLrc=true;
		  
		  //System.out.println("bllrc==="+blLrc);
		  FileInputStream stream = new FileInputStream(saveFile);//  context.openFileInput(file);
		  
		  
		  BufferedReader br = new BufferedReader(new InputStreamReader(stream,"GB2312"));   
		  int i = 0;
		  Pattern pattern = Pattern.compile("\\d{2}");
		  while ((data = br.readLine()) != null) {   
			 // System.out.println("++++++++++++>>"+data);
			    data = data.replace("[","");//将前面的替换成后面的
			    data = data.replace("]","@");
			    String splitdata[] =data.split("@");//分隔
			    if(data.endsWith("@")){
			    	for(int k=0;k<splitdata.length;k++){
				    	String str=splitdata[k];
				    	
				    	str = str.replace(":",".");
				    	str = str.replace(".","@");
					    String timedata[] =str.split("@");
					    Matcher matcher = pattern.matcher(timedata[0]);
					    if(timedata.length==3 && matcher.matches()){
						    int m = Integer.parseInt(timedata[0]);  //分
						    int s = Integer.parseInt(timedata[1]);  //秒
						    int ms = Integer.parseInt(timedata[2]); //毫秒
					    	int currTime = (m*60+s)*1000+ms*10;
					    	LyricObject item1= new LyricObject();
							item1.begintime = currTime;
							item1.lrc       = "";
							lrc_read.put(currTime,item1);
					    }
			    	}
				    
			    }
			    else{
				    String lrcContenet = splitdata[splitdata.length-1]; 
			
				    for (int j=0;j<splitdata.length-1;j++)
				    {
					    String tmpstr = splitdata[j];
					    
					    tmpstr = tmpstr.replace(":",".");
					    tmpstr = tmpstr.replace(".","@");
					    String timedata[] =tmpstr.split("@");
					    Matcher matcher = pattern.matcher(timedata[0]);
					    if(timedata.length==3 && matcher.matches()){
						    int m = Integer.parseInt(timedata[0]);  //分
						    int s = Integer.parseInt(timedata[1]);  //秒
						    int ms = Integer.parseInt(timedata[2]); //毫秒
					    	int currTime = (m*60+s)*1000+ms*10;
					    	LyricObject item1= new LyricObject();
							item1.begintime = currTime;
							item1.lrc       = lrcContenet;
							lrc_read.put(currTime,item1);// 将currTime当标签  item1当数据 插入TreeMap里
							i++;
					    }
				    }
			    }
			    
		  } 
		 stream.close();
		}
		catch (FileNotFoundException e) {
		}
		catch (IOException e) {
		}
		
		/*
		 * 遍历hashmap 计算每句歌词所需要的时间
		*/
		lrc_map.clear();
		data ="";
		Iterator<Integer> iterator = lrc_read.keySet().iterator();
		LyricObject oldval  = null;
		int i =0;
		while(iterator.hasNext()) {
			Object ob =iterator.next();
			
			LyricObject val = (LyricObject)lrc_read.get(ob);
			
			if (oldval==null)
				oldval = val;
			else
			{
				LyricObject item1= new LyricObject();
				item1  = oldval;
				item1.timeline = val.begintime-oldval.begintime;
				lrc_map.put(new Integer(i), item1);
				i++;
				oldval = val;
			}
			if (!iterator.hasNext()) {
				lrc_map.put(new Integer(i), val);
			}
			
		}

	}	
	
	/**
	 * @return the blLrc
	 */
	public static boolean isBlLrc() {
		return blLrc;
	}

	/**
	 * @return the offsetY
	 */
	public float getOffsetY() {
		return offsetY;
	}

	/**
	 * @param offsetY the offsetY to set
	 */
	public void setOffsetY(float offsetY) {
		this.offsetY = offsetY;
	}

	/**
	 * @return 返回歌词文字的大小
	 */
	public int getSIZEWORD() {
		return SIZEWORD;
	}

	/**
	 * 设置歌词文字的大小
	 * @param sIZEWORD the sIZEWORD to set
	 */
	public void setSIZEWORD(int sIZEWORD) {
		SIZEWORD = sIZEWORD;
	}
}
 

xml布局文件如下:

 

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:background="#FFFFFF" >

    <com.music.lyricsync.LyricView
        android:id="@+id/mylrc"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:layout_marginBottom="50dip"
        android:layout_marginTop="50dip" />

    <LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:orientation="horizontal" >

        <Button
            android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

        <SeekBar
            android:id="@+id/seekbarmusic"
            android:layout_width="205px"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical"
            android:layout_marginBottom="5px"
            android:progress="0" />
    </LinearLayout>

</RelativeLayout>
 

程序运行后如下图所示:


 

 

 

运行程序前,先在SDCard根目录下新建LyricSync目录,将 歌曲和歌词.zip 中的 1.mp3 和 1.lrc 文件解压放入LyricSync目录下即可。

 

 

  • 大小: 58.9 KB
  • src.zip (179.2 KB)
  • 下载次数: 602
7
1
分享到:
评论
4 楼 qq_31941407 2015-12-24  
请问怎么设置才不是自动播放啊,为什么我一运行就是自动播放啊。。
3 楼 whl_1990 2015-06-05  
这个有点抖动,不知道怎么去抖动啊?将线程睡眠时间降低也不行,可能要改成SurfaceView
2 楼 翼扬gg 2015-05-16  
      
1 楼 blue飞扬 2013-09-12  
求教那个歌词速度和文字大小设定是怎么完成的啊?新手,有点看不懂

相关推荐

    Android应用源码歌词随歌曲同步滚动项目

    【Android应用源码歌词随歌曲同步滚动项目】是一个专注于音乐播放体验的开发项目,它允许歌词随着音乐的播放进度实时滚动,为用户带来更沉浸式的听歌体验。这个项目是基于Android平台,利用Java编程语言实现的,适用...

    android实现歌词自动滚动效果

    Android 实现歌词自动滚动效果 Android 实现歌词自动滚动效果是 Android 开发中的一项重要功能,特别是在音乐播放器应用程序中。通过滚动展示歌词,可以提高用户体验和娱乐性。在本文中,我们将详细介绍 Android ...

    Android歌词高亮滚动显示,类千千静听,天天动听的显示效果

    总之,实现Android歌词高亮滚动显示涉及到LRC文件解析、自定义View、动态设置文本样式和时间同步等多个技术点。通过巧妙地组合这些技术,我们可以创建出类似千千静听、天天动听的歌词显示效果,提升音乐播放应用的...

    android 歌词同步 DEMO

    在Android平台上,歌词同步是一项常见的功能,特别是在音乐播放应用中,它可以为用户提供更好的听歌体验。这个"android 歌词同步 DEMO"是用于展示如何在Android应用中实现实时歌词同步显示的一个示例项目。下面我们...

    Android播放器歌词同步

    3. 平滑过渡:为了用户体验,歌词切换不宜过于突兀,可以添加淡入淡出效果或平滑滚动。 **四、优化与扩展** 1. 缓存机制:对于频繁访问的歌词,可以缓存到内存或本地,减少I/O操作。 2. 动态加载:若LRC文件过大,...

    textview实现歌词向上滚动效果

    在Android开发中,TextView是...总的来说,实现TextView的歌词向上滚动效果涉及到Android动画系统、数据处理以及音乐播放器的交互等多个方面。理解这些知识点并结合实践,你就能为你的音乐应用增添一个吸引人的特性。

    安卓Android源码——卡拉OK,歌词同步程序.zip

    4. **动画效果**:为了提升用户体验,歌词同步还常常伴有滚动和高亮效果。这需要结合Android的动画框架,如ValueAnimator或者ObjectAnimator,来实现歌词的平滑滚动和当前行的突出显示。 5. **性能优化**:考虑到...

    关于android播放mp3与歌词同步问题收集

    在实际开发中,还需要考虑一些细节,比如用户调整播放速度时的歌词同步、歌词的滚动效果、以及在网络不佳时的错误处理和用户体验优化。这些都需要结合具体的应用场景和需求进行设计和实现。 总之,Android平台上...

    android歌词滚动显示,上下滑动跳转DEMO

    在Android开发中,实现歌词滚动显示以及通过手势上下滑动来改变歌曲播放进度是一个常见的需求,尤其是在音乐播放应用中。这个DEMO项目“android歌词滚动显示,上下滑动跳转DEMO”为我们提供了一个很好的实践案例。...

    Android应用源码歌词随歌曲同步滚动项目.zip

    在Android应用开发中,歌词随歌曲同步滚动是一个常见的功能,特别是在音乐播放器应用中。这个项目的核心在于实现歌词与音频播放进度的精确匹配,并通过视觉效果呈现歌词的滚动。下面我们将详细探讨实现这一功能涉及...

    Android 卡拉OK,歌词同步程序.zip

    歌词的滚动效果可以通过动画或者定时器更新TextView的内容来实现。此外,UI设计应考虑用户友好性,如字体大小、颜色、背景等。 5. **数据存储**:歌词文件可能需要存储在本地,可以使用SQLite数据库或者文件系统。...

    Android自定义歌词同步控件

    4. **刷新显示**:随着音乐播放进度的变化,定时更新歌词控件的状态,以实现歌词的滚动显示效果。 ##### 3.2 实现细节 - **实例化**:由于 `LyricView` 不能直接在 XML 文件中定义,所以需要在 Java 代码中动态创建...

    Android 卡拉OK,歌词同步程序.rar

    6. **动画效果**:为了增强用户体验,歌词通常会有滚动、淡入淡出等动画效果。可以使用Android的ViewPropertyAnimator、ValueAnimator或者ObjectAnimator实现这些效果。 7. **数据存储**:可能使用SQLite数据库或...

    歌词同步播放器源码

    4. **时间同步**:当音乐播放时,应用需要获取当前播放时间,并通过比较找到匹配的歌词时间戳,实现歌词与音乐的同步滚动。这可能需要监听`MediaPlayer`的`OnSeekCompleteListener`或`OnBufferingUpdateListener`...

    CoCoPlayer歌词同步效果

    CoCoPlayer 可以让初学者学习使用android播放音乐,实现歌词同步滚动显示效果,自定义view等用法,也包含了LRC解析相关代码,解析LRC使用的正则表达式~请多多支持~~CoCoPlayer 完整正在开发中,开发完成之后将把源码...

    Android 自定义ViewFlipper 滚动效果

    总之,自定义ViewFlipper滚动效果是一项挑战性的任务,需要掌握Android的视图布局、动画系统以及事件处理等多方面知识。通过不断实践和调试,你可以创造出与百度手机助手类似的独特滚动动画,提升应用的用户体验。...

    Android卡拉OK,歌词同步程序.zip

    2. 滚动效果,歌词随着歌曲的播放自动滚动。 3. 高亮当前正在唱的部分,让用户知道接下来的歌词。 4. 快进/快退功能,用户可以通过点击歌词跳转到相应位置。 5. 显示背景图片或视频,增强视觉体验。 四、音频处理与...

    Android歌词秀源代码-Android歌词工程文件-Android源码

    首先,"Android歌词秀"是一款能够实时同步显示歌曲歌词的应用,它需要处理歌词文件的解析、时间同步、以及与音乐播放器的接口集成。歌词文件通常为LRC格式,这是一种基于时间戳的文本文件,包含多行歌词,每行歌词都...

Global site tag (gtag.js) - Google Analytics