android自定義控件(附例子)

最近閑的很,如是寫(xiě)下了本篇博客。

自定義控件基本步驟如下,反正基本上我覺(jué)得按我這個(gè)套路來(lái)寫(xiě),基本上都能很快上手。

推薦按順序閱讀。。
第一部分,構(gòu)造函數(shù),我相信這個(gè)大家都應(yīng)該是明白的。初始化和自定義屬性無(wú)關(guān)必要的成員變量,mDensity是屏幕密度,將dp轉(zhuǎn)px時(shí)要用到。

/**
     * 第一部分 構(gòu)造函數(shù)  所有的自定義view都可以用下面這段代碼直接copy改下名字就行
     **/

    //當(dāng)不需要使用xml聲明或者不需要使用inflate動(dòng)態(tài)加載時(shí)候,實(shí)現(xiàn)此構(gòu)造函數(shù)即可
    public RulerView(Context context) {
        super(context, null);
    }

    //當(dāng)需要在xml中聲明此控件,則需要實(shí)現(xiàn)此構(gòu)造函數(shù)。并且在構(gòu)造函數(shù)中把自定義的屬性與控件的數(shù)據(jù)成員連接起來(lái)。
    public RulerView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    //接受一個(gè)style資源
    public RulerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //所有的構(gòu)造函數(shù)都會(huì)匯總到這一步,API21的會(huì)走到一個(gè)包含4個(gè)參數(shù)的構(gòu)造函數(shù)。
        mContext = context;
        mDensity = mContext.getResources().getDisplayMetrics().density;
        initRulerView(attrs);
    }

第二部分,獲取自定義屬性,初始化需要獲取自定義屬性之后的畫(huà)筆等等之類(lèi)的成員變量。
值得注意的是,這個(gè)地方獲取屬性值的時(shí)候有2種寫(xiě)法,一種是我注釋了的,一種是沒(méi)有注釋的,寫(xiě)法的區(qū)別是:我注釋過(guò)的那段代碼,如果屬性在使用控件的時(shí)候沒(méi)有在xml中使用的話(huà),賦值的方法是不走的。那么就沒(méi)有默認(rèn)值。。我沒(méi)注釋的那種寫(xiě)法是肯定會(huì)走,獲取不到就給默認(rèn)值。推薦第二種,以免出錯(cuò)。

<declare-styleable name="RulerView">
此處的name我看過(guò)很多博客,都要么沒(méi)說(shuō),要么說(shuō)這個(gè)名字隨便寫(xiě)。其實(shí)不對(duì),這個(gè)名字,
代表了在這個(gè)declare-styleable之間的屬性只能在RulerView中使用,其他的空間是看
不到這個(gè)屬性的,同學(xué)們可以去試一下

關(guān)于format值的一些說(shuō)明

1,reference :通過(guò)@dimen  @color  @drawable @layout 等等獲取屬性值

2,color : 顏色值  "#ff0" "#ff0000"  "#0ff" "#00ff00000"

3,boolean :布爾值  true false

4, dimension : 尺寸 50dp  50px 50sp

5,float :浮點(diǎn)值  0.5

6,integer : 整型值  30

7,string : 字符串  abs

8,fraction :百分?jǐn)?shù) 25%

9,enum :枚舉

