2020-08-03
關鍵字:
這篇文章記錄一下我通過自定義View的方式實現的一個播放器進度條的過程以及完整源碼。希望能起到一個“備忘”的作用,如果能再幫助到其他有同樣需求的同學就更好了。
先來看看這個進度條的成品效果圖:
想要自定義一個View,首先要知道我們需要實現什么樣的效果。
要想實現我們想要的效果,就必須得能清晰地拆解效果圖。
這個View總體上可以分為兩個大類:
1、UI;
2、功能。
在 UI 上我們針對上面的效果圖可以作如下拆解:
1、時間戳;
2、滑塊;
3、進度條。
在功能上可以作如下拆解:
1、時長設置;
2、UI更新;
3、拖拉滑塊改變播放進度;
4、事件回調。
將拆解逐個實現並最終拼湊成一個整體,就是筆者常用的自定義View流程模板。
1、UI實現
根據效果圖來看,這個 View 通過繼承 RelativeLayout 來自定義容器會更方便一點。當然這里必須強調筆者這樣做僅僅是認為會更方便、開發周期上會更短一點而已。要說自定義 View 效果最好的還得是通過繼承 View 來完全自己實現。但在實際工作過程中項目往往不會給你這么多的時間和精力來將一件“小事”做到極致。算是遺憾也算是無奈吧~
於是,一個 Java 類就出現了:
public class PlayerProgressBar extends RelativeLayout { }
自定義View的基調確定了,其它的子模塊就好辦了。
1.1、時間戳
時間戳元素通常使用兩個。一個標示當前播放位置,另一個表示本次播放總時長或剩余時長。
筆者這邊采用的是 當前位置 + 剩余時長 模式的時間戳。
具體的實現也簡單,兩個 TextView 分另放置在View的兩側,進度條下方。設置好各自的屬性即可,沒什么特別的。
1.2、滑塊
滑塊筆者這邊直接是用一張圖片來實現的。
為了顯示地更有層次感,滑塊上最好做點陰影效果,筆者為了方便直接找美工做切圖實現了。
因此滑塊也沒什么特別的,就一個簡單的 ImageView。
不過因為滑塊是要運動,而在窗器中改變一個子View的位置最簡單的辦法就是改變這個子View的 LayoutParams 中的 margin 值。然后再通過容器父類的 requestLayout() 來更新整個View。
因此,在創建滑塊ImageView時要記下它的 RelativeLayout.LayoutParams 對象。
滑塊的切圖貼出如下:
1.3、進度條
進度條筆者這邊是直接使用Android自帶的 ProgressBar 來實現的。
同樣為了顯示效果更逼真更好,進度槽還是建議加上陰影效果。如果你有美工可以支持你,直接讓她做一個相關切圖就最好了。
但是筆者沒有!因此只能通過 xml 自定義 drawable 的方式勉強做了個槽+陰影的效果出來。
根據上面的效果圖來看,確實不咋的,但它在手機上實際顯示出來以后往往會比較小,一些缺陷也不會這么明顯。湊合着能用。
這邊筆者直接將這個xml代碼貼出來了:

<?xml version="1.0" encoding="UTF-8"?> <layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <item android:id="@android:id/background"> <shape> <corners android:radius="3.5dp" /> <stroke android:color="#dddddd" android:width="1dp" /> </shape> </item> <item android:left="1dp" android:right="1dp" android:top="1dp" android:bottom="1dp"> <shape> <corners android:radius="2.5dp" /> <solid android:color="#ececec"/> </shape> </item> <item android:top="1dp" android:left="1dp" android:right="1dp" android:height="0.7dp"> <shape> <corners android:radius="0.3dp" /> <gradient android:endColor="#dddddd" android:startColor="#cdcdcd" android:type="linear" /> </shape> </item> <!-- 設置進度條顏色 --> <item android:id="@android:id/progress"> <clip> <shape> <corners android:radius="3.5dp"/> <solid android:color="#EC1693"/> </shape> </clip> </item> </layer-list>
這個播放器進度條的幾個主要元素也基本就這樣了,剩下的就是將它們在 RelativeLayout 容器中拼湊起來了。
2、功能實現
2.1、時長設置
這沒啥好說的。建議按照Android播放器上的標准,以毫秒作為時長單位。同時ProgressBar還是得以“秒”作為單位來設置長度。
總得來說,提供一個設置時長、設置當前播放位置、獲取當前位置的方法也就差不多了。
2.2、UI更新
這里的UI更新主要就是時間戳值的改變顯示、滑塊位置的改變以及已播放進度顏色的改變。
時間戳值簡單,直接將播放進度的毫秒值轉變成適宜閱讀的時分秒形式就行了。這個轉換筆者這邊有一個簡單的算法如下:

