专栏名称: 安卓开发精选
伯乐在线旗下账号,分享安卓应用相关内容,包括:安卓应用开发、设计和动态等。
目录
相关文章推荐
鸿洋  ·  Google 为何设计了如此难用的 ... ·  昨天  
stormzhang  ·  时间不多了 ·  2 天前  
鸿洋  ·  Android14 WMS/AMS ... ·  2 天前  
stormzhang  ·  真正该刺激的是收入 ·  3 天前  
鸿洋  ·  一款高效的HarmonyOS工具包 ·  3 天前  
51好读  ›  专栏  ›  安卓开发精选

自定义View实现圆形水波进度条(上)

安卓开发精选  · 公众号  · android  · 2016-09-23 08:37

正文

(点击上方公众号,可快速关注)


来源:伯乐在线专栏作者 - Code4Android

链接:http://android.jobbole.com/84776/

点击 → 了解如何加入专栏作者


每次听到某大牛谈论自定义View,顿时敬佩之心,如滔滔江水连绵不绝,心想我什么时候能有如此境界,好了,心动不如行动,于是我开始了自定义View之路,虽然过程有坎坷,但是结果我还是挺满意的。我知道大牛还遥不可及,但是我已使出洪荒之力。此篇博客记录本人初入自定义View之路。


既然是初出茅庐,自然是按部就班的进行,先来一张效果图




本文章所写项目代码的GitHub链接

https://github.com/xiehui999/CustomBall


自定义属性


自定义属性,就是在资源文件夹下values目录中创建一个attrs.xml文件,

文件结构如下所示,atrr标签就是我们要自定义的一些属性,name就是自定义属性的名字,那么format是做什么的呢?


xml version="1.0" encoding="utf-8"?>

    declare-styleable name="">

        attr name="centerText" format="">attr>

        attr name=" ">

            enum name=""  value=" ">enum>

            enum name="" value=" ">enum>

        attr>

    declare-styleable>

resources>



format是属性对应的值的类型,有十个值


  • enm 枚举类型,例 android:orientation=”vertical” 此值有horizontal,和 vertical

  • dimension 尺寸值

  • color 颜色值,例 android:textColor = “#00FF00”

  • boolean 布尔值,true or false

  • flag 位或运算

  • float 浮点型

  • fraction 百分数,

  • reference 参考某一资源ID,例 android:background = “@drawable/ic_launcher”

  • string 字符串类型

  • integer 整型值


知道了这些值得含义,就可以自定义我们自己的属性了,对于这个进度条,我们可以自定义圆的半径,颜色,和圆中心文本的大小,颜色,文本,最后attrs.xml文件为


xml version="1.0" encoding="utf-8"?>

    declare-styleable name="CustomBallView">

        attr name="centerText" format="string">attr>

        attr name="centerTextSize" format="dimension">attr>

        attr name="centerTextColor" format="color">attr>

        attr name="ballColor" format="color">attr>

        attr name="ballRadius" format="dimension">attr>

    declare-styleable>

resources>


布局文件配置相关内容


在布局文件要配置我们自定义的属性,首先要自定义命名空间,



如上图,如果在as中命名空间写成http://schemas.android.com/apk/res/包名 此时as会报错,这是gradle造成的,在eclipse中如果自定义的属性 是不能用res-auto的 必须得替换成你自定义view所属的包名,如果你在恰好使用的自定义属性被做成了lib 那就只能使用res-auto了,而在android-studio里,无论你是自己写自定义view 还是引用的lib里的自定义的view 都只能使用res-auto这个写法。以前那个包名的写法 在android-studio里是被废弃无法使用的

所以配置后的布局文件如下


xml version="1.0" encoding="utf-8"?>

LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

    xmlns:customBallView="http://schemas.android.com/apk/res-auto"

    xmlns:tools="http://schemas.android.com/tools"

    android:layout_width="match_parent"

    android:layout_height="match_parent"

    android:orientation="vertical"

    tools:context="com.example.xh.customball.MainActivity"

    tools:showIn="@layout/activity_main">

 

    TextView

        android:id="@+id/textView"

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:visibility="gone"

        android:text="Hello World!" />

 

    com.example.xh.customball.CustomBall

        android:background="@color/colorPrimary"

        android:layout_centerInParent="true"

        android:layout_margin="10dp"

        customBallView:centerText="30%"

        customBallView:centerTextSize="28dp"

        customBallView:centerTextColor="#000000"

        customBallView:ballColor="@color/colorAccent"

        customBallView:ballRadius="30dp"

        android:layout_width="260dp"

        android:layout_height="260dp">

    com.example.xh.customball.CustomBall>

LinearLayout>


自定义控件


有了上边的操作,接下来就开始到了真正自定义控件的时候了,创建一个CustomBall类继承View类,先看构造方法,我们写成构造方法最终调用三个参数的构造方法,获取自定义属性的值及初始化工作就在三个参数构造方法中进行。下面我先来绘制一个圆,文字画在圆心试试手,效果如图



当然绘制这个图形,首先获取我们自定义属性值,可通过下面获取属性值

注意通过TypedArray 获取属性值后要执行typedArray.recycle();回收内存,防止内存泄漏。


/**

* 获取自定义属性

*/

TypedArray typedArray=context.obtainStyledAttributes(attrs,R.styleable.customBallView);

        centerText=typedArray.getString(R.styleable.customBallView_centerText);

        Log.e("TAG","centerText"+centerText);

        centerTextSize=typedArray.getDimension(R.styleable.customBallView_centerTextSize,24f);

        centerTextColor=typedArray.getColor(R.styleable.customBallView_centerTextColor,0xFFFFFF);

        ballColor=typedArray.getColor(R.styleable.customBallView_ballColor,0xFF4081);

        radius=typedArray.getDimension(R.styleable.customBallView_ballRadius,260f);

        typedArray.recycle();


