自定義View-實現簡易車速器(真的夠簡易)

自定義View-實現簡易車速器(真的夠簡易)

  學習自定義View挺久了,很久沒用都快忘了,這裏實現一個簡易的車速器算是一個回顧,項目比較簡單,代碼較少,但自定義View的流程基本都涉及到了.本文不是一篇講解自定義View基礎的文章,而是一個小的實戰,若是想看講解自定義View的文章,強烈推薦博客:http://blog.csdn.net/aigestudio,強烈=_=.
效果以下圖: android

圖片描述

接下來是代碼部分:git

attrs:github

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="SpeedometerView">
        <attr name="centerCircleWidth" format="dimension"/>
        <attr name="outCircleWidth" format="dimension"/>
        <attr name="whiteScaleWidth" format="dimension"/>
        <attr name="redScaleWidth" format="dimension"/>
        <attr name="numberWidth" format="dimension"/>
        <attr name="pointerWidth" format="dimension"/>
        <attr name="outCircleColor" format="color"/>
        <attr name="innerCircleColor" format="color"/>
        <attr name="numberColor" format="color"/>
        <attr name="normalScaleColor" format="color"/>
        <attr name="multipleOfTenScaleColor" format="color"/>
    </declare-styleable>
</resources>

attrs文件包含自定義的屬性,能夠在佈局文件或者代碼中使用.canvas

colors:dom

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorPrimary">#3F51B5</color>
    <color name="colorPrimaryDark">#303F9F</color>
    <color name="colorAccent">#FF4081</color>
    <color name="color_out_arc">@android:color/holo_red_dark</color>
    <color name="color_full_ten_number">@android:color/holo_red_dark</color>
    <color name="color_inner_arc">@android:color/white</color>
    <color name="color_full_ten_indicator">@android:color/holo_red_dark</color>
    <color name="color_not_full_ten_indicator">@android:color/white</color>
</resources>

佈局文件:ide

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:SpeedometerView="http://schemas.android.com/apk/res-auto"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/darker_gray"
    tools:context="com.example.why.speedometerview.MainActivity">

   <com.example.why.speedometerview.SpeedometerView
       android:background="@android:color/black"
       android:id="@+id/speedometer_view"
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       SpeedometerView:outCircleWidth="5dp"
       />
</FrameLayout>

接下來是Java代碼:
  
  SpeedometerView:函數

/**
 * Created by why on 17-3-9.
 * @author why
 * 簡易View實現類
 */

public class SpeedometerView extends View {

    private static final String TAG = "SpeedometerView";

    private Paint mInnerArcPaint; // 內部弧線畫筆
    private Paint mOutArcPaint; // 外部弧線畫筆
    private Paint mNotFullTenIndicatorPaint; // 非整10刻度畫筆
    private Paint mFullTenIndicatorPaint; // 整10加粗刻度畫筆
    private Paint mNumberPaint; // 數字畫筆
    private Paint mIndicatorPathPaint; // 刻度指針畫筆,計可旋轉的線條

    private int mInnerArcLineWidth; // 內部弧線線條寬度
    private int mOutArcLineWidth; // 外部弧線線條寬度
    private int mNormalIndicatorWidth; // 刻度線條寬度
    private int mFullTenScaleWidth; // 整10加粗刻度線條寬度
    private int mFullTenScaleLen ; // 非整10加粗線條長度
    private int mNotFullScaleLen; // 白色刻度線條長度
    private int mNumberWidth; // 數字線條刻度
    private int mIndicatorPathLineWidth;//
    private float mNumberTextSize; // 數字字體大小
    private int mCenterCircleRadius; // 內圓半徑
    private int mOutCircleRadius; // 外圓半徑

    private  RectF mOutRectF; // 外弧線的基準矩形
    private  RectF mCenterRectF; //內弧線基準矩形

    // 這兩個值是當view的width和height屬性爲wrap_content時,屬性不起做用配置的默認值,詳情間onMeasure();
    private int mViewDefaultWidth; // 默認的view的寬度
    private int mViewDefaultHeight; // 默認的view的高度,

    private int[] mWindowSize = new int[2]; // 保存屏幕的寬/高
    private Path mIndicatorPath; // 刻度指針,用path實現
    private int mScaleIndicatorLen; // 刻度指針的長度
    private int mIndicatorX; // 刻度指針指尖的X座標
    private int mIndicatorY; // 刻度指針指尖的Y座標

