对于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。
主界面主要用到了之前学过的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实现设置中心的功能视图