10,flag :位或運(yùn)算
 /**
     * 第二部分,獲取自定義屬性
     *
     * @param attrs
     */
    private void initRulerView(AttributeSet attrs) {


        if (null != attrs) {
            TypedArray ta = mContext.obtainStyledAttributes(attrs, R.styleable.RulerView);
//            int count = ta.getIndexCount();
//
//            for (int i = 0; i < count; i++) {
//                int attr = ta.getIndex(i);
//                switch (attr) {
//                    case R.styleable.RulerView_cursorColor:
//                        mCursorColor = ta.getColor(attr, DEFAULT_CURSOR_COLOR);
//                        break;
//
//                }
//            }

            mBorderWidth = ta.getDimension(R.styleable.RulerView_borderWidth, CustomViewUtil.dp2px(mContext, BORDER_WIDTH));
            mCornerRadius = ta.getDimension(R.styleable.RulerView_cornerRadius, CustomViewUtil.dp2px(mContext, CORNER_RADIUS));
            mBorderColor = ta.getColor(R.styleable.RulerView_borderColor, BORDER_COLOR);
            mCursorWidth = ta.getDimension(R.styleable.RulerView_cursorWidth, CustomViewUtil.dp2px(mContext, DEFAULT_CURSOR_WIDTH));

            if (ta.getDrawable(R.styleable.RulerView_cursorDrawable) == null) {
                mCursorColor = ta.getColor(R.styleable.RulerView_cursorColor, DEFAULT_CURSOR_COLOR);
            } else {
                mCursorDrawable = ta.getDrawable(R.styleable.RulerView_cursorDrawable);
            }

            mCalibrationTailsLength = ta.getDimension(R.styleable.RulerView_calibrationTailsLength, CustomViewUtil.dp2px(mContext, CALIBRATION_TAILS_LENGTH));
            mCalibrationTailsWidth = ta.getDimension(R.styleable.RulerView_calibrationTailsWidth, CustomViewUtil.dp2px(mContext, CALIBRATION_TAILS_WIDTH));
            mCalibrationTailsColor = ta.getColor(R.styleable.RulerView_calibrationTailsColor, CALIBRATION_TAILS_COLOR);
            mCalibrationTailsDistance = ta.getDimension(R.styleable.RulerView_calibrationTailsDistance, CustomViewUtil.dp2px(mContext, CALIBRATION_TAILS_DISTANCE));

            mImportantTailsLength = ta.getDimension(R.styleable.RulerView_importantTailsLength, CustomViewUtil.dp2px(mContext, IMPORTANT_TAILS_LENGTH));
            mImportantTailsWidth = ta.getDimension(R.styleable.RulerView_importantTailsWidth, CustomViewUtil.dp2px(mContext, IMPORTANT_TAILS_WIDTH));
            mImportantTailsColor = ta.getColor(R.styleable.RulerView_importantTailsColor, IMPORTANT_TAILS_COLOR);
            mImportantTailsGap = ta.getInteger(R.styleable.RulerView_importantTailsGap, IMPORTANT_TAILS_GAP);

            maxValue = ta.getInteger(R.styleable.RulerView_maxValue, MAX_VALUE);
            minValue = ta.getInteger(R.styleable.RulerView_minValue, MIN_VALUE);
            currentValue = ta.getInteger(R.styleable.RulerView_currentValue, DEFAULT_VALUE);

            mValueDistanceImportantTails = ta.getDimension(R.styleable.RulerView_valueDistanceImportantTails, CustomViewUtil.dp2px(mContext, VALUE_DISTANCE_IMPORTANT_TAILS));
            mValueColor = ta.getColor(R.styleable.RulerView_valueColor, VALUE_COLOR);
            mValueSize = ta.getDimension(R.styleable.RulerView_valueSize, CustomViewUtil.sp2px(mContext, VALUE_SIZE));

            dampNumber = ta.getFloat(R.styleable.RulerView_dampNumber, DAMP_NUMBER);

            ta.recycle();

        }

        if (mCalibrationTailsDistance <= Math.max(mImportantTailsWidth, mCalibrationTailsWidth)) {
            throw new IllegalArgumentException("mCalibrationTailsDistance must bigger than Math.max(mImportantTailsWidth,mCalibrationTailsWidth)");
        }

        mCalibrationTailsPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
        mImportantTailsPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mCursorPaint = new Paint(Paint.ANTI_ALIAS_FLAG);


        mCalibrationTailsPaint.setColor(mCalibrationTailsColor);
        mCalibrationTailsPaint.setStrokeWidth(mCalibrationTailsWidth);

        mImportantTailsPaint.setColor(mImportantTailsColor);
        mImportantTailsPaint.setStrokeWidth(mImportantTailsWidth);

        mCursorPaint.setColor(mCursorColor);
        mCursorPaint.setStrokeWidth(mCursorWidth);

        mTextPaint.setTextSize(mValueSize);
        mTextPaint.setColor(mValueColor);
        mTextPaint.setStyle(Paint.Style.FILL);

        mTouchSlop = ViewConfiguration.get(mContext).getScaledPagingTouchSlop();
        mScroller = new Scroller(mContext);
        mMinVelocity = ViewConfiguration.get(getContext()).getScaledMinimumFlingVelocity();

        rectOfBackGround = new Rect();
        setColors(colors);
    }

