前幾天,朋友找我幫忙實現一個這樣的效果,如下:
當時我看到這個效果圖,想了一下大概的思路如下:
1、自定義View繼承SeekBar,這樣方便使用SeekBar的一些自帶功能,比如觸摸事件及回調等;
2、藍色和灰色的進度條,自己實現,用Path這個類來繪制;
3、圓形的滑塊使用自帶屬性android:thumb=""添加一張圖片即可。
按照這個思路最終還是實現了這樣的一個效果,效果動圖如下:
雛形
我們先使用SeekBar控件,滑塊使用自定義的圖片,進度條置空@null,待會進度條效果我們自己來實現,代碼及預覽圖如下:
分析
OK,到目前為止就需要我們自己來完成進度條的繪制了,繪制之前我們先分析一下該如何繪制。因為滑塊有一定的寬度,假設是30dp,那么當滑塊位于起始位置的時候,它的中心位置距離左邊界的距離就是15dp,那么我們在繪制進度條的時候就是從這個位置開始的,也就是說起始位置x坐標是在半個滑塊寬度的位置。終點位置同理,為什么要這樣呢?因為如果不考慮這個因素從0坐標開始繪制的話將是這樣的效果,兩邊多出來的很丑有木有?
所以,我們需要考慮這個細節問題,讓起始位置和終點位置都對齊滑塊的中心位置。這樣,假如SeekBar控件的寬度是width,那么進度條的寬度就是width-滑塊寬度。
初始化
初始化畫筆等對象:
setMax(MAX_VALUE);
//獲取滑塊寬度的一半
sliderHalf = (float) (mContext.getResources().getDimension(R.dimen.sliderWidth) / 2.0);
paintDefault = new Paint();
paintDefault.setAntiAlias(true);
paintDefault.setColor(ContextCompat.getColor(mContext, R.color.seekBarBackgroundColor));
paintDefault.setStyle(Paint.Style.FILL);
paintFore = new Paint();
paintFore.setAntiAlias(true);
paintFore.setColor(ContextCompat.getColor(mContext, R.color.seekBarForegroundColor));
paintFore.setStyle(Paint.Style.FILL);
初始化Path及相關坐標:
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mViewWidth = w;
mViewHeight = h;
//獲取預設值
leftHeight = mContext.getResources().getDimension(R.dimen.leftHeight);
rightHeight = mContext.getResources().getDimension(R.dimen.rightHeight);
//計算進度條底部坐標
float bottomY = mViewHeight / 2 + rightHeight / 2;
//初始化背景色Path對象
pathDefault.moveTo(sliderHalf, bottomY);
pathDefault.lineTo(mViewWidth - sliderHalf, bottomY);
pathDefault.lineTo(mViewWidth - sliderHalf, bottomY - rightHeight);
pathDefault.lineTo(sliderHalf, bottomY - leftHeight);
pathDefault.close();
}
繪制
//計算進度條底邊的y坐標
float bottomY = mViewHeight / 2 + rightHeight / 2;
//計算當前progress對應的長度(減去初始高度leftHeight)
float topHeight = (rightHeight - leftHeight) * getProgress() / getMax();
//計算當前progress對應的右邊頂點y坐標
float rightTop = bottomY - topHeight - leftHeight;
//基準線每次切換的平均間隔
float dis = (float) (sliderHalf / (getMax() / 2.0));
//計算當前progress對應的像素,即x坐標,這個坐標是未校準的,不是一定處在滑塊的中間位置
float unCalibrationX = mViewWidth * getProgress() / getMax();
//計算出每次需要校準的距離,從sliderHalf長度像素逐次減少
float calibration = sliderHalf - getProgress() * dis;
float px = unCalibrationX + calibration;
//根據當前progress初始化Path對象
pathCurrent.reset();
pathCurrent.moveTo(sliderHalf, bottomY);
pathCurrent.lineTo(px, bottomY);
pathCurrent.lineTo(px, rightTop);
pathCurrent.lineTo(sliderHalf, bottomY - leftHeight);
pathCurrent.close();
//繪制底色背景
canvas.drawPath(pathDefault, paintDefault);
//繪制進度
canvas.drawPath(pathCurrent, paintFore);
完整代碼:
MySeekBar:
public class MySeekBar extends SeekBar {
private Context mContext;
/**
* SeekBar最大值
*/
private static final int MAX_VALUE = 100;
/**
* 背景色畫筆
*/
private Paint paintDefault;
/**
* 進度條畫筆
*/
private Paint paintFore;
/**
* 左邊初始高度
*/
private float leftHeight;
/**
* 右邊最大高度
*/
private float rightHeight;
/**
* 背景色Path對象
*/
private Path pathDefault = new Path();
/**
* 進度條Path對象
*/
private Path pathCurrent = new Path();
/**
* SeekBar寬度
*/
private int mViewWidth;
/**
* SeekBar高度
*/
private int mViewHeight;
/**
* 滑塊一半的寬度
*/
private float sliderHalf;
public MySeekBar(Context context) {
super(context);
mContext = context;
init();
}
public MySeekBar(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
init();
}
public MySeekBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mContext = context;
init();
}
/**
* 初始化
*/
private void init() {
setMax(MAX_VALUE);
//獲取滑塊寬度的一半
sliderHalf = (float) (mContext.getResources().getDimension(R.dimen.sliderWidth) / 2.0);
paintDefault = new Paint();
paintDefault.setAntiAlias(true);
paintDefault.setColor(ContextCompat.getColor(mContext, R.color.seekBarBackgroundColor));
paintDefault.setStyle(Paint.Style.FILL);
paintFore = new Paint();
paintFore.setAntiAlias(true);
paintFore.setColor(ContextCompat.getColor(mContext, R.color.seekBarForegroundColor));
paintFore.setStyle(Paint.Style.FILL);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mViewWidth = w;
mViewHeight = h;
//獲取預設值
leftHeight = mContext.getResources().getDimension(R.dimen.leftHeight);
rightHeight = mContext.getResources().getDimension(R.dimen.rightHeight);
//計算進度條底部坐標
float bottomY = mViewHeight / 2 + rightHeight / 2;
//初始化背景色Path對象
pathDefault.moveTo(sliderHalf, bottomY);
pathDefault.lineTo(mViewWidth - sliderHalf, bottomY);
pathDefault.lineTo(mViewWidth - sliderHalf, bottomY - rightHeight);
pathDefault.lineTo(sliderHalf, bottomY - leftHeight);
pathDefault.close();
}
@Override
protected synchronized void onDraw(Canvas canvas) {
/**
* 因為SeekBar在滑動的整個過程中,基準線是有一個漸變的過程的,這個寬度就是滑塊的寬度
* 在滑塊從0開始到(getMax() / 2.0)的次數內,也就是走完一半的距離時,基準線剛好對準滑塊的中間位置
* 利用這個規律,來校準滑塊的位置,使其不管滑到什么位置,都可以計算出其基準線的x坐標
*/
// //基準線每次切換的平均間隔
// float dis = (float) (sliderHalf / (getMax() / 2.0));
// //計算當前progress對應的像素,即x坐標,這個坐標是未校準的,不是一定處在滑塊的中間位置
// float unCalibrationX = mViewWidth * getProgress() / getMax();
// //計算出每次需要校準的距離,從sliderHalf長度像素逐次減少
// float calibration = sliderHalf - getProgress() * dis;
// float px = unCalibrationX + calibration;
// //繪制基準線
// canvas.drawLine(px, 0, px, mViewHeight, paintFore);
//繪制背景和進度
drawPath(canvas);
//super繪制滑塊
super.onDraw(canvas);
}
/**
* 繪制進度和背景
*
* @param canvas
*/
private void drawPath(Canvas canvas) {
//計算進度條底邊的y坐標
float bottomY = mViewHeight / 2 + rightHeight / 2;
//計算當前progress對應的長度(減去初始高度leftHeight)
float topHeight = (rightHeight - leftHeight) * getProgress() / getMax();
//計算當前progress對應的右邊頂點y坐標
float rightTop = bottomY - topHeight - leftHeight;
//基準線每次切換的平均間隔
float dis = (float) (sliderHalf / (getMax() / 2.0));
//計算當前progress對應的像素,即x坐標,這個坐標是未校準的,不是一定處在滑塊的中間位置
float unCalibrationX = mViewWidth * getProgress() / getMax();
//計算出每次需要校準的距離,從sliderHalf長度像素逐次減少
float calibration = sliderHalf - getProgress() * dis;
float px = unCalibrationX + calibration;
//根據當前progress初始化Path對象
pathCurrent.reset();
pathCurrent.moveTo(sliderHalf, bottomY);
pathCurrent.lineTo(px, bottomY);
pathCurrent.lineTo(px, rightTop);
pathCurrent.lineTo(sliderHalf, bottomY - leftHeight);
pathCurrent.close();
//繪制底色背景
canvas.drawPath(pathDefault, paintDefault);
//繪制進度
canvas.drawPath(pathCurrent, paintFore);
}
}
MainActivity:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
init();
}
private void init() {
final TextView mTextView = findViewById(R.id.textView);
final SeekBar mSeekBar = findViewById(R.id.seekbar);
//更新初始值
updateText(mTextView, 0);
mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
//監聽回調,開始更新
updateText(mTextView, progress);
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
});
}
/**
* 更新控件
*
* @param textView
* @param progress
*/
private void updateText(TextView textView, int progress) {
textView.setText(String.valueOf(progress));
}
}
另外,還有一些尺寸等參數如下:
<resources>
<!--進度條最左邊的高度-->
<dimen name="leftHeight">3dp</dimen>
<!--進度條最右邊的高度-->
<dimen name="rightHeight">20dp</dimen>
<!--SeekBar的滑塊的寬度-->
<dimen name="sliderWidth">30dp</dimen>
<!--SeekBar的高度-->
<dimen name="seekBarHeight">40dp</dimen>
</resources>
時間匆忙,很多細節沒有展開來講,如果有人需要可以來和我一起討論~