android自定義控件(附例子)

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

自定義控件基本步驟如下,反正基本上我覺得按我這個套路來寫,基本上都能很快上手。

推薦按順序閱讀。。
第一部分,構造函數,我相信這個大家都應該是明白的。初始化和自定義屬性無關必要的成員變量,mDensity是屏幕密度,將dp轉px時要用到。

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

    //當不需要使用xml聲明或者不需要使用inflate動態加載時候,實現此構造函數即可
    public RulerView(Context context) {
        super(context, null);
    }

    //當需要在xml中聲明此控件,則需要實現此構造函數。并且在構造函數中把自定義的屬性與控件的數據成員連接起來。
    public RulerView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

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

第二部分,獲取自定義屬性,初始化需要獲取自定義屬性之后的畫筆等等之類的成員變量。
值得注意的是,這個地方獲取屬性值的時候有2種寫法,一種是我注釋了的,一種是沒有注釋的,寫法的區別是:我注釋過的那段代碼,如果屬性在使用控件的時候沒有在xml中使用的話,賦值的方法是不走的。那么就沒有默認值。。我沒注釋的那種寫法是肯定會走,獲取不到就給默認值。推薦第二種,以免出錯。

<declare-styleable name="RulerView">
此處的name我看過很多博客,都要么沒說,要么說這個名字隨便寫。其實不對,這個名字,
代表了在這個declare-styleable之間的屬性只能在RulerView中使用,其他的空間是看
不到這個屬性的,同學們可以去試一下

關于format值的一些說明

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

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

3,boolean :布爾值  true false

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

5,float :浮點值  0.5

6,integer : 整型值  30

7,string : 字符串  abs

8,fraction :百分數 25%

9,enum :枚舉

10,flag :位或運算
 /**
     * 第二部分,獲取自定義屬性
     *
     * @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);
    }

第三部分,測量寬高,有點同學這一部分很是艱難,但是不用怕,賦值一下代碼,重寫其中的2個方法就行了

    /**
     * 第三部分,測量控件大小,一般情況下,前面的方法可以直接copy過去,只需要自己重寫measureWrapWitdh,measureWrapHeight
     *
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //這一部分,可寫可不寫,如若你不想重寫控件大小,直接用super就行。如果需要重寫的話套路也很簡單
//        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() {
        //測量wrapContent時寬度,這個請根據自己的需求自己計算,我這里默認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() {
        //測量wrapContent時高度,這個請根據自己的需求自己計算,我這里默認50dp
        return CustomViewUtil.dp2px(mContext, 50);
    }

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

 /**
     * 第四部分,在size發生變化時,重新賦值寬高
     *
     * @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);
    }

第五部分,屬于核心以及難點吧,具體實現我就不說了,自己去看代碼。其實代碼也沒啥看的,就是通過計算位置繪制而已。但是這個計算位置是很精細,很容易出錯的地方,得加倍小心

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

    private void drawCalibrationTails(Canvas canvas) {

        //計算需要畫多少刻度線
        //1,先計算可畫刻度線區域的總長度


        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 {

                //畫刻度線

                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);
                    }
                }

                //畫刻度線下面的文字
                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();
    }

第六部分:這個不解釋了

/**
     * 第六部分
     * 設置數據,用以第一次賦值,或者在listView recycleView復用的時候賦值,或者在交互中重新賦值等等
     */
    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);
        }
    }

第七部分 重難點部分,但是也很簡單。注意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;
    }

第八部分,控件滑動。Scroller和VelocityTracker的使用

 /**
     * 第八部分處理滑動相關的事情
     */
    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();
    }

最后

 /**
     * 定義相關的監聽器
     */

    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);
    }

實際上還有一部分是動畫,但是本例中未曾用到,以后再說吧

demo

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容