如何自定义圆弧按钮?

96
作者 GuaKin_Huang
2016.05.16 22:55 字数 3679

Android


番茄时钟工作法,有没有人认识这小不点的,哈哈,我来给大家解释下什么叫番茄时钟工作法。不过一看就知道是和番茄有关的,什么,你不信,哈哈,我也不信,告诉大家吧!简单来说嘛就是一套时间管理方法,选择一个一个你待完成的任务,例如打飞机。。。(嘘嘘嘘。。。),将番茄时钟,也就是一个计时器而已,设为25分钟,专注打飞机,专注打飞机。。。(打飞机这活中途不能停哦)直到番茄时钟响起就ok了。就这么简单。

喂喂喂,跑题啦!这次我们说的是圆弧按钮哦,也就图1-1这叼样。好,那咋们就进入正题,再说多一句,这番茄时钟工作法挺有用的,对于小编我这种定力不足的人来说,哈哈,犹如看着打码的片子突然变无码了,哈哈哈哈。。。


图1-1

咋们先来分析下。。。

咋一看去这东西是不是一个圆环呢,啊啊啊,五环,你比四环多一环,哈哈,小岳岳的歌终于被我用上了。既然是一个圆环,那肯定是由一个外圆和一个内圆组成的吧,中间呢,就是一个类似textView的东西吗?好,有这思路,那我们可以这样弄,先画两个半径不一的圆,再重叠在一起,(可以通过ImageView来弄),让后添加一个textView就大功告成了,这就是自定义控件中通过组合已有的控件来达到自定义控件的效果,这种方法比较简单,哈哈,都说程序员追求简单,但这也太没技术含量了,咱也是想装B的人,好下面就介绍点有技术含量的东西。

这东西所包含的元素,什么外圆,内圆,文字,咱们统统通过android的canvas画出来,哈哈,够逼格没,不过喜欢装逼的人,得先承受得起挫折,鄙视。。。

好,开始了!

咱们把上面一整块东西就看成个变了模样的Button,本来是眉清目秀的小姑娘,要变得如此惊艳,那得彻头彻尾地变啊,哈哈,那咱就自定义一个Button嘛,想她变成哪个美女据变成哪个美女,嘻嘻嘻。。。什么,自定义控件,搞啥子勒,别慌别慌,这东西入门是相当容易的,好,咱就开始入门自定义控件。

什么是自定义控件呢?

根据我的经验,虽然我没啥经验哈,这东西还是挺有用的,你要在手机上弄一些酷炫叼炸天的控件时,那大部分得用到自定义控件的,这东西呢,说复杂,那可以复杂到你头痛,复杂到你要去乖乖地拿起大学的高数,线性代数好好学;说简单呢,就简单的继承、重写,照着模板来就好,还是能弄个不错的东西出来的,拿来骗骗学弟学妹也是可以的哦!

控件的简单继承关系


控件继承

由上图可知,所有的控件都是继承自View 的,也就是说,View 是鼻祖,对于自定义控件来说,继承的祖先越大,灵活性就越好,越能创造出酷炫叼炸天的控件,但同时,难度也会有所增加,这次咱们就继承View ,然后开始酷炫叼炸天之旅行。

