一、View
Android所有的控件都是View或者View的子类,它其实表示的就是屏幕上的一块矩形区域,用一个Rect来表示,left,top表示View相对于它的parent View的起点,width,height表示View自己的宽高,通过这4个字段就能确定View在屏幕上的位置,确定位置后就可以开始绘制View的内容了。
二、View的绘制过程
1)Measure
View会先做一次测量,算出自己需要占用多大的面积。View的Measure过程给我们暴露了一个接口onMeasure;
2)Layout
Layout过程对于View类非常简单,同样View给我们暴露了onLayout方法,onLayout方法中,我们需要将所有子View的大小宽高设置好;
3)Draw
Draw过程,就是在canvas上画出我们需要的View样式。同样View给我们暴露了onDraw方法。
- onMeasure()会在初始化之后调用一到多次来测量控件或其中的子控件的宽高;
- onLayout()会在onMeasure()方法之后被调用一次,将控件或其子控件进行布局;
- onDraw()会在onLayout()方法之后调用一次,也会在用户手指触摸屏幕时被调用多次,来绘制控件。
三、自定义View的步骤
1)自定义View的属性
2)在View的构造方法中获得我们自定义的属性
3)重写onMesure
4)重写onDraw
四、ToggleButton自定义分析
1、自定义View的属性,首先在res/values/ 下建立一个attrs.xml , 在里面定义我们的属性和声明我们的整个样式。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="ToggleButton">
<attr name="tbBorderWidth" format="dimension"/>
<attr name="tbOffBorderColor" format="reference|color"/>
<attr name="tbOffColor" format="reference|color"/>
<attr name="tbOnColor" format="reference|color"/>
<attr name="tbSpotColor" format="reference|color"/>
<attr name="tbAnimate" format="reference|boolean"/>
<attr name="tbAsDefaultOn" format="reference|boolean"/>
</declare-styleable>
</resources>
format是值该属性的取值类型:一共有string,color,demension,integer,enum,reference,float,boolean,fraction,flag;
2、布局文件声明自定义View
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:toggle="http://schemas.android.com/apk/res-auto"
android:background="#fff"
android:orientation="vertical"
android:padding="20dp">
<LinearLayout
android:id="@+id/group1"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.zcw.togglebutton.ToggleButton
android:layout_width="54dp"
android:layout_height="30dp">
</com.zcw.togglebutton.ToggleButton>
<com.zcw.togglebutton.ToggleButton
android:layout_width="54dp"
android:layout_height="30dp"
android:layout_marginLeft="20dp">
</com.zcw.togglebutton.ToggleButton>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp">
<com.zcw.togglebutton.ToggleButton
android:layout_width="54dp"
android:layout_height="30dp"
toggle:tbBorderWidth="2dp"
toggle:tbOffBorderColor="#000"
toggle:tbOffColor="#ddd"
toggle:tbOnColor="#f00"
toggle:tbSpotColor="#00f">
</com.zcw.togglebutton.ToggleButton>
<com.zcw.togglebutton.ToggleButton
android:layout_width="54dp"
android:layout_height="30dp"
android:layout_marginLeft="20dp"
toggle:tbBorderWidth="2dp"
toggle:tbOffBorderColor="#000"
toggle:tbOffColor="#ddd"
toggle:tbOnColor="#f00"
toggle:tbSpotColor="#00f">
</com.zcw.togglebutton.ToggleButton>
</LinearLayout>
<com.zcw.togglebutton.ToggleButton
android:layout_width="70dp"
android:layout_height="30dp"
android:layout_marginTop="10dp"/>
<com.zcw.togglebutton.ToggleButton
android:layout_width="90dp"
android:layout_height="40dp"
android:layout_marginTop="10dp"/>
<com.zcw.togglebutton.ToggleButton
android:layout_width="54dp"
android:layout_height="30dp"
android:layout_marginTop="10dp"
toggle:tbAsDefaultOn="true"/>
</LinearLayout>
3、在构造方法中获得自定义的样式
private ToggleButton(Context context) {
super(context);
}
public ToggleButton(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setup(attrs);
}
public ToggleButton(Context context, AttributeSet attrs) {
super(context, attrs);
setup(attrs);
}
public void setup(AttributeSet attrs) {
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setStyle(Style.FILL);
paint.setStrokeCap(Cap.ROUND);
springSystem = SpringSystem.create();
spring = springSystem.createSpring();
spring.setSpringConfig(SpringConfig.fromOrigamiTensionAndFriction(50, 7));
this.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View arg0) {
toggle(defaultAnimate);
}
});
TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.ToggleButton);
offBorderColor = typedArray.getColor(R.styleable.ToggleButton_tbOffBorderColor, offBorderColor);
onColor = typedArray.getColor(R.styleable.ToggleButton_tbOnColor, onColor);
spotColor = typedArray.getColor(R.styleable.ToggleButton_tbSpotColor, spotColor);
offColor = typedArray.getColor(R.styleable.ToggleButton_tbOffColor, offColor);
borderWidth = typedArray.getDimensionPixelSize(R.styleable.ToggleButton_tbBorderWidth, borderWidth);
defaultAnimate = typedArray.getBoolean(R.styleable.ToggleButton_tbAnimate, defaultAnimate);
isDefaultOn = typedArray.getBoolean(R.styleable.ToggleButton_tbAsDefaultOn, isDefaultOn);
typedArray.recycle();
borderColor = offBorderColor;
if (isDefaultOn) {
toggleOn();
}
}
当我们定义一个新的类继承了View或ViewGroup时,系统都会提示我们重写它的构造方法。View / ViewGroup中又四个构造方法可以重写,它们分别有一、二、三、四个参数。四个参数的构造方法我们通常用不到。这个构造方法中有三个参数:Context上下文、AttributeSet属性集和defStyleAttr自定义属性的引用。这个构造方法不会默认调用,必须要手动调用,这个构造方法和两个参数的构造方法的唯一区别就是这个构造方法给我们默认传入了一个默认属性集。defStyleAttr指向的是自定义属性的<declare-styleable>标签中定义的自定义属性集,我们在创建TypedArray对象时需要用到defStyleAttr。
4、onMeasure
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
Resources r = Resources.getSystem();
if(widthMode == MeasureSpec.UNSPECIFIED || widthMode == MeasureSpec.AT_MOST){
widthSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 50, r.getDisplayMetrics());
widthMeasureSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);
}
if(heightMode == MeasureSpec.UNSPECIFIED || heightSize == MeasureSpec.AT_MOST){
heightSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 30, r.getDisplayMetrics());
heightMeasureSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY);
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
onMeasure()方法中主要负责测量,决定控件本身或其子控件所占的宽高。我们可以通过onMeasure()方法提供的参数widthMeasureSpec和heightMeasureSpec来分别获取控件宽度和高度的测量模式和测量值(测量 = 测量模式 + 测量值)。
widthMeasureSpec和heightMeasureSpec虽然只是int类型的值,但它们是通过MeasureSpec类进行了编码处理的,其中封装了测量模式和测量值,因此我们可以分别通过MeasureSpec.getMode(xMeasureSpec)和MeasureSpec. getSize(xMeasureSpec)来获取到控件或其子View的测量模式和测量值。
测量模式分为以下三种情况:
1) EXACTLY:当宽高值设置为具体值时使用,如100DIP、match_parent等,此时取出的size是精确的尺寸;
2) AT_MOST:当宽高值设置为wrap_content时使用,此时取出的size是控件最大可获得的空间;
3) UNSPECIFIED:当没有指定宽高值时使用(很少见)。
5、onLayout
@Override
protected void onLayout(boolean changed, int left, int top, int right,
int bottom) {
super.onLayout(changed, left, top, right, bottom);
final int width = getWidth();
final int height = getHeight();
radius = Math.min(width, height) * 0.5f;
centerY = radius;
startX = radius;
endX = width - radius;
spotMinX = startX + borderWidth;
spotMaxX = endX - borderWidth;
spotSize = height - 4 * borderWidth;
spotX = toggleOn ? spotMaxX : spotMinX;
offLineWidth = 0;
}
onLayout()方法负责布局,大多数情况是在自定义ViewGroup中才会重写,主要用来确定子View在这个布局空间中的摆放位置。
onLayout(boolean changed, int l, int t, int r, int b)方法有5个参数,其中changed表示这个控件是否有了新的尺寸或位置;l、t、r、b分别表示这个View相对于父布局的左/上/右/下方的位置。
6、onDraw
@Override
public void draw(Canvas canvas) {
//
rect.set(0, 0, getWidth(), getHeight());
paint.setColor(borderColor);
canvas.drawRoundRect(rect, radius, radius, paint);
if(offLineWidth > 0){
final float cy = offLineWidth * 0.5f;
rect.set(spotX - cy, centerY - cy, endX + cy, centerY + cy);
paint.setColor(offColor);
canvas.drawRoundRect(rect, cy, cy, paint);
}
rect.set(spotX - 1 - radius, centerY - radius, spotX + 1.1f + radius, centerY + radius);
paint.setColor(borderColor);
canvas.drawRoundRect(rect, radius, radius, paint);
final float spotR = spotSize * 0.5f;
rect.set(spotX - spotR, centerY - spotR, spotX + spotR, centerY + spotR);
paint.setColor(spotColor);
canvas.drawRoundRect(rect, spotR, spotR, paint);
}
onDraw()方法负责绘制,即如果我们希望得到的效果在Android原生控件中没有现成的支持,那么我们就需要自己绘制我们的自定义控件的显示效果。
要学习onDraw()方法,我们就需要学习在onDraw()方法中使用最多的两个类:Paint和Canvas。
注意:每次触摸了自定义View/ViewGroup时都会触发onDraw()方法。
Paint画笔对象,这个类中包含了如何绘制几何图形、文字和位图的样式和颜色信息,指定了如何绘制文本和图形。画笔对象右很多设置方法,大体上可以分为两类:一类与图形绘制有关,一类与文本绘制有关。
Canvas即画布,其上可以使用Paint画笔对象绘制很多东西。