初始化画笔


/**

     * 初始化画笔

     */

    private void initPaint() {

        roundPaint = new Paint();

        roundPaint.setColor(ballColor);

        roundPaint.setAntiAlias(true);//抗锯齿

        fontPaint = new Paint();

        fontPaint.setTextSize(centerTextSize);

        fontPaint.setColor(centerTextColor);

        fontPaint.setAntiAlias(true);

        fontPaint.setFakeBoldText(true);//粗体

 

    }


接下来我们先画一个圆,先通过下面方法获取空间本身的宽和高,然后调用canvas.drawCircle(width/2, height/2, radius, roundPaint);画圆,在原点设置为控件中心位置,即点(width/2, height/2),半径为radius,画笔roundPaint,接下来绘制文字,将位子绘制在圆的中心。


width = getWidth() ;

height = getHeight();


如果我们通过canvas.drawText(centerText, width/2, height/2, fontPaint);绘制文字的话,发现文字并不是在中心位置,那么我们可以做一下调整,canvas.drawText(centerText, width/2, height/2, fontPaint);先通过float textWidth = fontPaint.measureText(centerText);获取文字的宽度,canvas.drawText(centerText, width/2-textWidth /2, height/2, fontPaint);此时文字依然不在中心,那么此时我们研究一下文字到底是怎么绘制的,为什么坐标试试中心了,绘制出来的效果依然有偏差呢。


要关注文字绘制的话,FontMetrics这个类是必须要知道的因为它的作用是测量文字,它里面呢就定义了top,ascent,descent,bottom,leading五个成员变量其他什么也没有。先看源码


public static class FontMetrics {

        /**

         * The maximum distance above the baseline for the tallest glyph in

         * the font at a given text size.

         */

        public float   top;

        /**

         * The recommended distance above the baseline for singled spaced text.

         */

        public float   ascent;

        /**

         * The recommended distance below the baseline for singled spaced text.

         */

        public float   descent;

        /**

         * The maximum distance below the baseline for the lowest glyph in

         * the font at a given text size.

         */

        public float   bottom;

        /**

         * The recommended additional space to add between lines of text.

         */

        public float   leading;

    }


这个类是Paint的静态内部类,通过注释我们就知道了每个变量的含义,为了更生动的理解这几个变量含义,我们通过下面的一张图来分别解释每个变量的含义



  • Baseline(基线) 在Android中,文字的绘制都是从Baseline处开始的

  • ascent(上坡度)Baseline往上至文字“最高处”的距离我们称之为ascent,

  • descent(下坡度)Baseline往下至文字“最低处”的距离我们称之为descent(下坡度)

  • leading(行间距)表示上一行文字的descent到该行文字的ascent之间的距离

  • top 对于ascent上面还有一部分内边距,内边距加上ascent即为top值

  • bottom descent和内边距的加上descent距离值得注意的一点,Baseline上方的值为负,下方的值为正如下图文字30%的ascent,descent,top,bottom。



通过上面的分析,我们就得出了将文本绘制中心的代码如下


//测量文字的宽度

float textWidth = fontPaint.measureText(centerText);

        float x = width / 2 - textWidth / 2;

        Paint.FontMetrics fontMetrics = fontPaint.getFontMetrics();

        float dy = -(fontMetrics.descent + fontMetrics.ascent) / 2;

        float y = height / 2  + dy;

        canvas.drawText(centerText, x, y, fontPaint);


至此这个简单自定义的View基本实现,此时我改了布局配置文件为宽高


android:layout_width="wrap_content"

android:layout_height="wrap_content"


或者


 android:layout_width="match_parent"

  android:layout_height="match_parent"


Oh my God,为什么效果是一样的啊,此时再回到自定义的类,我们发现我们没有实现onMeasure里面测量的代码,接下来让我们实现onMeasure操作,如下


@Override

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        //测量模式

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);

        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        //测量规格大小

        int widthSize = MeasureSpec.getSize(widthMeasureSpec);

        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

 

        int width;

        int height;

        if (widthMode == MeasureSpec.EXACTLY) {

            width=widthSize;

        } else if (widthMode == MeasureSpec.AT_MOST) {

            width=(int)Math.min(widthSize,radius*2);

        } else {

 

            width=windowWidth;

        }

        if (heightMode == MeasureSpec.EXACTLY) {

            height=heightSize;

        } else if (heightMode == MeasureSpec.AT_MOST) {

            height=(int)Math.min(heightSize,radius*2);

        } else {

            height=windowHeight;

        }

        setMeasuredDimension(width,height);

    }


测量主要依靠MeasureSpec,MeasureSpec(测量规格)是一个32位的int数据.其中高2位代表SpecMode即某种测量模式,低32位为SpecSize代表在该模式下的规格大小,测量模式有三种


  • EXACTLY 确切的,在布局文件中设置的宽高是固定的,此时测量大小就是我们设置的宽高

  • AT_MOST 至多,不能超出

  • UNSPECIFIED 未指定


MeasureSpec的详细解释

http://blog.csdn.net/lfdfhl/article/details/50880382


通过上面的分析,绘制此图形的完整代码为 点击查看

https://github.com/xiehui999/CustomBall/blob/master/app/src/main/java/com/example/xh/customball/CustomBall.java


控件升级


上面我们已经实现了圆形和文本的绘制,那么接下来,我们先开始实现中心新进度的更新绘制。先看效果图



接下文


关注「安卓开发精选」

看更多精选安卓技术文章
↓↓↓