Android源代码学习笔记:Fit Chart环形计数器

在逛极客学院网站时,见到了一份很好的源代码,用于制作类似Google健身的环形计数器,感谢大大的源码共享,在分析代码过程中也学习到了很多,这是源码地址:http://download.jikexueyuan.com/detail/id/1040.html

接下来进行对源码的理解,以方便日后回顾学习,因为原作者对源码并没有注释,因此,有些东西都是边查边学习的。MainActivity中的代码如下所示:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        final FitChart fitChart=(FitChart)findViewById(R.id.fitChart);//FitChart是继承View的自定义控件,用于对图标进行初始化。
     //关于自定义控件的初步了解请看:http://www.cnblogs.com/0616--ataozhijia/p/4003380.html fitChart.setMinValue(0f); fitChart.setMaxValue(100f); findViewById(R.id.add).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Resources resources=getResources(); Collection<FitChartValue> values=new ArrayList<>();
          //FitChartValue用于设定图标元素的各项属性,此处共有四项,在效果图中可以得到显示
          //初始函数中的第一个数值是用于确定第一个value的旋转角度,而第二项则是用确定value的色彩

          values.add(new FitChartValue(30f,resources.getColor(R.color.chart_value_1)));
          values.add(new FitChartValue(20f,resources.getColor(R.color.chart_value_2))); values.add(new FitChartValue(15f,resources.getColor(R.color.chart_value_3))); values.add(new FitChartValue(10f,resources.getColor(R.color.chart_value_4))); fitChart.setValues(values); } });
} }

   将FitChartValue的集合传入fitChart中时,就可以进行界面的绘制了。可以打开fitChart的源码。

  1.绘制准备工作

  (1)构建背景属性。

  在初始化fitChart时,在其初始化函数中调用了initializeView函数。

1  private void initializeView(AttributeSet attrs) {
2         chartValues=new ArrayList<>();//构建表的各项属性的值
3         initalizeBackground();
4         readAttributes(attrs);
5         preparePaints();
6     }

其中initalizeBackground()用于绘制fitchart背景。其中有一个函数需要注意。

 1     private void initalizeBackground() {
 2 /*        在布局文件中加入了自定义控件,并在自定义控件的构造函数或者其他绘制相关地方使用系统依赖的代码,
 3         会导致可视化编辑器无法工作报错,一般会在下方提示:Use
 4         View.isInEditMode() in your custom views to skip code when shown in Eclipse
 5         解决方法:
 6         在自定义控件的构造函数中赋值语句后加如下判断:if
 7                 (isInEditMode()) { return; }
 8         isInEditMode:
 9         Indicates whether this View is currently in edit mode.
10         A View is usually in edit mode when displayed within a developer tool.
11                 For instance, if this View is being drawn by a visual user interface builder,
12         this method should return true. Subclasses should check the return value of this method to provide different behaviors
13         if their normal behavior might interfere with the host environment.
14                 For instance: the class spawns a thread in its constructor, the drawing code relies on device-specific features, etc.
15                 This method is usually checked in the drawing code of custom widgets.
16                 http://blog.csdn.net/luohai859/article/details/42029713
17                 */
18         if(!isInEditMode())
19         {
20             if(getBackground()==null){
21                 setBackgroundColor(getContext().getResources().getColor(R.color.default_back_color));//设置当前容器的背景颜色
22             }
23         }
24     }

从系统源码的英文注释可以看出,在布局文件中加入了自定义控件,并在自定义控件的构造函数或者其他绘制相关地方使用系统依赖的代码, 会导致可视化编辑器无法工作报错,因此需要进行isInEditMode()进行判断。相关信息可见:http://blog.csdn.net/luohai859/article/details/42029713

