Android应用界面开发——自定义控件(实现俯卧撑计数器)

对于Android应用的其他UI控件来说,它们都继承了View,然后在View提供的空白区域上进行绘制。当Android系统中提供的UI控件不足以满足需要时,我们可以来自定义控件。本篇文章主要介绍自定义控件的创建形式以及通过一个俯卧撑计数器Demo来介绍如何进行自定义控件。

在介绍自定义控件之前,先学习一下关于尺寸(dp,sp,px)和Inflater的知识。

px,dp,sp


px:像素,密度不同,效果不同,一般不用,在屏幕上画一条很细的线时偶尔会用。例如:

<View
        android:layout_width="match_parent"
        android:layout_height="1px"
        android:background="#000000"/>

dp:density-independent pixel,密度无关像素。非文字尺寸一律用dp。
dip = dp。
sp:scale-independent pixel,缩放无关像素。文字尺寸一律用sp。
dpi:dots per inch,一英寸多少个像素点。标准值为160,一般称作像素密度。
density:密度,和标准dpi的比例(160px/inc)。

换算公式如下:

  • dp = (dpi/(160像素/英寸))px = density px
    注意:这里都是带单位的。px是单位,dp是单位,density没单位。
  • 那么转换为数值计算的话,应该是下面这个式子:
    PX = density * DP
    也就是像素值 = density * 设备无关像素值 ,�请注意这里有个字。

Inflater

LayoutInflater:布局填充器,将xml布局转化为View对象。

获得LayoutInflater实例的三种方式:

  • getLayoutInflater();
  • getSystemService(LAYOUT_INFLATER_SERVICE);
  • LayoutInflater.from(context);

提取布局的属性:theme & style


当布局中的控件有一些共同的属性时,可以把公共的部分提取成theme或style,也可以通过为其设置parent为它们的父类,以继承父类的属性。

  • Theme是针对窗体级别的,改变窗体样式。
  • Style是针对窗体元素级别的,改变指定控件或者Layout的样式。

系统中默认的styles.xml如下:

<resources>

    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>

</resources>

如果自定义的话,可以模仿该文件,style定义好以后,那么它是被如何引用的,还是看看系统中引用吧,如下:

android:theme="@style/AppTheme"

由于style中定义的是主题,所以引用的时候为android:theme,然后@定义的主题名称路径即可引用定义好的主题。

自定义控件的三种创建形式


我们知道所有的控件都继承于View,所以自定义控件也需要继承View,那么View是如何工作的呢,和其他类一样,需要通过构造器对其进行初始化,接着调用它的各种方法来完成它的功能。

View的常用方法:

  • onMesure():定大小。
  • onLayout():定位置。
  • onDraw():绘制。
  • invalidate():刷新。

自定义控件的三种创建形式:

  • 继承已有的控件来实现自定义控件。
  • 通过继承一个布局文件实现自定义控件。
  • 通过继承View类来实现自定义控件。

自定义控件实现俯卧撑计数器


需求:

  1. 在主界面输入一个数字。
  2. 在计数器界面做一个圆形的黑色按钮。
  3. 将主界面上输入的数字显示到黑色按钮的中间。
  4. 数字起始为用户输入。
  5. 每点击一次(鼻子每接触一次按钮)减少1。

主界面主要用到了之前学过的EditText和Button,并将EditText中输入的数字通过Intent传到计数器界面,这里不多阐述,主要讲自定义控件部分。

主界面如图:

自定义控件的代码:CounterView.java

public class CounterView extends View implements View.OnClickListener {

    private Paint mPaint;
    private Rect mRect;
    private int number = Integer.parseInt(PushUpsActivity.number);
    private int mBackgroundColor;
    private int mTextColor;
    private int mTextSize;


    public CounterView(Context context) {
        //在构造器中用this调用后边的构造器,这样只在最后一个构造器中初始化view即可。
        this(context, null);
    }

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

    public CounterView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);
    }

    private void init(Context context, AttributeSet attrs) {
        mPaint = new Paint();
        mRect = new Rect();

        this.setOnClickListener(this);

        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CounterView);
        mBackgroundColor = typedArray.getColor(R.styleable.CounterView_backgroundColor, Color.BLACK);
        mTextColor= typedArray.getColor(R.styleable.CounterView_textColor, Color.YELLOW);
        mTextSize = (int) typedArray.getDimension(R.styleable.CounterView_textSize, 18);
    }

    /**
     * 在onDraw方法中画圆形按钮和数字
     *
     * @param canvas
     */
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //画图需要画笔,但不能在onDraw中创建Paint,因为onDraw使用频繁,会导致过多的内存消耗,所以创建工作放在了init中。
        mPaint.setColor(mBackgroundColor);
        mPaint.setAntiAlias(false);
        //画圆形按钮
        canvas.drawCircle(getWidth() / 2, getHeight() / 2, getWidth() / 2, mPaint);
        //画白色数字需要将画笔颜色设置为白色
        mPaint.setColor(mTextColor);
        //设置字体大小
        mPaint.setTextSize(mTextSize);
        String text = String.valueOf(number);
        mPaint.getTextBounds(text, 0, text.length(), mRect);

        int textWidth = mRect.width();
        int textHeight = mRect.height();

        canvas.drawText(text, getWidth() / 2 - textWidth / 2, getHeight() / 2 + textHeight / 2, mPaint);//居中显示
    }

    @Override
    public void onClick(View v) {
        if (number > 0) {
            number--;
            invalidate();
        } else if (number == 0) {
            Toast.makeText(getContext(), "训练结束", Toast.LENGTH_LONG).show();
        }
    }
}

