效果图
写在最前面
- 详细源码及demo地址:GradeLayout源码。
- 导进你的工程中直接使用:
compile 'jack.view:gradelayout:1.0'
- 上传到github中的已进行过拓展,可以动态更改一些属性,详见github的
README.md
。
实现前的分析
我们可以把整个布局分为两部分,一部分是上面的分数显示,一部分是下面的滑块显示。对于分数的显示我选择使用一个水平布局的LinearLayout
,使用addView
添加TextView
的方式来实现;对于滑块,可以使用Button
或者ImageView
来实现,那根黑色的线可以使用drawLine
来实现。整体来说,没什么难度,唯一要注意的地方是滑动冲突的解决。
实现步骤
1、编写属性文件attrs.xml代码
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="GradeLayout">
<!--刻度被选中时候的颜色-->
<attr name="grade_color_chosen" format="color" />
<!--刻度未被选中时候的颜色-->
<attr name="grade_color_unchosen" format="color" />
<!--刻度被选中后刻度的图标-->
<attr name="grade_ico_chosen" format="reference" />
<!--刻度未被选中时刻度的图标-->
<attr name="grade_ico_unchosen" format="reference" />
<!--刻度图标的宽高-->
<attr name="grade_ico_size" format="dimension" />
<!--刻度文字大小-->
<attr name="grade_text_size" format="dimension" />
<!--导航线未被选中部分的颜色-->
<attr name="nav_line_unchosen_color" format="color" />
<!--导航线被选中部分的颜色-->
<attr name="nav_line_chosen_color" format="color" />
<!--导航button的背景图片-->
<attr name="nav_button_ico" format="reference" />
<!--导航button的宽高-->
<attr name="nav_button_size" format="dimension" />
<!--刻度的数量-->
<attr name="max_grade" format="integer" />
<!--刻度图标和导航线之间的距离-->
<attr name="gap" format="dimension" />
<!--刻度图标和刻度文字之间的距离-->
<attr name="grade_ico_padding" format="dimension" />
<!--导航线被选中部分的宽度-->
<attr name="nav_line_chosen_width" format="dimension" />
<!--导航线未被选中部分的宽度-->
<attr name="nav_line_unchosen_width" format="dimension" />
</declare-styleable>
</resources>
2、自定义view类GradeLayout的编码实现
首先,我们需要在构造函数里对布局属性进行加载,当然我们也为一些属性设置了默认值,当用户没有指定属性的具体值得时候,就直接采用默认值。这部分代码如下:
private void loadAttrs(Context context, AttributeSet attrs) {
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.GradeLayout);
mMaxGrade = ta.getInt(R.styleable.GradeLayout_max_grade, DEFAULT_MAX_GRADE);
mGradeChosenColor =
ta.getColor(R.styleable.GradeLayout_grade_color_chosen, DEFAULT_CHOSEN_COLOR);
mGradeUnchosenColor =
ta.getColor(R.styleable.GradeLayout_grade_color_unchosen, DEFAULT_UNCHOSEN_COLOR);
mGradeChosenIcoId = ta.getResourceId(
R.styleable.GradeLayout_grade_ico_chosen, R.drawable.redpoint_icon);
mGradeUnchosenIcoId = ta.getResourceId(
R.styleable.GradeLayout_grade_ico_unchosen, R.drawable.graypoint_icon);
mGradeIcoSize = ta.getDimension(
R.styleable.GradeLayout_grade_ico_size, dip2px(context, DEFAULT_GRADE_ICO_SIZE));
mNavLineChosenColor = ta.getColor(
R.styleable.GradeLayout_nav_line_chosen_color, DEFAULT_CHOSEN_COLOR);
mNavLineUnchosenColor = ta.getColor(
R.styleable.GradeLayout_nav_line_unchosen_color, DEFAULT_UNCHOSEN_COLOR);
mNavButtonIcoId = ta.getResourceId(
R.styleable.GradeLayout_nav_button_ico, R.drawable.nav_button_icon);
mNavButtonSize = ta.getDimension(
R.styleable.GradeLayout_nav_button_size, dip2px(context, DEFAULT_NAV_BUTTON_SIZE));
mGap = ta.getDimension(R.styleable.GradeLayout_gap, dip2px(context, DEFAULT_GAP));
mGradeIcoPadding = ta.getDimension(R.styleable.GradeLayout_grade_ico_padding,
dip2px(context, DEFAULT_GRADE_ICO_PADDING));
mNavLineChosenWidth = ta.getDimension(R.styleable.GradeLayout_nav_line_chosen_width,
dip2px(context, DEFAULT_NAV_LINE_CHOSEN_WIDTH));
mNavLineUnchosenWidth = ta.getDimension(R.styleable.GradeLayout_nav_line_unchosen_width,
dip2px(context, DEFAULT_NAV_LINE_UNCHOSEN_WIDTH));
mGradeTextSize = ta.getDimension(R.styleable.GradeLayout_grade_text_size,
dip2px(context, DEFAULT_GRADE_TEXT_SIZE));
ta.recycle();
}
接下来,到了构造分数布局和滑块布局的时候了,实现代码如下:
//构建分数布局
LayoutParams params = new LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
LinearLayout gradeLy = new LinearLayout(context);
gradeLy.setLayoutParams(params);
gradeLy.setOrientation(HORIZONTAL);
try {
for (int i = 1; i <= mMaxGrade; i++) {
CharSequence str = mGradeTexts.get(i - 1);
TextView tv = buildTextView(context, str);
mTextViews.add(tv);
gradeLy.addView(tv);
}
} catch (NullPointerException e) {
Log.e(TAG, "Maybe mGradeTexts.size() != mMaxGrade", e);
}
addView(gradeLy);
//构建滑块布局
mPullButton = new ImageView(context);
mPullButton.setBackgroundDrawable(
context.getResources().getDrawable(mNavButtonIcoId));
mPullButtonParams
= new LayoutParams((int) mNavButtonSize, (int) mNavButtonSize);
mPullButtonParams.topMargin = (int)
addView(mPullButton, mPullButtonParams);
private TextView buildTextView(Context context, CharSequence grade) {
TextView textView = new TextView(context);
textView.setText(grade);
textView.setTextSize(px2dip(context, mGradeTextSize));
textView.setGravity(Gravity.CENTER_HORIZONTAL);
textView.setTextColor(mGradeUnchosenColor);
textView.setCompoundDrawables(null, null, null, mGradeUnchosenIco);
textView.setCompoundDrawablePadding((int) mGradeIcoPadding);
LayoutParams params =
new LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT);
params.weight = 1;
textView.setLayoutParams(params);
return textView;
}
最后需要画出下面的那根线,我们叫它导航线。要画出那根线,就必须要进行一些简单的计算,要保证导航线要和上面的文字对齐。另外需要注意的是,Android中的ViewGroup是默认不调用onDraw()
方法的,因此我们需要在构造函数中调用setWillNotDraw(false)
方法迫使ViewGroup调用onDraw()
,否则我们没有办法在onDraw()
里面进行划线。下面是详细代码。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mCanvas = canvas;
TextView first = mTextViews.get(0);
TextView last = mTextViews.get(mMaxGrade - 1);
int startX = first.getLeft() + (first.getRight() - first.getLeft()) / 2;
int startY = (mPullButton.getBottom() - mPullButton.getTop()) / 2 + mPullButton.getTop();
int grayEndX = last.getRight() - (last.getRight() - last.getLeft()) / 2;
int redEndX = mPullButton.getRight() - (mPullButton.getRight() - mPullButton.getLeft()) / 2;
//绘制滑动线段
drawLine(startX, startY, grayEndX, startY, mUnchosenPaint);
drawLine(startX, startY, redEndX, startY, mChosenPaint);
//绘制线段两端的圆弧
canvas.drawCircle(startX, startY, mNavLineChosenWidth / 2, mChosenPaint);
canvas.drawCircle(grayEndX, startY, mNavLineUnchosenWidth / 2, mUnchosenPaint);
//一开始只是在构造函数里使用addView将滑块加入了整体布局中,
//这样的话,滑块和最左边的分数是对不齐的,因此当走完onLayout方法后,
//选择在onDraw方法里对滑块的布局重新优化,达到与最左边分数对齐的效果。
//当然,实现的时候不一定非要选择在onDraw方法里实现。
if (isFirstDraw) {
isFirstDraw = false;
mExtraLeftMargin = startX - mPullButton.getRight() / 2;
mPullButtonParams.leftMargin = mExtraLeftMargin;
mPullButton.requestLayout();
MAX_LEFT_MARGIN = last.getLeft() + mExtraLeftMargin;
}
}
布局差不多都画完了,接下来要对滑块设置滑动监听,来处理滑动事件以及滑动冲突。构造函数中为滑块设置滑动监听:
mPullButton.setOnTouchListener(this);
onTouch
里处理滑动事件:
private int mLastX = 0;
@Override
public boolean onTouch(View v, MotionEvent event) {
requestDisallowInterceptTouchEvent(true);//处理滑动冲突,让父控件把滑动事件交给自己处理。
int x = (int) event.getRawX();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastX = x;
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
mPullButtonParams.leftMargin += deltaX;
if (mPullButtonParams.leftMargin < mExtraLeftMargin) {//左边界的限定
mPullButtonParams.leftMargin = mExtraLeftMargin;
}
if (mPullButtonParams.leftMargin > MAX_LEFT_MARGIN) {//右边界的限定
mPullButtonParams.leftMargin = MAX_LEFT_MARGIN;
}
mPullButton.requestLayout();
mLastX = x;
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
int nowMargin = mPullButtonParams.leftMargin;
//根据mPullButtonParams的leftMargin来计算出当前分数的index。
int index = (nowMargin * (mMaxGrade - 1) * 2 + MAX_LEFT_MARGIN)
/ (2 * MAX_LEFT_MARGIN) + 1;
updateUI(index - 1);//对UI进行更新
notifyGradeHasChanged(mTextViews.get(index - 1).getText().toString());//内部实现是通过接口通知当前选择的分数。
break;
default:
break;
}
return true;
}
当分数选择完成后我们通过接口来通知调用者:
public interface OnGradeUpdateListener {
void onGradeUpdate(GradeLayout view, String grade);
}
在Activity里实现此接口,并且设置监听即可。
Demo
详细的demo,已上传到了github中,demo文件夹下就是。点我直达