readAttributes(attrs)用于读取传入的属性,将自定义控件类中变量与attrs.xml中的属性连接起来。

 1 private void readAttributes(AttributeSet attrs) {
 2         Resources resourses=getContext().getResources();
 3         valueStrokeColor=resourses.getColor(R.color.default_chart_value_color);
 4         backStrokeColor=resourses.getColor(R.color.default_back_stroke_color);
 5         /*
 6         •getDimension()是基于当前DisplayMetrics进行转换,获取指定资源id对应的尺寸。
 7         文档里并没说这里返回的就是像素,要注意这个函数的返回值是float,像素肯定是int。
 8         •getDimensionPixelSize()与getDimension()功能类似,不同的是将结果转换为int,并且小数部分四舍五入。
 9         •getDimensionPixelOffset()与getDimension()功能类似,不同的是将结果转换为int,
10         并且偏移转换(offset conversion,函数命名中的offset是这个意思)是直接截断小数位,
11         即取整(其实就是把float强制转化为int,注意不是四舍五入哦)。
12         http://www.eoeandroid.com/thread-322627-1-1.html?_dsign=91c59c8f
13         */
14         strokeSize=resourses.getDimension(R.dimen.default_stroke_size);
15         if(attrs!=null)
16         {
17            /* http://blog.csdn.net/ff313976/article/details/7949614
18            * http://www.cnblogs.com/DonkeyTomy/archive/2012/07/26/2609971.html
19            *
20         set         The base set of attribute values.  May be null.
21 
22          attrs         The desired attributes to be retrieved.
23 
24         defStyleAttr    An attribute in the current theme that contains a reference to a style resource that supplies defaults values for the StyledAttributes.
25         Can be 0 to not look for defaults.当前主题的默认风格属性来自一个风格设计资源的引用。能为0或者不再寻找默认(上面优先级的第四个)
26 
27         defStyleRes    A resource identifier of a style resource that supplies default values for the StyledAttributes,
28         used only if defStyleAttr is 0 or can not be found in the theme.
29          Can be 0 to not look for defaults.一个风格类资源的ID,只当defStyleAttr为0或者找不到时使用.
30 
31          */
32             TypedArray attributes=getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.FitChart, 0, 0);
33             /**
34              * Retrieve a dimensional unit attribute at <var>index</var> for use
35              * as a size in raw pixels.  This is the same as
36              * {@link #getDimension}, except the returned value is converted to
37              * integer pixels for use as a size.  A size conversion involves
38              * rounding the base value, and ensuring that a non-zero base value
39              * is at least one pixel in size.
40              * <p>
41              * This method will throw an exception if the attribute is defined but is
42              * not a dimension.
43              *
44              * @param index Index of attribute to retrieve.
45              * @param defValue Value to return if the attribute is not defined or
46              *                 not a resource.
47              *
48              * @return Attribute dimension value multiplied by the appropriate
49              *         metric and truncated to integer pixels, or defValue if not defined.
50              * @throws RuntimeException if the TypedArray has already been recycled.
51              * @throws UnsupportedOperationException if the attribute is defined but is
52              *         not a dimension.
53              *
54              * @see #getDimension
55              * @see #getDimensionPixelOffset
56              */
57             strokeSize=attributes.getDimensionPixelSize(R.styleable.FitChart_strokeSize, (int) strokeSize);
58             valueStrokeColor=attributes.getColor(R.styleable.FitChart_valueStrokeColor, valueStrokeColor);
59             backStrokeColor=attributes.getColor(R.styleable.FitChart_backStrokeColor,backStrokeColor);
60             /**
61              * Retrieve the integer value for the attribute at <var>index</var>.
62              * <p>
63              * Unlike {@link #getInt(int, int)}, this method will throw an exception if
64              * the attribute is defined but is not an integer.
65              *
66              * @param index Index of attribute to retrieve.
67              * @param defValue Value to return if the attribute is not defined or
68              *                 not a resource.
69              *
70              * @return Attribute integer value, or defValue if not defined.
71              * @throws RuntimeException if the TypedArray has already been recycled.
72              * @throws UnsupportedOperationException if the attribute is defined but is
73              *         not an integer.
74              */
75             int attrAnimationMode=attributes.getInteger(R.styleable.FitChart_animationMode,ANIMATION_MODE_LINEAR);
76             if(attrAnimationMode==ANIMATION_MODE_LINEAR){
77                 animationMode=AnimationMode.LINEAR;
78             }else{
79                 animationMode=AnimationMode.OVERDRAW;
80             }
81             /**
82              * Recycles the TypedArray, to be re-used by a later caller. After calling
83              * this function you must not ever touch the typed array again.
84              *
85              * @throws RuntimeException if the TypedArray has already been recycled.
86              * \回收TypedArray,以便后面重用。在调用这个函数后,你就不能再使用这个TypedArray。
87 
88             在TypedArray后调用recycle主要是为了缓存。当recycle被调用后,这就说明这个对象从现在可以被重用了
89             http://blog.csdn.net/qq157819567/article/details/46737937
90              */
91             attributes.recycle();
92         }
93 
94 
95 
96     }
preparePaints()则是用于创建一个画笔类,并在其中设置了画笔的各项属性。
(2)填充fitchartvalue属性
 1     public void setValues(Collection<FitChartValue> values)
 2     {
 3         chartValues.clear();//清空原有的属性
 4         maxSweepAngle=0;
 5         float offsetSweepAngle =START_ANGLE;
 6         for(FitChartValue chartValue:values){
 7         float sweepAngle = calculateSweepAngle(chartValue.getValue());
 8         chartValue.setPaint(buildPaintForValue());//设置画笔
 9         chartValue.setStartAngle(offsetSweepAngle);//设置起始角度 = -90;
10         chartValue.setSweepAngle(sweepAngle);//设置扫描角的幅度
11         chartValues.add(chartValue);
12         offsetSweepAngle+=sweepAngle;
13         maxSweepAngle+=sweepAngle;
14          }
15         palyAnimation();
16     }