自定义控件的三种形式

  1. 继承已有的控件来实现自定义控件;
  2. 通过继承一个布局文件来实现自定义控件;
  3. 通过继承View类来实现自定义控件。

  • 新建CircleButton并继承View,并添加三个构造方法,重写OnDraw() 方法:
 /*构造方法*/
    public CircleButton(Context context) {
        this(context, null);
    }

    public CircleButton(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CircleButton(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView(context, attrs);//初始化界面,后面有具体实现
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
    }
  • 既然是自定义控件,那归根到底,它还是一个控件嘛,和ButtonTextView 可都是亲兄弟嘛,兄弟间必然是有相似的特征的,例如属性,点击事件。。。那下面咱们就来自定义属性先。咱们最终做出来的结果就是下图这逼:

效果图

那这里包括哪些自定义属性呢?一个是:中间文字的颜色(textColor)、文字的大小(textSize)、文字的内容(text);另一个是:圆环的颜色(backgroundColor)。

好,那咱们就自定义这些属性,该如何定义呢?别急,首先在res/values 下新建atts.xml 文件(前提是该目录下不存在该文件),然后就开始自定义上面的属性了

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!--name:属性的名字  format:该属性可以拥有的数据类型-->
    <declare-styleable name="CircleButton">
        <attr name="backgroundColor" format="color"/>
        <attr name="textSize" format="dimension"/>
        <attr name="text" format="string"/>
        <attr name="textColor" format="color"/>
    </declare-styleable>
</resources>

既然定义好了属性,那就得用他们,接下来就是获取自定义属性并赋值,在这里可以通过上下文的obtainStyledAttributes() 方法获取 :

private int backgroundColor;
private float textSize;
private String text;
private int textColor;
......
......
......

 /*获取自定义属性并赋值*/
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleButton);

        backgroundColor = typedArray.getColor(R.styleable.CircleButton_backgroundColor, Color.RED);
        textColor = typedArray.getColor(R.styleable.CircleButton_textColor, Color.WHITE);
        textSize = typedArray.getDimension(R.styleable.CircleButton_textSize, 80);
        text = typedArray.getString(R.styleable.CircleButton_text);

这里需要注意的是通过R.styleable.xxx 来找到属性的,因为在定义属性的时候就声明了
<declare-styleable name="CircleButton">

在获取完后,可以通过typedArray.recycle();进行回收,优化性能。

  • 定义完属性又获取到属性了,那下面就是去应用这些属性了。好,那咱们就先画圆,这得在OnDraw() 内画了,开始展示泡妞的功力了。

既然要画画,那是不是得需要画笔和画纸啊,总不能在哪淫想嘛,那样妹子早就跟别人跑了,这画笔和画纸就是传说中的PaintCanvas 了。好,咱们就拿来一根画笔Paint mPaint = new Paint(),先挑个颜色先mPaint.setColor(backgroundColor),为了画出细节,咱们需要挑一个笔尖比较细的画笔mPaint.setStrokeWidth(4),这画笔还分风格的,mPaint.setStyle(Paint.Style.STROKE),好了,画笔搞好了,接下来就是在画纸上画画了canvas.drawArc(10, 10, getWidth()-10, getHeight()-10, -90, -360+mAngle, true, mPaint), OK,打工搞成,详细代码在后面会给出。

这样就把圆给画出来了,下面就是画中间的文字了,同理,需要先配置好画笔,然后再在画纸上画画,不过这里需要注意的是,我们的先确认文字的起始点和长度,这样才能画出好看的文字可通过以下代码获取:

mPaint.getTextBounds(text, 0, text.length(), mRect);
int textWidth = mRect.width();
int textHeight = mRect.height();

接下来就是在画纸上画画
canvas.drawText(text, (getWidth()-textWidth) / 2, (getHeight()+textHeight) / 2, mPaint);


好,到这里为止,已经把一个静态的控件构建好了,接下来就可以在布局中调用了,咱们在acytivity_main.xml 调用吧:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:circleBtn="http://schemas.android.com/apk/res-auto"
    android:background="@color/colorPrimary"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.nlte.test_diyview.MainActivity">

    <com.nlte.test_diyview.CircleButton
        android:id="@+id/btn_circle_gray"
        android:layout_gravity="center"
        android:layout_width="200dp"
        android:layout_height="200dp"
        circleBtn:backgroundColor="#b0adad"
        circleBtn:textSize="30dp"
        circleBtn:textColor="@android:color/holo_red_light" />

    <com.nlte.test_diyview.CircleButton
        android:id="@+id/btn_circle_red"
        android:layout_gravity="center"
        android:layout_width="200dp"
        android:layout_height="200dp"
        circleBtn:backgroundColor="#ff0202"
        circleBtn:text="20"
        circleBtn:textSize="30dp"
        circleBtn:textColor="@android:color/holo_red_light" />

</FrameLayout>

为了后面的方便,在这里我们用FrameLayout, 并在里面添加的颜色不同的两个CircleButton控件,在这里需要注意的是,引用自定义控件是需要添加下面代码:xmlns:circleBtn="http://schemas.android.com/apk/res-auto"circleBtn 可以自由定义。

