Android 自定义View基本绘制流程及实例
来源:互联网 发布:好听的淘宝店铺英文名 编辑:程序博客网 时间:2024/04/27 23:20
Android 自定义View基本绘制流程及实例
- Android 自定义View基本绘制流程及实例
- 思路
- 自定义属性
- onMeasure
- onDraw
- 总结
前面已经学习了绘图和动画这两个“工具”,可以很好的丰富我们的自定义控件了。那么今天带来的内容就是自定义View基本的绘制流程。通过它来结束这一阶段的学习。当然,这些还不是全部的知识点,还有事件分发以及ViewGroup的相关知识,这部分可能会再过些时间再来学习。
这次和之前不一样了,先来看下效果图,再来进行下面的学习。(不过做好心里准备,可能有点low^_^”,不过我尽力了)
这里说一下,这个背景我是不想弄的,只是为了方便下面讲解。
1.思路
看完效果觉得比较简单,但是真正去写代码的时候还是遇到了些小问题。所以在学习阶段,能多写代码就多写。
主要分为3个步骤:
① 确定初始图形,即计算一些点的坐标,并绘制图形
② 扇片旋转动画
③ 字符串的缩放动画
①和③感觉相对简单一些,但是②就没那么简单了。我考虑的是将三个弧线作为三阶贝赛尔曲线,通过数据点及控制点做圆形运动,从而不断绘制出图形。虽说描述的挺简单的,但是代码一点也不短。这是我的思路,如果大家有更好的思路可以偷偷告诉我哦。
2.自定义属性
在values文件夹下新建Xml文件 attrs.xml
,然后写自定义View所需要的属性。先给出我例子里的吧。
<?xml version="1.0" encoding="utf-8"?><resources> <declare-styleable name="ProgressView"> <attr name="circleColor" format="color"></attr> <attr name="progressColor" format="color"></attr> <attr name="progressText" format="string"></attr> <attr name="progressTextSize" format="dimension"></attr> </declare-styleable></resources>
这里有几点要说明一下。
① declare-styleable中的 name
可以不是自定义View的名字,这里主要为了好辨识。
② declare-styleable
可以不声明,但是 R.java
就无法生成对应的常量,需要我们自行在View中写。
③ 在item中可以使用定义好了的属性,只是不需要指定 format
属性了,如 android:textSize
。
④ format
的类型,color、boolean、dimension、float、integer、string、fraction(百分数)等。
既然先说了Xml文件,就把自定义View在布局中的使用情况先说了吧。
public ProgressView(Context context) { this(context, null);}public ProgressView(Context context, AttributeSet attrs) { this(context, attrs, 0);}public ProgressView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); //得到自定义属性 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ProgressView); //默认颜色为黑色 circleColor = a.getColor(R.styleable.ProgressView_circleColor, Color.BLACK); progressColor = a.getColor(R.styleable.ProgressView_progressColor, Color.BLACK); progressText = a.getString(R.styleable.ProgressView_progressText); progressTextSize = a.getDimensionPixelSize(R.styleable.ProgressView_progressTextSize, 25); a.recycle(); initPaint(); initPos();}
① 我们可以使用构造函数中的第二个参数 AttributeSet
来直接获得属性,但是如果属性的值是引用类型(如 @color/black
),那么得到的就是它的id,还必须解析id才能获得属性值。所以,一般不会使用它来获取属性。
② obtainStyledAttributes的四个参数含义 obtainStyledAttributes(AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes)
。可以看到上面例子中只是用了两个参数,比较容易理解。第三个参数从名字上就容易理解,默认的style属性,就是使用在theme中指定的属性值。具体做法就是,先在attrs.xml文件中用 <attr name="myStyleAttr" format="reference"></attr>
指向theme中的style,然后再在当前theme中设置对应的属性。而第四个参数是指定一个styles.xml中编写好的style,但是读取条件是defStyleAttr为0或者在当前的theme中没有找到相关属性。
③ 属性值定义的优先级:xml > defStyleAttr > defStyleRes。
3.onMeasure
当属性获取完后,就要开始测量View的大小了,而这一过程是在onMeasure中完成的。再通过setMeasuredDimension方法设置已测量好的宽和高,系统才能确定显示该View时需要多大的空间。先看下代码。
@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); //得到View宽和高的specSize及specMode int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int mWidth; int mHeight; if(widthMode == MeasureSpec.EXACTLY) { mWidth = widthSize; }else { mWidth = (int) CONSTANT; } if(heightMode == MeasureSpec.EXACTLY) { mHeight = heightSize; }else { mHeight = (int) CONSTANT; } radius = (Math.min(mWidth, mHeight) - 10)/2; Log.i("TAG", "radius = " + String.valueOf(radius)); setMeasuredDimension(mWidth, mHeight);}
MeasureSpec为测量规格,可以通过它得到View的SpecSize及SpecMode。SpecMode可以分为MeasureSpec.EXACTLY , MeasureSpec.AT_MOST , MeasureSpec.UNSPECIFIED。
① MeasureSpec.EXACTLY表示父容器已经检测出子View所需要的精确大小,即View的测量大小就为SpecSize。通常该模式对应的是我们在Xml中设置的match_parent或者明确的值。可以理解为,设置了明确的值,或者match_parent即为和父View一样大之后,在父View中大小就已经确定了,无需再进行测量,就是为该模式下的规格大小。
② MeasureSpec.AT_MOST表示父容器未能检测出子View所需要的精确大小,但是指定了一个可用大小即specSize,但是View的测量大小不能超过SpecSize。通常该模式对应的是我们在Xml中设置的wrap_content,就是能包裹View里内容的一个合适大小。名义上是要尽可能的大,但是也不要浪费父View的空间吧,相当于绘画,不要留白太多;当然也不能小到画个鸭子,连个头都画外面了;而且也必须小于specSize,图纸当然不要大于桌子的大小啊。可能举得这个例子没那么好,只要能理解就行。所以,我这里给的结论是能包裹View里内容的一个合适大小。
③ MeasureSpec.UNSPECIFIED一般用于ListView和ScrollView等滑动控件,所以略。
在这里,在第二情况时我直接设置了一个常量。当然这个常量是小于父View大小的,同时考虑到我需要的圆半径是随着View的大小改变而变化的,自然不能由内容决定View大小了,也免去了因为加上padding而导致计算复杂的麻烦。最后有一点需要注意的地方,就是如果直接使用常量除以2作为半径,系统就会将笔触宽度的一部分削去,所以减去了10,用来预留笔触的宽度。可以从上面的效果图看出,这就是我就加上背景颜色的原因了。
4.onDraw
看过前面的绘图篇,应该对它有所了解了吧。这里最主要的就是静下心来,一点点去实现。我们就来大致走一下流程吧。
@Overrideprotected void onDraw(Canvas canvas) { super.onDraw(canvas); centerX = getMeasuredWidth()/2; centerY = getMeasuredHeight()/2; RectF mRectF = new RectF(centerX - radius, centerY - radius, centerX + radius, centerY + radius); circlePath.addArc(mRectF, 0, 359.9f); //绘制整圆 canvas.drawPath(circlePath, mPaint); if(arcPath == null) { getPosition(); startAnimator(canvas); drawArc(canvas); }else { if(!flag) { drawArc(canvas); }else { drawText(canvas); } }}
代码中getMeasuredWidth()和getMeasuredHeight()的返回值是通过setMeasuredDimension()方法得到的,即可以理解为在onMeasure()中的mWidth和mHeight。下面的判断的逻辑是,在初始状态时,先计算出所有控制点和数据点并绘制,并开始旋转的动画,其次是由于旋转动画而不断的绘制图形,最后是当旋转动画结束时开始绘制字符串。
mPathMeasure = new PathMeasure(circlePath, false);//计算三个数据点的坐标mPathMeasure.getPosTan(mPathMeasure.getLength()/12, rightPos, null);mPathMeasure.getPosTan(mPathMeasure.getLength() * 5/12, leftPos, null);mPathMeasure.getPosTan(mPathMeasure.getLength() * 3/4, midPos, null);//小圆弧的半径smallRadius = (float) (radius * Math.sin(Math.PI/4));Log.i("TAG", "smallRadius = " + String.valueOf(smallRadius));//计算控制点至数据点距离distance = C * smallRadius;Log.i("TAG", "distance = " + String.valueOf(distance));//计算控制点的坐标midConPos1.setX((float) (midPos[0] + distance * Math.sin(Math.PI/4)));midConPos1.setY((float) (midPos[1] + distance * Math.sin(Math.PI/4)));midConPos2.setX((float) (centerX + distance * Math.sin(Math.PI/4)));midConPos2.setY((float) (centerY - distance * Math.sin(Math.PI/4)));
这段代码是getPosition()方法里的。先是通过 getPosTan()
计算出3个数据点,然后计算出控制点的坐标。这一块建议大家画一下图,比较直观,这里我就不画了。毕竟我现在脑子里容不下别的了,其实掌握一个画图工具是有必要的。如果有相关知识不懂的,可以看看我前面的Android 自定义View绘图篇之进阶。
animator1 = ValueAnimator.ofObject(new PathEvaluator(centerX, centerY, radius, degree[0]), new Point(startPos[0], startPos[1]), new Point(startPos[0], startPos[1]));animator1.setInterpolator(new LinearInterpolator());animator1.setRepeatCount(count);animator1.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { Point mPoint1 = (Point) animation.getAnimatedValue(); midPos[0] = mPoint1.getX(); midPos[1] = mPoint1.getY(); invalidate(); }});
再来看下旋转动画的实现。这里我自定义了一个TypeEvaluator,就是一个点做圆运动。同时我传入了四个参数,就是中心点的横纵坐标、运动半径以及初始角度。它的代码就不贴了,可以看我上一篇文章或者下面的源码。当然上面代码只是一个点的运动,我们需要的是所有的数据点和控制点都一起运动。其实可以考虑将这些动画封装一下,算了,就这样吧。
AnimatorSet set = new AnimatorSet();set.setDuration(time);set.play(animator1).with(animator2).with(animator3).with(animator4).with(animator5). with(animator6).with(animator7).with(animator8).with(animator9);set.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); //旋转动画结束 flag = true; //开始字符串缩放动画 endAnimator(); }});set.start();
这里使用 AnimatorListenerAdapter
,在旋转动画结束时,将标志位变为true,并且开始下面的动画。
//绘制3段四分之一圆弧private void drawArc(Canvas canvas) { arcPath = new Path(); arcPath.moveTo(midPos[0], midPos[1]); arcPath.cubicTo(midConPos1.getX(), midConPos1.getY(), midConPos2.getX(), midConPos2.getY(), centerX, centerY); arcPath.cubicTo(rightConPos1.getX(), rightConPos1.getY(), rightConPos2.getX(), rightConPos2.getY(), rightPos[0], rightPos[1]); arcPath.moveTo(centerX, centerY); arcPath.cubicTo(leftConPos1.getX(), leftConPos1.getY(), leftConPos2.getX(), leftConPos2.getY(), leftPos[0], leftPos[1]); canvas.drawPath(arcPath, mPaint);}
这里是使用三阶贝塞尔曲线去绘制3段四分之一圆弧,就不多说了。
private void endAnimator() { scaleAnimator1 = ValueAnimator.ofFloat(0.3f, 1.3f); scaleAnimator1.setInterpolator(new AccelerateInterpolator()); scaleAnimator1.setDuration(500); scaleAnimator1.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mPaint.setTextSize(progressTextSize * (float)animation.getAnimatedValue()); //测量字符串的宽高 mPaint.getTextBounds(progressText, 0, progressText.length(),mBound); invalidate(); } }); scaleAnimator2 = ValueAnimator.ofFloat(1.5f, 1); scaleAnimator2.setInterpolator(new LinearInterpolator()); scaleAnimator2.setDuration(600); scaleAnimator2.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mPaint.setTextSize(progressTextSize * (float)animation.getAnimatedValue()); mPaint.getTextBounds(progressText, 0, progressText.length(),mBound); invalidate(); } }); AnimatorSet animatorSet = new AnimatorSet(); //字符串先放大再缩小 animatorSet.play(scaleAnimator2).after(scaleAnimator1); animatorSet.start();}
上面有个小知识点,就是测量字符串的宽高,就是使用Paint的 getTextBounds()
方法来获得。mBound是传入的一个空的矩形。
最后,根据需要可以暴露两个接口出来,从注释可以看到。主要的目的就是可以随意设置,方便使用。
//设置旋转一圈的时长public void setTime(long time) { this.time = time;}//设置旋转的圈数public void setCount(int count) { this.count = count - 1;}
5.总结
当然,这个只是自定义View比较基础的流程了,如果是像我一样接触android时间不长的盆友,已足够满足大部分需求了。好了,终于暂时结束了这一阶段的学习,鼓掌。接下来,我会开始学习比较常见的一些控件,偏向于Material Design风格的,毕竟在网上有很多类似用于学习的应用程序,感觉非常时尚。紧跟时尚前沿,当然学习也是这样。
好了,下次见!等等,忘了发源码,请猛戳这里
PS:本人水平实在有限,如有错误之处,还望不吝赐教!
参考:
自定义View系列教程02–onMeasure源码详尽分析
Android 自定义View属性相关细节
Android中自定义样式与View的构造函数中的第三个参数defStyle的意义
Android 深入理解Android中的自定义属性
Android中attrs.xml文件的使用详解
- Android 自定义View基本绘制流程及实例
- Android View 绘制流程 及 自定义View
- Android的自定义View及View的绘制流程
- android 自定义view绘制流程
- 有关Android View 绘制流程 & 自定义View
- Android View绘制流程,如何自定义View
- Android 自定义view ViewRootImpl绘制流程
- Android进阶——自定义View之View的绘制流程及实现onMeasure完全攻略
- View绘制流程(3)----view的绘制流程及自定义View的相关问题
- Android学习笔记之-自定义View实例及View的绘制过程(一)
- Android自定义view 必须知道的 Android View绘制流程
- Android自定义View之View的绘制流程
- 自定义View的绘制流程初探(含实例)
- Android自定义view的基本流程
- Android View绘制流程
- Android View绘制流程
- Android View绘制流程
- Android View绘制流程
- u-boot-2016.09移植(4)-u-boot.bin
- 在 Docker 上配置 Oracle
- Android控件的继承关系图
- 2016.11.7人力资源管理讲座有感
- JAVA中如何用接口实现多继承和多态 (非常好)
- Android 自定义View基本绘制流程及实例
- C——位运算简介及实用技巧
- 详解Message,Handler,MessageQueue,Looper的关系
- javascript之工厂模式
- 我竟然博士了
- 天天学习
- Spring MVC 的@RequestParam注解和request.getParameter("XXX")
- 作者找不到了,但是非常感谢作者(编码)
- nyoj915 +-字符串(贪心)