學習自定義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/...學習