基于 RecyclerView 实现的歌词滚动自定义控件

      先来一张效果图: 



       这几天打算作一个控件,来让本身复习一下自定义 view 的知识以及事件分发机制的原理与应用。对于这个控件,我已经封装好了,只要调用就能够了。这是个人 gitHub 欢迎 star 和 fork,以前没怎么用过,请你们多多捧场,哈哈! github.com/Yeahlz/Word…git

      该控件分为如下几个部分: github

      1.歌词自动滚动 2. 歌词颜色字体变化 3.触碰屏幕歌词不滚动,高亮显示,离开时自动移动到当前歌词位置 4.触碰屏幕中间线条出现以及显示该歌词的时间 5.点击歌词跳转到当前位置并输出当时时间 6. 可设置跳转时间跳到相应歌词位置 bash

接下来我一个一个大概讲述一下思路。框架

      1.对于滚动,咱们能够调用 RecyclerView.smoothScrollBy() 方法, 相对于 ScrollBy() 方法,该方法可以实现平滑滑动。 我设置了总共显示九句歌词。并且由于我想在歌词前面和后面留一些空白,这些看起来会好看些。因此,在歌词列表里面我加多了一些空白。ide

List<String> wordList = new ArrayList<>(); //  添加歌词列表中的一些空白
wordList.add(""); 
wordList.add(""); 
wordList.add(""); 
wordList.add(""); 
wordList.addAll(mWordList); 
wordList.add(""); 
wordList.add(""); 
wordList.add(""); 
wordList.add("");
复制代码

      因此咱们须要使用 Runable 来执行滚动操做。并且为了不内存泄漏。将 Runable 实现类修饰为 static 。因为歌词的滚自动滚动是根据歌词时间来进行移动的。前面已经看到歌词列表索引位置跟时间列表位置有所变化,因此下面索引操做有些变化布局

private static class AutoPullWork implements Runnable {   //执行歌词滚动的 Runable 类
    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)); 
        // 因为歌词列表前面添加了四个空白,因此 cuurrentWord 是从第 5 个开始。
        ......
    }
}复制代码

      2.对于歌词的高亮显示,咱们能够调用 notifyItemChange(int position) 方法,这个方法调用会从新去绘制特定 position 上的 viewHolder 。hightLightItem() 在这个方法中设置咱们想要改变 viewHolder 的位置,并调用 notifyItemChange(int position) 。而后在 onBindViewHolder() 中的设置能够判断当前是否须要高亮显示。post

public void hightLightItem(int position){  // 外部调用 adapter 中这个办法,用于设置要高亮显示的位置,并调用重绘特定 position
        mHighLightPosition = position; 
        notifyItemChanged(position-1); 
        notifyItemChanged(position); 
}
复制代码

private boolean isHighLight(int position){ // 在 onBindViewHolder 中调用 用于判断当前是否须要高亮显示
        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.对于歌词自动移动到当前语句: 自己个人想法就是多设置一个变量仍是在这个 Runable() 里面进行操做。可是一个很严重的问题,致使我连续几天一直想不到对策方法。因为手指离开屏幕的时候我使用 postDelayed() 方法有可能跟里面 Runable 里面使用的 postDelayed() 时间上可能会相互冲突,事件的执行状况就颇有可能变得跟你想不同。因此咱们应该从新写一个 Runable() 来控制它的自动移动到当前位置。这样子的话各作各的事情,在写逻辑的时候会比较容易理顺。(当时没想好害我调了很久,一直都不对,哈哈). 字体

private static class AutoBackWork implements Runnable{  //开启另外一个任务来控制歌词自动移动到当前位置
    @Override public void run() { 
    }
 }
复制代码

       对于点击屏幕时就重写 onTouchEvent() 方法, 在 down 事件中 ,设置变量让 Runable () 事件中不滚动。 而对于歌词在离开屏幕后的一段时间后自动回到该位置。一样的,仍是须要使用 smoothScrollBy() 方法移动。而移动多少呢?ui

这是个问题。这个要分为四种状况: spa

第一种: 当前歌词在屏幕以外:因为我是打算将歌词移动到屏幕中的第四个位置。 那么我就须要找到屏幕中的第一个位置,还有当前显示的是哪一句歌词。 因为我是想要让他显示在屏幕的第四行,因此是相差 currentWord + 5 - firstPosition 个位置 。 

第二种: 当歌词在第四行以前可是在第一行以后。 

第三种: 当歌词在第四行以后可是在最后一行以前。 

第四种: 当歌词在最后一行以后。 其实咱们就根据本身想要在显示在第几行来判断须要移动多少个位置。 我就不详说啦,具体看代码:

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。


      中间线的逻辑是当点击屏幕的时候显示出中间的线,离开屏幕的时候过一小段时间消失。也就是须要处理 down 事件和 up 事件 。可是咱们在 RecyclerView 中是处理了点击事件的,并且自己 RecyclerView 就已经重写了拦截了该事件的。并且通常是父 View 是不拦截事件的。那咱们要怎么在里面设置 down 时间和 up 事件呢?咱们怎么能让父 View 接收到事件处理了一下同时最后又是子 view 处理事件呢? 在此,我推荐一篇博客,里面很详细地介绍了事件分发处理机制的流程。 

www.jianshu.com/p/e99b5e8bd… 

      我先说一下结论吧。就是重写 dispatchTouchEvent() 。由于假如咱们重写 onTouchEvent 的话,因为 RecyclerView 处理了事件。是不会处理这个方法的。 而对于 dispatchTouchEvent() 方法 ,若是你是在子 view 中处理事件。那么每次事件都会从 dispatchTouchEvent() 往下传递。具体原理能够看一下源码。

@Override public boolean dispatchTouchEvent(MotionEvent ev) {  // 父 view 在这个方法中处理 down 和 up 事件
        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); //除去原先全部事件,由于有可能有多个 up 操做,咱们只须要保留最后一个。
                    view.postDelayed(runnable,4000); // 调用拦截器
                    break; 
                    default: 
                    break; 
            } 
        return super.dispatchTouchEvent(ev); 
} 
复制代码

       对于显示歌词的时间,因为线条是在最中间的部分,我想要的是中间的线在哪个 item 里面显示该 item 对应时间。对于最原先的作法,我是经过 firstPosition 第一个看到的 item 变化时便变化时间。可是若是只是靠第一个可视化位置的话,因为中间线的位置,这样会致使刚好在中间的位置往上移动一点和往下移动一点是两个不一样的时间变化。可是此时都是在同一 item 中 。因此我作的是去第二个可视化位置,判断该位置离 top 与 item/2 的距离的比较。从而解决问题。 最开始只是根据第一个可视化位置而显示的时间,可是显示时间变化的位置不对。


     改了思路根据第二个可视化位置以后根据位移来判断。

 

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; 
        }