到这里可以说是大功告成了,但这只是一个静态的就控件,没有交互能力,既然如此,那我们还不如用张图片代替就ok了,还高搞这么麻烦干嘛,别急别急,咱们接下来开始让他活起来,简单粗暴点,给他添加一个点击事件,当点击按钮一次,就让里面的数字减一。

好的,接下来开始了!。。。

简单够粗暴,咱们可以直接在CircleButton添加点击事件,并将里面的数字减一,然后再重新刷新视图(修改视图时,一般需要刷新视图),代码如下(在initView()方法内添加):

this.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
               number--;
                //刷新视图
               invalidate();
            }
        });

但咱们不能这样子做,咱们是懒人,为了下次在其他地方也能够用这样一个控件,并根据需要来进行不同的时间处理,咱们可以通过接口回调的方法来实现调用者对控件事件的出处理,什么,接口回调,不懂啊,哈哈,悄悄告诉你,我也是半懂半懂,也是套着模板来的:

    private CirclebuttonClickListener mListener;
    public interface CirclebuttonClickListener{
        void circleButtonClick();
    }
    public void setOnCirclebuttonClickListener(CirclebuttonClickListener listener){
        mListener = listener;
    }
  1. 首先定义一个接口 interface CirclebuttonClickListener
  2. 创建接口变量 CirclebuttonClickListener mListener
  3. 暴露一个方法给调用者来注册接口回调,通过接口来获得回调者对接口方法的实现

然后刚才的代码就可以改成:

        this.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                mListener.circleButtonClick();
            }
        });

是不是没那么复杂呢?

接下来就就交给别人处理了,自己的任务完成了,(听着咋那么像卖身呢)

好,别人该如何用呢,其实和普通控件添加事件一样,该怎么弄就怎么弄:

private CircleButton mCircleButtonRed;
mCircleButtonRed = (CircleButton) findViewById(R.id.btn_circle_red);
mCircleButtonRed.setOnCirclebuttonClickListener(new CircleButton.CirclebuttonClickListener() {
            @Override
            public void circleButtonClick() {
               //事件处理
            }
        });

ok,大功告成,l来个效果图


效果图

接下来就是完整的代码

MainActivity.java

package com.nlte.test_diyview;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {
    private CircleButton mCircleButtonGray;
    private CircleButton mCircleButtonRed;
    private static int number = 69;
    private float angle = 0;
    private float EachAngle = 0;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mCircleButtonRed = (CircleButton) findViewById(R.id.btn_circle_red);
        mCircleButtonRed.setText(number+"");

        //必须要将360*1.0 / number转为float或者double,这样可以避免因为取整的问题而导致没有完全平分360度
        EachAngle = Float.parseFloat(String.valueOf(360*1.0 / number));

        mCircleButtonRed.setOnCirclebuttonClickListener(new CircleButton.CirclebuttonClickListener() {
            @Override
            public void circleButtonClick() {
                mCircleButtonRed.setText(--number+"");
                angle += EachAngle;
                System.out.println(angle);
                mCircleButtonRed.setAngle(angle);
            }
        });

    }
}

CircleButton

package com.nlte.test_diyview;

import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.os.Build;
import android.util.AttributeSet;
import android.view.View;

/**
 * Founction:
 * NLTE
 * 2016/5/15 0015
 */
public class CircleButton extends View {

    private Paint mPaint;
    private Rect mRect;
    private float mAngle;//弧的角度

    private int backgroundColor;
    private float textSize;
    private String text;
    private int textColor;

    /*通过接口回调的方式实现控件的点击事件添加和处理
   * 步骤:
   *   1.首先定义一个接口 interface CirclebuttonClickListener
   *   2.创建接口变量  CirclebuttonClickListener mListener
   *   3.暴露一个方法给调用者来注册接口回调,通过接口来获得回调者对接口方法的实现
   *   */
    private CirclebuttonClickListener mListener;
    public interface CirclebuttonClickListener{
        void circleButtonClick();
    }
    public void setOnCirclebuttonClickListener(CirclebuttonClickListener listener){
        mListener = listener;
    }

    /*构造方法*/
    public CircleButton(Context context) {
        this(context, null);
    }

