自定义View的过程
1.自定义View的属性
2.在构造方法中获得我们的属性
3.[重写onMeasure方法]
我把3用[]标出了 所以说3不一定是必须的,当然了大部分情况下还是需要重写的。
4.重写onDraw方法
1.自定义view的属性,在value/attrs目录下,在里面定义我们的属性和样式:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="myText">
<attr name="title_Text" format="string"/>
<attr name="title_text_Color" format="color"/>
<attr name="title_Text_Size" format="dimension"/>
</declare-styleable>
</resources>
我们定义了字体,字体颜色,字体大小3个属性,format是值该属性的取值类型:
一共有:string,color,demension,integer,enum,reference,float,boolean,fraction,flag;不清楚的可以google一下吧。
我们可以在我们的布局引入我们自定义的控件,
注意,平时,我们直接使用SDK的系统自带的控件,一般位于android.widget这个包路径下,在xml文件中,系统也会跟着这个路径找到我们的控件但是对于我们自定义的控件,我们需要填入控件的包路径,比如com.mytext.myview.MyView:
<?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:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.mytext.myview.MyView
android:layout_width="100dp"
android:layout_height="100dp"
app:title_Text=zhihao的练习的View"
app:title_Text_Color="#ff0000"
app:title_Text_Size="18sp"/>
</RelativeLayout>
2.在构造方法,获得我们自定义样式,
我们先了解下几个重要的东西:
obtainStyledAttributes函数获取属性
其实我们在前面已经使用了obtainStyledAttributes来获取属性了,现在来看看这个函数的声明吧:
obtainAttributes(AttributeSet set, int[] attrs) //从layout设置的属性集中获取attrs中的属性
obtainStyledAttributes(int[] attrs) //从系统主题中获取attrs中的属性
obtainStyledAttributes(int resId,int[] attrs) //从资源文件定义的style中读取属性
obtainStyledAttributes (AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes)//这是最复杂的一种情况,后面细说。
==这么多重载的方法是不是已经看懵了?其实你只需要理解其中的参数就能掌握各个方法的使用方法。所谓获取属性,无非就是需要两个参数:第一,我需要获取那些属性;第二:我从哪里去获取这些属性(数据源)。==
attrs
attrs:int[],每个方法中都有的参数,就是告诉系统需要获取那些属性的值。
set
set:表示从layout文件中直接为这个View添加的属性的集合,如:android:layout_width=”match_parent”。注意,这里面的属性必然是通过xml配置添加的,也就是由LayoutInflater加载进来的布局或者View`才有这个属性集。
现在你知道为啥我们在自己定义View的时候至少要重写(Context context, AttributeSet set)构造器了吧?因为不重写时,我们将无法获取到layout中配置的属性!!当然,也因为这样,LayoutInflater在inflater布局时会通过反射去调用View的(Context context, AttributeSet attrs)构造器。
set 中实际上又有两种数据来源,当然最后都会包含在set中。一种是直接使用android:layout_width=”wrap_content”这种直接指定的,还有一种是通过style=”@style/somestyle”这样指定的。
defStyleAttr
这个参数是本文的关键所在,也是自定义一个可以在Theme中配置的样式的关键,先看个栗子吧:
如果我想通过在系统主题里面设置一个样式,修改所有textview的样式,你一般会这么做:
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
//在主题中设置textview的style
<item name="android:textViewStyle">@style/textviewstyle</item>
</style>
<style name="textviewstyle" parent="android:style/Widget.TextView">
<!--指定一些属性-->
</style>
首先android:textViewStyle其实就是一个普通的在资源文件中定义的属性attr,它的format=”reference”。那问题来了,TextView是怎么得知我们自己定义的textviewstyle的呢?这其实就是defStyleAttr的应用场景:定义Theme可配置样式。
public TextView(Context context, AttributeSet attrs) {
//指定属性textViewStyle为defStyleAttr,然后系统会去搜索Theme中你为这个
//属性配置的style
this(context, attrs, com.android.internal.R.attr.textViewStyle);
}
public TextView(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
//最终调用到View的第四个构造器时,调用了obtainStyledAttributes
TypedArray a = theme.obtainStyledAttributes(attrs,
com.android.internal.R.styleable.TextViewAppearance, defStyleAttr, defStyleRes);
}
resId=defStyleRes
resId or defStyleRes:直接从资源文件中定义的某个样式中读取。
NULL
看看第二个方法吧,里面除了指定了attrs属性集之外没有任何属性值来源,数据从哪儿来呢?原来我们可以直接在Theme中指定属性的值,那么NULL表示直接从Theme中读取属性。
是不是看到这里你已经有点迷糊了?不要紧,耐心看下去,后面有一个例子,看完例子你再回头看看这里的说明就ok了。
四个参数的obtainStyledAttributes
看看这个方法,返回的结果还是我们所关心的attrs(int[])中包含的属性集。那么数据来源呢?一共有4个,set,defStyleAttr,NULL,defStyleRes,如果一个属性在多个地方都被定义了,那么以哪个为准?
优先级如下:
set>defStyleAttr(主题可配置样式)>defStyleRes(默认样式)>NULL(主题中直接指定)
TypedArray
我们看到在获取到属性值之后,都会返回一个TypedArray对象,它又是什么鬼?TypedArray主要有两个作用,第一是内部去转换attrid和属性值数组的关系;第二是提供了一些类型的自动转化,比如我们getString时,如果你是通过@string/hello这种方式设置的,TypedArray会自动去将ResId对应的string从资源文件中读出来。说到底,都是为了方便我们获取属性参数。
回到主题: 现在我们应该知道如何为我们的自定义View添加在主题中可配置的Style,主要是通obtainStyledAttributes (AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes)方法来做,需要注意的是,defStyleAttr和defStyleRes都可以设置成0表示不去搜索可配置的风格和默认风格。
如下:
package com.mytext.myview;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;
/**
* @author Created by zhihao on 2016/10/14.
* @describe
* @version_
**/
public class MyView extends View {
/**
* 文本
*/
private String mTitleText;
/**
* 文本的颜色
*/
private int mTitleTextColor;
/**
* 文本的大小
*/
private int mTitleTextSize;
/**
* 绘制时控制文本绘制的范围
*/
private Rect mBound;
private Paint mPaint;
public MyView(Context context, AttributeSet attrs)
{
this(context, attrs, 0);
}
public MyView(Context context)
{
this(context, null);
}
/**
* 获得我自定义的样式属性
*
* @param context
* @param attrs
* @param defStyle
*/
public MyView(Context context, AttributeSet attrs, int defStyle)
{
super(context, attrs, defStyle);
/**
* 获得我们所定义的自定义样式属性
*/
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.myText, defStyle, 0);
int n = a.getIndexCount();
for (int i = 0; i < n; i++)
{
int attr = a.getIndex(i);
switch (attr)
{
case R.styleable.myText_title_Text:
mTitleText = a.getString(attr);
break;
case R.styleable.myText_title_Text_Color:
// 默认颜色设置为黑色
mTitleTextColor = a.getColor(attr, Color.BLACK);
break;
case R.styleable.myText_title_Text_Size:
// 默认设置为16sp,TypeValue也可以把sp转化为px
mTitleTextSize = a.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP, 16, this.getResources().getDisplayMetrics()));
break;
}
}
a.recycle();
/**
* 获得绘制文本的宽和高
*/
mPaint = new Paint();
mPaint.setTextSize(mTitleTextSize);
// mPaint.setColor(mTitleTextColor);
mBound = new Rect();
mPaint.getTextBounds(mTitleText, 0, mTitleText.length(), mBound);
}
}
3.重写onDraw方法,利用系统提供的onMeasure方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onDraw(Canvas canvas)
{
mPaint.setColor(Color.YELLOW);
canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint);
mPaint.setColor(mTitleTextColor);
canvas.drawText(mTitleText, getWidth() / 2 - mBound.width() / 2, getHeight() / 2 + mBound.height() / 2, mPaint);
}
效果如下:
此处,需要注意的是,当我们设置wrap_content时,如果不重写onMeasure方法,效果会跟match_parent一样。结果如下:
此时,得到的结果跟我们所需要的截然不同。
why???
系统帮我们测量的高度和宽度都是MATCH_PARNET,当我们设置明确的宽度和高度时,系统帮我们测量的结果就是我们设置的结果,当我们设置为WRAP_CONTENT,或者MATCH_PARENT系统帮我们测量的结果就是MATCH_PARENT的长度。
所以,当设置了WRAP_CONTENT时,我们需要自己进行测量,即重写onMesure方法”:
重写之前先了解MeasureSpec的specMode,一共三种类型:
EXACTLY:一般是设置了明确的值或者是MATCH_PARENT
AT_MOST:表示子布局限制在一个最大值内,一般为WARP_CONTENT
UNSPECIFIED:表示子布局想要多大就多大,很少使用
下面贴出重写onMeasure方法的代码:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int hegihtSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
int width;
int height;
if (widthSpecMode == MeasureSpec.EXACTLY) {//MATH
width = widthSpecSize;
} else {
mPaint.setTextSize(mTitleTextSize);
mPaint.getTextBounds(mTitleText, 0, mTitleText.length(), mBound);
float textWidth = mBound.width();
int desired = (int) (getPaddingLeft() + textWidth + getPaddingRight());
width = desired;
}
if (hegihtSpecMode == MeasureSpec.EXACTLY) {//MATH
height = heightSpecSize;
} else {
mPaint.setTextSize(mTitleTextSize);
mPaint.getTextBounds(mTitleText, 0, mTitleText.length(), mBound);
float textWidth = mBound.width();
int desired = (int) (getPaddingLeft() + textWidth + getPaddingRight());
height = desired;
}
setMeasuredDimension(width, height);
}
最后效果如下:
完全复合我们的预期,现在我们可以对高度、宽度进行随便的设置了,基本可以满足我们的需求。
当然了,这样下来我们这个自定义View与TextView相比岂不是没什么优势,所有我们觉得给自定义View添加一个事件:
在构造中添加:
public interface MyonClickLister {
public void MyOnclick(View v);
}
public void setMyonClickLister(MyonClickLister lister) {
this.myonClickLister = lister;
}
this.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
myonClickLister.MyOnclick(v);
}
});
}
//actvity运用如下:
myView.setMyonClickLister(new MyView.MyonClickLister() {
@Override
public void MyOnclick(View v) {
Toast.makeText(getApplicationContext(), "被点击了", Toast.LENGTH_LONG).show();
}
});
最后的最后,需要说几个需要注意的点:
1.现在一般都用android studio开发了,使用自定义View,必须在xml文件头部引入:一定要引入 xmlns:app=”http://schemas.android.com/apk/res-auto”
2.之前我设置text的颜色引用的是titleTextColor,我运行的时候回报错:”titleTextSize has been defined…”,证明titleTextColor被其他控件运用过,无奈之下改为title_text_Color。。这里为了以后防止出现引用相同的属性名,可以声明属性,重复使用,只不会重复声明属性了,如下:
<attr name="titleText" format="string"/>
<attr name="title_Text_color" format="color"/>
<attr name="title_Text_Size" format="dimension"/>
3.对于重写onMeasure方法,可以查看我写的另外的文章: http://blog.csdn.net/zhuangxiaozhi/article/details/52793567
好了,View的初步教程到这里了,github地址为:https://github.com/zhuangzhitu/MyView 欢迎stars,谢谢了。