2.动画效果展示。
绘制准备工作完成后,就将进行,动画效果展示了,首先介绍一下ObjectAnimator类。

  请看http://blog.csdn.net/new_abc/article/details/40106569*/,以下截取几段要点。

 

  ObjectAnimator是ValueAnimator的子类,他本身就已经包含了时间引擎和值计算,所以它拥有为对象的某个属性设置动画的功能。这使得为任何对象设置动画更加的容易。
    你不再需要实现 ValueAnimator.AnimatorUpdateListener接口,因为ObjectAnimator动画自己会自动更新相应的属性值。
    ObjectAnimator的实例和ValueAnimator是类似的,但是你需要描叙该对象,需要设置动画的属性的名字(一个字符串),以及动画属性值的变化范围:
    ObjectAnimator anim = ObjectAnimator.ofFloat(foo, "alpha", 0f, 1f);
    anim.setDuration(1000);
    anim.start();
    为了使ObjectAnimator正确的更新属性值,你需要:
            1、你要设置动画的对象的属性必须有一个set该值的方法。因为ObjectAnimator在动画的过程中自动更新属性值,这是通过调用该属性的set方法来实现的。
    例如,如果属性的名字是foo,你需要有一个setFoo()的方法,如果不存在set方法,你可以有下面三个选择:
            1)、如果你有权限,你可以为该类添加一个set方法;
    2)、使用一个包裹类,通过该包裹类你可以去修改和获取属性值的变化,然后把它向前定向到原来的值
    3)、使用ValueAnimator类替换
    2、如果你在一个ObjectAnimator中只为属性值设置一个值,这个值被任务是动画的结束值。 这样的话,该对象必须有一个get方法来获取该属性值作为动画的起始值.
            get方法必须类似于get<属性名>.例如,如果属性的名字叫foo,你需要有一个getFoo(),方法。
            3、动画的属性值的gettter()方法(如果需要)和setter方法必须作用跟ObjectAnimator中的起始值是一个类型,例如如果你构造ObjectAnimator的方式是如下这样的,
            则该属性值的getter和setter方法必须如targetObject.setPropName(float) 和targetObject.getPropName(float),即都是浮点型
    ObjectAnimator.ofFloat(targetObject, "propName", 1f)
            4、依赖于你设置动画的对象和属性,你可能需要调用View的invalidate来强制屏幕重现绘制以及更新动画值。你可以在 onAnimationUpdate()中做这个工作。
    例如,为一个Drawable对象的颜色属性设置动画,你仅仅需要在该对象重绘的时候更新屏幕。所有View属性的set方法,例如setAlpha()和setTranslationX()自己会调用invalid方法,
    所以当这些属性值有更新时,你不需要再次调用invalid方法。要获取更多关于监听器的信息。你可以查看监听器章节。

 

palyAnimation用于执行绘制动画效果。
 1 private void palyAnimation() {
 2         ObjectAnimator animator=ObjectAnimator.ofFloat(this,"animationSeek",0.0f,1.0f);
 3 /*        Represents a group of Animations that should be played together.
 4         The transformation of each individual animation are composed together into a single transform.
 5                 If AnimationSet sets any properties that its children also set (for example, duration or fillBefore),
 6         the values of AnimationSet override the child values.*/
 7         AnimatorSet animationSet=new AnimatorSet();
 8         animationSet.setDuration(ANIMATION_DURATION);
 9         /**
10          * Sets the TimeInterpolator for all current {@link #getChildAnimations() child animations}
11          * of this AnimatorSet. The default value is null, which means that no interpolator
12          * is set on this AnimatorSet. Setting the interpolator to any non-null value
13          * will cause that interpolator to be set on the child animations
14          * when the set is started.
15          *
16          * @param interpolator the interpolator to be used by each child animation of this AnimatorSet
17          *
18          *                     Interpolator可以定义动画播放的速度
19          *                     DecelerateInterpolator:动画开始的地方比较慢,然后开始减速
20          */
21         animationSet.setInterpolator(new DecelerateInterpolator());
22         /**
23          * Sets the target object for all current {@link #getChildAnimations() child animations}
24          * of this AnimatorSet that take targets ({@link ObjectAnimator} and
25          * AnimatorSet).
26          *
27          * @param target The object being animated
28          */
29         animationSet.setTarget(this);
30         animationSet.play(animator);
31         animationSet.start();
32     }

