关闭

Android自定义View基础详解

160人阅读 评论(0) 收藏 举报
分类:

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

本文出自:【清晨码农的博客】



0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:9399次
    • 积分:296
    • 等级:
    • 排名:千里之外
    • 原创:20篇
    • 转载:2篇
    • 译文:1篇
    • 评论:1条
    文章分类
    文章存档
    最新评论