第三部分,測(cè)量寬高,有點(diǎn)同學(xué)這一部分很是艱難,但是不用怕,賦值一下代碼,重寫(xiě)其中的2個(gè)方法就行了

    /**
     * 第三部分,測(cè)量控件大小,一般情況下,前面的方法可以直接copy過(guò)去,只需要自己重寫(xiě)measureWrapWitdh,measureWrapHeight
     *
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //這一部分,可寫(xiě)可不寫(xiě),如若你不想重寫(xiě)控件大小,直接用super就行。如果需要重寫(xiě)的話(huà)套路也很簡(jiǎn)單
//        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int measureWidth = measureWitdh(widthMeasureSpec);
        int measureHeight = measureHeight(heightMeasureSpec);
        setMeasuredDimension(measureWidth, measureHeight);
    }

    private int measureWitdh(int widthMeasureSpec) {
        int result = 0;
        int specMode = MeasureSpec.getMode(widthMeasureSpec);
        int specSize = MeasureSpec.getSize(widthMeasureSpec);

        switch (specMode) {
            case MeasureSpec.UNSPECIFIED:
                result = measureWrapWitdh();
                break;
            case MeasureSpec.AT_MOST:
                result = Math.min(measureWrapWitdh(), specSize);
                break;
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
        }
        return result;

    }

    private int measureWrapWitdh() {
        //測(cè)量wrapContent時(shí)寬度,這個(gè)請(qǐng)根據(jù)自己的需求自己計(jì)算,我這里默認(rèn)300dp
        return CustomViewUtil.dp2px(mContext, 300);
    }

    private int measureHeight(int heightMeasureSpec) {
        int result = 0;
        int specMode = MeasureSpec.getMode(heightMeasureSpec);
        int specSize = MeasureSpec.getSize(heightMeasureSpec);

        switch (specMode) {
            case MeasureSpec.UNSPECIFIED:
                result = measureWrapHeight();
                break;
            case MeasureSpec.AT_MOST:
                result = Math.min(measureWrapHeight(), specSize);
                break;
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
        }
        return result;
    }

    private int measureWrapHeight() {
        //測(cè)量wrapContent時(shí)高度,這個(gè)請(qǐng)根據(jù)自己的需求自己計(jì)算,我這里默認(rèn)50dp
        return CustomViewUtil.dp2px(mContext, 50);
    }

第四部分,在控件發(fā)生變化的時(shí)候 重新賦值寬高,以及重新為一些需要在控件大小發(fā)生變化時(shí)需要充值賦值的屬性賦值。

 /**
     * 第四部分,在size發(fā)生變化時(shí),重新賦值寬高
     *
     * @param w
     * @param h
     * @param oldw
     * @param oldh
     */
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth = w;
        mHeight = h;
        LayerDrawable background = (LayerDrawable) getBackground();
        Drawable drawable = background.getDrawable(0);
        drawable.getPadding(rectOfBackGround);
    }

