学习自定义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/...学习