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文件的使用详解

0 0
原创粉丝点击