本文介紹了android/212161.html">Android 基于RecyclerView實現(xiàn)的歌詞滾動自定義控件,分享給大家,具體如下:
先來幾張效果圖:


這幾天打算做一個控件,來讓自己復(fù)習(xí)一下自定義 view 的知識以及事件分發(fā)機制的原理與應(yīng)用。對于這個控件,我已經(jīng)封裝好了,只要調(diào)用就可以了。
本來是想放上 gitHub 和 添加依賴的。但是提交 github 出了問題一直不會弄,所以就只能先等等了。((;′⌒`))
接下來說一下實現(xiàn)原理:
該控件分為以下幾個部分:
接下來我一個一個大概講述一下思路。
1.對于滾動,我們可以調(diào)用 RecyclerView.smoothScrollBy() 方法,
相對于 ScrollBy() 方法,該方法能夠?qū)崿F(xiàn)平滑滑動。
我設(shè)置了總共顯示九句歌詞。而且因為我想在歌詞前面和后面留一些空白,這些看起來會好看些。所以,在歌詞列表里面我加多了一些空白。
List<String> wordList = new ArrayList<>(); wordList.add(""); wordList.add(""); wordList.add(""); wordList.add(""); wordList.addAll(mWordList); wordList.add(""); wordList.add(""); wordList.add(""); wordList.add("");由于歌詞的滾自動滾動是根據(jù)歌詞時間來進行移動的。所以我們需要需要使用 Runable 來執(zhí)行滾動操作。而且為了避免內(nèi)存泄漏。將 Runable 實現(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.對于歌詞的高亮顯示,我們可以調(diào)用 notifyItemChange(int position) 方法,這個方法調(diào)用會重新去繪制特定 position 上的 viewHolder 。hightLightItem() 在這個方法中設(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.對于歌詞自動移動到當(dāng)前語句:
本身我的想法就是多設(shè)置一個變量還是在這個 Runable() 里面進行操作。但是一個很嚴重的問題,導(dǎo)致我連續(xù)幾天一直想不到對策方法。由于手指離開屏幕的時候我使用 postDelayed() 方法有可能跟里面 Runable 里面使用的 postDelayed() 時間上可能會相互沖突,事件的執(zhí)行情況就很有可能變得跟你想不一樣。所以我們應(yīng)該重新寫一個 Runable() 來控制它的自動移動到當(dāng)前位置。這樣子的話各做各的事情,在寫邏輯的時候會比較容易理順。(當(dāng)時沒想好害我調(diào)了好久,一直都不對,哈哈).
/** * 歌詞自動滑動到特定位置任務(wù) */ private static class AutoBackWork implements Runnable{ @Override public void run() { } }對于點擊屏幕時就重寫 onTouchEvent() 方法,
在 down 事件中 ,設(shè)置變量讓 Runable () 事件中不滾動。
而對于歌詞在離開屏幕后的一段時間后自動回到該位置。同樣的,還是需要使用 smoothScrollBy() 方法移動。而移動多少呢?這是個問題。這個要分為四種情況:
第一種:
當(dāng)前歌詞在屏幕之外:由于我是打算將歌詞移動到屏幕中的第四個位置。
那么我就需要找到屏幕中的第一個位置,還有當(dāng)前顯示的是哪一句歌詞。
由于我是想要讓他顯示在屏幕的第四行,所以是相差 currentWord + 5 - firstPosition 個位置 。
第二種:
當(dāng)歌詞在第四行之前但是在第一行之后。
第三種:
當(dāng)歌詞在第四行之后但是在最后一行之前。
第四種:
當(dāng)歌詞在最后一行之后。
其實我們就根據(jù)自己想要在顯示在第幾行來判斷需要移動多少個位置。
我就不詳說啦,具體看代碼:
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.顯示中間線條以及顯示該歌詞時間
中間的 view 不可能鑲嵌在 RecyclerView 中。所以我們要自定義一個布局來放自定義 RecyclerView 和中間的 view。
這個是整個的 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)點擊屏幕的時候顯示出中間的線,離開屏幕的時候過一小段時間消失。也就是需要處理 down 事件和 up 事件 。但是我們在 RecyclerView 中是處理了點擊事件的,而且本身 RecyclerView 就已經(jīng)重寫了攔截了該事件的。而且一般是父 View 是不攔截事件的。那我們要怎么在里面設(shè)置 down 時間和 up 事件呢?我們怎么能讓父 View 接收到事件處理了一下同時最后又是子 view 處理事件呢?
在此,我推薦一篇博客,里面很詳細地介紹了事件分發(fā)處理機制的流程。
我先說一下結(jié)論吧。就是重寫 dispatchTouchEvent() 。因為假如我們重寫 onTouchEvent 的話,由于 RecyclerView 處理了事件。是不會處理這個方法的。
而對于 dispatchTouchEvent() 方法 ,如果你是在子 view 中處理事件。那么每次事件都會從 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); }對于顯示歌詞的時間,由于線條是在最中間的部分,我想要的是中間的線在哪一個 item 里面顯示該 item 對應(yīng)時間。對于最原先的做法,我是通過 firstPosition 第一個看到的 item 變化時便變化時間。但是如果只是靠第一個可視化位置的話,由于中間線的位置,這樣會導(dǎo)致恰好在中間的位置往上移動一點和往下移動一點是兩個不同的時間變化。但是此時都是在同一 item 中 。所以我做的是去第二個可視化位置,判斷該位置離 top 與 item/2 的距離的比較。從而解決問題。
最開始只是根據(jù)第一個可視化位置而顯示的時間,但是顯示時間變化的位置不對。

改了思路根據(jù)第二個可視化位置之后根據(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; }點擊歌詞跳轉(zhuǎn)并且返回時間
點擊歌詞的時候改變高亮的位置和恢復(fù)原先的高亮的位置,并且通過回調(diào)返回時間。
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;/** * 點擊歌詞滑動 */ public void setComeToPlay(){ type =3; comeToPlay = true; lastWord = currentWord-1; removeCallbacks(autoPullWork); post(autoPullWork); }5.點擊進度條跳轉(zhuǎn)到相應(yīng)位置
先調(diào)用 seekBar 的 onSeekBarChangeListener() 中監(jiān)聽方法,獲取當(dāng)前時間,根據(jù)時間獲得當(dāng)前應(yīng)該所處的索引。然后調(diào)用自動移動滾動方法和高亮方法。
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)前進度 worldRelativeLayout.setChangeTime(progress); } });這次做一個自定義 View 控件,讓我有好幾點感觸,我記錄一下,一方面是希望告誡自己,一方面也算是分享給他人吧。
當(dāng)你要做某個控件或項目的時候,不要著急著動筆。要先想好整個流程和框架。這方面先考慮清楚在動筆寫。你的邏輯一定要現(xiàn)在白紙上實現(xiàn)一遍后才開始敲代碼。就像我之前做的項目還有這次這個控件,我都比較著急寫。等到開始運行的時候,出現(xiàn)了跟我想的不太一樣。那我又根據(jù)結(jié)果去改代碼,但是這可能只是代表著某一個方面而已,下次有可能其他方面出問題了。這樣你就會被問題牽著走,而不能從整體上去看問題。
事情總是一點一點一點地解決。在寫代碼的過程中,總有我們當(dāng)時不知道的,不會的,不知道怎么做的。但是也正是因為這些東西我們才會擴展了更多,豐富了許多,從另一個方面講,這也是在跳出舒適區(qū)吧,所以不要慌張,作為工程師,或者說作為生活的人,我們都需要有耐心和熱情。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持VEVB武林網(wǎng)。
新聞熱點
疑難解答
圖片精選