Android 自定义控件在如今是每个程序员必备的技能,为了使app的ui更加精美,交互性更强,传统的控件无法满足多样化的ui,需要我们去自定义view来实现。
自定义view主要分为如下几种:
1.重写view实现全新控件
2.自定义ViewGroup实现组合控件
3.系统控件功能扩展,如继承TextView,ImageVIew等
下面讲一讲Android的控件架构
先来讲一讲Android控件在屏幕中的表现,Android的每个控件在界面中占据一个矩形块,这个块占用一个空间,控件可能在屏幕内也可能超出屏幕。控件大致分为两类:View控件和ViewGroup控件。所有控件都继承View根类,ViewGroup控件可以包含若干个view控件,有父与子的关系,层次和清晰,形成了一种树形结构,上层控件负责下层控件的测量和绘制,并进行拦截分发传递事件。
如下图:在每个空件树的顶部,都有一个ViewParent对象,这是整棵树的控制核心,所有的交互管理事件都由它来统一调度和分配,以便对整个视图进行整体控制。
我们知道,Activity设置一个布局需要使用setContentView()方法,只有调用这个方法,布局内容才能真正显示出来。那么这个方法究竟做了些什么事情 呢?如下图:
每个Activity都包含一个Window对象,在Android中用PhoneWindow来实现。PhoneWindow将一个DecorView设置为整个应用窗口的根View,DecorView作为窗口界面的顶层视图,封装了一些窗口的通用方法。通俗来说,DecorView将所要显示的具体内容呈现在了PhoneWindow上,这里面所有View的监听事件,都是通过WindowManagerService来进行接收。在显示上,屏幕分为两部分,一个是TitleVIew,一个是ContentView,ContentView是一个id为content的Framelayout,默认Framelayout势不可见的,我们编辑xml文件的时候,不用管他。
在代码中,如果我们去掉标题栏,必须注意一点,当设置requestWindowFeature(Window.FEATURE_NO_TITLE)时,必须要在调用setContentVIew()方法之前,因为TitleView和ContentView是上下关系,这就解释了为什么放在前面的原因。
还有一点需要注意:当程序在onCreate()方法中用setContentView()方法后,ActivityManagerService会回调onResume()方法,此时系统才会把整个DecorVIew添加到PhoneWindow中,并让其显示出来,从而最终完成界面的绘制。
-----------------------------------------------------------------------------------华丽的分割线------------------------------------------------------------------------------------
以上简单介绍了控件的架构,下面具体分析一下自定义View的流程:
view的绘制流程一般需要以下步骤:
1.在value/attrs.xml定义属性
2.在构造方法中获取属性
3.重写onMesure方法(不是必须的步骤)
4.重写onLayout方法(组合控件用到,ViewGroup中子View的布局方法)
5.重写onDraw方法
View测量
在绘制view之前,请大家思考一个问题,系统是如何绘制出这些View的。举个例子,你在画画的时候,在画板上的每一个图案都会有一个空间和位置,前提是你必须听老师的指导,精确地画到画纸上。老师让你在画纸的中间位置画一个边长为5厘米的正方形,你知道怎么画,如果只和你说画一个正方形,你就无法定位了。同样,在Android中,系统在绘制View之前,也必须对view进行测量,即告诉你具体画多大尺寸的view。这个过程就在onMesure方法中执行。
View绘制
当测量好了一个View之后,我们就可以重写onDraw方法了,在Canvas对象上来绘制所需要的图形。Canvas就像是一个画板,使用Paint就可以在上面作画了。
何为Canvas呢?onDraw方法中有一个Canvas canvas对象,使用这个对象就可以进行绘制了。当然在其他地方,通常需要代码单独创建一个Canvas对象,简单代码实例:
Canvas canvas = new Canvas(bitmap);
大家或许有疑问,为什么传入一个bitmap对象呢?因为传入的bitmap对象与通过这个bitmap创建的Canvas画布是紧紧联系在一起的,这个过程我们称之为装载画布。这个bitmap我们用来装载所有绘制在Canvas上的像素信息。所以你通过这种方式创建Canvas对象后,所有相关的Canvas.drawXXXX方法都发生在这个bitmap上。
onDraw方法中,绘制图形要多灵活有多灵活。不管是多么复杂而精美的控件,他们都可以被拆分成一个个小的图形单元,我们要做的就是找到这些小的绘图单元并将它们绘制出来,不要怕麻烦,一点一点来,相信自己。
注意:
1.继承ViewGroup时,测量完毕之后,需要将子view放到合适的位置上。这个过程就是onLayout的过程,在执行Layout过程中,遍历所有子view的Layout方法,并置顶其具体显示的位置,从而决定其布局位置。
2.ViewGroup通常情况下不需要绘制,因为它本身就没有需要绘制的东西,如果不是指定了ViewGroup的背景颜色,那么ViewGroup的onDraw方法都不会被调用。但ViewGroup会使用dispatchDraw()方法来绘制其子view,其过程同样是遍历所有子view,并调用子view的绘制方法来完成绘制工作。
上面是理论,下面结合简单实例讲解一下:
1.重写view实现全新控件
当Android原生控件无法满足需求的时候,我们可以创建一个全新的控件来实现所需功能。创建一个自定义view难在绘制控件和实现交互。通常需要集成View,实现onMesure和onDraw方法。如果想让自定义view灵活性更强一点,需要自定义属性值,这样就可以在xml文件里灵活设置了,这里为了简单演示,暂时不做这块了。
如下图:这张图实现起来很简单,为了演示方便,暂时简单一点,百变不离其宗,请大家举一反三
分析一下,这个自定义View由两部分组成,圆和中间的文字,有了这样的思路,只要在onDraw方法中一个一个绘制就可以了。
圆的代码如下:
mCircleXY = length/2;//圆心
mRadius = (float)(length*0.5/2);//半径
绘制文字,只需要设置好文字的起始绘制位置即可。
代码如下:
//绘制圆
canvas.drawCircle(mCircleXY,mCircleXY,mRadius,mCirclePaint);
//绘制文字
canvas.drawText(mShowText,0,mShowText.length(),mCircleXY,mCircleXY+(mShowTextSize/4),mTextPaint);
这些代码都很简单,其实复杂的图形也是由多个简单图形组成,关键在于你如何去分解,设计这些图形 。当你脑海中有了一副设计图,,剩下的工作就是对坐标的计算了。
2.自定义ViewGroup实现组合控件
所谓组合控件,顾名思义就是控件的集合体。通过这种方式自定义的view,我们一般会指定一些可配置的属性,扩展性强。在这里,我将详细给大家讲解一下。
我们以Topbar为例,像如图的页面,topbar是公共部分,一般我们会写一个公用的控件,形成一个共通的UI组件。在需要标题栏的地方,引入这个view即可。自定义view,最难的地方在于绘制和交互。我们可以给TopBar增加相应的接口,供调用者灵活调用。可以更改模板中的文字、颜色、行为等等,而不是所有页面都一样,那就失去了重用的意义。具体流程如下:
1).定义属性
为View自定义属性非常简单,只需要在res目录下的value目录下创建一个attrs.xml的文件,并在该文件中声明所需要的属性即可。
在书写代码之前,介绍一下各属性的含义
"reference" //引用
"color" //颜色
"boolean" //布尔值
"dimension" //尺寸值
"float" //浮点值
"integer" //整型值
"string" //字符串
"fraction" //百分数,比如200%
代码attrs.xml代码如下:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="TopBar">
<attr name="title" format="string" />
<attr name="titleTextSize" format="dimension" />
<attr name="titleTextColor" format="color" />
<attr name="leftTextColor" format="color" />
<attr name="leftBackground" format="reference|color" />
<attr name="leftText" format="string" />
<attr name="rightText" format="string" />
<attr name="rightTextColor" format="color" />
<attr name="rightBackground" format="reference|color" />
</declare-styleable>
</resources>
<declare-styleable name="TopBar">用来声明属性名称
<attr />声明具体的自定义属性,name是属性名称,format是属性类型。format可以有多个,如上background既可以是颜色值也可以引用图片。
在确定了各种属性之后,我们就可以写代码逻辑了。先把主要的属性写好,后续优化时可以随时增改。
接下来我们可以集成ViewGroup来书写代码了,这里为了简单起见,我们集成RelativeLayout。我们在构造方法中这样书写,代码片段如下:
private int mTitleTextColor,mLeftTextColor,mRightTextColor;
private String mTitle,mLeftText,mRightText;
private float mTitleTextSize = 10;
private Drawable mLeftBackground,mRightBackground;
private void initStyles(Context context,AttributeSet attrs){
//通过这个方法,将你在attrs.xml中定义的declare-styleable的所有属性的值存储到TypedArray中
TypedArray ta = context.obtainStyledAttributes(attrs,R.styleable.TopBar);
//从TypedArray中取出对应的值来为要设置的属性赋值
mLeftTextColor = ta.getColor(R.styleable.TopBar_leftTextColor,0);
mRightTextColor = ta.getColor(R.styleable.TopBar_rightTextColor,0);
mTitle = ta.getString(R.styleable.TopBar_title);
mTitleTextSize = ta.getDimension(R.styleable.TopBar_titleTextSize,10);
mTitleTextColor = ta.getColor(R.styleable.TopBar_titleTextColor,0);
mLeftText = ta.getString(R.styleable.TopBar_leftText);
mRightText = ta.getString(R.styleable.TopBar_rightText);
mLeftBackground = ta.getDrawable(R.styleable.TopBar_leftBackground);
mRightBackground = ta.getDrawable(R.styleable.TopBar_rightBackground);
//获取完TypedArray的值后,一般要调用recycle方法避免重新创建的时候的错误
ta.recycle();
}
接下来我们开始组合控件:
我们需要建立左右两个Button,一个TextView。
private Button mLeftButton,mRightButton;
private TextView mTitleView;
给各个组件赋值
mLeftButton = new Button(context);
mRightButton = new Button(context);
mTitleView = new TextView(context);
//为创建的组件元素赋值,值来源于我们在引用的xml文件中给对应属性的赋值
mLeftButton.setTextColor(mLeftTextColor);
mLeftButton.setText(mLeftText);
mLeftButton.setBackground(mLeftBackground);
mRightButton.setText(mRightText);
mRightButton.setTextColor(mRightTextColor);
mRightButton.setBackground(mRightBackground);
mTitleView.setText(mTitle);
mTitleView.setTextColor(mTitleTextColor);
mTitleView.setGravity(Gravity.CENTER);
//为组件元素设置相应的布局元素
mLeftParams = new LayoutParams(LayoutParams.WRAP_CONTENT,LayoutParams.MATCH_PARENT);
mLeftParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT,TRUE);
//添加到ViewGroup
addView(mLeftButton,mLeftParams);
mRightParams = new LayoutParams(LayoutParams.WRAP_CONTENT,LayoutParams.MATCH_PARENT);
mRightParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT,TRUE);
//添加到ViewGroup
addView(mRightButton,mRightParams);
mTitleParams = new LayoutParams(LayoutParams.WRAP_CONTENT,LayoutParams.MATCH_PARENT);
mTitleParams.addRule(RelativeLayout.CENTER_IN_PARENT,TRUE);
//添加到ViewGroup
addView(mTitleView,mTitleParams);
为了让左右两个按钮有点击效果,下面定义接口来实现
在自定义类中,定义一个接口,用于实现按钮点击,声明两个方法:
public interface topbarClickListener{
//左按钮点击
void leftClick();
//右按钮点击
void rightClick();
}
有了接口我们要暴漏给调用者:
首先在自定义类中,调用这两个方法,但不去实现它
mLeftButton.setOnClickListener(View OnClickLisener(){
@override
public void onClick(View v){
mListener.leftClick();
}
});
右侧按钮同理
暴露一个方法供调用者赋值:
public void setOnTopBarClickListener(topbarClickListener mListener){
this.mListener = mListener;
}
调用者如何实现呢,代码如下:
private TopBar mTopBar;
mTopBar.setOnTopbarClickListener(new TopBar.topbarClickListener(){
@override
public void rightClick(){
//我是右侧点击
}
@override
public void leftClick(){
//我是左侧点击
}
});
最后一步,需要使用的地方引用TopBar控件,和系统控件书写方式差不多,路径对应正确即可;
首先声明命名空间:
xmlns:topbar="http://schemas.android.com/apk/res-auto"
<com.qingchen.widget.TopBar
android:id="@+id/topBar"
android:layout_width="match_parent"
android:layout_height="40dp"
topbar:leftText="返回"
topbar:title="我是标题"
topbar:leftBackground="@drawable/bg"
........
/>
此刻,ViewGroup组合控件的简单流程到此结束,流程不难,敲一边就会了。
3.对现有系统控件的扩展
这种自定义方式暂时不讲,后续会陆续推出各个扩展控件的教程。
转载请标明出处:
http://blog.csdn.net/qingchenba/article/details/53994834
本文出自:【清晨码农的博客】