第五部分,屬于核心以及難點(diǎn)吧,具體實(shí)現(xiàn)我就不說(shuō)了,自己去看代碼。其實(shí)代碼也沒(méi)啥看的,就是通過(guò)計(jì)算位置繪制而已。但是這個(gè)計(jì)算位置是很精細(xì),很容易出錯(cuò)的地方,得加倍小心

 /**
     * 第五部分,繪制,繪制時(shí),建議分層重下往上繪制
     *
     * @param canvas
     */
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        drawCalibrationTails(canvas);
        drawCursor(canvas);
    }

    private void drawCalibrationTails(Canvas canvas) {

        //計(jì)算需要畫(huà)多少刻度線(xiàn)
        //1,先計(jì)算可畫(huà)刻度線(xiàn)區(qū)域的總長(zhǎng)度


        int totalLengthCanDrawCalibrationTails = (int) (mWidth - rectOfBackGround.left - rectOfBackGround.right - 2 * mBorderWidth);
        int count = (int) (totalLengthCanDrawCalibrationTails / mCalibrationTailsWidth);


        canvas.save();


        for (int i = 0; i < count; i++) {
            int offset = i % 2 == 0 ? (i + 1) / 2 : -(i + 1) / 2;

            if (currentValue + offset > maxValue || currentValue + offset < minValue) {
                continue;
            } else {

                //畫(huà)刻度線(xiàn)

                if (i == 0) {
                    if ((currentValue + offset) % mImportantTailsGap == 0) {
                        canvas.drawLine(mWidth / 2, rectOfBackGround.top + mBorderWidth, mWidth / 2, rectOfBackGround.top + mBorderWidth + mImportantTailsLength, mImportantTailsPaint);
                    } else {
                        canvas.drawLine(mWidth / 2, rectOfBackGround.top + mBorderWidth, mWidth / 2, rectOfBackGround.top + mBorderWidth + mCalibrationTailsLength, mCalibrationTailsPaint);
                    }
                } else if (i % 2 == 0) {

                    if (mWidth / 2 + ((i + 1) / 2) * mCalibrationTailsDistance >= mWidth - rectOfBackGround.right - mBorderWidth) {
                        continue;
                    }

                    if ((currentValue + offset) % mImportantTailsGap == 0) {
                        canvas.drawLine(mWidth / 2 + ((i + 1) / 2) * mCalibrationTailsDistance, rectOfBackGround.top + mBorderWidth, mWidth / 2 + ((i + 1) / 2) * mCalibrationTailsDistance, rectOfBackGround.top + mBorderWidth + mImportantTailsLength, mImportantTailsPaint);
                    } else {
                        canvas.drawLine(mWidth / 2 + ((i + 1) / 2) * mCalibrationTailsDistance, rectOfBackGround.top + mBorderWidth, mWidth / 2 + ((i + 1) / 2) * mCalibrationTailsDistance, rectOfBackGround.top + mBorderWidth + mCalibrationTailsLength, mCalibrationTailsPaint);
                    }

                } else {
                    if (mWidth / 2 - ((i + 1) / 2) * mCalibrationTailsDistance <= rectOfBackGround.left + mBorderWidth) {
                        continue;
                    }

                    if ((currentValue + offset) % mImportantTailsGap == 0) {
                        canvas.drawLine(mWidth / 2 - ((i + 1) / 2) * mCalibrationTailsDistance, rectOfBackGround.top + mBorderWidth, mWidth / 2 - ((i + 1) / 2) * mCalibrationTailsDistance, rectOfBackGround.top + mBorderWidth + mImportantTailsLength, mImportantTailsPaint);
                    } else {
                        canvas.drawLine(mWidth / 2 - ((i + 1) / 2) * mCalibrationTailsDistance, rectOfBackGround.top + mBorderWidth, mWidth / 2 - ((i + 1) / 2) * mCalibrationTailsDistance, rectOfBackGround.top + mBorderWidth + mCalibrationTailsLength, mCalibrationTailsPaint);
                    }
                }

                //畫(huà)刻度線(xiàn)下面的文字
                if ((currentValue + offset) % mImportantTailsGap == 0) {
                    if ((mWidth / 2 + offset * mCalibrationTailsDistance + mTextPaint.measureText(String.valueOf(currentValue + offset)) / 2) > mWidth - rectOfBackGround.right - mBorderWidth) {
                        continue;
                    }

                    if ((mWidth / 2 + offset * mCalibrationTailsDistance - mTextPaint.measureText(String.valueOf(currentValue + offset)) / 2) < rectOfBackGround.left + mBorderWidth) {
                        continue;
                    }

                    canvas.drawText(String.valueOf(currentValue + offset), mWidth / 2 + offset * mCalibrationTailsDistance - mTextPaint.measureText(String.valueOf(currentValue + offset)) / 2, CustomViewUtil.drawTextFromTop(rectOfBackGround.top + mBorderWidth + Math.max(mCalibrationTailsLength, mImportantTailsLength) + mValueDistanceImportantTails, mTextPaint), mTextPaint);
                }
            }

        }


        canvas.restore();
    }

    private void drawCursor(Canvas canvas) {
        canvas.save();
        if (mCursorDrawable != null) {
            mCursorDrawable.setBounds(mWidth / 2 - mCursorDrawable.getIntrinsicWidth() / 2, (int) (rectOfBackGround.top + mBorderWidth), mWidth / 2 + mCursorDrawable.getIntrinsicWidth() / 2, (int) (rectOfBackGround.top + mBorderWidth + mCursorDrawable.getIntrinsicHeight()));
            mCursorDrawable.draw(canvas);
        } else {
            mCursorPaint.setColor(mCursorColor);
            mCursorPaint.setStrokeWidth(mCursorWidth);
            mCursorPaint.setStyle(Paint.Style.FILL_AND_STROKE);
            canvas.drawLine(mWidth / 2, 0 + rectOfBackGround.top + mBorderWidth, mWidth / 2, mHeight - rectOfBackGround.bottom - mBorderWidth, mCursorPaint);
        }
        canvas.restore();
    }

