美文网首页Android自定义ViewAndroid自定义控件Android开发经验谈
手把手教你自定义View(一):实现QQ运动界面

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

作者: Android平头哥 | 来源:发表于2018-09-19 17:17 被阅读266次

    最近好长一段时间都没有写博客了,这段时间一直在学习自定义View,把任玉刚的《Android开发艺术探索》自定义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.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群:78797078

    相关文章

      网友评论

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

      本文链接:https://www.haomeiwen.com/subject/qaxdvftx.html