Android 基于RecyclerView實(shí)現(xiàn)的歌詞滾動(dòng)自定義控件
本文介紹了Android 基于RecyclerView實(shí)現(xiàn)的歌詞滾動(dòng)自定義控件,分享給大家,具體如下:
先來幾張效果圖:
這幾天打算做一個(gè)控件,來讓自己復(fù)習(xí)一下自定義 view 的知識(shí)以及事件分發(fā)機(jī)制的原理與應(yīng)用。對(duì)于這個(gè)控件,我已經(jīng)封裝好了,只要調(diào)用就可以了。
本來是想放上 gitHub 和 添加依賴的。但是提交 github 出了問題一直不會(huì)弄,所以就只能先等等了。((;′⌒`))
接下來說一下實(shí)現(xiàn)原理:
該控件分為以下幾個(gè)部分:
- 歌詞自動(dòng)滾動(dòng)
- 歌詞顏色字體變化
- 觸碰屏幕歌詞不滾動(dòng),高亮顯示,離開時(shí)自動(dòng)移動(dòng)到當(dāng)前歌詞位置
- 觸碰屏幕中間線條出現(xiàn)以及顯示該歌詞的時(shí)間
- 點(diǎn)擊歌詞跳轉(zhuǎn)到當(dāng)前位置并輸出當(dāng)時(shí)時(shí)間
- 可設(shè)置跳轉(zhuǎn)時(shí)間跳到相應(yīng)歌詞位置
接下來我一個(gè)一個(gè)大概講述一下思路。
1.對(duì)于滾動(dòng),我們可以調(diào)用 RecyclerView.smoothScrollBy() 方法,
相對(duì)于 ScrollBy() 方法,該方法能夠?qū)崿F(xiàn)平滑滑動(dòng)。
我設(shè)置了總共顯示九句歌詞。而且因?yàn)槲蚁朐诟柙~前面和后面留一些空白,這些看起來會(huì)好看些。所以,在歌詞列表里面我加多了一些空白。
List<String> wordList = new ArrayList<>(); wordList.add(""); wordList.add(""); wordList.add(""); wordList.add(""); wordList.addAll(mWordList); wordList.add(""); wordList.add(""); wordList.add(""); wordList.add("");
由于歌詞的滾自動(dòng)滾動(dòng)是根據(jù)歌詞時(shí)間來進(jìn)行移動(dòng)的。所以我們需要需要使用 Runable 來執(zhí)行滾動(dòng)操作。而且為了避免內(nèi)存泄漏。將 Runable 實(shí)現(xiàn)類修飾為 static 。所以歌詞列表索引位置有所變化。
private static class AutoPullWork implements Runnable { public AutoPullWork(AutoPullRecyclerView autoPullRecyclerView) { weakReference = new WeakReference<AutoPullRecyclerView>(autoPullRecyclerView); } @Override public void run() { autoPullRecyclerView.smoothScrollBy(0, autoPullRecyclerView.getMeasuredHeight() / 9); autoPullRecyclerView.postDelayed(autoPullRecyclerView.autoPullWork, autoPullRecyclerView.timeList.get(autoPullRecyclerView.currentWord - 4) - autoPullRecyclerView.timeList.get(autoPullRecyclerView.currentWord - 5)); ......
2.對(duì)于歌詞的高亮顯示,我們可以調(diào)用 notifyItemChange(int position) 方法,這個(gè)方法調(diào)用會(huì)重新去繪制特定 position 上的 viewHolder 。hightLightItem() 在這個(gè)方法中設(shè)置我們想要改變 viewHolder 的位置,并調(diào)用 notifyItemChange(int position) 。然后在 onBindViewHolder() 中的設(shè)置可以判斷當(dāng)前是否需要高亮顯示。
public void hightLightItem(int position){ mHighLightPosition = position; notifyItemChanged(position-1); notifyItemChanged(position); }
private boolean isHighLight(int position){ return mHighLightPosition == position; }
@Override public void onBindViewHolder(ViewHolder holder, int position) { String word = mWordList.get(position); holder.textView.setText(word); try { if (!isHighLight(position)) { holder.textView.setTextSize(mOrdinarySize); holder.textView.setTextColor(Color.parseColor(mOrdinaryColor)); } else if (isHighLight(position)) { holder.textView.setTextSize(mHighLightSize); holder.textView.setTextColor(Color.parseColor(mHighLightColor)); } }catch ( Exception e){ e.printStackTrace(); } }
3.對(duì)于歌詞自動(dòng)移動(dòng)到當(dāng)前語句:
本身我的想法就是多設(shè)置一個(gè)變量還是在這個(gè) Runable() 里面進(jìn)行操作。但是一個(gè)很嚴(yán)重的問題,導(dǎo)致我連續(xù)幾天一直想不到對(duì)策方法。由于手指離開屏幕的時(shí)候我使用 postDelayed() 方法有可能跟里面 Runable 里面使用的 postDelayed() 時(shí)間上可能會(huì)相互沖突,事件的執(zhí)行情況就很有可能變得跟你想不一樣。所以我們應(yīng)該重新寫一個(gè) Runable() 來控制它的自動(dòng)移動(dòng)到當(dāng)前位置。這樣子的話各做各的事情,在寫邏輯的時(shí)候會(huì)比較容易理順。(當(dāng)時(shí)沒想好害我調(diào)了好久,一直都不對(duì),哈哈).
/** * 歌詞自動(dòng)滑動(dòng)到特定位置任務(wù) */ private static class AutoBackWork implements Runnable{ @Override public void run() { } }
對(duì)于點(diǎn)擊屏幕時(shí)就重寫 onTouchEvent() 方法,
在 down 事件中 ,設(shè)置變量讓 Runable () 事件中不滾動(dòng)。
而對(duì)于歌詞在離開屏幕后的一段時(shí)間后自動(dòng)回到該位置。同樣的,還是需要使用 smoothScrollBy() 方法移動(dòng)。而移動(dòng)多少呢?這是個(gè)問題。這個(gè)要分為四種情況:
第一種:
當(dāng)前歌詞在屏幕之外:由于我是打算將歌詞移動(dòng)到屏幕中的第四個(gè)位置。
那么我就需要找到屏幕中的第一個(gè)位置,還有當(dāng)前顯示的是哪一句歌詞。
由于我是想要讓他顯示在屏幕的第四行,所以是相差 currentWord + 5 - firstPosition 個(gè)位置 。
第二種:
當(dāng)歌詞在第四行之前但是在第一行之后。
第三種:
當(dāng)歌詞在第四行之后但是在最后一行之前。
第四種:
當(dāng)歌詞在最后一行之后。
其實(shí)我們就根據(jù)自己想要在顯示在第幾行來判斷需要移動(dòng)多少個(gè)位置。
我就不詳說啦,具體看代碼:
AutoPullRecyclerView autoPullRecyclerView = weakReference.get(); LinearLayoutManager linearLayoutManager = (LinearLayoutManager) autoPullRecyclerView.getLayoutManager(); int firtPosition = linearLayoutManager.findFirstVisibleItemPosition(); int lastPosition = linearLayoutManager.findLastVisibleItemPosition(); if (firtPosition>autoPullRecyclerView.currentWord){ // 第一種 autoPullRecyclerView.smoothScrollBy(0, -(firtPosition - autoPullRecyclerView.currentWord + 5) * height); }else if(firtPosition+9>autoPullRecyclerView.currentWord){ if (firtPosition+3>autoPullRecyclerView.currentWord){ // 第二種 int top = autoPullRecyclerView.getChildAt(autoPullRecyclerView.currentWord-firtPosition).getTop(); autoPullRecyclerView.smoothScrollBy(0, -(4*height-top)); //-- }else{ // 第三種 int top = autoPullRecyclerView.getChildAt(autoPullRecyclerView.currentWord-firtPosition).getTop(); autoPullRecyclerView.smoothScrollBy(0,top-(4*height)); //++ } }else { // 第四種 autoPullRecyclerView.smoothScrollBy(0, (autoPullRecyclerView.currentWord - lastPosition + 5) * height); } } }
4.顯示中間線條以及顯示該歌詞時(shí)間
中間的 view 不可能鑲嵌在 RecyclerView 中。所以我們要自定義一個(gè)布局來放自定義 RecyclerView 和中間的 view。
這個(gè)是整個(gè)的 xml 文件。
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:clickable="true" android:layout_width="match_parent" android:layout_height="match_parent"> <com.example.administrator.animationview.AutoPullRecyclerView android:id="@+id/auto_word" android:layout_width="match_parent" android:layout_height="match_parent"/> <RelativeLayout android:layout_centerVertical="true" android:id="@+id/divide_line" android:layout_width="match_parent" android:layout_height="wrap_content"> <ImageView android:id="@+id/item_play_here" android:layout_marginStart="8dp" android:layout_centerVertical="true" android:src="@drawable/play" android:layout_width="20dp" android:layout_height="20dp" /> <View android:id="@+id/divide_line1" android:layout_marginEnd="48dp" android:layout_marginStart="4dp" android:layout_toEndOf="@+id/item_play_here" android:layout_centerVertical="true" android:background="#E6E6FA" android:layout_width="match_parent" android:layout_height="1px"/> <TextView android:id="@+id/time1" android:layout_marginEnd="4dp" android:layout_alignParentEnd="true" android:layout_centerVertical="true" android:textSize="12sp" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </RelativeLayout> </RelativeLayout>
中間線的邏輯是當(dāng)點(diǎn)擊屏幕的時(shí)候顯示出中間的線,離開屏幕的時(shí)候過一小段時(shí)間消失。也就是需要處理 down 事件和 up 事件 。但是我們?cè)?RecyclerView 中是處理了點(diǎn)擊事件的,而且本身 RecyclerView 就已經(jīng)重寫了攔截了該事件的。而且一般是父 View 是不攔截事件的。那我們要怎么在里面設(shè)置 down 時(shí)間和 up 事件呢?我們?cè)趺茨茏尭?View 接收到事件處理了一下同時(shí)最后又是子 view 處理事件呢?
在此,我推薦一篇博客,里面很詳細(xì)地介紹了事件分發(fā)處理機(jī)制的流程。
http://www.dbjr.com.cn/article/103134.htm
http://www.dbjr.com.cn/article/103141.htm
我先說一下結(jié)論吧。就是重寫 dispatchTouchEvent() 。因?yàn)榧偃缥覀冎貙?onTouchEvent 的話,由于 RecyclerView 處理了事件。是不會(huì)處理這個(gè)方法的。
而對(duì)于 dispatchTouchEvent() 方法 ,如果你是在子 view 中處理事件。那么每次事件都會(huì)從 dispatchTouchEvent() 往下傳遞。具體原理可以看一下源碼。
@Override public boolean dispatchTouchEvent(MotionEvent ev) { switch (ev.getAction()){ case MotionEvent.ACTION_DOWN: performClick(); view.setVisibility(VISIBLE); show = true; view.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { autoPullRecyclerView.setComeToPlay(); onClickListener.onClickListener(mCurrentTime); } }); break; case MotionEvent.ACTION_UP: view.removeCallbacks(runnable); view.postDelayed(runnable,4000); break; default: break; } return super.dispatchTouchEvent(ev); }
對(duì)于顯示歌詞的時(shí)間,由于線條是在最中間的部分,我想要的是中間的線在哪一個(gè) item 里面顯示該 item 對(duì)應(yīng)時(shí)間。對(duì)于最原先的做法,我是通過 firstPosition 第一個(gè)看到的 item 變化時(shí)便變化時(shí)間。但是如果只是靠第一個(gè)可視化位置的話,由于中間線的位置,這樣會(huì)導(dǎo)致恰好在中間的位置往上移動(dòng)一點(diǎn)和往下移動(dòng)一點(diǎn)是兩個(gè)不同的時(shí)間變化。但是此時(shí)都是在同一 item 中 。所以我做的是去第二個(gè)可視化位置,判斷該位置離 top 與 item/2 的距離的比較。從而解決問題。
最開始只是根據(jù)第一個(gè)可視化位置而顯示的時(shí)間,但是顯示時(shí)間變化的位置不對(duì)。
改了思路根據(jù)第二個(gè)可視化位置之后根據(jù)位移來判斷。
private void showTime(){ int height = autoPullRecyclerView.getMeasuredHeight() / 9; int top = autoPullRecyclerView.getChildAt(1).getTop(); int currentPosition = linearLayoutManager.findFirstVisibleItemPosition(); int position; if (top > height / 2) { position = currentPosition; } else { position = currentPosition + 1; }
點(diǎn)擊歌詞跳轉(zhuǎn)并且返回時(shí)間
點(diǎn)擊歌詞的時(shí)候改變高亮的位置和恢復(fù)原先的高亮的位置,并且通過回調(diào)返回時(shí)間。
case MotionEvent.ACTION_DOWN: performClick(); view.setVisibility(VISIBLE); show = true; view.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { autoPullRecyclerView.setComeToPlay(); onClickListener.onClickListener(mCurrentTime); } }); break;
/** * 點(diǎn)擊歌詞滑動(dòng) */ public void setComeToPlay(){ type =3; comeToPlay = true; lastWord = currentWord-1; removeCallbacks(autoPullWork); post(autoPullWork); }
5.點(diǎn)擊進(jìn)度條跳轉(zhuǎn)到相應(yīng)位置
先調(diào)用 seekBar 的 onSeekBarChangeListener() 中監(jiān)聽方法,獲取當(dāng)前時(shí)間,根據(jù)時(shí)間獲得當(dāng)前應(yīng)該所處的索引。然后調(diào)用自動(dòng)移動(dòng)滾動(dòng)方法和高亮方法。
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int i, boolean b) { } @Override public void onStartTrackingTouch(SeekBar seekBar) { } @Override public void onStopTrackingTouch(SeekBar seekBar) { int progress = seekBar.getProgress(); // 獲取當(dāng)前進(jìn)度 worldRelativeLayout.setChangeTime(progress); } });
這次做一個(gè)自定義 View 控件,讓我有好幾點(diǎn)感觸,我記錄一下,一方面是希望告誡自己,一方面也算是分享給他人吧。
當(dāng)你要做某個(gè)控件或項(xiàng)目的時(shí)候,不要著急著動(dòng)筆。要先想好整個(gè)流程和框架。這方面先考慮清楚在動(dòng)筆寫。你的邏輯一定要現(xiàn)在白紙上實(shí)現(xiàn)一遍后才開始敲代碼。就像我之前做的項(xiàng)目還有這次這個(gè)控件,我都比較著急寫。等到開始運(yùn)行的時(shí)候,出現(xiàn)了跟我想的不太一樣。那我又根據(jù)結(jié)果去改代碼,但是這可能只是代表著某一個(gè)方面而已,下次有可能其他方面出問題了。這樣你就會(huì)被問題牽著走,而不能從整體上去看問題。
事情總是一點(diǎn)一點(diǎn)一點(diǎn)地解決。在寫代碼的過程中,總有我們當(dāng)時(shí)不知道的,不會(huì)的,不知道怎么做的。但是也正是因?yàn)檫@些東西我們才會(huì)擴(kuò)展了更多,豐富了許多,從另一個(gè)方面講,這也是在跳出舒適區(qū)吧,所以不要慌張,作為工程師,或者說作為生活的人,我們都需要有耐心和熱情。
以上就是本文的全部內(nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Android組件之DrawerLayout實(shí)現(xiàn)抽屜菜單
DrawerLayout組件同樣是V4包中的組件,也是直接繼承于ViewGroup類,所以這個(gè)類也是一個(gè)容器類。接下來通過本文給大家介紹Android組件之DrawerLayout實(shí)現(xiàn)抽屜菜單,感興趣的朋友一起學(xué)習(xí)吧2016-02-02Android編程實(shí)現(xiàn)啟動(dòng)另外的APP及傳遞參數(shù)的方法
這篇文章主要介紹了Android編程實(shí)現(xiàn)啟動(dòng)另外的APP及傳遞參數(shù)的方法,涉及Activity啟動(dòng)及Intent設(shè)置相關(guān)操作技巧,需要的朋友可以參考下2017-05-05Android中post請(qǐng)求傳遞json數(shù)據(jù)給服務(wù)端的實(shí)例
下面小編就為大家分享一篇Android中post請(qǐng)求傳遞json數(shù)據(jù)給服務(wù)端的實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2018-01-01Android7.0以上Uri轉(zhuǎn)路徑的方法實(shí)現(xiàn)(已驗(yàn)證)
這篇文章主要介紹了Android7.0以上Uri轉(zhuǎn)路徑的方法實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-03-03Android自定義View實(shí)現(xiàn)圓環(huán)交替效果
這篇文章給大家介紹如何基于Android自定義View實(shí)現(xiàn)圓環(huán)交替的效果,實(shí)現(xiàn)后效果很贊,有需要的小伙伴們可以參考借鑒。2016-08-08