    public CircleButton(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CircleButton(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView(context, attrs);
    }

    /**
     * Founction:Init the View
     * NLTE
     * 2016/5/15
     * @param context
     * @param attrs
     */
    private void initView(Context context, AttributeSet attrs) {
        mPaint = new Paint();
        //给Paint加上抗锯齿标志
        mPaint.setAntiAlias(true);
        mRect = new Rect();
        this.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                //方法一:
               /* number--;
                //刷新视图
                invalidate();*/

                //方法二:
                mListener.circleButtonClick();
            }
        });

        /*获取自定义属性并赋值*/
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleButton);

        backgroundColor = typedArray.getColor(R.styleable.CircleButton_backgroundColor, Color.RED);
        textColor = typedArray.getColor(R.styleable.CircleButton_textColor, Color.WHITE);
        textSize = typedArray.getDimension(R.styleable.CircleButton_textSize, 80);
        text = typedArray.getString(R.styleable.CircleButton_text);
        if (text == null){
            text = "";
        }
        //回收,避免浪费
        typedArray.recycle();

    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        //画圆
        mPaint.setColor(backgroundColor);
        mPaint.setStrokeWidth(4); //设置圆弧的宽度
        mPaint.setStyle(Paint.Style.STROKE); //设置圆弧
//        canvas.drawCircle(getWidth()/2, getHeight()/2, getWidth()/2-15, mPaint);
        /*
        * 第一 二个参数:暂且理解为圆的外切正方形左上角的坐标
        * 第三 四个参数:暂且理解为圆的外切正方形右下角的坐标
        * 第五 六个参数:绘制的起始点和终点
        * 第七个参数:是否绘制扇形
        * 第八个参数:画笔
        * */
        canvas.drawArc(10, 10, getWidth()-10, getHeight()-10, -90, -360+mAngle, true, mPaint);

        //中间有一个白色的数字 mRect是数字四周的边距
        mPaint.setColor(textColor);
        mPaint.setTextSize(textSize);
        mPaint.setTypeface(Typeface.DEFAULT);
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setStrokeWidth(0.1f);
        mPaint.getTextBounds(text, 0, text.length(), mRect);
        int textWidth = mRect.width();
        int textHeight = mRect.height();
        canvas.drawText(text, (getWidth()-textWidth) / 2, (getHeight()+textHeight) / 2, mPaint);
    }

    public int getBackgroundColor() {
        return backgroundColor;
    }

    @Override
    public void setBackgroundColor(int backgroundColor) {
        invalidate();
        this.backgroundColor = backgroundColor;
    }

    public float getTextSize() {
        return textSize;
    }

    public void setTextSize(float textSize) {
        invalidate();
        this.textSize = textSize;
    }

    public String getText() {
        return text;
    }

    public void setText(String text) {
        invalidate();
        this.text = text;
    }

    public int getTextColor() {
        return textColor;
    }

    public void setTextColor(int textColor) {
        invalidate();
        this.textColor = textColor;
    }

    public void setAngle(float angle) {
        mAngle = angle;
        invalidate();
    }
}

atts.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!--name:属性的名字  format:该属性可以拥有的数据类型-->
    <declare-styleable name="CircleButton">
        <attr name="backgroundColor" format="color"/>
        <attr name="textSize" format="dimension"/>
        <attr name="text" format="string"/>
        <attr name="textColor" format="color"/>
    </declare-styleable>
</resources>

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:circleBtn="http://schemas.android.com/apk/res-auto"
    android:background="@color/colorPrimary"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.nlte.test_diyview.MainActivity">

    <com.nlte.test_diyview.CircleButton
        android:id="@+id/btn_circle_gray"
        android:layout_gravity="center"
        android:layout_width="200dp"
        android:layout_height="200dp"
        circleBtn:backgroundColor="#b0adad"
        circleBtn:textSize="30dp"
        circleBtn:textColor="@android:color/holo_red_light" />

    <com.nlte.test_diyview.CircleButton
        android:id="@+id/btn_circle_red"
        android:layout_gravity="center"
        android:layout_width="200dp"
        android:layout_height="200dp"
        circleBtn:backgroundColor="#ff0202"
        circleBtn:text="20"
        circleBtn:textSize="30dp"
        circleBtn:textColor="@android:color/holo_red_light" />

</FrameLayout>

ps:如有错误或不足或需要优化的地方,请多多指教,不胜感激!!

Android