手把手教你自定义View(一):实现QQ运动界面

最近好长一段时间都没有写博客了,这段时间一直在学习自定义View,把任玉刚的《Android开发艺术探索》自定义View章节看了好几遍,决心写篇博客记录一下,巩固一下知识点。今天给大家带来的是QQ运动界面的实现,先看效果图。

《手把手教你自定义View(一):实现QQ运动界面》 demo

可以设置字体的颜色,步数。接下来我们一起来看看是怎么实现的把,大体上分为以下四个步骤:

  • 自定义View属性
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!--自定义view的属性-->
    <declare-styleable name="MySportView">
        <!--黄色圆环 颜色-->
        <attr name="yellowRingColor" format="color"></attr>
        <!--红色圆环 颜色-->
        <attr name="redRingColor" format="color"></attr>
    </declare-styleable>
</resources>

依次定义了黄色圆环和红色圆环的颜色,name是该属性的名字,format是该属性的取值类型,比如颜色是color,字体大小是dimension等等。接下来就是在我们的布局文件中申明我们自定义的属性了。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:mySportView="http://schemas.android.com/apk/res-auto"
    >
    <my.zzg.qq.View.MySportView
        android:id="@+id/mySportViw"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        mySportView:redRingColor="@color/redRing"
        mySportView:yellowRingColor="@color/yellowRing"
        />
</LinearLayout>

注意千万不要忘记引入我们的命名空间, xmlns:mySportView=”http://schemas.android.com/apk/res-auto“。

自定义了View的属性后,接下来就要获取自定义View的属性了。

  • 获取自定义View属性。
    public MySportView(Context context) {
        this(context, null);
    }

    public MySportView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MySportView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.context = context;
        TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.MySportView, defStyleAttr, 0);
        int indexCount = typedArray.getIndexCount();
        for (int i = 0; i < indexCount; i++) {
            int index = typedArray.getIndex(i);
            switch (index) {
                case R.styleable.MySportView_redRingColor:
                    // 默认给 黑色
                    redRing = typedArray.getColor(index, Color.BLACK);
                    break;
                case R.styleable.MySportView_yellowRingColor:
                    yellowRing = typedArray.getColor(index, Color.BLACK);
                    break;
            }
        }
        typedArray.recycle();
        init();
     }

自定义View需要我们去实现三个构造方法。需要注意的地方:

《手把手教你自定义View(一):实现QQ运动界面》 view.png

第一个构造函数: 当不需要使用xml声明或者不需要使用inflate动态加载时候,实现此构造函数即可,一般情况下,我们在代码中生成控件使用。
第二个构造函数: 当需要在xml中声明此控件,则需要实现此构造函数,并且在构造函数中把自定义的属性与控件的数据成员连接起来。
第三个构造函数:在第二个构造函数的基础上,接受一个style资源 。
我们可以看到,这三个构造函数之间是一种递进的关系,所以我们在第三个构造函数中获取自定义View的属性了。

第一步通过theme.obtainStyledAttributes方法获得自定义控件的主题样式数组。

  public TypedArray obtainStyledAttributes(AttributeSet set,
                @StyleableRes int[] attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
            return mThemeImpl.obtainStyledAttributes(this, set, attrs, defStyleAttr, defStyleRes);
        }

我们需要关注一下第二个参数,第二个参数的意思是想要获取的属性集合,也就是我们自定义的View的属性集合。
第二步去遍历这个主题样式数组,获取属性值,也就是我们在xml文件中所写的属性值。
第三步在循环结束的时候,要调用typedArray.recycle()进行资源的回收。
第四步在获取到自定义View的属性值后,去做一些必要的初始化工作。比如初始化画笔颜色等等,需要注意的地方是,不要在onDraw()方法里去做初始化工作,因为onDraw()方法是一个频繁操作的过程,如果在里面频繁的new对象会造成大量的内存浪费,不可取。

  • 确定View的大小,重写onMeasure()方法。

如果我们没有重新onMeasure()方法的话,那么系统会调用其默认的onMeasure()方法。

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

该方法的作用是测量控件的大小,系统在加载布局文件的时候,会测量各子View的大小,来告诉父View,我需要占用多大空间,父View根据自己的大小分配空间给子View。

为了更好的理解测量的这个过程,我们还需要理解一下 MeasureSpec,MeasureSpec代表了一个32位int值,高2两位代表SpecMode,低30位代表SpecSize。SpecMode是指测量模式,而SpecSize表示在某种测量模式下的大小。

SpecMode模式一共有3种:
MeasureSpec.EXACTLY:父容器已经检测到了子View所需要的精确的大小值,这时子View的最终大小值就是SpecSize所指定的值。一般是在布局文件中设置了明确的数值或match_parent
MeasureSpec.AT_MOST:子视图的大小最多是specSize中的大小;表示子布局限制在一个最大值内,一般为WARP_CONTENT
MeasureSpec.UNSPECIFIED:父视图不对子视图施加任何限制,子视图可以得到任意想要的大小,一般用于系统内部。

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        int width, height;

        if (heightMode == MeasureSpec.EXACTLY) {
        // 如果设置了明确值,最终的高就是这个明确值;如果布局中设置的是match_parent,最终的高就是父布局的大小。
            height = heightSize;
        } else {
       // 反之,最终的高为布局的3/4
            height = heightSize * 3 / 4;
        }

        if (widthMode == MeasureSpec.EXACTLY) {
            width = widthSize;
        } else {
            width = widthSize * 1 / 2;
        }
        mWidth = width;
        mHeight = height;
        setMeasuredDimension(width, height);
    }

