封面
1.写在前面
SeekBar控件在开发中还是比较常见的,比如音视频进度、音量调节等,但是原生控件有时还不能满足我们的需求,今天就来学习一下如何自定义SeekBar控件,本文主要实现了一个带文字指示器效果的SeekBar控件,看下最终效果:
IndicatorSeekBar
2.实现
IndicatorSeekBar
public class IndicatorSeekBar extends AppCompatSeekBar {
// 画笔
private Paint mPaint;
// 进度文字位置信息
private Rect mProgressTextRect = new Rect();
// 滑块按钮宽度
private int mThumbWidth = dp2px(50);
// 进度指示器宽度
private int mIndicatorWidth = dp2px(50);
// 进度监听
private OnIndicatorSeekBarChangeListener mIndicatorSeekBarChangeListener;
public IndicatorSeekBar(Context context) {
this(context, null);
}
public IndicatorSeekBar(Context context, AttributeSet attrs) {
this(context, attrs, R.attr.seekBarStyle);
}
public IndicatorSeekBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mPaint = new TextPaint();
mPaint.setAntiAlias(true);
mPaint.setColor(Color.parseColor("#00574B"));
mPaint.setTextSize(sp2px(16));
// 如果不设置padding,当滑动到最左边或最右边时,滑块会显示不全
setPadding(mThumbWidth / 2, 0, mThumbWidth / 2, 0);
// 设置滑动监听
this.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
// NO OP
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
if (mIndicatorSeekBarChangeListener != null) {
mIndicatorSeekBarChangeListener.onStartTrackingTouch(seekBar);
}
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
if (mIndicatorSeekBarChangeListener != null) {
mIndicatorSeekBarChangeListener.onStopTrackingTouch(seekBar);
}
}
});
}
@Override
protected synchronized void onDraw(Canvas canvas) {
super.onDraw(canvas);
String progressText = getProgress() + "%";
mPaint.getTextBounds(progressText, 0, progressText.length(), mProgressTextRect);
// 进度百分比
float progressRatio = (float) getProgress() / getMax();
// thumb偏移量
float thumbOffset = (mThumbWidth - mProgressTextRect.width()) / 2 - mThumbWidth * progressRatio;
float thumbX = getWidth() * progressRatio + thumbOffset;
float thumbY = getHeight() / 2f + mProgressTextRect.height() / 2f;
canvas.drawText(progressText, thumbX, thumbY, mPaint);
if (mIndicatorSeekBarChangeListener != null) {
float indicatorOffset = getWidth() * progressRatio - (mIndicatorWidth - mThumbWidth) / 2 - mThumbWidth * progressRatio;
mIndicatorSeekBarChangeListener.onProgressChanged(this, getProgress(), indicatorOffset);
}
}
/**
* 设置进度监听
*
* @param listener OnIndicatorSeekBarChangeListener
*/
public void setOnSeekBarChangeListener(OnIndicatorSeekBarChangeListener listener) {
this.mIndicatorSeekBarChangeListener = listener;
}
/**
* 进度监听
*/
public interface OnIndicatorSeekBarChangeListener {
/**
* 进度监听回调
*
* @param seekBar SeekBar
* @param progress 进度
* @param indicatorOffset 指示器偏移量
*/
public void onProgressChanged(SeekBar seekBar, int progress, float indicatorOffset);
/**
* 开始拖动
*
* @param seekBar SeekBar
*/
public void onStartTrackingTouch(SeekBar seekBar);
/**
* 停止拖动
*
* @param seekBar SeekBar
*/
public void onStopTrackingTouch(SeekBar seekBar);
}
/**
* dp转px
*
* @param dp dp值
* @return px值
*/
public int dp2px(float dp) {
return (int) TypedValue.applyDimension(PLEX_UNIT_DIP, dp,
getResources().getDisplayMetrics());
}
/**
* sp转px
*
* @param sp sp值
* @return px值
*/
private int sp2px(float sp) {
return (int) TypedValue.applyDimension(PLEX_UNIT_SP, sp,
getResources().getDisplayMetrics());
}
}
重点看下onDraw方法:
@Override
protected synchronized void onDraw(Canvas canvas) {
super.onDraw(canvas);
String progressText = getProgress() + "%";
mPaint.getTextBounds(progressText, 0, progressText.length(), mProgressTextRect);
// 进度百分比
float progressRatio = (float) getProgress() / getMax();
// thumb偏移量
float thumbOffset = (mThumbWidth - mProgressTextRect.width()) / 2 - mThumbWidth * progressRatio;
float thumbX = getWidth() * progressRatio + thumbOffset;
float thumbY = getHeight() / 2f + mProgressTextRect.height() / 2f;
canvas.drawText(progressText, thumbX, thumbY, mPaint);
if (mIndicatorSeekBarChangeListener != null) {
float indicatorOffset = getWidth() * progressRatio - (mIndicatorWidth - mThumbWidth) / 2 - mThumbWidth * progressRatio;
mIndicatorSeekBarChangeListener.onProgressChanged(this, getProgress(), indicatorOffset);
}
}
再看一遍效果图:
IndicatorSeekBar
可以看到,进度百分比文字是跟着进度变化在平移的,所以X轴坐标根据进度动态计算就可以了【总宽度 * 进度百分比】(getWidth() * progressRatio),文字需要居中显示,所以需要向右平移【(滑块宽度 - 文字宽度)/ 2】((mThumbWidth - mProgressTextRect.width()) / 2)。
为了避免滑块滑动到终点时布局被隐藏,需要为SeekBar设置左右padding,距离分别为滑块宽度的一半,,所以【控件总长度 = 控件实际长度 + 滑块宽度】,向右平移的过程中就要动态减去滑块宽度【滑块宽度 * 进度百分比】(mThumbWidth * progressRatio),到这里文字的X轴坐标就计算完成了。
文字在平移的过程中始终是垂直居中的,所以Y轴坐标可以这样计算【控件高度 / 2 + 文字高度 / 2】(getHeight() / 2f + mProgressTextRect.height() / 2f),注意drawText方法默认是从左下角开始绘制文字的,如果对绘制文字还不太了解,可以看下这篇文章《Android 图解Canvas drawText文字居中的那些事》
指示器跟随滑块移动
在IndicatorSeekBar中,向外提供了一个setOnSeekBarChangeListener方法用来回调SeekBar的状态,其中onProgressChanged方法中的indicatorOffset参数就是指示器控件的X坐标,计算方式与上文中进度百分比文字的计算方式一致:
// 【总宽度 * 进度百分比 -(指示器宽度 - 滑块宽度)/ 2 - 滑块宽度 * 进度百分比】
float indicatorOffset = getWidth() * progressRatio - (mIndicatorWidth - mThumbWidth) / 2 - mThumbWidth * progressRatio;
mIndicatorSeekBarChangeListener.onProgressChanged(this, getProgress(), indicatorOffset);
看下如何使用:
public class MainActivity extends AppCompatActivity {
private TextView tvIndicator;
private IndicatorSeekBar indicatorSeekBar;
private Paint mPaint = new Paint();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tvIndicator = findViewById(R.id.tv_indicator);
indicatorSeekBar = findViewById(R.id.indicator_seek_bar);
initData();
}
private void initData() {
final LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) tvIndicator.getLayoutParams();
indicatorSeekBar.setOnSeekBarChangeListener(new IndicatorSeekBar.OnIndicatorSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, float indicatorOffset) {
String indicatorText = progress + "%";
tvIndicator.setText(indicatorText);
params.leftMargin = (int) indicatorOffset;
tvIndicator.setLayoutParams(params);
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
tvIndicator.setVisibility(View.VISIBLE);
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
tvIndicator.setVisibility(View.INVISIBLE);
}
});
}
}
布局文件:
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:orientation="vertical">
android:id="@+id/tv_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/bg_indicator"
android:gravity="center"
android:textColor="#FFFFFF"
android:textSize="16sp"
android:visibility="invisible" />
android:id="@+id/indicator_seek_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:background="@null"
android:max="100"
android:maxHeight="2dp"
android:minHeight="2dp"
android:progress="50"
android:progressDrawable="@drawable/seekbar_progress_drawable"
android:thumb="@drawable/seekbar_thumb" />
3.写在最后
代码已上传至GitHub,欢迎Star、Fork!