一,前言
看过我的blog的小伙伴都知道,我的博客的第一条都是概述,这是第一次写前言。此次写前言一方面是为了对自己未来一段时间关于写博客的规划,另外一方法将我的规划告诉志同道合的小伙伴,接下来很长一段时间我们将一起学习自定义View以及Android中View的绘制机制。
我的自定义View的学习起源于CSDN,当时由于工作的需要开始在CSDN上搜索自定义view的blog查看,慢慢的可以实现了简单的自定义View的需求。随着时间的推移,自定义View的blog看的多了,也在工作中积累了相关经验,慢慢的自定义View的知识点掌握了很多,也做了很多笔记,大家都知道我早期总结的笔记都保存到本地。最近又看View相关的源码,又在网上看了很多精彩的blog,也看了任玉刚大神的《Android开发艺术探索》一书上对Android中View的绘制机制的讲解,现在对View的绘制机制,有了整体的理解。明白了setContentView方法是怎么将View添加到Activity上的,为什么view的显示是在onResume方法执行之后,ViewRootImpl与DecorView在View机制中的作用,View显示的触发机制,以及View树的绘制机制。下面会花很长一段时间将关于view方面的知识逐渐分享到CSDN上,与志同道合的小伙伴们一起学习。
任玉刚大神的《Android开发艺术探索》一书中首先讲解的是View的绘制原理,之后再讲解自定义View,这个顺序是对的,明白了View的绘制原理后在自定义View中就会明白各个方法的原理和作用。但是若直接讲解View的绘制原理对不了解自定义View的小伙伴来说是非常苦涩难懂的。所以我从我自己的体会出发,首先讲解怎么自定义View,当学会了自定义View时再讲解view的绘制原理。当然首先讲解自定义View大家可能会对用到的某些方法不明白原理,但是我会告诉大家这些方法的作用,大家只需先记住,当讲解View的绘制原理时肯定会恍然大悟,肯定会发出一个响彻大地的声音:哦,原来是这样!
二,概述
在Android体系中View扮演者重要的角色,可以说它是android中唯一可以“看得见,摸得着”的东西,尤其对于用户来说View是他们对APP的唯一认识,对于我们以“用户至上”为理念的程序员来说,必须重视View。
在Android中提供了一整套GUI库,比如TextView,ImageView,还有LinearLayout和RelativeLayout等等,这些View可以帮助我们实现绝大多数的界面需求。但是往往我们会遇到一个“艺术家”产品经理,他就想与众不同,就想有新意,他会设计出各种奇葩的UI,此时Android提供的View可能满足不了我们的需求,此时就需要自定义View。
自定义View很简单,一般分为四种情况:
1. 直接继承于View类。查看源码会发现TextView和Imageview都是继承的View类。
2. 直接继承ViewGroup类。Linearlayout和RelativeLayout都是继承ViewGroup类,注意ViewGroup也是继承View类。
3. 继承于其它View类。此种情况常用于对特定View的功能进行增强,比如:继承ListView变成下拉刷新ListView。
4. 组合方式。这种情况常用于某个特性样式的view组合在App中可能会出现多次,此时就把这个组合样式定义成一个View。
注意:在实际开发中有时某个需求使用多个方案都可以实现,我们要根据实际情况进行选择,力求view执行高效且节约开发时间。
下面首先实现一个自定义View类,先直观的感受一下自定义View。
三,需求和步骤总述
在写代码之前首先要定义需求,我们的需求是:
1,自定义一个View,显示“女神”两个字。
2,字体颜色为red。
3,背景色为yellow。
4,提供设置文本,字体颜色,背景颜色的方法。
5,可以在布局中文件中设置文本,字体颜色,背景颜色。
有人会说,这个直接用TextView就可以实现,何必费大劲去自定义View呢。注意,此时我们是学习如何自定义View,所以要从简单的开始,每一种自定义View原理都是相同的,学会简单的自定义View后便能发挥我们的聪明才智去实现复杂的View。
四,最简单的方式实现自定义View
首先以最简单的方式,把我们的“女神”显示出来。
1,自定义一个类GoddessView,继承View,并重写构造方法
直接上代码,具体的讲解在代码注释中。
public class GoddessView extends View {
/**
* 在继承View的类中必须实现一个有参的构造方法。实现一个即可,但具体实现哪一个要看应用场景。
*
* 我们在继承View时,如果明确知道在哪使用,则重写一个即可。若为了增加使用场景最好三个都写上。
*/
/**
* 此构造方法用于使用new创建对象时。
*/
public GoddessView(Context context) {
super(context);
}
/**
* 此构造方法用于在布局文件中使用时
*/
public GoddessView(Context context, AttributeSet attrs) {
super(context, attrs);
}
/**
* 此构造方法用于需要设置主题样式时。
*/
public GoddessView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
}
2,定义一个方法,初始化画笔
定义一个方法,方法里面初始化画笔,这个方法在三个构造方法中都要调用。
private void initPaint(){
paint = new Paint();//创建paint对象
paint.setAntiAlias(true);//设置锯齿,让边界光滑
paint.setColor(Color.RED);//设置画笔颜色
paint.setStrokeWidth(2f);//设置画笔的宽度,单位是px.
paint.setStyle(Paint.Style.FILL);//设置画笔的样式,在画矩形或圆形时STROKE表示空心,FILL表示实心。
paint.setTextSize(60f);//设置字体的大小,
}
3,重写onMeasure方法
重写onMeasure方法,方法中只调用super的onMeasure方法,其他什么也不做
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
4,重写onDraw方法
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.YELLOW);//画背景颜色
canvas.drawText("女神",100f,100f,paint);//画文字,并指定位置
}
5,在Activity中使用
在Activity的onCreate方法中会调用setContentView设置view,这个方法可以接收一个布局文件,也可以接收一个view对象,此时接收一个view对象。代码如下:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(new GoddessView(this));
}
这5步就显示出来了女神两个字,效果如图:
6,总结
这里说下三个注意点:
1,构造方法。构造方法的作用是创建对象和初始化参数。与view的显示机制没有任何关系。第一个构造方法只有创建对象的作用,所以适用于直接new的情况。第二个构造方法可以接收布局文件中的参数,所以可以使用在布局文件中。第三个构造方法可以接收主题参数。
2,onMeasure方法。这个方法很重要,但此时并没有显示出来其重要性,下面会修改onMeasure方法来体会onMeasure的重要性。
3,onDraw方法。这个方法也很重要,view上显示的内容全是这个方法画出来的。注意canvas和paint类的使用。
五,初始onMeasure方法
在上面的例子中并没有对onMeasure方法进行任何操作,但Text也显示出来了。难道说onMeasure方法不重要吗。其实不是,onMeasure方法是个很重要的方法,它决定view的显示大小。在onMeasure方法中有两个参数widthMeasureSpec和heightMeasureSpec,这两个参数称为测量规则,在测量规则中包含两层含义,一个是测量模式,另外一个是测量大小。我们可以从这个参数规则中获取测量大小。
在自定义的GoddessView中并没有指定大小,但为什么全屏显示呢?这是因为测量大小的默认值是全屏显示。我们做如下实验:
此时只在onMeasure方法中添加两个打印log,其他都不变,onMeasure方法代码如下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);//获取宽度
int heightSize = MeasureSpec.getSize(heightMeasureSpec);//获取高度
Log.d("kwwl","widthSize====="+widthSize);
Log.d("kwwl","heightSize====="+heightSize);
}
执行结果如下:
D/kwwl: widthSize=====720
D/kwwl: heightSize=====1022
此时测试手机的分辨率是720*1080。由于statuBar和ActionBar的存在所以高度会小于手机高度分辨率。
那么这个值是从哪儿来的呢?此时就需要小伙伴先记住这个答案,这个值是从view所在的容器View中传递过来的,onMeasure方法的调用也是在上级容器View中。
系统默认的是全屏显示,但我们可以修改显示大小,代码如下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);//获取测量模式
int heightMode = MeasureSpec.getMode(heightMeasureSpec);//获取测量模式
int mWidthMeasureSpec = MeasureSpec.makeMeasureSpec(300,widthMode);//得到新的测量规则,宽度为300px
int mHeightMeasureSpec = MeasureSpec.makeMeasureSpec(300,heightMode);//得到新的测量规则,高度为300px
super.onMeasure(mWidthMeasureSpec, mHeightMeasureSpec);
}
此时的显示效果如下:
我们发现此时就显示为我们指定的宽高了。
实现指定宽高还有下面方式:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(300,300);//设置宽高都为300px
}
setMeasuredDimension是个很重要的方法,此时一定要记住,在super的onMeasure方法内部也是调用这个方法来设置宽高的。
总结:
这部分主要是体会onMeasure方法的作用,具体原理后面blog会讲解,这里必须记住以下几个方法:
1,MeasureSpec.getSize(widthMeasureSpec);
2,MeasureSpec.getMode(widthMeasureSpec);
3,MeasureSpec.makeMeasureSpec(300,widthMode);
4,setMeasuredDimension(300,300);
六,在GoddessView类中定义方法
在使用TextView时我们知道,可以在代码中使用方法改变文本,文本的颜色,背景颜色等。此时我们也实现这个方法。
修改后的代码如下:
public class GoddessView extends View {
private String mText = "女神";//将要显示的文本声明为成员变量,初始为“女神”
private int textColorId = R.color.myRed;//将文本颜色的colorId声明为成员变量
private int backColorId = R.color.myYellow;//将背景颜色的colorId声明为成员变量
private Paint paint;
public GoddessView(Context context) {
super(context);
initPaint();
}
public GoddessView(Context context, AttributeSet attrs) {
super(context, attrs);
initPaint();
}
public GoddessView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initPaint();
}
private void initPaint(){
paint = new Paint();
paint.setAntiAlias(true);
paint.setColor(getResources().getColor(textColorId));//设置画笔颜色
paint.setStrokeWidth(2f);
paint.setStyle(Paint.Style.FILL);
paint.setTextSize(60f);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(300,300);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(getResources().getColor(backColorId));//画背景颜色
canvas.drawText(mText,100f,100f,paint);//画文字,并指定位置
}
/**
* 设置显示的文本
*/
public void setMyText(String text){
mText = text;
}
/**
* 设置文本的颜色
*/
public void setMyTextColor(int colorId){
textColorId = colorId;
paint.setColor(getResources().getColor(textColorId));//改变paint的颜色
}
/**
* 设置背景颜色
*/
public void setMyBackgroudColor(int colorId){
backColorId = colorId;
}
}
此时直接运行,显示效果同上图。下面修改activity中的代码:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
GoddessView goddessView = new GoddessView(this);
setContentView(goddessView);
goddessView.setMyText("God");//修改文本为“God”
goddessView.setMyTextColor(R.color.myYellow);//修改文本颜色未Yellow
goddessView.setMyBackgroudColor(R.color.myRed);//修改背景颜色未Red
}
此时的显示效果如下:
修改成功。
方法这样写有一定的问题,如果把goddessView.setMyText写在点击事件响应方法中会发现没有效果。原因是:只是修改了mText的字段值,但并没有要求view重新绘制,所以不会改变UI显示。
那么怎么能让view重新绘制呢,方法应该这样写:
public void setMyText(String text){
mText = text;
invalidate();//会重新执行onDraw方法
}
此时就可以随时调用setMyText方法了。注意invalidate的使用,它会重新执行onDraw方法。
七,可以在布局中文件中设置文本,字体颜色,背景颜色
在android SDK中提供的View中几乎都可以在布局中使用,并设置相关属性。自定义View也可以实现在布局文件中设置相关属性。具体实现如下:
1,创建attrs.xml文件
在res->values目录下新建一个attrs.xml文件,里面的代码如下:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="GoddessView">
<attr name="myText" format="string"/><!--文本-->
<attr name="myTextColor" format="color"/><!--文本颜色-->
<attr name="myBackgorudColor" format="color"/><!--背景颜色-->
</declare-styleable>
</resources>
注意:一个Module中只有一个attrs文件,一个文件可以供很多View使用,一个declare-styleable代表一个View。
2,在GoddessView类的第二个构造方法的参数中获取属性值
public GoddessView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.GoddessView);
mText = ta.getString(R.styleable.GoddessView_myText);
textColorId = ta.getResourceId(R.styleable.GoddessView_myTextColor, 0);
backColorId = ta.getResourceId(R.styleable.GoddessView_myBackgorudColor, 0);
initPaint();
}
3,在布局文件中使用GoddessView类
代码如下;
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="honor.com.customview.MainActivity">
<honor.com.customview.view.GoddessView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:myText="God"
app:myTextColor="@color/myYellow"
app:myBackgorudColor="@color/myRed"/>
</RelativeLayout>
注意:
1,GoddessView类要全类名使用,必须带包名。
2,自定义的属性以app开头。
3,使用自定义的属性前先在根容器中增加命名空间:
xmlns:app=”http://schemas.android.com/apk/res-auto”
4,在activity中使用该布局文件
setContentView(R.layout.activity_main);
此时就显示出GoddessView了。
八,总结
实现上面的demo后发现自定义View并不难,所有的自定义view都是这个流程,总结如下:
1,重写构造方法,如果需要自定义属性就创建attrs.xml文件。
2,重写onMeasure方法,指定view的测量大小。
3,创建Paint对象并设置参数。
4,重写onDraw方法,绘制UI。
5,若需要提供修改属性的方法则定义方法。
自定义View基本都是这个流程,而且1,3,5,这三步几乎都是这样来写,在复杂的View中只是onMeasure方法和onDraw方法比较麻烦。后面会对onMeasure方法,Paint类,Canvas类专门进行讲解。要求不会自定义View的小伙伴要熟练掌握这篇blog中用到的知识点,等这篇blog熟练后再向下看。