新建CounterView.java继承View,并实现其构造器,前两个构造器中用this调用后面的构造器,这样只在最后一个构造器中用init初始化View即可。

接下来重写onDraw方法,在该方法中画圆形按钮和数字,onDraw方法中传入了画布canvas,还缺一个画笔,创建一个画笔Paint,因为onDraw使用的比较频繁,所以Paint的创建以及后面Rect的创建都放入init中。创建好画笔,为画笔设置颜色,通过paint.setAntiAlias去掉画笔的锯齿,利用canvas.drawCircle就可以画圆了,drawCircle的前两个参数为圆心坐标,第三个参数为半径,第四个参数为画笔。

圆形按钮画好了,接着画中间的数字,同样设置画笔颜色,字体大小,这里需要注意,画数字之前需要通过getTextBounds测量一下数字的大小。getTextBounds字面意思是获取text边界,即把text用一个Rect框起来,它的第一个参数是要画的text,第二个参数是起始位置,第三个参数是结束位置,其实就是text的长度,第四个参数是Rect。

设置完毕,通过canvas.drawText来画数字,第一个参数是要画的text,第二和第三个参数分别为x,y坐标,第四个参数为paint。

处理按钮点击,这里的视图本身就是一个按钮,所以this.setOnClickListener(this);如果数字大于零,每点击一次让数字减一,并用invalidate刷新视图,直到数字为零。

写好自定义控件,在布局文件中引用即可,引用之前需要了解一下自定义控件的属性。自定义控件属性的步骤为:

1.定义attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CounterView">
        <attr name="backgroundColor" format="color"/>
        <attr name="textColor" format="color"/>
        <attr name="textSize" format="dimension"/>
    </declare-styleable>
</resources>

2.添加命名空间

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

定义好自定义控件的属性,引用自定义控件的布局如下:

activity_pushups.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:paddingLeft="20dp"
        android:paddingTop="20dp"
        android:text="开始训练吧,练出完美胸肌"
        android:textColor="#000000"
        android:textSize="25sp" />

    <com.trampcr.pushupcounter.CounterView
        android:id="@+id/counter_view"
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:layout_below="@+id/text"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="80dp"
        app:backgroundColor="#000000"
        app:textColor="#ffff00"
        app:textSize="80sp" />

    <Button
        android:id="@+id/btn_stop"
        android:layout_width="200dp"
        android:layout_height="60dp"
        android:layout_below="@+id/counter_view"
        android:layout_centerHorizontal="true"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="60dp"
        android:text="结束训练"
        android:textColor="#000000"
        android:textSize="20sp" />

</RelativeLayout>

引用了自定义控件,并使用了自定义控件属性,那么如何获取自定义控件中属性的内容呢?先通过Context.obtainStyleAttributes方法检索到TypeArray类型的style属性,再通过TypeArray.getXxx获取每个属性的实例,具体代码如下:

TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CounterView);
        mBackgroundColor = typedArray.getColor(R.styleable.CounterView_backgroundColor, Color.BLACK);
        mTextColor= typedArray.getColor(R.styleable.CounterView_textColor, Color.YELLOW);
        mTextSize = (int) typedArray.getDimension(R.styleable.CounterView_textSize, 18);

到这里,用自定义控件实现的俯卧撑计数器已经完成,如图:


全部代码放在了github上了,点击跳转到源代码地址


之前写过一篇关于自定义控件的文章,和这篇有一点不一样,不同之处是:

这篇文章中自定义控件中的布局是自己画的,而之前那篇文章中自定义控件中布局是通过Inflater将xml文件转化而来的。

感兴趣的可以看一下:自定义View实现设置中心的功能视图

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,462评论 25 707
  • 《裕语言》速成开发手册3.0 官方用户交流:iApp开发交流(1) 239547050iApp开发交流(2) 10...
    叶染柒丶阅读 25,969评论 5 19
  • 《ilua》速成开发手册3.0 官方用户交流:iApp开发交流(1) 239547050iApp开发交流(2) 1...
    叶染柒丶阅读 10,522评论 0 11
  • 吃完晚饭后在操场跑步跑出一身汗回寝室洗个热水澡似乎是每天的例行公事一样。可能以后这样的日子真的不多了,所以很...
    视觉湿阅读 238评论 0 0
  • 由陌生到熟悉需要多久?我们对的朋友圈的拥蹙渐渐超过qq,但朋友圈里都是真的朋友吗?放在几年前,一个陌生电话一辆陌生...
    斯嘉绿阅读 154评论 0 0