Android實(shí)現(xiàn)音樂播放器歌詞顯示效果
這兩天有個(gè)任務(wù),說(shuō)是要寫一個(gè)QQ音樂播放器歌詞的那種效果,畢竟剛學(xué)自定義View,沒有什么思路,然后就Google.寫了一個(gè)歌詞效果,效果圖在后面,下面是我整理的代碼。
首先實(shí)現(xiàn)這種效果有兩種方式:
1.自定義View里重載onDraw方法,自己繪制歌詞
2.用ScrollView實(shí)現(xiàn)
第一種方式比較精確,但要支持滑動(dòng)之后跳轉(zhuǎn)播放的話難度很大,所以我選擇第二種,自定義ScrollView。
我也不多說(shuō),直接上代碼,代碼中有注釋。
一.自定義LycicView extends ScrollView
里面包括一個(gè)空白布局,高度是LycicView的一半,再是一個(gè)布局存放歌詞的,最后是一個(gè)空白布局高度是LycicView的一半。
這里動(dòng)態(tài)的向第二個(gè)布局里面添加了顯示歌詞的TextView,并利用ViewTreeObserver得到每個(gè)textview的高度,方便知道每個(gè)textview歌詞所要滑動(dòng)到的高度。
public class LycicView extends ScrollView {
LinearLayout rootView;//父布局
LinearLayout lycicList;//垂直布局
ArrayList<TextView> lyricItems = new ArrayList<TextView>();//每項(xiàng)的歌詞集合
ArrayList<String> lyricTextList = new ArrayList<String>();//每行歌詞文本集合,建議先去看看手機(jī)音樂里的歌詞格式和內(nèi)容
ArrayList<Long> lyricTimeList = new ArrayList<Long>();//每行歌詞所對(duì)應(yīng)的時(shí)間集合
ArrayList<Integer> lyricItemHeights;//每行歌詞TextView所要顯示的高度
int height;//控件高度
int width;//控件寬度
int prevSelected = 0;//前一個(gè)選擇的歌詞所在的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);
//創(chuàng)建視圖樹,會(huì)在onLayout執(zhí)行后立即得到正確的高度等參數(shù)
ViewTreeObserver vto = rootView.getViewTreeObserver();
vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
height = LycicView.this.getHeight();
width = LycicView.this.getWidth();
refreshRootView();
}
});
addView(rootView);//把布局加進(jìn)去
}
/**
*
*/
void refreshRootView(){
rootView.removeAllViews();//刷新,先把之前包含的所有的view清除
//創(chuàng)建兩個(gè)空白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);//加入一個(gè)歌詞顯示布局
rootView.addView(blank2,params);
}
}
/**
*設(shè)置歌詞,
*/
void refreshLyicList(){
if(lycicList == null){
lycicList = new LinearLayout(getContext());
lycicList.setOrientation(LinearLayout.VERTICAL);
//刷新,重新添加
lycicList.removeAllViews();
lyricItems.clear();
lyricItemHeights = new ArrayList<Integer>();
prevSelected = 0;
//為每行歌詞創(chuàng)建一個(gè)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);
//對(duì)高度進(jìn)行測(cè)量
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());//將高度添加到對(duì)應(yīng)的item位置
}
});
lycicList.addView(textView);
lyricItems.add(index,textView);
}
}
}
/**
* 滾動(dòng)到index位置
*/
public void scrollToIndex(int index){
if(index < 0){
scrollTo(0,0);
}
//計(jì)算index對(duì)應(yīng)的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);
}
}
/**
* 歌詞一直滑動(dòng),小于歌詞總長(zhǎng)度
* @param length
* @return
*/
int getIndex(int length){
int index = 0;
int sum = 0;
while(sum <= length){
sum+=lyricItemHeights.get(index);
index++;
}
//從1開始,所以得到的是總item,腳標(biāo)就得減一
return index - 1;
}
/**
* 設(shè)置選擇的index,選中的顏色
* @param index
*/
void setSelected(int index){
//如果和之前選的一樣就不變
if(index == prevSelected){
return;
}
for(int i = 0;i<lyricItems.size();i++){
//設(shè)置選中和沒選中的的顏色
if(i == index){
lyricItems.get(i).setTextColor(Color.BLUE);
}else{
lyricItems.get(i).setTextColor(Color.WHITE);
}
prevSelected = index;
}
}
/**
* 設(shè)置歌詞,并調(diào)用之前寫的refreshLyicList()方法設(shè)置view
* @param textList
* @param timeList
*/
public void setLyricText(ArrayList<String> textList,ArrayList<Long> timeList){
//因?yàn)槟銖母柙~lrc里面可以看出,每行歌詞前面都對(duì)應(yīng)有時(shí)間,所以兩者必須相等
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);
//滑動(dòng)時(shí),不往回彈,滑到哪就定位到哪
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>
具體實(shí)現(xiàn)代碼如下:
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+"======");
//滾動(dòng)handle不能放在這,因?yàn)椋@是滾動(dòng)監(jiān)聽事件,滾動(dòng)到下一次,handle又會(huì)發(fā)送一次消息,出現(xiàn)意想不到的效果
}
});
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://時(shí)間別消耗了
break;
}
return false;
}
});
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE|WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN);
}
}
其中utils類和LycicMusic是一個(gè)工具類和存放Music信息實(shí)體類
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實(shí)體類
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;
}
}
效果圖:

大體就這樣,如有無(wú)情糾正,附上源碼地址:點(diǎn)擊打開鏈接
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
談?wù)凙ndroid中的Divider是個(gè)什么東東
在Android應(yīng)用開發(fā)中會(huì)經(jīng)常碰到一個(gè)叫divider的東西,就是兩個(gè)View之間的分割線,本文主要給大家介紹android中的divider相關(guān)知識(shí),需要的朋友可以參考下2016-03-03
Android通過ImageView設(shè)置手指滑動(dòng)控件縮放
這篇文章主要介紹了Android通過ImageView設(shè)置手指滑動(dòng)控件縮放效果,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2017-12-12
Android實(shí)現(xiàn)從網(wǎng)絡(luò)獲取圖片顯示并保存到SD卡的方法
這篇文章主要介紹了Android實(shí)現(xiàn)從網(wǎng)絡(luò)獲取圖片顯示并保存到SD卡的方法,涉及Android操作多媒體文件及系統(tǒng)硬件設(shè)備的相關(guān)技巧,需要的朋友可以參考下2015-12-12
Android BottomNavigationView結(jié)合ViewPager實(shí)現(xiàn)底部導(dǎo)航欄步驟詳解
這篇文章主要介紹了Android BottomNavigationView結(jié)合ViewPager實(shí)現(xiàn)底部導(dǎo)航欄步驟,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)吧2023-02-02
Android自定義view之利用drawArc方法實(shí)現(xiàn)動(dòng)態(tài)效果(思路詳解)
這篇文章主要介紹了Android自定義view之利用drawArc方法實(shí)現(xiàn)動(dòng)態(tài)效果,drawArc方法包含了五個(gè)參數(shù),具體細(xì)節(jié)在本文中給大家提到過,需要的朋友可以參考下2021-08-08

