1. 程式人生 > >自定義View系列-滑動選擇分數或者刻度

自定義View系列-滑動選擇分數或者刻度

效果圖

效果圖

寫在最前面

  • 導進你的工程中直接使用: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資料夾下就是。點我直達