了解了基本的自定义view基础后,现在我们就来实践下自定义view,也是看到我华为手机上自带的天气预报软件后,想着模仿做一个,于是,我自己尝试了下,虽然不算太像,但是还算能看,期待后期的改进。

通过本文你可以用到以下技术:

1)view的测量

2)canvas绘图技巧

3)接口回调

4)触摸事件的处理

最终效果如下所示

可以通过输入框自己设定最低、最高温度和当前温度(这是为我天气预报软件做的方法)


1.绘制流程

1)其中最主要的是中间的圆环的绘制,采用的是

canvas的drawArc(RectF oval,float startAngle, float sweepAngle, boolean useCenter, Paint paint)方法。

其中参数意义为

oval:圆外接的矩形 
startAngle:开始角度 
sweepAngle:扫描角度 
useCenter:是否和圆心连线 
paint:画笔 

角度大小,以此图为标准


当然中间的圆环可能看起来是两个圆之间的区域,开始我以为是要画2个园,没想到只要把画笔设置一下就好了

circlePaint.setStrokeWidth(60.0f);
上面就是两个圆与圆之间(圆环)的区域。

值得一提的是canvas的这个画圆的方法设计的很强大,还可以画椭圆的,就看你圆外接的矩形的宽高了。


2)而中间的那个刻度线,采用的是

Canvas.drawLine(float startX, float startY, float stopX, float stopY, Paint paint)方法
其中参数的意义为
   startX:线起始点的x轴位置
   startY:线起始点的y轴位置
   stopX:线结束点的x轴位置
   stopY:线结束点的y轴位置
   paint:画线的画笔

画了一根刻度线后,采用旋转的是
Canvas.rotate(float degrees, float px, float py)方法
其中参数意义为
degrees: 每次要旋转的角度
px: 旋转的圆心x轴坐标
py: 旋转的圆心y轴坐标


3)圆盘中间的文字的采用的是
canvas的Canvas.drawText(String text, float x, float y, Paint paint)方法
参数意义这里就不说了,应该猜的出的。


4)中间的指示针采用的是一个小实心圆,采用的是

drawCircle(float cx, float cy, float radius,Paint paint)方法

参数意义

cx: 中心点的x轴

cy: 中心点的y轴

radius: 半径

paint: Paint画笔对象


5)中间的颜色渐变,采用的是SweepGradient
SweepGradient(float cx, float cy, int[] colors, float[] positions)
参数意义
 cx:渐变圆心的x轴坐标
 cy:渐变圆心的y轴坐标
 colors[]: 颜色数组
 positions: 颜色分隔的位置数组,可以为null,系统自己分
 

2. 绘制的计算过程
好了,基本的绘制内容就是这么多了,其他的就是计算过程了,这些都是在onDraw()内完成的。
先说圆环的绘制的计算过程吧。
0)在绘制之前,你首先得准备好笔
工欲善其事必先利其器。初始化画笔的工作是在两个参数的构造方法里的,因为只需初始化一次,而onDraw()方法会随着绘制过程很可能会不断的调用,所以初始化的工作放在构造方法里   

 public MyCircleView(Context context, AttributeSet attrs) {super(context, attrs);screenWidth=MeasureUtil.getScreenWidth(context);screenHeight=MeasureUtil.getScreenHeight(context);Log.e("My----->", "2  "+screenWidth+"  "+screenHeight);initPaint();}private void initPaint() {linePaint = new Paint();linePaint.setColor(Color.CYAN);linePaint.setStyle(Style.FILL);linePaint.setAntiAlias(true);linePaint.setStrokeWidth(1.0f);textPaint = new Paint();textPaint.setColor(Color.BLACK);textPaint.setTextAlign(Paint.Align.CENTER);textPaint.setAntiAlias(true);textPaint.setTextSize(30);centerTextPaint = new Paint();centerTextPaint.setColor(Color.BLUE);centerTextPaint.setTextAlign(Paint.Align.CENTER);centerTextPaint.setAntiAlias(true);centerTextPaint.setTextSize(80);circlePaint = new Paint();circlePaint.setColor(Color.WHITE);circlePaint.setAntiAlias(true);circlePaint.setStyle(Paint.Style.STROKE);circlePaint.setStrokeCap(Cap.ROUND);//实现末端圆弧circlePaint.setStrokeWidth(60.0f);indicatorPaint=new Paint();indicatorPaint.setColor(0xFFF7F709);indicatorPaint.setAntiAlias(true);indicatorPaint.setStyle(Paint.Style.FILL);// 着色的共有270度,这里设置了12个颜色均分360度sint[] colors = { 0xFFD52B2B, 0xFFf70101, 0xFFFFFFFF, 0xFFFFFFFF,0xFF6AE2FD, 0xFF8CD0E5, 0xFFA3CBCB, 0xFFD1C299, 0xFFE5BD7D,0xFFAA5555, 0xFFBB4444, 0xFFC43C3C };mCenter = screenWidth / 2;mRadius = screenWidth/ 2 - 100;// 渐变色mSweepGradient = new SweepGradient(mCenter, mCenter, colors, null);// 构建圆的外切矩形mRectF = new RectF(mCenter - mRadius, mCenter - mRadius, mCenter+ mRadius, mCenter + mRadius);}

1)圆环设计的宽度为wrap_content,故需要重写onMeasure方法,告诉画笔它的外接圆矩形的宽度

