溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

Android 基于RecyclerView實(shí)現(xiàn)的歌詞滾動(dòng)自定義控件

發(fā)布時(shí)間:2020-09-05 19:13:15 來(lái)源:腳本之家 閱讀:326 作者:恒夕 欄目:移動(dòng)開(kāi)發(fā)

本文介紹了Android 基于RecyclerView實(shí)現(xiàn)的歌詞滾動(dòng)自定義控件,分享給大家,具體如下:

先來(lái)幾張效果圖:

Android 基于RecyclerView實(shí)現(xiàn)的歌詞滾動(dòng)自定義控件

Android 基于RecyclerView實(shí)現(xiàn)的歌詞滾動(dòng)自定義控件

這幾天打算做一個(gè)控件,來(lái)讓自己復(fù)習(xí)一下自定義 view 的知識(shí)以及事件分發(fā)機(jī)制的原理與應(yīng)用。對(duì)于這個(gè)控件,我已經(jīng)封裝好了,只要調(diào)用就可以了。

本來(lái)是想放上 gitHub 和 添加依賴的。但是提交 github 出了問(wèn)題一直不會(huì)弄,所以就只能先等等了。((;′⌒`))

接下來(lái)說(shuō)一下實(shí)現(xiàn)原理:

該控件分為以下幾個(gè)部分:

  1. 歌詞自動(dòng)滾動(dòng)
  2. 歌詞顏色字體變化
  3. 觸碰屏幕歌詞不滾動(dòng),高亮顯示,離開(kāi)時(shí)自動(dòng)移動(dòng)到當(dāng)前歌詞位置
  4. 觸碰屏幕中間線條出現(xiàn)以及顯示該歌詞的時(shí)間
  5. 點(diǎn)擊歌詞跳轉(zhuǎn)到當(dāng)前位置并輸出當(dāng)時(shí)時(shí)間
  6. 可設(shè)置跳轉(zhuǎn)時(shí)間跳到相應(yīng)歌詞位置

接下來(lái)我一個(gè)一個(gè)大概講述一下思路。

1.對(duì)于滾動(dòng),我們可以調(diào)用 RecyclerView.smoothScrollBy() 方法,

相對(duì)于 ScrollBy() 方法,該方法能夠?qū)崿F(xiàn)平滑滑動(dòng)。

我設(shè)置了總共顯示九句歌詞。而且因?yàn)槲蚁朐诟柙~前面和后面留一些空白,這些看起來(lái)會(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í)間來(lái)進(jìn)行移動(dòng)的。所以我們需要需要使用 Runable 來(lái)執(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)前語(yǔ)句:

本身我的想法就是多設(shè)置一個(gè)變量還是在這個(gè) Runable() 里面進(jìn)行操作。但是一個(gè)很嚴(yán)重的問(wèn)題,導(dǎo)致我連續(xù)幾天一直想不到對(duì)策方法。由于手指離開(kāi)屏幕的時(shí)候我使用 postDelayed() 方法有可能跟里面 Runable 里面使用的 postDelayed() 時(shí)間上可能會(huì)相互沖突,事件的執(zhí)行情況就很有可能變得跟你想不一樣。所以我們應(yīng)該重新寫(xiě)一個(gè) Runable() 來(lái)控制它的自動(dòng)移動(dòng)到當(dāng)前位置。這樣子的話各做各的事情,在寫(xiě)邏輯的時(shí)候會(huì)比較容易理順。(當(dāng)時(shí)沒(méi)想好害我調(diào)了好久,一直都不對(duì),哈哈).

/**
   * 歌詞自動(dòng)滑動(dòng)到特定位置任務(wù)
   */
  private static class AutoBackWork implements Runnable{

    @Override
    public void run() {
    } 
  }

對(duì)于點(diǎn)擊屏幕時(shí)就重寫(xiě) onTouchEvent() 方法,

在 down 事件中 ,設(shè)置變量讓 Runable () 事件中不滾動(dòng)。

而對(duì)于歌詞在離開(kāi)屏幕后的一段時(shí)間后自動(dòng)回到該位置。同樣的,還是需要使用 smoothScrollBy() 方法移動(dòng)。而移動(dòng)多少呢?這是個(gè)問(wèn)題。這個(gè)要分為四種情況:

第一種:

當(dāng)前歌詞在屏幕之外:由于我是打算將歌詞移動(dòng)到屏幕中的第四個(gè)位置。

那么我就需要找到屏幕中的第一個(gè)位置,還有當(dāng)前顯示的是哪一句歌詞。

由于我是想要讓他顯示在屏幕的第四行,所以是相差 currentWord + 5 - firstPosition 個(gè)位置 。

第二種:

當(dāng)歌詞在第四行之前但是在第一行之后。

第三種:

當(dāng)歌詞在第四行之后但是在最后一行之前。

第四種:

當(dāng)歌詞在最后一行之后。

其實(shí)我們就根據(jù)自己想要在顯示在第幾行來(lái)判斷需要移動(dòng)多少個(gè)位置。

我就不詳說(shuō)啦,具體看代碼:

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è)布局來(lái)放自定義 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>

Android 基于RecyclerView實(shí)現(xiàn)的歌詞滾動(dòng)自定義控件

中間線的邏輯是當(dāng)點(diǎn)擊屏幕的時(shí)候顯示出中間的線,離開(kāi)屏幕的時(shí)候過(guò)一小段時(shí)間消失。也就是需要處理 down 事件和 up 事件 。但是我們?cè)?RecyclerView 中是處理了點(diǎn)擊事件的,而且本身 RecyclerView 就已經(jīng)重寫(xiě)了攔截了該事件的。而且一般是父 View 是不攔截事件的。那我們要怎么在里面設(shè)置 down 時(shí)間和 up 事件呢?我們?cè)趺茨茏尭?View 接收到事件處理了一下同時(shí)最后又是子 view 處理事件呢?

在此,我推薦一篇博客,里面很詳細(xì)地介紹了事件分發(fā)處理機(jī)制的流程。

https://www.jb51.net/article/103134.htm
https://www.jb51.net/article/103141.htm

我先說(shuō)一下結(jié)論吧。就是重寫(xiě) dispatchTouchEvent() 。因?yàn)榧偃缥覀冎貙?xiě) 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ì)于最原先的做法,我是通過(guò) 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 的距離的比較。從而解決問(wèn)題。

最開(kāi)始只是根據(jù)第一個(gè)可視化位置而顯示的時(shí)間,但是顯示時(shí)間變化的位置不對(duì)。

Android 基于RecyclerView實(shí)現(xiàn)的歌詞滾動(dòng)自定義控件

改了思路根據(jù)第二個(gè)可視化位置之后根據(jù)位移來(lái)判斷。

Android 基于RecyclerView實(shí)現(xiàn)的歌詞滾動(dòng)自定義控件

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ù)原先的高亮的位置,并且通過(guò)回調(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)聽(tīng)方法,獲取當(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ě)。你的邏輯一定要現(xiàn)在白紙上實(shí)現(xiàn)一遍后才開(kāi)始敲代碼。就像我之前做的項(xiàng)目還有這次這個(gè)控件,我都比較著急寫(xiě)。等到開(kāi)始運(yùn)行的時(shí)候,出現(xiàn)了跟我想的不太一樣。那我又根據(jù)結(jié)果去改代碼,但是這可能只是代表著某一個(gè)方面而已,下次有可能其他方面出問(wèn)題了。這樣你就會(huì)被問(wèn)題牽著走,而不能從整體上去看問(wèn)題。

事情總是一點(diǎn)一點(diǎn)一點(diǎn)地解決。在寫(xiě)代碼的過(guò)程中,總有我們當(dāng)時(shí)不知道的,不會(huì)的,不知道怎么做的。但是也正是因?yàn)檫@些東西我們才會(huì)擴(kuò)展了更多,豐富了許多,從另一個(gè)方面講,這也是在跳出舒適區(qū)吧,所以不要慌張,作為工程師,或者說(shuō)作為生活的人,我們都需要有耐心和熱情。

以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持億速云。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI