首先看一下最后实现的效果图
可左右滑动,点击加减按钮动态改变数值和位置显示。
思路:
1.根据图上UI知道有三种状态
<li>未滑中区域的灰色底部状态</li>
<li>滑中的颜色改变状态</li>
<li>椭圆的边框颜色,和内部字体显示</li>
实现:
首先我们自定义一个类MySeekBar继承View。并实现最重要的onDraw,onMeasure方法。
1.首先定义一个样式,用来以后设置底部颜色,滑中颜色,字体大小,颜色等
资源样式
<declare-styleable name="MySeekBar">
<attr name="mTopBarColor" format="color"></attr> 滑动时候横条颜色
<attr name="mBottomBarColor" format="color"></attr>默认的横条颜色
<attr name="strokeColor" format="color"></attr>椭圆边框颜色
<attr name="month" format="boolean"></attr>判断是按年份显示还是月份显示
<attr name="mytext" format="string"></attr>获得textview显示的后缀
</declare-styleable>
2.实现MySeekBar构造器。初始化资源,与需要的画笔。(注意获得资源后一定要使用recycle(),去进行回收)
public MySeekBar(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray attr = getTypedArray(context, attrs, R.styleable.MySeekBar);
mTopBarColor = attr.getColor(R.styleable.MySeekBar_mTopBarColor, getResources().getColor(R.color.color_EB7046));//设置滑动时候横条颜色
mBottomBarColor = attr.getColor(R.styleable.MySeekBar_mBottomBarColor, getResources().getColor(R.color.login_font));//默认的横条颜色
month = attr.getBoolean(R.styleable.MySeekBar_month, false);//判断是按年份显示还是月份显示
myText = attr.getString(R.styleable.MySeekBar_mytext);//获得textview显示的后缀
mContext = context;
Data(Calendar.getInstance());//得到当前的日期
windows_width = DisplayMetricsTool.getWidth(mContext);//获得屏幕宽度用来适配各种屏幕
dip_size = DisplayMetricsTool.dip2px(mContext, 75);
//矩形的白色底部画笔
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint = new Paint(); //设置一个笔刷大小是3的黄色的画笔
paint.setStyle(Paint.Style.FILL);//充满
paint.setColor(Color.WHITE);
paint.setAntiAlias(true);// 设置画笔的锯齿效果
//矩形边框的的画笔
whit = new Paint(); //设置一个笔刷大小是3的黄色的画笔
whit.setStyle(Paint.Style.STROKE);
whit.setColor(mTopBarColor);
whit.setStrokeWidth(1);
whit.setAntiAlias(true);// 设置画笔的锯齿效果
//设置字体
textPaint = new Paint();
textPaint.setColor(mTopBarColor);
textPaint.setTextSize(DisplayMetricsTool.canvasTextSize(mContext));
textPaint.setStyle(Paint.Style.FILL);
//该方法即为设置基线上那个点究竟是left,center,还是right 这里我设置为center
textPaint.setTextAlign(Paint.Align.CENTER);
textPaint.setAntiAlias(true);
attr.recycle();
}
3.自定义一个方法设置进度值,默认满进度100.
/**
* 顶部进度条初始百分比进度
*/
private float mOriginalPercent = 0f;
/**
* 设置初始进度值,默认进度最大值为100
*
* @param progress
*/
public void setOriginalProgress(int progress) {
if (month) {//按年显示
progress = 100 - (year - progress);//总进度减去今年和传入年份的差值
} else {
//按月显示
progress = (int) (progress * 8.4);//12月占100的百分比8.4
}
mOriginalPercent = progress / 100f;
if (mOriginalPercent < 0) {
mOriginalPercent = 0.0f;
} else if (mOriginalPercent > 1.0f) {
mOriginalPercent = 1.0f;
}
}
4.需要在onMeasure方法中计算,当前传入的进度换算之后占在屏幕的哪个位置。
//获得密度转像素的大小
private int dip_size;
// view的宽
private int mViewWidth;
// view的高
private int mViewHeight;
// 底部进度条的宽
private int mBottomBarWidth;
//底部进度条左边位置
private int mBottomBarLeft;
// 底部进度条上边位置
private int mBottomBarTop;
// 底部进度条右边位置
private int mBottomBarRight;
// 底部进度条底边位置
private int mBottomBarBottom;
// 顶部进度条的右边位置
private int mTopBarRight;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (widthMode == MeasureSpec.AT_MOST) {
mViewWidth = mTvWidth * 4;
} else if (widthMode == MeasureSpec.EXACTLY) {
if (mTvWidth * 4 >= widthSize) {
mViewWidth = mTvWidth * 4;
} else {
mViewWidth = widthSize;
}
}
widthMeasureSpec = MeasureSpec.makeMeasureSpec(mViewWidth, MeasureSpec.EXACTLY);
if (heightMode == MeasureSpec.AT_MOST) {
mViewHeight = mTvHeight + 4 * mPadding;
} else if (heightMode == MeasureSpec.EXACTLY) {
if (heightSize <= mTvHeight + 4 * mPadding) {
mViewHeight = mTvHeight + 4 * mPadding;
} else {
mViewHeight = heightSize;
}
}
heightMeasureSpec = MeasureSpec.makeMeasureSpec(mViewHeight, MeasureSpec.EXACTLY);
mBottomBarWidth = mViewWidth - 2 * mTvWidth - 6 * mPadding;
mBottomBarLeft = mTvWidth + 3 * mPadding;
mBottomBarRight = mViewWidth - mTvWidth - 3 * mPadding;
mBottomBarBottom = (mViewHeight - mTvHeight) / 2 + mTvHeight;
mBottomBarTop = mBottomBarBottom - mBarHeight;
mCircleY = mBottomBarBottom - mBarHeight / 2;//圆心的坐标
mPosition = (int) Math.round((mBottomBarWidth - DisplayMetricsTool.getWidth(mContext) / 6) * mOriginalPercent + 0.5) + mBottomBarLeft;
if (mPosition <= mBottomBarLeft) {
mPosition = mBottomBarLeft;
} else if (mPosition >= mBottomBarRight) {
mPosition = mBottomBarRight;
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
5.因为需要左右滑动,所以我们采用GestureDetector的手势滑动管理,判断当用户点击在椭圆中才能让滑动改变
GestureDetector mGestureDetector = new GestureDetector(mContext, new GestureDetector.OnGestureListener() {
@Override
public boolean onSingleTapUp(MotionEvent e) {
if (mWhereClickedState == WHERR_CLICKED_CIRCLE) {
} else if (mWhereClickedState == WHERR_CLICKED_BAR) {
mPosition = (int) Math.round(e.getX() + 0.5);
if (mPosition >= mBottomBarRight) {
mPosition = mBottomBarRight;
} else if (mPosition <= mBottomBarLeft) {
mPosition = mBottomBarLeft;
}
MySeekBar.this.invalidate();
}
return false;
}
@Override
public void onShowPress(MotionEvent e) {
// TODO Auto-generated method stub
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
float distanceY) {
/* if (mWhereClickedState == WHERR_CLICKED_CIRCLE) {*/
mPosition = (int) Math.round(e2.getX() + 0.5);
if (mPosition >= mBottomBarRight) {
mPosition = mBottomBarRight;
} else if (mPosition <= mBottomBarLeft) {
mPosition = mBottomBarLeft;
}
MySeekBar.this.invalidate();
/* } else if (mWhereClickedState == WHERR_CLICKED_BAR) {
}*/
return false;
}
@Override
public void onLongPress(MotionEvent e) {
// TODO Auto-generated method stub
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
float velocityY) {
// TODO Auto-generated method stub
return false;
}
@Override
public boolean onDown(MotionEvent e) {
float event_x = e.getX();
float event_y = e.getY();
mWhereClickedState = judgeWhereClicked(event_x, event_y);
if (mWhereClickedState == WHERR_CLICKED_VIEW) {
return false;
} else {
return true;
}
}
});
/**
* 判断用户点击位置状态
*
* @param x 用户点击的x坐标
* @param y 用户点击的y坐标
* @return 返回用户点击未知状态:
* WHERR_CLICKED_CIRCLE
* WHERR_CLICKED_BAR
* WHERR_CLICKED_VIEW
*/
public int judgeWhereClicked(float x, float y) {
if (rectF.contains(x, y)) {
return WHERR_CLICKED_CIRCLE; //s_x和s_y界定的区域,即圆圈区域
} else {
return WHERR_CLICKED_VIEW; //view除了上述两部分的部分
}
}
6.现在画东西的笔有了,架子有了,装饰材料有了,只剩往一块白布里面画东西。就成了。 接下来最重要的就是在onDraw方法中,把UI画上去。
@Override
protected void onDraw(Canvas canvas) {
mTopBarRight = mPosition;
mCircleX = mTopBarRight;
mPaint.setColor(mBottomBarColor);
canvas.drawRect(mBottomBarLeft, mBottomBarTop, mBottomBarRight, mBottomBarBottom, mPaint);
mPaint.setColor(mTopBarColor);
canvas.drawRect(mBottomBarLeft, mBottomBarTop, mTopBarRight, mBottomBarBottom, mPaint);
if (mBottomBarRight - mTopBarRight > windows_width / 6 - 10) {//用来防止百分比是100的时候椭圆形画出边界的问题。
aaa = mTopBarRight;
rectF = new RectF(mTopBarRight, dip_size / 2 - dip_size / 5, mTopBarRight + windows_width / 6, dip_size / 2 + dip_size / 5);
canvas.drawRoundRect(rectF,
(windows_width / 6) / 3, //x轴的半径
(windows_width / 6) / 3 - 5, //y轴的半径
paint);//用来画矩形白色背景
canvas.drawRoundRect(rectF,
(windows_width / 6) / 3, //x轴的半径
(windows_width / 6) / 3 - 5, //y轴的半径
whit);//用来画矩形边框
} else {
rectF = new RectF(aaa, dip_size / 2 - dip_size / 5, aaa + windows_width / 6, dip_size / 2 + dip_size / 5);
canvas.drawRoundRect(rectF,
(windows_width / 6) / 3, //x轴的半径
(windows_width / 6) / 3 - 5, //y轴的半径
paint);//用来画矩形白色背景
canvas.drawRoundRect(rectF,
(windows_width / 6) / 3, //x轴的半径
(windows_width / 6) / 3 - 5, //y轴的半径
whit);//用来画矩形边框
}
Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();
float top = fontMetrics.top;//为基线到字体上边框的距离,即上图中的top
float bottom = fontMetrics.bottom;//为基线到字体下边框的距离,即上图中的bottom
int baseLineY = (int) (rectF.centerY() - top / 2 - bottom / 2);//基线中间点的y轴计算公式
canvas.drawText(getProgress() + myText, rectF.centerX(), baseLineY, textPaint);
}
7.获得当前进度由于这个项目是按年或者月显示的,所以获取进度时和设置进度时都需要按照满进度值100来进行换算,设置获得进度的方法getProgress()。
/**
* 获得当前进度值
*
* @return int
*/
public int getProgress() {
float percent = (mPosition - mBottomBarLeft) * 1.0f / (mBottomBarWidth -windows_width/ 6);
if (percent < 0.0) {
percent = 0f;
} else if (percent > 1) {
percent = 1f;
}
int progress = ((int) Math.round(percent * 100 + 0.5) - 1);
if (progress <= 0) {
progress = 1;
} else if (progress > 100) {
progress = 100;
}
if (month) {
return year - (100 - progress);
} else {
return progress / 8 == 0 ? 1 : progress / 8;
}
}
8.项目需要按左右按钮的时候必要你动态设置进度值,并改变椭圆的位置,那么我们只需要改变进度值,然后不断去onDraw重新绘制界面就行了。代码如下
/**
* 通过传入进度值来更新进度条
*/
public void updatePositionFromProgress(int progress) {
if (month) {//按年显示
progress = 100 - (year - progress);//总进度减去今年和传入年份的差值
} else {
//按月显示
progress = (int) (progress * 8.4);
}
float percent = progress / 100f;
if (percent < 0) {
percent = 0.0f;
} else if (percent > 1.0f) {
percent = 1.0f;
}
mPosition = (int) Math.round((mBottomBarWidth -windows_width / 6)* percent + 0.5) + mBottomBarLeft;
if (mPosition <= mBottomBarLeft) {
mPosition = mBottomBarLeft;
} else if (mPosition >= mBottomBarRight) {
mPosition = mBottomBarRight;
}
this.invalidate();
}
#######此时基本的实现已经完成。使用方式基本就是在XML设置颜色,字体之类的事,然后动态去设置进度--->代码如下
<ui.view.MySeekBar
android:id="@+id/seekbar"
android:layout_width="match_parent"
android:layout_height="75dp"
android:layout_centerVertical="true"
android:layout_toLeftOf="@+id/right_ll"
android:layout_toRightOf="@+id/left_ll"
app:mTopBarColor="@color/color_EB7046"
app:month="true"
app:mytext="年" />
year = calendar.get(Calendar.YEAR);//当前的年
seekbar = (MySeekBar) view.findViewById(R.id.seekbar);
seekbar.setOriginalProgress(year);
至此整个功能差不多已经完成。刚开始的时候也是研究了网上有关的项目去进行研究,然后自己重新更改封装,弄成符合自己项目需要东西,希望能给有需要的同学一点思路,不要想轮子能完全给你解决项目的难点,而是提供给你一个解决问题的思路,google,github,爆栈等等,有难点的时候大家可以好好利用。
<li>下一篇准备写自定义recyclerview的上拉加载功能,因为看了网上大多数的开源项目,基本上都是华丽但不实用,功能太多真正能在项目用得上的很少,增加了方法树,包的大小,所以将会写一个只有上拉加载的recyclerview功能<li>