// 因为自定义的空间的高度设置的是wrap_content,所以我们必须要重写onMeasure方法去测量高度,否则布局界面看不到// 其他控件(被覆盖)@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {setMeasuredDimension(measureWidth(widthMeasureSpec),measureHeight(heightMeasureSpec));}
测量方法

/** * 测量宽度 * * @param widthMeasureSpec * @return */private int measureWidth(int widthMeasureSpec) {int mode = MeasureSpec.getMode(widthMeasureSpec);int size = MeasureSpec.getSize(widthMeasureSpec);// 默认宽高;defaultValue=screenWidth;switch (mode) {case MeasureSpec.AT_MOST:// 最大值模式 当控件的layout_Width或layout_height属性指定为wrap_content时Log.e("cmos---->", "size " + size + " screenWidth " + screenWidth);size = Math.min(defaultValue, size);break;case MeasureSpec.EXACTLY:// 精确值模式// 当控件的android:layout_width=”100dp”或android:layout_height=”match_parent”时break;default:size = defaultValue;break;}defaultValue = size;return size;}

圆环的外接矩形

                mCenter = screenWidth / 2;//圆心坐标mRadius = screenWidth/ 2 - 100;//圆的半径  留了100dp,是为了给绘制文字留空间// 构建圆的外切矩形mRectF = new RectF(mCenter - mRadius, mCenter - mRadius, mCenter+ mRadius, mCenter + mRadius);        canvas.drawArc(mRectF, 135, 270, false, circlePaint);
这样,圆环就绘制计算完成。


2)刻度值的计算绘制

因为整个圆盘是270度,设置每3度画一刻度线,故要绘制90根,这里选择的是以圆盘正上方的刻度线为基准刻度,将其绘制好后,然后进行旋转绘制,这样绘制过程就Ok了

                for (int i = 0; i < 120; i++) {if (i <= 45 || i >= 75) {//空白部分不用绘制刻度canvas.drawLine(mCenter, mCenter - mRadius - 30, mCenter,mCenter - mRadius + 30, linePaint);//30是因为设置的填充圆环的宽度为60的原因}canvas.rotate(3, mCenter, mCenter);}


3)圆盘刻度值计算绘制

这个就稍微复杂一点了,不过也还好,计算过程无非就是高中三角值公式的使用过程,在此之前,你先要了解角度的起始值和象限。



其中黄线的长度即是圆环的半径

              // x代表文字的x轴距离圆心x轴的距离 因为刚好是45度,所以文字x轴值和y值相等int x = 0;// 三角形的斜边int c = mRadius + 60 / 2 + 40;// 40代表这个字距离圆外边的距离// 因为是每45度写一次文字,故根据到圆心的位置,利用三角形的公式,可以算出文字的坐标值x = (int) Math.sqrt((c * c / 2));canvas.drawText("10", mCenter - x, mCenter + x, textPaint);canvas.drawText("15", mCenter - c, mCenter,textPaint);canvas.drawText("20", mCenter - x, mCenter - x,textPaint);canvas.drawText("25", mCenter, mCenter - c,textPaint);canvas.drawText( "30", mCenter + x, mCenter - x,textPaint);canvas.drawText( "35", mCenter + c, mCenter,textPaint);canvas.drawText( "40", mCenter + x, mCenter + x,textPaint);

可以看出,这里刻度值我把它写死了,当然,你也可以暴露一个方法,让调用者去设置起始值和结束值。


4)中间的小圆点指示器

了解了文字的计算过程后,再来算这个的画,就相对容易了很多。

           currentScanDegree=(getCurrentDegree()-10)*3;int insideIndicator=mRadius-60;//离圆环的距离        if (currentScanDegree<=45) {//第三象限        canvas.drawCircle((float)(mCenter-insideIndicator*Math.sin(Math.PI*(currentScanDegree+45)/180)),(float)(mCenter+insideIndicator*Math.cos(Math.PI*(currentScanDegree+45)/180)), 10, indicatorPaint);}else if(45
过程也就不解释了,同3的过程一致


5)中间文字的具体温度值

   这个是根据扫描的角度值进行换算的

canvas.drawText((currentScanDegree/9+10)+"℃", mCenter, mCenter, centerTextPaint);

相信了解了上面的过程,这个就很简单了吧。


到此整个计算过程就全部完成了。


那么,怎么让我们的view动起来呢,有点动态感,对此,我们只要不断的改变扫描的度数就好,对此我设定了3个值,最低温度值(起始)、最高温度值(结束)、当前温度。

