尼古拉丁 说过吃别人嚼过的馍不香 ,如果能再嚼一遍那很好 嚼过之后能咽进自己的肚子里 那更好
这篇文章将进入实质自定义视图阶段
这篇文章将介绍几个复写view中常用的方法,和我自己复写view时的一点心得。
常用的方法包括 onMesure onDraw onLayout onSizeChanged 我们边实现项目边介绍。上一篇已经介绍过自定义属性的属性在这里就不在赘述了。
项目
我们实现的项目就是一个小球绕着圆环转动
思路分析
1 我们需要画什么: 一个小球 一个圆环
2 我们需要什么参数: 小球的半径 小球的中心点坐标 小球的颜色 圆环的半径 圆环的中心点坐标 圆环的颜色(圆环的环径为小球的直径)
好我们先开始画圆环和小球
//画 圆环的思路就是画两个圆环 环径就是小球的直径,如果学过机械制图的小伙伴们都知道 作图需要先画法线 我们也是这个思路 先确定视图的中心点
onSizeChange 这个方法每次视图尺寸发生改变的时候调用 每次视图尺寸发生改变意味着这个视图的中心点发生了改变 所以确定视图的中心点 就在这里确定
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mCenterX = w / 2;
mCenterY = h / 2;
}
中心点确定之后 那我们现在就开始画圆环了 初始化画笔 圆环画笔的宽度设为3f
// init the ring of outer
mOuterRingPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mRingColor = Color.GREEN;
mOuterRingPaint.setColor(mRingColor);
mOuterRingPaint.setStyle(Paint.Style.STROKE);
mOuterRingPaint.setStrokeWidth(3f);
// init the ring of inner
mInnerRingPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mInnerRingPaint.setColor(mRingColor);
mInnerRingPaint.setStyle(Paint.Style.STROKE);
mInnerRingPaint.setStrokeWidth(3f);
// init the globule
mGlobulePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mGlobuleColor = Color.RED;
mGlobulePaint.setColor(mGlobuleColor);
画内外圆环 和小球
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// draw the outer of the ring
canvas.drawCircle(mCenterX, mCenterY, mRingRadius, mOuterRingPaint);
// draw the inner of the ring
canvas.drawCircle(mCenterX, mCenterY, mRingRadius - 2 * mGlobuleRadius, mInnerRingPaint);
// draw the globule
drawTheGlobule(canvas);
/* re draw the view*/
invalidate();
}
private void drawTheGlobule(Canvas canvas) {
float mG2R = 0f;//the distance of globule center to the ring center
mG2R = mRingRadius - mGlobuleRadius;
float hudu = (float) Math.abs(Math.PI * mCurrentAngle / 180);
mCenterGlobuleX = (float) (mCenterX + mG2R * Math.cos(hudu));
mCenterGlobuleY = (float) (mCenterY + mG2R * Math.sin(hudu));
canvas.drawCircle(mCenterGlobuleX, mCenterGlobuleY, mGlobuleRadius, mGlobulePaint);
}
开个线程改变小球当前的位置
public void start() {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 36; i++) {
try {
Thread.sleep(1000 / speed);
setCurrentAngle(10 * i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
能让小球动起来就是不停的改变小球的角度,而小球变换位置就要重新刷新视图这里用的就是在onDraw()中的 invalidate这个方法。
在onDraw 这个方法中 有一点需要注意 这里不要创建对象,在里面log的时候你会发现他不停的在run,这是因为他跟你屏幕刷新率一至,为什么这么做??这个,好像所有的屏幕都这样啊。
所以注意 创建对象 耗时的方法都不要放在这里,如果你不听话,对你就是和我一样不信邪,把刚刚开的线程 放进了onDraw 里试了一下。听我的把你的手放在你的笔记本上你很快就会感受到了什么叫温暖。
现在所有界面显示的图我都画了愉快的到XML文件中看一下,额 我设置的 wrap_content 怎么会这样
这完全没有wrap_content呀 这是怎么回事啊??
这个时候你会发现 我们还有一个方法没有讲呢?对,就是onMeasure,我们来看一下我写的onMeasure
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (MeasureSpec.UNSPECIFIED != MeasureSpec.getMode(widthMeasureSpec)) {
width = Math.min((int) (mRingRadius * 2), MeasureSpec.getSize(widthMeasureSpec));
}
if (MeasureSpec.UNSPECIFIED != MeasureSpec.getMode(heightMeasureSpec)) {
height = Math.min((int) (mRingRadius * 2), MeasureSpec.getSize(heightMeasureSpec));
}
setMeasuredDimension(width, height);
}
我们来做个小实验 我在XML布局文件中设定尺寸为20dp 那么 相当于我JAVA文件中40px(有些东西此时你应该可以推算出来了),然而我在调用setMeasureDimension(500,500) 结果是怎么样的呢
<com.newstart.zhangjianlong.demoview.view.CircleView
android:background="#fff"
android:id="@+id/cv_test"
android:layout_gravity="center"
android:layout_width="20dp"
android:layout_height="20dp"
app:globuleRadius="10"
app:globuleSpeed="2"
app:ringRadius="80"
/>
从上图来看 ,view的尺寸是setMeasureDimension决定的,XML中设置的尺寸失效了,那我们再看一下onSizeChange中的尺寸 和 Mea’sureSpec.getSize(widthMeasureSpec)中的尺寸各是多少
从Log中我们可以看出 w: 500 h: 500,easureSpec.getSize(widthMeasureSpec):40 这说明视图最终的尺寸是根据我们的setMesureDimension()中的参数决定的。
我们上面的这些操作的目的是什么呢?
首先我们可以明确的是 通过onSizeChange确定视图的中心点是没有问题,
其次我们可以计算出我们画的视图的实际尺寸将这个尺寸setMeasureDimension中就会使视图的尺寸实现 wrap_content。
关于MeasureSpec的几个问题
MeasureSpec 翻译过来就是测量标准 什么是测量标准;
这里的MeasureSpec 代表一个32位的 int值 其中最高的两位 代表SpecMode,低30位代表 SpecSize,SpecMode代表测量模式,SpecSize代表这种模式下的 的规格大小, 这个就不详细的说了,说多了我也会头晕的。当你对view的绘制过程有了深刻的理解,你看《Android 开发艺术》就会有很深的理解了,在这里向任玉刚致敬 。
我们需要知道的是通过了MeasureSpec.getSize 我们能获得我们视图的实际尺寸。MeasureSpec.getMode(int measureSpec) 通过他我们能获得Mode,那么我们为什么要做MeasureSpec.UNSPECIED != MeasureSpec.getMode 这个判断呢?UNSPECIFIED通过字面意思我们可以知道是不符合规格或者没有规格的意思,那么这个不符合规格或者是没有规格 指的是: 父容器不对View有任何限制 View需要多大的尺寸就给多大尺寸直至你的视图超出屏幕(ps:这是多么好的一个父容器啊,真是无比包容)。
现在应该明白我们的判断了吧?我们的意思就是如果父容器不是给我无限的尺寸那么为了边超过父容器的要求我就自我限缩,在onMeasure这个方法中计算出我视图的实际尺寸然后将这个视图的尺寸设定为我的实际尺寸。
MeasureSpec.UNSPECIFIED - MeasureSpec.AT_MOST - MeasureSpec.EXACTLY
这三个参数 从名字上我们就可以很直接的判断出 1 代表的事有没有给一个无限的尺寸 2 代表有没有给一个最大的尺寸 3 代表有没有精确的测量出视图的尺寸, 以我们的自定义视图为例 打印Log,结果如下
通过Log我们可以看到 这个View并没有获得一个无限的尺寸,也没有获得一个精确的测量结果,但是有一个最大尺寸,通过MeasureSpec.getSize(int measureSpec)得到的这个最大尺寸就是屏幕的宽高。
最后一个方法
好了我们还剩下最后一个方法没有说了,就是onLayout() 这个方法是做什么用的呢?我们理一下我们的思路 我们用了onDraw(Canvas canvas) 画了我们需要的视图 同时我们还调用了 invalidate();用于不停的更新我们的视图,我们用onMeasure(widthMeasureSpec,heightMeasureSpec)用来确定我们视图的大小,那么我们还有什么事没有做?我们确定了视图是什么,视图的尺寸,可是我们还有确定视图的位置 那么位置就是跟这个onLayout(boolean changed,int left , int top , int right , int bottom) 这四个参数,那么这个参数有什么用呢?我们来做个试验,思路大概是这样的:我们首先让我们的是同跟Botton是一个相对位置关系 toRight 然后我们通过改变Button的文字内容来改变Botton的尺寸,通过改变button尺寸的方法来改变视图的位置我们看看有什么会发生:
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
Log.d("CircleView", "getTop():" + getTop());
Log.d("CircleView", "getLeft():" + getLeft());
Log.d("CircleView", "getRight():" + getRight());
Log.d("CircleView", "getBottom():" + getBottom());
Log.d("CircleView", "left:" + left);
Log.d("CircleView", "right:" + right);
Log.d("CircleView", "top:" + top);
Log.d("CircleView", "bottom:" + bottom);
Log.d("CircleView", "changed:" + changed);
}
我们来看我们点击后打印出来的Log
我们可以看到当视图位置发生变化的时候,就会调用onLayout这个方法,也就是说这个方法是确定视图位置的。
以上就是自定义View的全部内容 接下来更多的方法和内容就需要大家自己多加练习了 还是尼古拉丁的那句话 吃别人嚼过的馍不香 自己多咀嚼会体味到更多的风味