private String calTime(int ms){ if(ms < 1){ return DEFAULT_DURATION; } sb.delete(0, sb.length()); if(ms < 60000){ // below 1 minute sb.append("00:00:"); if(ms < 10000) sb.append("0"); sb.append(ms/1000); }else if(ms < 3600000){ // below 1 hour sb.append("00:"); int tmp = ms / 60000; if(tmp < 10) sb.append("0"); sb.append(tmp); sb.append(":"); tmp = ms % 60000; if(tmp < 10000) sb.append("0"); sb.append(tmp / 1000); }else if(ms < 360000000){ // below 100 hour int tmp = ms / 3600000; if(tmp < 10) sb.append("0"); sb.append(tmp); sb.append(":"); tmp = ms % 3600000; int tmp2 = tmp / 60000; if(tmp2 < 10) sb.append("0"); sb.append(tmp2); sb.append(":"); tmp2 = tmp % 60000; if(tmp2 < 10000) sb.append("0"); sb.append(tmp2 / 1000); }else{ sb.append("99:59:59"); } return sb.toString(); }
這個算法不求有多好,但求能正常使用即可。
滑塊的位置前面已經提到過,主要就是控制滑塊的 LayoutParams 的 leftMargin 的值就可以了。根據當前播放進度,再結合滑塊自身的尺寸計算得出滑塊的 leftMargin 值,再調用 requestLayout() 方法更新容器即可。
最后是已播放進度的顏色改變。這點直接交給 ProgressBar 實現就行。這也是直接用原生View的好處。
2.3、拖拉滑塊改變播放位置
給滑塊注冊一個 View.OnTouchListener。根據 按下、移動、抬起幾個事件,計算觸摸在水平方向上的移動位置,並實時改變滑塊 LayoutParams 的 leftMargin 的值再 requestLayout() 一下就能實現拖動改變滑塊位置的功能了。同時不要忘記改變 ProgressBar 的進度值。
2.4、事件監聽
這個就沒什么好說的了。拖動改變位置、播放進度改變通知、播放開始、暫停、結束通知等都可以根據自己的實際需要來設置。
總得來說,這個進度條的自定義還是很簡單的。
以下就直接貼出筆者的完整源碼。

package com.example.multiscreen.sender.view; import android.annotation.SuppressLint; import android.content.Context; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.widget.ImageView; import android.widget.ProgressBar; import android.widget.RelativeLayout; import android.widget.TextView; import com.example.multiscreen.sender.R; import com.example.multiscreen.sender.util.UnitManager; @SuppressLint("ClickableViewAccessibility") public class PlayerProgressBar extends RelativeLayout { private static final String TAG = "PlayerProgressBar"; private static final String DEFAULT_DURATION = "00:00:00"; private boolean isInDragMode; private int duration; private int curDuration; private float indicatorMaxWidth; private StringBuilder sb; private RelativeLayout.LayoutParams indicatorLp; private OnProgressChangedListener onProgressChangedListener; private ProgressBar pb; private ImageView indicator; private TextView spendTime; private TextView leftTime; public PlayerProgressBar(Context context, AttributeSet attrs) { super(context, attrs); sb = new StringBuilder(); indicatorMaxWidth = -1; pb = new ProgressBar(context, null, android.R.attr.progressBarStyleHorizontal); pb.setProgressDrawable(context.getResources().getDrawable(R.drawable.progressbar_bg)); RelativeLayout.LayoutParams rlp = new RelativeLayout.LayoutParams(-1, UnitManager.px2dp(7)); rlp.topMargin = UnitManager.px2dp(5); pb.setLayoutParams(rlp); indicator = new ImageView(context); rlp = new RelativeLayout.LayoutParams(UnitManager.px2dp(18), UnitManager.px2dp(18)); indicator.setLayoutParams(rlp); indicatorLp = rlp; indicator.setScaleType(ImageView.ScaleType.CENTER); indicator.setImageDrawable(context.getResources().getDrawable(R.mipmap.dcactivity_playcontroller_posquare)); spendTime = new TextView(context); rlp = new RelativeLayout.LayoutParams(UnitManager.px2dp(46), UnitManager.px2dp(15)); rlp.topMargin = UnitManager.px2dp(20); spendTime.setLayoutParams(rlp); spendTime.setText(DEFAULT_DURATION); spendTime.setTextSize(11); spendTime.setTextColor(context.getResources().getColor(R.color.gray_666666)); leftTime = new TextView(context); rlp = new RelativeLayout.LayoutParams(UnitManager.px2dp(46), UnitManager.px2dp(15)); rlp.topMargin = UnitManager.px2dp(20); rlp.addRule(RelativeLayout.ALIGN_PARENT_END); leftTime.setLayoutParams(rlp); leftTime.setText(DEFAULT_DURATION); leftTime.setTextSize(11); leftTime.setTextColor(context.getResources().getColor(R.color.gray_666666)); indicator.setOnTouchListener(onIndicatorTouchListener); addView(pb); addView(indicator); addView(spendTime); addView(leftTime); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); if(indicatorMaxWidth == -1) { indicatorMaxWidth = (float)pb.getWidth() - (float)indicator.getWidth(); } } public void setDuration(int duration){ if(duration < 0) return; this.duration = duration; pb.setMax(duration / 1000); applyDuration(); } public void setCurrentPos(int duration){ if(duration <= this.duration) { curDuration = duration; applyDuration(); } } private void applyDuration(){ leftTime.post(new Runnable() { @Override public void run() { applyLeftDuration(); applyCurDuration(); } }); } private void applyLeftDuration(){ if(duration < 0){ leftTime.setText(DEFAULT_DURATION); }else{ leftTime.setText(calTime(duration - curDuration)); } } private void applyCurDuration(){ if(curDuration < 0){ spendTime.setText(DEFAULT_DURATION); pb.setProgress(0); }else{ spendTime.setText(calTime(curDuration)); pb.setProgress(curDuration / 1000); applyIndicator(); } } private void applyIndicator(){ indicatorLp.leftMargin = (int)((float)curDuration / (float)duration * indicatorMaxWidth); requestLayout(); } private String calTime(int ms){ if(ms < 1){ return DEFAULT_DURATION; } sb.delete(0, sb.length()); if(ms < 60000){ // below 1 minute sb.append("00:00:"); if(ms < 10000) sb.append("0"); sb.append(ms/1000); }else if(ms < 3600000){ // below 1 hour sb.append("00:"); int tmp = ms / 60000; if(tmp < 10) sb.append("0"); sb.append(tmp); sb.append(":"); tmp = ms % 60000; if(tmp < 10000) sb.append("0"); sb.append(tmp / 1000); }else if(ms < 360000000){ // below 100 hour int tmp = ms / 3600000; if(tmp < 10) sb.append("0"); sb.append(tmp); sb.append(":"); tmp = ms % 3600000; int tmp2 = tmp / 60000; if(tmp2 < 10) sb.append("0"); sb.append(tmp2); sb.append(":"); tmp2 = tmp % 60000; if(tmp2 < 10000) sb.append("0"); sb.append(tmp2 / 1000); }else{ sb.append("99:59:59"); } return sb.toString(); } public boolean isInDragMode(){ return isInDragMode; } public int getCurDuration(){ return curDuration; } public void setOnProgressChangedListener(OnProgressChangedListener listener){ onProgressChangedListener = listener; } private View.OnTouchListener onIndicatorTouchListener = new OnTouchListener() { private boolean isInvalidEvent; @Override public boolean onTouch(View v, MotionEvent event) { if(v != indicator) { return false; } if(isInvalidEvent) { if(event.getAction() == MotionEvent.ACTION_UP) { isInvalidEvent = false; isInDragMode = false; } return true; } if(duration == 0){ return true; } switch(event.getAction()){ case MotionEvent.ACTION_DOWN:{ isInDragMode = true; }break; case MotionEvent.ACTION_UP:{ if(onProgressChangedListener != null) { onProgressChangedListener.onProgressChanged(duration, curDuration); } isInDragMode = false; isInvalidEvent = false; }break; case MotionEvent.ACTION_MOVE:{ if(event.getY() < -40 || event.getY() > 130){ //Exit the drag mode. isInvalidEvent = true; isInDragMode = false; refreshView(event); if(onProgressChangedListener != null) { onProgressChangedListener.onProgressChanged(duration, curDuration); } }else{ refreshView(event); } }break; } return true; } private void refreshView(MotionEvent event){ indicatorLp.leftMargin += (int)event.getX(); curDuration = (int) ((float)indicatorLp.leftMargin / indicatorMaxWidth * duration); applyDuration(); requestLayout(); } }; public interface OnProgressChangedListener { void onProgressChanged(int total, int current); } }