最近项目中碰到一条新需求:播放器的进度条上显示片头片尾,关键帧和关键片段打点,如下图:
且关键帧开始时间用白色点,结束时间用黄色点。
一开始在github上面找了几个不错的项目,比如:BubbleSeekBar,IndicatorSeekBar,可是这两个项目都没办法直接拿来用,后来同事叫我自己画打点内容,额,一开始有点偷懒觉得画太复杂了,有现成的多好,后来实在没找到合适的开源库,愣是硬着头皮自己写了个。。。下面就来说说实现过程吧。
一般来说自定义的打点SeekBar他们都是自己重写了一套SeekBar的内容,而我想偷懒啊,所以我直接继承了SeekBar,在SeekBar上面画打点效果,但是要知道画布是分层的,这样子的话,我就是在SeekBar的最上层画内容了,这会导致当打点圆点和拖动滑动重合的时候,打点圆点会显示在拖动滑块上面,这显然不是我们想要的效果,所以怎么办呢?鄙人没想到好方法,参考了下API25中SeekBar的 android:tickMark属性的实现方式,也很简单,就是在SeekBar的onDraw方法中重新绘制内容,但是你会发现滑块是显示在TickMark上面的,这里可能是由于重写了drawableStateChanged方法吧,但是后来没找到当滑块和tickMark重合的处理逻辑,待以后验证吧。说了这么多你肯定觉得晕,那看代码吧,一目了然。
AppCompatSeekBar.java
public class AppCompatSeekBar extends SeekBar {
private AppCompatSeekBarHelper mAppCompatSeekBarHelper;
public AppCompatSeekBar(Context context) {
this(context, null);
}
public AppCompatSeekBar(Context context, AttributeSet attrs) {
this(context, attrs, R.attr.seekBarStyle);
}
public AppCompatSeekBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mAppCompatSeekBarHelper = new AppCompatSeekBarHelper(this);
mAppCompatSeekBarHelper.loadFromAttributes(attrs, defStyleAttr);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mAppCompatSeekBarHelper.drawTickMarks(canvas);
}
@Override
protected void drawableStateChanged() {
super.drawableStateChanged();
mAppCompatSeekBarHelper.drawableStateChanged();
}
@RequiresApi(11)
@TargetApi(11)
@Override
public void jumpDrawablesToCurrentState() {
super.jumpDrawablesToCurrentState();
mAppCompatSeekBarHelper.jumpDrawablesToCurrentState();
}
}
drawTickMarks()方法实现:
/**
* Draw the tick marks.
*/
void drawTickMarks(Canvas canvas) {
if (mTickMark != null) {
final int count = mView.getMax();
if (count > 1) {
final int w = mTickMark.getIntrinsicWidth();
final int h = mTickMark.getIntrinsicHeight();
final int halfW = w >= 0 ? w / 2 : 1;
final int halfH = h >= 0 ? h / 2 : 1;
mTickMark.setBounds(-halfW, -halfH, halfW, halfH);
final float spacing = (mView.getWidth() - mView.getPaddingLeft()
- mView.getPaddingRight()) / (float) count;
final int saveCount = canvas.save();
canvas.translate(mView.getPaddingLeft(), mView.getHeight() / 2);
for (int i = 0; i <= count; i++) {
mTickMark.draw(canvas);
canvas.translate(spacing, 0);
}
canvas.restoreToCount(saveCount);
}
}
}
drawableStateChanged()方法实现:
void drawableStateChanged() {
final Drawable tickMark = mTickMark;
if (tickMark != null && tickMark.isStateful()
&& tickMark.setState(mView.getDrawableState())) {
mView.invalidateDrawable(tickMark);
}
}
从上面代码可以看出,drawableStateChanged()方法其实是在重绘View了。当View的状态发生改变的时候都会调用这个方法重绘view,从源码中可以看出,setSelected(),setActivated(),拖动View,焦点改变,setEnabled(),setPressed(),onWindowFocusChanged(),setHovered(),dispatchAttachedToWindow()等等都会回调,所以当我们拖拽SeekBar的时候就会触发这个方法,这时候重绘SeekBar就可以重绘View,并判断他们是否重合,如果重合则不绘制打点圆点,从而解决打点圆点悬浮在拖动滑块上面的内容的问题。
整理思路和遇到的问题如何解决已经说完了,下面看看代码实现和使用吧。
TickSeekBar代码实现:
public class TickSeekBar extends AppCompatSeekBar {
private int mPaddingLeft;
private float mSeekBlockLength;//每个片段的宽度
private float mTrackY;//时间点y坐标
private int mMaxProgress;//进度条最大进度
private float mThumbCenterX;//滑块位置
private float mLeft, mRight;//seekbar的左右位置
private int mMeasureHeight;
//节点时间
//节点大小--属性
private int mTickWith;//单位:dp
private int mTickHeight;
//自动吸附--属性
private boolean isAutoAdjustTick;
//画笔
private Paint mStockPaint;
private List<TickData> mTickDataList;
public TickSeekBar(Context context) {
super(context);
}
public TickSeekBar(Context context, AttributeSet attrs) {
super(context, attrs);
initAttrs(context, attrs);
initStrokePaint();
}
public TickSeekBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
//初始化属性
private void initAttrs(Context context, AttributeSet attrs) {
if (attrs == null) {
return;
}
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.TickSeekBar);
mTickWith = ta.getInt(R.styleable.TickSeekBar_tick_with, 2);
mTickHeight = ta.getInt(R.styleable.TickSeekBar_tick_height, 2);
isAutoAdjustTick = ta.getBoolean(R.styleable.TickSeekBar_auto_adjust_tick, true);
ta.recycle();
}
//初始化画笔
private void initStrokePaint() {
if (mStockPaint == null) {
mStockPaint = new Paint();
}
mStockPaint.setAntiAlias(true);
}
private void initSeekBarInfo() {
int mMeasuredWidth = getMeasuredWidth();
mMeasureHeight = getMeasuredHeight();
mPaddingLeft = getPaddingLeft();
int mPaddingRight = getPaddingRight();
int mPaddingTop = getPaddingTop();
float mSeekLength = mMeasuredWidth - mPaddingLeft - mPaddingRight;
mSeekBlockLength = mSeekLength / mMaxProgress;
mTrackY = mPaddingTop;
mMaxProgress = getMax();
mLeft = getPaddingLeft();
mRight = getMeasuredWidth() - getPaddingRight();
}
private void initTickLocation(List<TickData> tickDataList) {
if (mTickDataList == null) {
mTickDataList = new ArrayList<>();
} else {
mTickDataList.clear();
}
mTickDataList = tickDataList;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
initSeekBarInfo();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawTicks(canvas);
}
private void drawTicks(Canvas canvas) {
if (mTickDataList == null || mTickDataList.size() == 0) {
return;
}
for (int i = 0; i < mTickDataList.size(); i++) {
float locationX = mSeekBlockLength * mTickDataList.get(i).getLocation();
// if (getThumbPosOnTick() == i) {//Seekbar滑块和tick点重合则不绘制tick点
// continue;
// }
int rectWidth = mTickWith;
float top = mTrackY + mMeasureHeight / 2f - mTickWith / 2f;
RectF roundRect = new RectF(locationX - rectWidth, top, locationX + rectWidth, top +
mTickHeight);
mStockPaint.setColor(getResources().getColor(mTickDataList.get(i).getColor()));
canvas.drawRoundRect(roundRect, 5, 5, mStockPaint);
}
}
//设置时间点
public void setTicks(List<TickData> tickDataList) {
initStrokePaint();
initTickLocation(tickDataList);
//requestLayout();
invalidate();
}
public static class TickData {
private float location;
private int color;
public TickData(float location, int color) {
this.location = location;
this.color = color;
}
public float getLocation() {
return location;
}
public void setLocation(float location) {
this.location = location;
}
public int getColor() {
return color;
}
public void setColor(int color) {
this.color = color;
}
}
}
TickSeekBar的使用:
xml文件使用:
<com.demo.customseekbar.TickSeekBar
android:id="@+id/tickSeekbar2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="40dp"
android:max="100"
android:maxHeight="2dp"
android:progress="10"
android:progressDrawable="@drawable/seekbar"
android:thumb="@drawable/thumb"
android:thumbOffset="2dp"
app:auto_adjust_tick="true"
app:tick_height="6"
app:tick_with="8"/>
java代码实现:
tickSeekBar = (TickSeekBar) findViewById(R.id.tickSeekbar2);
List<CustomSeekBar.TickData> tickDataList = new ArrayList<>();
CustomSeekBar.TickData tickData1 = new CustomSeekBar.TickData(10, android.R.color.white);
tickDataList.add(tickData1);
CustomSeekBar.TickData tickData2 = new CustomSeekBar.TickData(45, android.R.color
.holo_orange_dark);
tickDataList.add(tickData2);
CustomSeekBar.TickData tickData3 = new CustomSeekBar.TickData(60, android.R.color.white);
tickDataList.add(tickData3);
CustomSeekBar.TickData tickData4 = new CustomSeekBar.TickData(90, android.R.color
.holo_orange_dark);
tickDataList.add(tickData4);
customSeekBar.setTicks(tickDataList);
注意:
一定要设置android:max,但这个max不一定是100,也可以自定义自己的规则,例如:视频的长度;
customSeekBar.setTicks(tickDataList);可以多次调用改变打点的位置和颜色哦。