    private int mOutArcColor = Color.RED; // 外部弧線默認顏色
    private int mNumberColor =Color.RED; // 刻度數字默認顏色
    private int mInnerArcColor = Color.WHITE; //內部弧線默認顏色
    private int mFullTenIndicatorColor = Color.RED; // 整10刻度顏色
    private int mNotFullTenIndicatorColor = Color.WHITE; // 非整10刻度顏色

    public SpeedometerView(Context context) {
        super(context);
        init(context,null,0);
    }

    public SpeedometerView(Context context, AttributeSet attrs) {
        super(context,attrs);
        init(context,attrs,0);
    }

    public SpeedometerView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context,attrs,defStyleAttr);
        init(context,attrs,defStyleAttr);
    }

    /**
     * 初始化默認數值
     */
    private void initSize(){
        mWindowSize = getWindowSize();
        mViewDefaultWidth = mWindowSize[0];
        mViewDefaultHeight = mWindowSize[1];
        mOutCircleRadius = mWindowSize[0]/2-20;
        mNumberTextSize = mOutCircleRadius / 13;
        mInnerArcLineWidth = mOutCircleRadius/150;
        mCenterCircleRadius = mOutCircleRadius / 8;
        mOutArcLineWidth = mOutCircleRadius /150;
        mNormalIndicatorWidth =  mOutCircleRadius /150;
        mFullTenScaleWidth = mOutCircleRadius / 150;
        mFullTenScaleLen = mOutCircleRadius /6;
        mNotFullScaleLen = mOutCircleRadius / 7;
        mIndicatorPathLineWidth = mOutCircleRadius/100;
        mNumberWidth =  mOutCircleRadius/100;
    }

    /**
     * 初始化默認顏色
     */
    private void initColor() {
        Context context = getContext();
        mOutArcColor = ContextCompat.getColor(context, R.color.color_out_arc);
        mNumberColor = ContextCompat.getColor(context, R.color.color_full_ten_number);
        mInnerArcColor = ContextCompat.getColor(context, R.color.color_inner_arc);
        mFullTenIndicatorColor = ContextCompat.getColor(context, R.color.color_full_ten_indicator);
        mNotFullTenIndicatorColor = ContextCompat.getColor(context,R.color.color_not_full_ten_indicator);
    }

    /**
     * 初始化
     * @param context
     * @param attrs
     * @param defStyleAttr
     */
    private void init(Context context,AttributeSet attrs, int defStyleAttr){

        initSize(); //爲各個屬性設置默認的值
        initColor(); //設置默認顏色
        //從xml獲取設置的屬性,注意:這裏只提供顏色屬性
        if (attrs != null) {
            TypedArray typedArray = context.obtainStyledAttributes(attrs,R.styleable.SpeedometerView,defStyleAttr,0);
            mOutArcColor = typedArray.getColor(R.styleable.SpeedometerView_outCircleColor,mOutArcColor);
            mNumberColor = typedArray.getColor(R.styleable.SpeedometerView_numberColor, mNumberColor);
            mInnerArcColor = typedArray.getColor(R.styleable.SpeedometerView_innerCircleColor, mInnerArcColor);
            mNotFullTenIndicatorColor = typedArray.getColor(R.styleable.SpeedometerView_normalScaleColor,                                                       mNotFullTenIndicatorColor);
            mFullTenIndicatorColor = typedArray.getColor(R.styleable.SpeedometerView_multipleOfTenScaleColor,                                                   mFullTenIndicatorColor);
            typedArray.recycle();
        }

        //設置內弧線畫筆
        mInnerArcPaint = new Paint();
        mInnerArcPaint.setStyle(Paint.Style.STROKE);
        mInnerArcPaint.setColor(mInnerArcColor);
        mInnerArcPaint.setAntiAlias(true); //抗鋸齒
        mInnerArcPaint.setStrokeWidth(mInnerArcLineWidth);

        //設置外弧線畫筆
        mOutArcPaint = new Paint();
        mOutArcPaint.setStyle(Paint.Style.STROKE);
        mOutArcPaint.setColor(mOutArcColor);
        mOutArcPaint.setAntiAlias(true);
        mOutArcPaint.setStrokeWidth(mOutArcLineWidth);

        //設置非整十刻度畫筆
        mNotFullTenIndicatorPaint = new Paint();
        mNotFullTenIndicatorPaint.setStyle(Paint.Style.FILL);
        mNotFullTenIndicatorPaint.setColor(mNotFullTenIndicatorColor);
        mNotFullTenIndicatorPaint.setAntiAlias(true);
        mNotFullTenIndicatorPaint.setStrokeWidth(mNormalIndicatorWidth);
        //設置整十刻度畫筆
        mFullTenIndicatorPaint = new Paint();
        mFullTenIndicatorPaint.setStyle(Paint.Style.FILL);
        mFullTenIndicatorPaint.setColor(mFullTenIndicatorColor);
        mFullTenIndicatorPaint.setAntiAlias(true);
        mFullTenIndicatorPaint.setStrokeWidth(mFullTenScaleWidth);

        //設置數字畫筆
        mNumberPaint = new Paint();
        mNumberPaint.setStyle(Paint.Style.FILL);
        mNumberPaint.setColor(mNumberColor);
        mNumberPaint.setAntiAlias(true);
        mNumberPaint.setStrokeWidth(mNumberWidth);
        mNumberPaint.setTextSize(mNumberTextSize);
        mNumberPaint.setTextAlign(Paint.Align.CENTER);

        //設置刻度指針畫筆
        mIndicatorPathPaint = new Paint();
        mIndicatorPathPaint.setAntiAlias(true);
        mIndicatorPathPaint.setStyle(Paint.Style.STROKE);
        mIndicatorPathPaint.setColor(Color.RED);
        mIndicatorPathPaint.setStrokeWidth(mIndicatorPathLineWidth);

        mScaleIndicatorLen = mOutCircleRadius*8/13;//指針的長度
        mIndicatorX = -mScaleIndicatorLen;//由於指針一開始是放在-180°的位置
        mIndicatorY = 0; //指針的起止座標爲內圓的圓心(onDraw()中會把原點座標平移到屏幕的中心,方便畫圖),因此y爲0
        mIndicatorPath = new Path();
        mOutRectF = new RectF();
        mCenterRectF = new RectF();

    }
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        //經過MeasureSpec獲取各自的mode和size
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);

        /**
         * 這裏這麼作的緣由是自定義View的時,width或者height的屬性wrap_content不起做用,效果和match_parant相同,這裏有牽扯到自
         * View扥繪製流程這裏暫時不表,建議看看《Android開發藝術探索》中自定義View篇章或者看官網和相應的博客,這裏只是當屬性爲
         * wrap_content時設置默認的mViewDefaultWidth,mViewDefaultHeight;
         */
        if (widthSpecMode == MeasureSpec.AT_MOST && heightMeasureSpec == MeasureSpec.AT_MOST) {
            setMeasuredDimension(mViewDefaultWidth, mViewDefaultHeight);
        }else if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode != MeasureSpec.AT_MOST) {
            setMeasuredDimension(mViewDefaultWidth, heightSpecSize);
        }else if (widthMeasureSpec != MeasureSpec.AT_MOST && heightMeasureSpec == MeasureSpec.AT_MOST){
            setMeasuredDimension(widthSpecSize, heightMeasureSpec);
        }else {
            setMeasuredDimension(widthSpecSize, heightSpecSize);
        }

    }

    /**
     * 在onDraw(Canvas canvas)中畫圖,每次調用重繪方法invalidate()都會再次調用onDraw(Canvas canvas);
     * @param canvas
     */
    @Override
    protected void onDraw(Canvas canvas) {
        canvas.translate(mWindowSize[0]/2, mWindowSize[1]/2);//canvas座標平移到屏幕中心,這樣容易操做
        //畫外層的弧形,默認紅色
        drawOutArc(mOutRectF, canvas);
        //畫內層的弧形,默認白色
        drawInnerWhiteArc(mCenterRectF, canvas);
        //畫整十刻度以及整10數字
        drawFullTenIndicatorAndNumber(canvas);
        //接下來畫普通的刻度,即未加粗的刻度
        drawNotFullTenIndicator(canvas);
        //接下來畫指示圖標,注意這裏使用path的drawPath()方法畫line,每次畫以前path調用path.lineTo(mIndicatorX, mIndicatorY)
        //鏈接內圓心和該點
        drawIndicator(mIndicatorPath, canvas);

    }

    /**
     * 獲取觸摸的座標
     * @param event
     * @return
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int action = MotionEventCompat.getActionMasked(event);
        float posX;
        float posY;
        double scale;
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                posX  = event.getX();
                posY = event.getY();
                //若是觸摸的座標不在弧線的基準矩形中,那就不作處理,直接return
                if (!mOutRectF.contains(posX-mWindowSize[0]/2,posY - mWindowSize[1]/2) || posY - mWindowSize[1]/2 > 0){
                    return true;
                }

                /**
                 * 因爲刻度指針的長度是固定的,可是手指觸摸的點不是固定的,不能直接將指針的終點座標設置爲獲取的x,y,但能夠先算出
                 *手指觸摸的點到內圓中心的距離,而後除於指針的長度,獲得比例,根據類似三角形的性質可知,這個比例對於三角形的三條邊都是同樣的
                 *注意:這裏的三角形是假設圓心,觸摸點,以及X座標相連得到的直角三角形,因而咱們能夠根據比例算出在點與內圓心的距離爲默認的指針
                 *長度時,此時另外兩條直角邊的長度,便可求出,x,y的值
                 */
                scale = Math.sqrt((posX-mWindowSize[0]/2)*(posX-mWindowSize[0]/2)+ (posY - mWindowSize[1]/2)*(posY -                                        mWindowSize[1]/2))/mScaleIndicatorLen;
                mIndicatorX = rawXToIndicatorX(posX, scale);
                mIndicatorY = rawXToIndicatorY(posY, scale);
                break;
            case MotionEvent.ACTION_MOVE:

                //移動過程當中不斷的獲取觸摸點的座標,並按比例轉換爲相應的指針終點座標
                posX = event.getX();
                posY = event.getY();
                if (!mOutRectF.contains(posX-mWindowSize[0]/2,posY - mWindowSize[1]/2)|| posY - mWindowSize[1]/2 > 0){
                    return true;
                }
                Log.d(TAG,"x的="+posX+"--y的值=" +
                        ""+posY);
                scale = Math.sqrt((posX-mWindowSize[0]/2)*(posX-mWindowSize[0]/2)+ (posY - mWindowSize[1]/2)*(posY -                                        mWindowSize[1]/2))/mScaleIndicatorLen;

                //或最終的指針終點座標,onDraw()根據mIndicatorX.mIndicatorY畫指針的終點
                mIndicatorX = rawXToIndicatorX(posX, scale);
                mIndicatorY = rawXToIndicatorY(posY, scale);
                break;
        }
        //從新繪製view,onDraw()會被調用
        invalidate();
        //return true表示本身處理觸摸事件,parent的onTouchEvent()方法不會被調用了
        return true;
    }

    /**
     * 按邊長比例算出最終的X
     * @param posX
     * @param scale
     * @return
     */
    private int rawXToIndicatorX(double posX, double scale) {
        return (int)((posX-mWindowSize[0]/2) / scale);
    }

    /**
     * 按邊長比例算出最終的Y
     * @param posY
     * @param scale
     * @return
     */
    private int rawXToIndicatorY(double posY, double scale) {
        return (int)((posY - mWindowSize[1]/2) / scale);
    }

    /**
     * 畫出外弧線
     * @param rectF
     * @param canvas
     */
    private void drawOutArc(RectF rectF, Canvas canvas) {
        rectF.set(-mOutCircleRadius,  - mOutCircleRadius, mOutCircleRadius, mOutCircleRadius);
        canvas.drawArc(mOutRectF,0,-180,false,mOutArcPaint);
    }
    //畫出內弧線
    private void drawInnerWhiteArc(RectF rectF, Canvas canvas) {
        rectF.set(-mCenterCircleRadius, -mCenterCircleRadius,mCenterCircleRadius, mCenterCircleRadius);
        canvas.drawArc(mCenterRectF, 0, -180, false, mInnerArcPaint);
    }

    /**
     * 畫整10的刻度,由於數字對應整時,可在同一個循環中畫出
     * @param canvas
     */
    private void drawFullTenIndicatorAndNumber(Canvas canvas) {

        //整十刻度終點離內圓心的長度
        int distance = mOutCircleRadius - mFullTenScaleLen;
        //接下來畫10的倍數的刻度
        for (int i = -180; i<=0; i += 15) {
            double radian = ((double)i/180)*Math.PI;//弧度
            double cosValue = Math.cos(radian);//三角函數值
            double sinValue = Math.sin(radian);

            //根據弧度算出起始點和終點座標
            float startX = (float)(distance*cosValue);
            float startY = (float)(distance*sinValue);
            float endX = (float)(mOutCircleRadius*Math.cos(radian));
            float endY = (float)(mOutCircleRadius*Math.sin(radian));
            canvas.drawLine(startX,startY,endX,endY,mFullTenIndicatorPaint);

            //刻度數字標識的座標,
            float numberX =(float)((distance-50)*cosValue);
            float numberY =(float)((distance-50)*sinValue);
            canvas.drawText(""+Math.abs(i+180)*2/3,numberX, numberY, mNumberPaint);
        }
    }

    /**
     * 畫非整10刻度
     * @param canvas
     */
    private void drawNotFullTenIndicator(Canvas canvas) {
        float normalIntervalRadian = (float)Math.PI / 60;
        int  normalIntervalRadianCount = (int)(Math.PI/normalIntervalRadian);
         int distance = mOutCircleRadius - mNotFullScaleLen;
        for (int i = 0; i <= normalIntervalRadianCount; i++) {
            if (!(i%5 == 0) ){
                float cosValue = (float)Math.cos(-normalIntervalRadian*i);
                float sinValue = (float)Math.sin(-normalIntervalRadian*i);
                float startX = (distance*cosValue);
                float startY = (distance*sinValue);
                float endX = (float)((mOutCircleRadius-10)*Math.cos((-normalIntervalRadian*i)));
                float endY = (float)((mOutCircleRadius-10)*Math.sin((-normalIntervalRadian*i)));
                canvas.drawLine(startX,startY,endX,endY,mNotFullTenIndicatorPaint);
            }
        }
    }

    /**
     * 畫刻度指針
     * @param path
     * @param canvas
     */
    private void drawIndicator(Path path, Canvas canvas) {
        path.lineTo(mIndicatorX, mIndicatorY);
        canvas.drawPath(path,mIndicatorPathPaint);
        path.reset();
    }

    /**
     * 獲取屏幕的寬,高,按屏幕的比例設置view的各個屬性
     * @return
     */
    private int[] getWindowSize() {
        int[] size = new int[2];
        Resources resources = getContext().getResources();
        DisplayMetrics displayMetrics = resources.getDisplayMetrics();
        size[0] = displayMetrics.widthPixels;
        size[1] = displayMetrics.heightPixels;
        return size;
    }

    //這裏只提供修改顏色的方法
    public int getOutCircleColor() {
        return mOutArcColor;
    }

    public void setOutCircleColor(int outCircleColor) {
        this.mOutArcColor = outCircleColor;
    }

    public int getNumberColor() {
        return mNumberColor;
    }

    public void setmNumberColor(int numberColor) {
        this.mNumberColor = numberColor;
    }

    public int getInnerCircleColor() {
        return mInnerArcColor;
    }

    public void setInnerCircleColor(int innerArcColor) {
        this.mInnerArcColor = innerArcColor;
    }

    public int getmOutArcColor() {
        return mOutArcColor;
    }

    public void setmOutArcColor(int outArcColor) {
        this.mOutArcColor = outArcColor;
    }

    public int getmNumberColor() {
        return mNumberColor;
    }

    public int getmInnerArcColor() {
        return mInnerArcColor;
    }

    public void setmInnerArcColor(int innerArcColor) {
        this.mInnerArcColor = innerArcColor;
    }

    public int getmFullTenIndicatorColor() {
        return mFullTenIndicatorColor;
    }

    public void setmFullTenIndicatorColor(int fullTenIndicatorColor) {
        this.mFullTenIndicatorColor = fullTenIndicatorColor;
    }

    public int getmNotFullTenIndicatorColor() {
        return mNotFullTenIndicatorColor;
    }

    public void setmNotFullTenIndicatorColor(int notFullTenIndicatorColor) {
        this.mNotFullTenIndicatorColor = notFullTenIndicatorColor;
    }
}

Activity:佈局

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        SpeedometerView speedometerView = (SpeedometerView)findViewById(R.id.speedometer_view);
    }
}

註釋仍是很清楚的,源碼地址https://github.com/whyrookie/...學習

相關文章
相關標籤/搜索