由ObjectAnimator的讲解可知,必须设置一个setAnimationSeek类,用于调度动画。由上述代码可知,setAnimationSeek中的progress数值将在1秒内由0.0变为1.0,在这段时间内setAnimationSeek被连续调用。

 1     void setAnimationSeek(float value) {
 2         animationProgress = value;
 3         /**
 4          * Invalidate the whole view. If the view is visible,
 5          * {@link #onDraw(android.graphics.Canvas)} will be called at some point in
 6          * the future.
 7          * <p>
 8          * This must be called from a UI thread. To call from a non-UI thread, call
 9          * {@link #postInvalidate()}.
10          * 说明:请求重绘View树,即draw()过程,假如视图发生大小没有变化就不会调用layout()过程,并且只绘制那些“需要重绘的”
11 
12          视图,即谁(View的话,只绘制该View ;ViewGroup,则绘制整个ViewGroup)请求invalidate()方法,就绘制该视图。
13 
14 
15 
16          一般引起invalidate()操作的函数如下:
17 
18          1、直接调用invalidate()方法,请求重新draw(),但只会绘制调用者本身。
19 
20          2、setSelection()方法 :请求重新draw(),但只会绘制调用者本身。
21 
22          3、setVisibility()方法 : 当View可视状态在INVISIBLE转换VISIBLE时,会间接调用invalidate()方法,
23          http://blog.csdn.net/qinjuning/article/details/7110211
24          */
25         invalidate();
26     }


上述代码中调用了invalidate函数,而调用invalidate函数,则会直接调用onDraw函数,对当前控件进行重新绘制。

1     protected void onDraw(Canvas canvas) {
2         super.onDraw(canvas);
3         renderBack(canvas);
4         renderValues(canvas);
5     }

其中renderBack和renderValues用于对控件的重绘。renderBack用于绘制背景的那个圆,而renderValues用于绘制圆的各个色彩阶段。

 1 private void renderBack(Canvas canvas) {
 2         /**
 3          * The Path class encapsulates compound (multiple contour) geometric paths
 4          * consisting of straight line segments, quadratic curves, and cubic curves.
 5          * It can be drawn with canvas.drawPath(path, paint), either filled or stroked
 6          * (based on the paint's Style), or it can be used for clipping or to draw
 7          * text on a path.
 8          * Path类可以预先在View上将N个点连成一条"路径",
 9          * 然后调用Canvas的drawPath(path,paint)即可沿着路径绘制图形
10          * http://blog.csdn.net/lee576/article/details/7865121
11          */
12         Path path=new Path();
13         float viewRadius=getViewRadius();
14         /**
15          * Add a closed circle contour to the path
16          *
17          * @param x   The x-coordinate of the center of a circle to add to the path
18          * @param y   The y-coordinate of the center of a circle to add to the path
19          * @param radius The radius of a circle to add to the path
20          * @param dir    The direction to wind the circle's contour
21          */
22         path.addCircle(drawingArea.centerX(), drawingArea.centerY(), viewRadius,
23                 Path.Direction.CCW);
24 /*
25         绘制的方向,CW为顺时针方向,而CCW是逆时针方向。
26 */
27         /**
28          * Draw the specified path using the specified paint. The path will be
29          * filled or framed based on the Style in the paint.
30          *
31          * @param path  The path to be drawn
32          * @param paint The paint used to draw the path
33          */
34         canvas.drawPath(path, backStrokePaint);
35     }
36 
37     private float getViewRadius() {
38         if(drawingArea!=null){
39             return (drawingArea.width()/2);
40         }else {
41             return  DEFAULT_VIEW_RADIUS;
42         }
43 
44     }
 1    private void renderValues(Canvas canvas) {
 2       /*   Indicates whether this View is currently in edit mode. A View is usually
 3         in edit mode when displayed within a developer tool. For instance, if
 4         this View is being drawn by a visual user interface builder, this method
 5                 should return true.*/
 6         if(!isInEditMode())
 7         {
 8             int valuesCounter=(chartValues.size()-1);
 9             for(int index=valuesCounter;index>=0;index--){
10                 renderValue(canvas, chartValues.get(index));
11             }
12         }else{
13             renderValue(canvas,null);
14         }
15     }
16 
17     private void renderValue(Canvas canvas, FitChartValue fitChartValue) {
18         if(!isInEditMode()){
19             float animationSeek=calculateAnimationSeek();
20             Renderer renderer= RendererFactory.getRenderer(animationMode,fitChartValue,
21                  drawingArea);
22             Path path=renderer.buildPath(animationProgress,animationSeek);
23             if(path!=null){
24                 canvas.drawPath(path, fitChartValue.getPaint());
25             }
26         }else{
27           /*  static final int DESIGN_MODE_SWEEP_ANGLE = 216;*/
28             Path path=new Path();
29             path.addArc(drawingArea,START_ANGLE,DESIGN_MODE_SWEEP_ANGLE);
30             canvas.drawPath(path, valueDesignPaint);
31         }
32     }