我这里高为布局的高的3/4,宽为布局的1/2,具体情况因人而异,最后调用setMeasuredDimension()方法。

  • onDraw进行绘制。
    绘制View的方法。我们分析我们的View,首先要绘制两个圆弧,先绘制黄色的圆弧,接着绘制红色的圆弧,接着绘制文字。我们一步一步分析。
/***
     *  绘制红色圆弧度
     * @param canvas
     */
    private void drawRedRing(Canvas canvas) {
        RectF rectf = new RectF(mWidth * 1 / 4, mWidth * 1 / 4, mWidth * 3 / 4, mWidth * 3 / 4);
        canvas.drawArc(rectf, startAngle, currentAngle, false, redRingPaint);
    }

绘制圆弧首页要知道圆弧的范围,drawArc()方法接收五个参数,第一个参数的该圆弧所在圆的外接矩形的坐标,第二个参数是圆弧开始的角度,第三个参数是圆弧张开的角度大小,第四个参数为true时,表示在绘制圆弧时,同时绘制圆弧到圆心的连线,通常用来绘制扇形,我们这里传false,第五个参数是画笔。

 /***
     *  绘制黄色圆环
     * @param canvas
     */
    private void drawYellowRing(Canvas canvas) {
        RectF rectf = new RectF(mWidth * 1 / 4, mWidth * 1 / 4, mWidth * 3 / 4, mWidth * 3 / 4);
        canvas.drawArc(rectf, startAngle, sweepAngle, false, yellowRingPaint);
    }

绘制黄色圆弧,圆弧的张开角度是一个动态的过程,它的大小随着步数的增加而发生动态变化。

/***
     * 绘制 '步数' 这两个字
     * @param canvas
     */
    private void drawStepText(Canvas canvas) {
        // 设置文字可以水平居中显示
        canvas.drawText(totalStep,(mWidth-mBound.width())/2,mWidth*1/2+100,stepTextPaint);
    }

    /***
     * 绘制 一共走了多少步数
     * @param canvas
     */
    private void drawText(Canvas canvas) {
        canvas.drawText(currentStepText,(mWidth-mTextBound.width())/2,mWidth*1/2+50,textPaint);
    }

绘制文字就显得简单多了,计算好绘制的文字的位置就好了。

在进行重写onDraw()方法的时候,我们需求明确View的坐标位置,然后分析需要调用哪些方法去绘制,一步一步的去绘制,把逻辑搞清楚了,绘制出来应该不难。

  • 关于动画的实现
 /***
     * 执行 红色圆弧 动画
     * @param
     */
    private void startAnimation(float start, float end, int duration) {
        Log.i("当前",end+"");

        ValueAnimator valueAnimator = ValueAnimator.ofFloat(start, end);
        valueAnimator.setDuration(duration);
        valueAnimator.setTarget(currentAngle);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                currentAngle = (float) animation.getAnimatedValue();
                Log.i("当前的进度",currentAngle+"");
                postInvalidate();
            }
        });
        valueAnimator.start();
    }

    /**
     * 执行文字的动画
     * @param start
     * @param end
     * @param duration
     */
    private void startTextAnimation(int start, int end, int duration) {
        Log.i("当前",end+"");

        ValueAnimator valueAnimator = ValueAnimator.ofInt(start, end);
        valueAnimator.setDuration(duration);
        valueAnimator.setTarget(currentAngle);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                currentStepText = (int)animation.getAnimatedValue()+"";
                Log.i("当前的进度",currentStepText+"");
                postInvalidate();
            }
        });
        valueAnimator.start();

    }

这里我采用了属性动画,只需要设置好开始值和一个结束值,并设置动画监听,就能够得到变化的值,并且调用postInvalidate()方法进行重绘,在onDraw方法里进行数值的改变。
最后,我们在View里写一个设置数据的方法供Activity调用:

/***
     *  设置所走的步数
     * @param totalNum 所有的步数
     * @param currentNum 当前的步数
     */
    public void setData(int totalNum, int currentNum) {

        currentStepText = currentNum+"";
        textPaint.getTextBounds(currentStepText,0,currentStepText.length(),mTextBound);
        float percent = (float) currentNum/totalNum;
        currentAngle = percent*sweepAngle;
        Log.i("当前的",currentAngle+"");
        startAnimation(0,currentAngle,duration);
        startTextAnimation(0,currentNum,duration);
    }

然后在Activity里调用:

protected void onCreate(@Nullable Bundle savedInstanceState) 
{
        setContentView(R.layout.qq_sport_activity);
        super.onCreate(savedInstanceState);
        mySportView = (MySportView) findViewById(R.id.mySportViw);
        mySportView.setData(4066,997);
 }

自己根据情况设置值就好了。

总结

走一遍流程下来,我们发现自定义View并没有想象中的那么复杂,我们需要走好其中的几个关键的步骤,第一,测量View的大小,根据自己的情况,选择是wrap_content还是具体的数值还是 match_parent,重新onMeasure()方法,第二,重新onDraw()方法,根据你要绘制什么View,调用不同的绘制方法,需要注意的是View的坐标。只要我们多加练习,就没有什么View能够难得住我们的,加油!

后续代码我会更新到Github上去,欢迎下载。

Android技术讨论Q群:947460837

    原文作者:Android平头哥
    原文地址: https://www.jianshu.com/p/f0a50ee6e417
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