点击按钮让其重绘。具体代码如下

  private void showDegree(final int minDegree, final int maxDegree, final int currentDegree) {    myCircleView.setMinDegree(minDegree);    new Thread(new Runnable() {@Overridepublic void run() {for (int i = minDegree; i < (maxDegree-minDegree)*3+minDegree; i++) {try {Thread.sleep(30);} catch (InterruptedException e) {// TODO 自动生成的 catch 块e.printStackTrace();}myCircleView.setMaxDegree(i);myCircleView.postInvalidate();}for (int j = 10; j<=(currentDegree-10)*3+10; j++) {if (j<=(currentDegree-10)*3+10) {//23*3+10=79myCircleView.setCurrentDegree(j);}myCircleView.postInvalidate();}}}).start();}

上面要注意的地方是要注意使用postInvalidata()方法,因为我们是在子线程操控UI线程,所以不能单纯的用invalidata()方法。


3. view添加滑动触摸事件

最后作为扩展,我为此控件添加了滑动触摸事件

代码如下:

@Overridepublic boolean onTouchEvent(MotionEvent event) {// TODO 自动生成的方法存根switch (event.getAction()) {case MotionEvent.ACTION_DOWN:isCanMove = true;break;case MotionEvent.ACTION_MOVE:float x = event.getX();float y = event.getY();float StartX = event.getX();float StartY = event.getY();// 判断当前手指距离圆心的距离 如果大于mCenter代表在圆心的右侧if (x > mCenter) {x = x - mCenter;} else {x = mCenter - x;}if (y > mCenter) {y = y - mCenter;} else {y = mCenter - y;}// 判断当前手指是否在圆环上的(30+10多加了10个像素)if ((mRadius + 40) < Math.sqrt(x * x + y * y)|| Math.sqrt(x * x + y * y) < (mRadius - 40)) {Log.e("cmos---->", "终止滑动");isCanMove = false;return false;}float cosValue = x / (float) Math.sqrt(x * x + y * y);// 根据cosValue求角度值double acos = Math.acos(cosValue);// 弧度值acos = Math.toDegrees(acos);// 角度值if (StartX > mCenter && StartY < mCenter) {acos = 360 - acos;// 第一象限Log.e("象限---->", "第一象限");} else if (StartX < mCenter && StartY < mCenter) {acos = 180 + acos;// 第二象限Log.e("象限---->", "第二象限");} else if (StartX < mCenter && StartY > mCenter) {acos = 180 - acos;// 第三象限Log.e("象限---->", "第三象限");} else {// acos=acos;Log.e("象限---->", "第四象限");}Log.e("旋转的角度---->", acos + "");scanDegree = (int) acos;if (scanDegree >= 135 && scanDegree < 360) {scanDegree = scanDegree - 135;int actualDegree = (int) (scanDegree / 9);if (mGetDegreeInterface != null) {mGetDegreeInterface.getActualDegree(actualDegree+ minDegrees);}} else if (scanDegree <= 45) {scanDegree = (int) (180 + 45 + acos);int actualDegree = (int) (scanDegree / 9);if (mGetDegreeInterface != null) {mGetDegreeInterface.getActualDegree(actualDegree+ minDegrees);}} else {return false;}postInvalidate();return true;}return true;}
其中上面用到了接口回调事件

if (mGetDegreeInterface != null) {mGetDegreeInterface.getActualDegree(actualDegree+ minDegrees);}
假如我们主界面需要当前滑动到的温度值,可以利用此方法

/** * 获取当前温度值接口 *  */public interface GetDegreeInterface {void getActualDegree(int degree);}public void setGetDegreeInterface(GetDegreeInterface arg) {this.mGetDegreeInterface = arg;}

例子源码点我下载


好的,全文到此结束,对文章内容有疑问的、或者有错的地方、或者代码可以有优化的方法的,欢迎留言探讨,共同进步!!!








更多相关文章

  1. Android(安卓)仿微信, QQ 裁剪
  2. OpenGL ES 实现可视化实时音频
  3. Android(安卓)自定义View--从源码理解View的绘制流程
  4. Anroid ListView分组和悬浮Header实现
  5. Android实用View:炫酷的进度条
  6. Android自定义View使用总结
  7. Android中文API——TabWidget
  8. Android(安卓)UI绘制流程之测量篇
  9. Android(安卓)Rect类的构造函数参数说明

随机推荐

  1. Android Lib Project与Android Project中
  2. Android WebView使用全面解析(加载网络资
  3. 6.2、Android中向Internet发送xml数据
  4. android view的width或者height变化的动
  5. 《android关于WIFI的操作》
  6. android 彩信分析
  7. Android 点击两次返回键退出程序
  8. Android 图片旋转(使用Matrix.setRotate方
  9. Android中当前时间的动态显示
  10. Android的简单图形和view刷新