這幾天打算作一個控件,來讓本身複習一下自定義 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 處理事件呢? 在此,我推薦一篇博客,裏面很詳細地介紹了事件分發處理機制的流程。
我先說一下結論吧。就是重寫 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.事情老是一點一點一點地解決。在寫代碼的過程當中,總有咱們當時不知道的,不會的,不知道怎麼作的。可是也正是由於這些東西咱們纔會擴展了更多,豐富了許多,從另外一個方面講,這也是在跳出溫馨區吧,因此不要慌張,做爲工程師,或者說做爲生活的人,咱們都須要有耐心和熱情。
共勉