自定義View系列-滑動選擇分數或者刻度
阿新 • • 發佈:2019-01-30
效果圖
寫在最前面
- 導進你的工程中直接使用:
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資料夾下就是。點我直達