复制代码

         5.点击歌词跳转而且返回时间 点击歌词的时候改变高亮的位置和恢复原先的高亮的位置,而且经过回调返回时间。 

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(){ //这是子 view 中的方法
        type =3;  //点击歌词跳转类型
        comeToPlay = true;
        lastWord = currentWord-1;
        removeCallbacks(autoPullWork);
        post(autoPullWork);
    }复制代码

if (type==3&&autoPullRecyclerView.comeToPlay){
                            type = 1;  // 自动滚动类型
                            if (-top>height/2){   //理由跟上面的同样
                                autoPullAdapter.changeToHighLight(autoPullRecyclerView.lastWord,firtPosition+5);
                                autoPullRecyclerView.currentWord = firtPosition+5; //当前歌词从新设置
                            }else {
                                autoPullAdapter.changeToHighLight(autoPullRecyclerView.lastWord,firtPosition+4);
                                autoPullRecyclerView.currentWord = firtPosition+4;
                            }
                            autoPullRecyclerView.comeToPlay = false;
复制代码

5.点击进度条跳转到相应位置 先调用 seekBar 的 onSeekBarChangeListener() 中监听方法,获取当前时间,根据时间得到当前应该所处的索引。而后调用自动移动滚动方法和高亮方法。

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(); // 获取当前进度 
                worldRelativeLayout.setChangeTime(progress); 
            } 
});   复制代码

/** 设置歌词时间相应歌词滑动
     * @param time
     */
    public void setChangeTime(int time){
        type =2; 
        if (time<=timeList.get(0)){  //时间小于第一句时间
            removeCallbacks(autoPullWork);  //清除以前的任务
            removeCallbacks(autoBackWork);
            lastWord = currentWord;   // 上一次高亮的位置
            currentWord = 3;
            post(autoBackWork); //从新移动位置
            postDelayed(autoPullWork,timeList.get(0)-time); 
        }else if (time>=timeList.get(timeList.size()-1)){  //时间大于最后一句位置
            removeCallbacks(autoPullWork);
            removeCallbacks(autoBackWork);  //清除以前的任务
            lastWord = currentWord; 
            currentWord = wordLength+3; 当前应该显示的歌词位置
            post(autoPullWork);
            postDelayed(autoBackWork,2000);
        }else {  
            removeCallbacks(autoPullWork);
            removeCallbacks(autoBackWork);
            int position = 0;
            for (int i=0;i<timeList.size()-1;i++){   //找出比这个时间快一点的歌词
                if (time>timeList.get(i)&&time<timeList.get(i+1)){
                    position =i;
                    break;
                }
            }
            int a = timeList.get(currentWord-3)-time;
            lastWord = currentWord-1;
            currentWord = position+4;
            post(autoBackWork);
            postDelayed(autoPullWork,timeList.get(currentWord-3)-time); 与下一句单词间隔
        }
    }
复制代码

   此次作一个自定义 View 控件,让我有好几点感触,我记录一下,一方面是但愿告诫本身,一方面也算是分享给他人吧。 

        1.当你要作某个控件或项目的时候,不要着急着动笔。要先想好整个流程和框架。这方面先考虑清楚在动笔写。你的逻辑必定要如今白纸上实现一遍后才开始敲代码。就像我以前作的项目还有此次这个控件,我都比较着急写。等到开始运行的时候,出现了跟我想的不太同样。那我又根据结果去改代码,可是这可能只是表明着某一个方面而已,下次有可能其余方面出问题了。这样你就会被问题牵着走,而不能从总体上去看问题。 -

        2.事情老是一点一点一点地解决。在写代码的过程当中,总有咱们当时不知道的,不会的,不知道怎么作的。可是也正是由于这些东西咱们才会扩展了更多,丰富了许多,从另外一个方面讲,这也是在跳出温馨区吧,因此不要慌张,做为工程师,或者说做为生活的人,咱们都须要有耐心和热情。 

         共勉 

相关文章
相关标签/搜索