此处还有一个onMeasure函数。

onMeasure函数的作用是,当子控件向父控件申请控件时,会调用onMeasure函数,并向父控件调入两个参数——widthMeasureSpec和heightMeasureSpec.
这两个参数指明控件可获得的空间以及关于这个空间描述的元数据.

 1 /**
 2      * <p>
 3      * Measure the view and its content to determine the measured width and the
 4      * measured height. This method is invoked by {@link #measure(int, int)} and
 5      * should be overridden by subclasses to provide accurate and efficient
 6      * measurement of their contents.
 7      * </p>
 8      *
 9      * <p>
10      * <strong>CONTRACT:</strong> When overriding this method, you
11      * <em>must</em> call {@link #setMeasuredDimension(int, int)} to store the
12      * measured width and height of this view. Failure to do so will trigger an
13      * <code>IllegalStateException</code>, thrown by
14      * {@link #measure(int, int)}. Calling the superclass'
15      * {@link #onMeasure(int, int)} is a valid use.
16      * </p>
17      *
18      * <p>
19      * The base class implementation of measure defaults to the background size,
20      * unless a larger size is allowed by the MeasureSpec. Subclasses should
21      * override {@link #onMeasure(int, int)} to provide better measurements of
22      * their content.
23      * </p>
24      *
25      * <p>
26      * If this method is overridden, it is the subclass's responsibility to make
27      * sure the measured height and width are at least the view's minimum height
28      * and width ({@link #getSuggestedMinimumHeight()} and
29      * {@link #getSuggestedMinimumWidth()}).
30      * </p>
31      *
32      * @param widthMeasureSpec horizontal space requirements as imposed by the parent.
33      *                         The requirements are encoded with
34      *                         {@link android.view.View.MeasureSpec}.
35      * @param heightMeasureSpec vertical space requirements as imposed by the parent.
36      *                         The requirements are encoded with
37      *                         {@link android.view.View.MeasureSpec}.
38      *
39      * @see #getMeasuredWidth()
40      * @see #getMeasuredHeight()
41      * @see #setMeasuredDimension(int, int)
42      * @see #getSuggestedMinimumHeight()
43      * @see #getSuggestedMinimumWidth()
44      * @see android.view.View.MeasureSpec#getMode(int)
45      * @see android.view.View.MeasureSpec#getSize(int)
46      * http://blog.csdn.net/pi9nc/article/details/18764863
47      */
48     @Override
49     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
50         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
51         /**
52          * Like {@link #getMeasuredWidthAndState()}, but only returns the
53          * raw width component (that is the result is masked by
54          * {@link #MEASURED_SIZE_MASK}).
55          *
56          * @return The raw measured width of this view.
57          */
58         int width=getMeasuredWidth();
59         /**
60          * Like {@link #getMeasuredHeightAndState()}, but only returns the
61          * raw width component (that is the result is masked by
62          * {@link #MEASURED_SIZE_MASK}).
63          *
64          * @return The raw measured height of this view.
65          */
66         int height=getMeasuredHeight();
67         int size=Math.max(width,height);
68         /**
69          * <p>This method must be called by {@link #onMeasure(int, int)} to store the
70          * measured width and measured height. Failing to do so will trigger an
71          * exception at measurement time.</p>
72          *
73          * @param measuredWidth The measured width of this view.  May be a complex
74          * bit mask as defined by {@link #MEASURED_SIZE_MASK} and
75          * {@link #MEASURED_STATE_TOO_SMALL}.
76          * @param measuredHeight The measured height of this view.  May be a complex
77          * bit mask as defined by {@link #MEASURED_SIZE_MASK} and
78          * {@link #MEASURED_STATE_TOO_SMALL}.
79          */
80         setMeasuredDimension(size, size);
81     }

 

demo的大致逻辑已经说明清楚了。在学习代码时,我喜欢将源码的英文注释和自己的注释放在一起,可能有点乱,但是看起来很安心。

 

转载于:https://www.cnblogs.com/wakerLight/p/4933202.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值