Android自定义View实现一个播放器进度条
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); } }