第六部分:這個(gè)不解釋了

/**
     * 第六部分
     * 設(shè)置數(shù)據(jù),用以第一次賦值,或者在listView recycleView復(fù)用的時(shí)候賦值,或者在交互中重新賦值等等
     */
    public void setData(int currentValue) {
        if (currentValue > maxValue || currentValue < minValue) {
            throw new IllegalArgumentException("the value must between minValue and maxValue");
        }

        this.currentValue = currentValue;
        invalidate();
        if (mListener != null) {
            mListener.onValueChange(currentValue);
        }if (mListener != null) {
            mListener.onValueChange(currentValue);
        }
    }

第七部分 重難點(diǎn)部分,但是也很簡(jiǎn)單。注意true和false的返回,

 /**
     * 第七部分
     * 處理觸摸事件
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(event);

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mScroller.forceFinished(true);
                mLastX = (int) event.getX();
                mMove = 0;
                isMove = false;
                break;
            case MotionEvent.ACTION_MOVE:
                mCurrentX = (int) event.getX();
                mMove += (int) ((mLastX - mCurrentX) * dampNumber);

                if (mMove < mTouchSlop) {
                    isMove = true;
                }

                if (currentValue > maxValue) {
                    return false;
                }

                if (currentValue < minValue) {
                    return false;
                }

                if (isMove) {
                    changeMoveAndValue();
                }
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                isMove = false;
                countVelocityTracker(event);
                break;
        }

        mLastX = (int) event.getX();

        return true;
    }

第八部分,控件滑動(dòng)。Scroller和VelocityTracker的使用

 /**
     * 第八部分處理滑動(dòng)相關(guān)的事情
     */
    private void changeMoveAndValue() {
        int tValue = (int) (mMove / mCalibrationTailsDistance);
        if (Math.abs(tValue) > 0) {
            currentValue = currentValue + tValue;
            mMove -= tValue * mCalibrationTailsDistance;
            if (currentValue < minValue || currentValue > maxValue) {
                currentValue = currentValue < minValue ? minValue : maxValue;
                mMove = 0;
                mScroller.forceFinished(true);
            }
            notifyValueChange(currentValue);
        }
        postInvalidate();
    }

    private void countMoveEnd() {
        int roundMove = Math.round(mMove / mCalibrationTailsDistance);
        currentValue = currentValue + roundMove;
        if (currentValue < minValue) {
            currentValue = minValue;
        }

        if (currentValue > maxValue) {
            currentValue = maxValue;
        }

        mLastX = 0;
        mMove = 0;

        notifyValueChange(currentValue);
        postInvalidate();
    }


    private int mLastScrollX;

    
    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mScroller.computeScrollOffset()) {
            mCurrentX = mScroller.getCurrX();

            if (mScroller.getCurrX() == mScroller.getFinalX()) { // over
                countMoveEnd();
            } else {
                mMove += (mLastScrollX - mCurrentX);
                changeMoveAndValue();
            }
            mLastScrollX = mCurrentX;
        }
    }

    private void countVelocityTracker(MotionEvent event) {
//        mVelocityTracker.addMovement(event);
        mVelocityTracker.computeCurrentVelocity(200);
        float xVelocity = mVelocityTracker.getXVelocity();


        if (Math.abs(xVelocity) > mMinVelocity) {
            mScroller.fling(0, 0, (int) xVelocity, 0, Integer.MIN_VALUE, Integer.MAX_VALUE, 0, 0);
        }
        postInvalidate();
    }

最后

 /**
     * 定義相關(guān)的監(jiān)聽(tīng)器
     */

    private OnValueChangeListener mListener;

    public void setOnValueChangeListener(OnValueChangeListener mListener) {
        this.mListener = mListener;
    }

    public void notifyValueChange(int value) {
        if (null != mListener) {
            mListener.onValueChange(value);
        }
    }


    public interface OnValueChangeListener {
        void onValueChange(int value);
    }

實(shí)際上還有一部分是動(dòng)畫(huà),但是本例中未曾用到,以后再說(shuō)吧

demo

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容