使用c 代码自定义控件件与使用该控件的文件应该怎样放置才对哪

cache file NOT exists!《Android群英传》阅读笔记——Android控件架构与自定义控件详解_Android开发_动态网站制作指南
《Android群英传》阅读笔记——Android控件架构与自定义控件详解
来源:人气:338
《Android群英传》阅读笔记——Android控件架构与自定义控件详解
一、Android控件架构
Android中的每个控件都会在界面中占得一块矩形的区域,控件大致分为两类:ViewGroup控件与View控件。ViewGroup控件相当于一个容器可以包含多个View控件,并管理其包含的View控件。通过ViewGroup,整个界面上的控件形成了一个树形结构,这也就是我们常说的控件树了。上层控件负责下层子控件的测量与绘制,并传递交互事件。而我们在Activity中使用的findViewById()方法,就是在控件树中遍历查找View控件。在每棵控件树的顶部,都有一个ViewParent对象,这就是整棵树的控制核心,所有到的交互管理事件都由它来统一调度和分配,从而可以对整个视图进行整体控制。如下图便是一个View视图树:
通常情况下,当我们要显示一个Activity时,会在Activity生命周期的onCreate()的方法中,setContentView(),然后参数我们都是传R.layout.XXXX的,在调用该方法后,布局内容才能真正地显示出出来,那么setContentView()方法具体做了些什么呢?来看一眼Android界面的架构图:
我们可以看到,每个Activity都包含着一个Window对象,在Android中Window对象通常由PhoneWindow来实现的。PhoneWindow将一个DecorView设置为整个应用窗口的根View。DecorView作为窗口界面的顶层视图,封装了一些窗口操纵的通用方法,可以说,DecorView将要显示的具体内容呈现在了PhoneWindow上,这里面的所有View监听事件,都通过WindowManagerService来接受,并通过Activity对象来回调相应的onClickListener。在显示上,它将屏幕分成两部分,一个是TitleView,另一个是ContentView。看到这里大家一定看见了一个非常熟悉的界面ContentView。它是一个ID为content的Framelayout,activity_main.就是设置在这样一个Framelayout里,我们可以建立一个标准视图树,如下:
该视图树的第二层是一个LinearLayout,作为ViewGroup,这一层的布局结构会根据对应的参数设置不同的布局,如最常用的就是上面一个TitleBar,下面就是Content内容了,而如果我们通过设置requestWindowFeature(Window.FEATURE_NO_TITLE)来设置全屏的话,视图树中就只有Content了(也就是上图中的FrameLayout),这也就解释了为什么调用requestWindowFeature()方法一定要在setContentView()方法之前才能生效的原因了。
而在代码中,当程序在onCreate()方法中调用setContentView()方法后,ActivityManagerService会回调onResume()方法(Activity的生命周期中,onResume()就是和用户交互了),此时系统才会把整个DecorView添加到PhoneWindow中,并让其显示出来,从而最终返程界面的绘制。
二、View的测量
在现实中,如果我们要去画一个图形,就必须知道它的大小和位置,同理,在Android中,我们若想绘制一个View,也必须也要先知道绘制该View的大小,这个过程在onMeasure()方法中进行。
* @param widthMeasureSpec
* @param heightMeasureSpec
otected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Android给我们提供了一个设计短小精悍的类——MeasureSpec类,通过它来帮助我们测量View, MeasureSpec是一个32位的int值,其中高2位为测量模式,低30为测量的大小,在计算中使用位运算是为了提高并且优化效率。
精确值模式。当控件的layout_width属性或者layout_height属性指定为具体数值时,或,指定为match_parent属性时,系统会使用该模式。
最大值模式。当控件的layout_width属性或者layout_height属性指定为wrap_content时,控件的大小会随着该控件的内容或子控件的大小变化而变化,此时控件的尺寸只要不超过父控件允许的最大尺寸就好。
UNSPECIFIED
这个属性不指定其大小测量模式,我们可以按照我们的意愿设置成任意大小,一般不会用到,也不建议用。
View类默认的onMeasure()方法只支持EXACTLY模式,所以在自定义View时若不写onMeasure()方法,则只能使用EXACTLY模式,控件可以响应你指定的具体宽高值以及match_parent属性,而如果要让自定义View支持wrap_content属性,那么就必须重写onMeasure()方法。
通过MeasureSpec这一个类,我们就获取了View的测量模式和View想要绘制的大小。有了这些信息,我们就可以控制View最后显示的大小,接下来,我们可以看一个简单的小例子,我们重写onMeasure这个方法:
* @param widthMeasureSpec
* @param heightMeasureSpec
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
windows系统下按住Ctrl查看super.onMeasure()这个方法,可以发现,系统最终还是会调用setMeasuredDimension()这个方法将测量的宽高设置进去从而完成测量工作。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
通过我们知道,我们自定义的宽高是如何设置的,下面我们通过这个例子,来讲一下自定义的测量值。
第一步,我们从MeasureSpec类中提取出具体的测量模式和大小:
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
然后我们通过判断测量的模式给出不同的测量值,当specMode为EXACTLY时,直接使用指定的specSize,当为其他两种模式地时候,我们就需要一个默认的值了,特别是wrap_content时,即AT_MOST模式,measureWidth()方法是这样的:
private int measureWidth(int measureSpec) {
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if (specMode == MeasureSpec.EXACTLY) {
result = specS
result = 200;
if (specMode == MeasureSpec.AT_MOST) {
result = Math.min(result, specSize);
宽高的设置方法是一样的,所以,当我们在布局中设置match_parent时它就铺满容器了,要是设置wrap_content它就是包裹内容,如果不设置的话,那他就只有200px的大小了。
三、View的绘制
当我们用onMeasure()方法测量完成之后,我们就该重写onDraw()方法来绘制了,这个应该大家都很熟悉吧,首先我们要知道2D绘制的一些相关API:
什么是Canvas?我们知道,在现实中我们若想画一个图像,则需要纸啦一类的,来画,那么Canvas顾名思义就是画布喽,我们的绘制都是在Canvas上进行的。而onDraw()中有一个参数,就是Canvas canvas对象,使用这个Canvas对象就可以进行绘图了,而在其他地方,通常需要使用代码创建一个Canvas对象,代码如下:
Canvas canvas = new Canvas(bitmap);
当创建一个Canvas对象时,为什么要传进去一个bitmap对象呢?不传入可不可以?其实,不传入也没关系,IDE编译虽然不会报错,但是一般我们不会这样做的,这是因为传进去的bitmap与通过这个bitmap创建的Canvas是紧密相连的,这个过程就是装载画布,这个bitmap用来存储所有绘制在Canvas上的像素信息。所以当我们通过这种方式创建了Canvas对象后,后面调用所有的Canvas.drawXXX方法都发生在这个bitmap上。
如下,是Canvas一些最基本的用法:
//绘制直线
canvas.drawLine(float startX, float startY, float stopX, float stopY, Paint paint);
//绘制矩形
canvas.drawRect(float left, float top, float right, float bottom, Paint paint);
//绘制圆形
canvas.drawCircle(float cx, float cy, float radius, Paint paint);
//绘制字符
canvas.drawText(String text, float x, float y, Paint paint);
//绘制图形
canvas.drawBirmap(Bitmap bitmap, float left, float top, Paint paint);
四、ViewGroup的测量
在前面我们说了,ViewGroup是用来管理View的,顾名思义ViewGroup是老大喽~,既然是老大,那么手下的小弟必定是对老大言听计从的,当ViewGroup的大小为wrap_content时,ViewGroup就会遍历手下的子View,来获取View的大小,从而决定自己的大小,而在其他模式下则会通过具体的值来设置自身的大小。
ViewGroup在遍历所有的子View时,会调用子View的onMeasure()方法来获取测量结果。
当ViewGroup的子View测量完毕后,就需要将子View放到合适的位置,这部分则是由onLayout()来进行的。当ViewGroup在执行onLayout()时,同样也会遍历子View的onLayout()方法,并制定其具体显示的位置,从而来决定其布局位置。
在自定义ViewGroup时,通常会去重写onLayout()方法来控制其子View显示位置的逻辑。同样,若是要支持wrap_content属性,那么我们还是要重写onMeasure()方法的,这点与View是相同的。
五、ViewGroup的绘制
ViewGroup通常情况下不需要绘制,因为它本身就没有需要绘制的东西,如果不是指定了ViewGroup的背景颜色,那么ViewGroup的onDraw()方法都不会被调用。但是!ViewGroup会使用dispatchDraw()方法来绘制其子View,其过程同样是遍历所有的子View,并调用子View的绘制方法来完成绘制的。
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
六、自定义View
虽然Android给我们提供了丰富的库来创建丰富的UI效果,同时也提供了非常方便的拓展方法,通过继承Android的系统组件,我们可以非常方便地拓展现有功能,再系统组件的基础上创建新的功能,甚至可以自定义控件,来实现Android系统控件所没有的功能。
适当地使用自定义View,可以丰富应用程序的体验效果,但滥用自定义View则会带来适得其反的效果,所以要慎用哦~,而且,在系统原生控件经过多代版本的迭代后,在如今的版本中,依然还存在不少Bug,就更不要说我们自定义的View了,特别是现在Android ROM的多样性,导致Android的适配变得越来越复杂,很难保证自定义的View在其他手机上也能达到你想要的效果。
当然,了解Android系统自定义View的过程,可以帮助我们了解系统的绘图机制,可以通过自定义View来帮我们创建更加灵活的布局。
在View中通常有以下比较重要的回调方法
onFinishInflate()
//从XML加载组件后回调
protected void onFinishInflate() {
super.onFinishInflate();
onSizeChanged()
//组件大小改变时回调
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
onMeasure()
//回调该方法进行测量
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
onLayout()
//回调该方法来确定显示的位置
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
onTouchEvent()
//监听到触摸事件时回调
public boolean onTouchEvent(MotionEvent event) {
return super.onTouchEvent(event);
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
上面的方法并不需要都写出来,看个人需要,需要哪个写哪个。
通常情况下,实现自定义View有三种方法:
对现有的控件进行拓展
通过组合来实现新的控件
重写View来实现全新的控件
1、对现有的控件进行拓展
先来看一眼效果图,如下:
可以看到,第二个的Hello World,有了背景,且背景还带上了蓝色的描边,那么是怎么实现的呢,首先我们需要一个Canvas,然后需要两根画笔,颜色姑且不说哈,若是大家仔细看的话就会发现我们的Hello World向右移动了一点儿。没错我们就需要一个画布+两根画笔就是实现了这种效果。
我们先分步骤的实现一下:
//实例blue_paint画笔
blue_paint = new Paint();
//设置颜色
blue_paint.setColor(getResources().getColor(android.R.color.holo_blue_light));
//设置画笔style(实心)
blue_paint.setStyle(Paint.Style.FILL);
//实例yellow_paint画笔
yellow_paint = new Paint();
//设置颜色
yellow_paint.setColor(Color.YELLOW);
//设置画笔style(实心)
yellow_paint.setStyle(Paint.Style.FILL);
其实就是设置下我们的画笔,也没啥。
最重要的部分就是什么时候调用super了,然后我们开始绘制
protected void onDraw(Canvas canvas) {
//绘制外层
canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), blue_paint);
//绘制内层
canvas.drawRect(5, 5, getMeasuredWidth() - 5, getMeasuredHeight() - 5, yellow_paint);
//保存更改的画布
canvas.save();
//绘制文字前向右平移10像素
canvas.translate(10, 0);
//父类完成方法,即绘制文本
super.onDraw(canvas);
canvas.restore();
其实在onDraw()方法中,绘制了两个矩形,然后在第一个矩形中又套了一个矩形,这样就达到了背景+描边的效果了,怎么样简单吧!然后若想将自定义TextView中的文字进行左右移动的话调用 canvas.translate()方法就好喽,第一个参数是控制左右的,向右平移为+,向左平移为-,第二个参数是控制上下的,向下平移为+,向上平移为-,怎么样,还算简单吧!
好的,上完整代码,代码如下:
package com.llx.lenovo.
import android.content.C
import android.graphics.C
import android.graphics.C
import android.graphics.P
import android.util.AttributeS
import android.widget.TextV
CustomView
com.llx.lenovo.customview
CustomTextView
自定义TextView
public class CustomTextView extends TextView {
//声明画笔
private Paint blue_paint, yellow_
public CustomTextView(Context context, AttributeSet attrs) {
super(context, attrs);
private void init() {
//实例blue_paint画笔
blue_paint = new Paint();
//设置颜色
blue_paint.setColor(getResources().getColor(android.R.color.holo_blue_light));
//设置画笔style(实心)
blue_paint.setStyle(Paint.Style.FILL);
//实例yellow_paint画笔
yellow_paint = new Paint();
//设置颜色
yellow_paint.setColor(Color.YELLOW);
//设置画笔style(实心)
yellow_paint.setStyle(Paint.Style.FILL);
protected void onDraw(Canvas canvas) {
//绘制外层
canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), blue_paint);
//绘制内层
canvas.drawRect(5, 5, getMeasuredWidth() - 5, getMeasuredHeight() - 5, yellow_paint);
//保存更改的画布
canvas.save();
//绘制文字前向右平移10像素
canvas.translate(10, 0);
//父类完成方法,即绘制文本
super.onDraw(canvas);
canvas.restore();
接下来,我们看一个稍微复杂一点儿的,如图:
想实现这个效果并不太难,我们可以借助Android中LinearGradient,Shader,Matrix,来完成,来实现一个闪闪发光的闪动效果,我们充分的利用Shader渲染器,来设置一个不断变化的LinearGradient,首先我们要在onSizeChanged()方法中完成一些初始化操作:
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (mViewWidth == 0) {
//获取View测量值
mViewWidth = getMeasuredWidth();
if (mViewWidth & 0) {
//获取画笔对象
mPaint = getPaint();
mLinearGradient = new LinearGradient(0, 0, mViewWidth, 0, new int[]{Color.BLUE, 0xffffffff, Color.BLUE},
null, Shader.TileMode.CLAMP);
mPaint.setShader(mLinearGradient);
matrix = new Matrix();
其中最关键的就是getPaint()方法中获取当前特效他view的paint对象,并且设置LinearGradient属性,最后用矩阵不断平移渐变效果,就实现了这个效果,代码如下:
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (matrix != null) {
mTranslate += mViewWidth + 5;
if (mTranslate & 2 * mViewWidth / 5) {
mTranslate = -mViewW
matrix.setTranslate(mTranslate, 0);
mLinearGradient.setLocalMatrix(matrix);
//每隔100毫秒闪动一下
postInvalidateDelayed(100);
最后,完整代码如下:
package com.llx.lenovo.
import android.content.C
import android.graphics.C
import android.graphics.C
import android.graphics.LinearG
import android.graphics.M
import android.graphics.P
import android.graphics.S
import android.util.AttributeS
import android.widget.TextV
CustomView
com.llx.lenovo.customview
CustomTextViewGradient
自定义TextView,文字渐变
public class CustomTextViewGradient extends TextView {
private int mViewWidth = 0;
private Paint mP
//线性渐变
private LinearGradient mLinearG
private int mT
public CustomTextViewGradient(Context context, AttributeSet attrs) {
super(context, attrs);
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (mViewWidth == 0) {
//获取View测量值
mViewWidth = getMeasuredWidth();
if (mViewWidth & 0) {
//获取画笔对象
mPaint = getPaint();
mLinearGradient = new LinearGradient(0, 0, mViewWidth, 0, new int[]{Color.BLUE, 0xffffffff, Color.BLUE},
null, Shader.TileMode.CLAMP);
mPaint.setShader(mLinearGradient);
matrix = new Matrix();
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (matrix != null) {
mTranslate += mViewWidth + 5;
if (mTranslate & 2 * mViewWidth / 5) {
mTranslate = -mViewW
matrix.setTranslate(mTranslate, 0);
mLinearGradient.setLocalMatrix(matrix);
//每隔100毫秒闪动一下
postInvalidateDelayed(100);
2、创建复合控件
创建一个复合控件可以很好的创建出具有重要功能的控件集合,这种方式经常需要继承一个合适的ViewGroup,再给他添加指定功能的控件,从而组成一个新的合适的控件,通过这种方式创建的控件,我们一般都会给他指定的一些属性,让他具有更强的扩展性,下面就以一个TopBar为例子,讲解如何创建复合控件。
我们还是来看下效果,如下图:
乍一看,两种其实没什么区别,但是还是稍微有点儿区别的,下面我们先说一下,实现前的准备工作吧,如下:
为一个View提供可自定义的属性很简单,只需要在res-&values下新建一个attrs.xml的属性定义文件,并在该文件中通过如下代码定义相应的属性即可。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="rightTextColor" format="color" /&
&attr name="rightBackground" format="reference|color" /&
&attr name="rightText" format="string" /&
&/declare-styleable&
&/resources&
一步一步来哈,先来说明一下这个attrs.xml的文件是做什么的,我们都知道在原生的控件中,不管是Button还是TextView当我们进行设置的时候都是android:layout_width……这一类格式的(android是命名空间),那么我们的这个attrs.xml是做什么的呢,我们不是自定义View嘛,没错,我们的这个attrs.xml就是做这个的,当我们要用的时候,要有自己的命名空间,然后后面跟的属性就是这个文件中的属性喽,假如说我们新建的命名空间为app,那么我们要引用leftText时,格式就是,app:leftText=”“,就是这种格式的。
我们在代码中通过标签声明了使用自定义属性,然后name相当于ID可以让我们的类可以找到。确定好后,我们新建一个CustomTopBarView类就可以开始搞啦!
CustomTopBarView完整代码如下:
package com.llx.lenovo.
import android.content.C
import android.content.res.TypedA
import android.graphics.drawable.D
import android.util.AttributeS
import android.view.G
import android.view.V
import android.view.ViewG
import android.widget.B
import android.widget.RelativeL
import android.widget.TextV
CustomView
com.llx.lenovo.customview
CustomTopBarView
自定义TopBarView
public class CustomTopBarView extends RelativeLayout {
//左边文本颜色
private int mLeftTextC
//左边背景
private Drawable mLeftB
//左边文本
private String mLeftT
//右边文本颜色
private int mRightTextC
//右边背景
private Drawable mRightB
//右边文本
private String mRightT
//Title文字大小
private float mTitleS
//Title颜色
private int mTitleC
//Title文本
private String mT
//TypedArray
private TypedA
private Button mLeftB
private Button mRightB
//TextView
private TextView mTitleV
//LayoutParams
private LayoutParams mLeftP
private LayoutParams mRightP
private LayoutParams mTitlepP
//监听回调接口
private CustomTopBarClickListener mL
//带参构造方法
public CustomTopBarView(Context context, AttributeSet attrs) {
super(context, attrs);
if (attrs != null) {
//通过这个方法,从attrs.xml文件下读取读取到的值存储到我们的TypedArray中
ta = context.obtainStyledAttributes(attrs, R.styleable.TopBar);
//读取相对应的属性
//左边View属性
mLeftTextColor = ta.getColor(R.styleable.TopBar_leftTextColor, 0);
mLeftBackground = ta.getDrawable(R.styleable.TopBar_leftBackground);
mLeftText = ta.getString(R.styleable.TopBar_leftText);
//右边View属性
mRightTextColor = ta.getColor(R.styleable.TopBar_rightTextColor, 0);
mRightBackgroud = ta.getDrawable(R.styleable.TopBar_rightBackground);
mRightText = ta.getString(R.styleable.TopBar_rightText);
//Title属性
mTitleSize = ta.getDimension(R.styleable.TopBar_titleTextSize, 10);
mTitleColor = ta.getColor(R.styleable.TopBar_titleTextColor, 0);
mTitle = ta.getString(R.styleable.TopBar_title);
//获取完TypedArray的值之后,一般要调用recyle方法来避免重复创建时候的错误
ta.recycle();
initView(context);
mRightButton.setOnClickListener(new OnClickListener() {
public void onClick(View view) {
mListener.rightClick();
mLeftButton.setOnClickListener(new OnClickListener() {
public void onClick(View view) {
mListener.leftClick();
private void initView(Context context) {
mLeftButton = new Button(context);
mRightButton = new Button(context);
mTitleView = new TextView(context);
//为创建的mLeftButton赋值
//文字颜色
mLeftButton.setTextColor(mLeftTextColor);
mLeftButton.setBackground(mLeftBackground);
mLeftButton.setText(mLeftText);
//为创建的mRightButton赋值,与mLeftButton同理
mRightButton.setTextColor(mRightTextColor);
mRightButton.setBackground(mRightBackgroud);
mRightButton.setText(mRightText);
//为创建的mTitleView赋值
mTitleView.setText(mTitle);
mTitleView.setTextColor(mTitleColor);
//文字大小
mTitleView.setTextSize(mTitleSize);
//文字Gravity(靠左,居中,靠右等)
mTitleView.setGravity(Gravity.CENTER);
//为组件元素设置相应的布局元素
//左边View
mLeftParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
//显示位置
mLeftParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT, TRUE);
//将设置好的属性添加到自定义Layout中,并将mLeftButton与mLeftParams绑定,以下同理
addView(mLeftButton, mLeftParams);
mRightParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
mRightParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, TRUE);
addView(mRightButton, mRightParams);
mTitlepParams = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT);
mTitlepParams.addRule(RelativeLayout.CENTER_IN_PARENT, TRUE);
addView(mTitleView, mTitlepParams);
public void setOnTopbarClickListener(CustomTopBarClickListener mListener) {
this.mListener = mL
public void setButtonVisable(int id, boolean flag) {
if (flag) {
if (id == 0) {
mLeftButton.setVisibility(View.VISIBLE);
mRightButton.setVisibility(View.VISIBLE);
if (id == 0) {
mLeftButton.setVisibility(View.VISIBLE);
mRightButton.setVisibility(View.VISIBLE);
OK,现在我们来仔细的研究一下这个代码哈,虽然,群英传讲的已经够详细的了,但是,无奈,理解能力差,又查了几篇博客,然后自己又仔细的看了下终于看懂了,这个坑终于爬出来了,我们一点点的分析哈。
大家应该发现了,在该类的属性中,其实可以分成三个主要的,大致就是,左边,中间,右边,因为我们在attrs.xml文件中也就是最多的设置了左边,中间,右边,所以在该类的属性中最多也就是三个喽。
然后看下构造方法,构造方法的代码如下:
//带参构造方法
public CustomTopBarView(Context context, AttributeSet attrs) {
super(context, attrs);
if (attrs != null) {
//通过这个方法,从attrs.xml文件下读取读取到的值存储到我们的TypedArray中
ta = context.obtainStyledAttributes(attrs, R.styleable.TopBar);
//读取相对应的属性
//左边View属性
mLeftTextColor = ta.getColor(R.styleable.TopBar_leftTextColor, 0);
mLeftBackground = ta.getDrawable(R.styleable.TopBar_leftBackground);
mLeftText = ta.getString(R.styleable.TopBar_leftText);
//右边View属性
mRightTextColor = ta.getColor(R.styleable.TopBar_rightTextColor, 0);
mRightBackgroud = ta.getDrawable(R.styleable.TopBar_rightBackground);
mRightText = ta.getString(R.styleable.TopBar_rightText);
//Title属性
mTitleSize = ta.getDimension(R.styleable.TopBar_titleTextSize, 10);
mTitleColor = ta.getColor(R.styleable.TopBar_titleTextColor, 0);
mTitle = ta.getString(R.styleable.TopBar_title);
//获取完TypedArray的值之后,一般要调用recyle方法来避免重复创建时候的错误
ta.recycle();
initView(context);
mRightButton.setOnClickListener(new OnClickListener() {
public void onClick(View view) {
mListener.rightClick();
mLeftButton.setOnClickListener(new OnClickListener() {
public void onClick(View view) {
mListener.leftClick();
在这个构造方法中,我们可以看到参数的类型分别为Context与AttributeSet,Context我们都熟悉哈,所以我来简单的说一下AttributeSet,这个其实…它就是我们刚开始时定义的那个属性,然后传到这里来,我们去用这些属性,毕竟,attrs与layout通过命名空间相关联,那么我们是不是感觉少点儿东西呢?在xml布局中,我们若是使用了该自定义的属性…,那么自定义在哪儿呢?我们并没有敲一行代码,所以不可能实现自定义的,所以,我们就需要attrs与我们的自定义View相关联了,那么在我们的构造方法下的AttributeSet,就弥补了这个空白,然后剩下的…就劳烦各位读者,看下注释吧,我感觉挺清楚的了,嘿嘿,然后构造方法中剩下的方法中就没什么的了哈,但是在initView(context);中还是有点儿东西的,下面我贴一下initView(context)哈,如下:
private void initView(Context context) {
mLeftButton = new Button(context);
mRightButton = new Button(context);
mTitleView = new TextView(context);
//为创建的mLeftButton赋值
//文字颜色
mLeftButton.setTextColor(mLeftTextColor);
mLeftButton.setBackground(mLeftBackground);
mLeftButton.setText(mLeftText);
//为创建的mRightButton赋值,与mLeftButton同理
mRightButton.setTextColor(mRightTextColor);
mRightButton.setBackground(mRightBackgroud);
mRightButton.setText(mRightText);
//为创建的mTitleView赋值
mTitleView.setText(mTitle);
mTitleView.setTextColor(mTitleColor);
//文字大小
mTitleView.setTextSize(mTitleSize);
//文字Gravity(靠左,居中,靠右等)
mTitleView.setGravity(Gravity.CENTER);
//为组件元素设置相应的布局元素
//左边View
mLeftParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
//显示位置
mLeftParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT, TRUE);
//将设置好的属性添加到自定义Layout中,并将mLeftButton与mLeftParams绑定,以下同理
addView(mLeftButton, mLeftParams);
mRightParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
mRightParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, TRUE);
addView(mRightButton, mRightParams);
mTitlepParams = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT);
mTitlepParams.addRule(RelativeLayout.CENTER_IN_PARENT, TRUE);
addView(mTitleView, mTitlepParams);
因为我们在布局中并没有一个Button控件,自然就不能findViewById()了,所以只能new…了,然后它就需要传入一个上下文,直接把构造方法中的那个context拿来用就可以了,哈哈!三个都是一样的,然后设置的属性也就是类似的了,但是,将这个三个控件属性都设置完毕后,还是不可以的,因为我们毕竟继承的是RelativeLayout,所以…这三个控件的位置我们并不能确定,现在就需要我们的布局参数了,即!LayoutParams!实例化相应的对象时,我们必须告诉它,该布局占的空间是多大的,所以各位是不是看它的参数值很熟悉呢,当然自己也可以定义的哈!我们都知道在RelativeLayout中若是不规定某个控件的位置时,它默认的位置就是左上角的,所以第二步我们就要设置显示位置了,当显示的位置设定好以后,还有一个问题,没错!我们该设定的设了,但是…我们还没有将该属性设置到布局中去,所以就调用addView(),让该属性生效,当然我们也要将我们的控件传入进去,毕竟,所有的属性都设置好了,若是没有使用该属性的控件,岂不是很尴尬!所以以下同理喽~
剩下的就没有什么说的了,毕竟大框架都搭好,剩下的就是优化了,下面来看下我们自定义的View到底是什么样子的吧,在layout下新建topbar.xml,并修改如下:
&?xml version="1.0" encoding="utf-8"?&
&com.llx.lenovo.customview.CustomTopBarView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/top_bar"
android:layout_width="match_parent"
android:layout_height="48dp"
android:orientation="vertical"
app:leftBackground="@color/colorAccent"
app:leftText="BACK"
app:leftTextColor="#Fff"
app:rightBackground="@color/colorAccent"
app:rightText="MORE"
app:rightTextColor="#fff"
app:title="自定义标题"
app:titleTextColor="#000"
app:titleTextSize="12sp" /&
OK,我们在该节的开头时,就说了,要新建一个命名空间,故:
xmlns:app="http://schemas.android.com/apk/res-auto"
名字可以随便起哈,前提是,要符合规矩,哈哈!
然后很简答的一步了,修改MainActivity.如下:
public class MainActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.topbar);
其实就是修改了,setContentView中的参数,将加载的布局改为我们刚刚新建的topbar了,然后运行后,就是我们的那个第一种效果,如下:
然后第二种呢,也是这样的,就是将topbar,include到activity_main中了,activity_main.xml代码如下:
&?xml version="1.0" encoding="utf-8"?&
&LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"&
layout="@layout/topbar"
android:layout_width="match_parent"
android:layout_height="40dp" /&
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:text="Hello World!"
android:textSize="30sp" /&
&com.llx.lenovo.customview.CustomTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:text="Hello World!"
android:textSize="30sp" /&
&com.llx.lenovo.customview.CustomTextViewGradient
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:text="Hello World!"
android:textSize="30sp" /&
&com.llx.lenovo.customview.CustomTopBarView
android:id="@+id/custom_top_bar_view"
android:layout_width="wrap_content"
android:layout_height="40dp" /&
&/LinearLayout&
然后修改MainActivity.java的代码如下:
public class MainActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
这样运行的效果就是第二种了,如下:
OK,这个就是实现自定义控件的第二种方式,通过组合的方式实现新的控件了。下面我们来看下第三种方式是怎么实现控件的,继续看书吧。
3、重写View来实现全新的控件
当Android系统原生的控件无法满足我们的需求时,我们可以继承原有的控件进行修改,也可以将几个控件进行组合,当然也可以继承View来创建一个新的View喽。先看下效果:
我们就来实现一下它哈,不难的,首先,新建一个CustomCircleView类,代码如下:
package com.llx.lenovo.
import android.content.C
import android.graphics.C
import android.graphics.C
import android.graphics.P
import android.graphics.RectF;
import android.util.AttributeS
import android.util.DisplayM
import android.view.V
import android.view.WindowM
CustomView
com.llx.lenovo.customview
CustomCircleView
自定义半弧圆
public class CustomCircleView extends View {
//圆的直径
private int mCircleXY;
//屏幕高宽
private int w,
//圆的半径
private float mR
//圆的画笔
private Paint mCircleP
//弧线的画笔
private Paint mArcP
//文本画笔
private Paint mTextP
//需要显示的文字
private String mShowText = "李林雄";
//文字大小
private int mTextSize = 50;
//圆心扫描的弧度
private float mSweepAngle = 270;
public CustomCircleView(Context context, AttributeSet attrs) {
super(context, attrs);
//获取屏幕高宽
DisplayMetrics dm = new DisplayMetrics();
WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
wm.getDefaultDisplay().getMetrics(dm);
w = dm.widthP
h = dm.heightP
private void init() {
mCircleXY = w / 2;
mRadius = (float) (w * 0.5 / 2);
//画笔实例化
mCirclePaint = new Paint();
//画笔设置颜色
mCirclePaint.setColor(Color.BLUE);
mArcPaint = new Paint();
//设置线宽
mArcPaint.setStrokeWidth(100);
//设置空心
mArcPaint.setStyle(Paint.Style.STROKE);
//设置颜色
mArcPaint.setColor(Color.BLUE);
mTextPaint = new Paint();
mTextPaint.setColor(Color.WHITE);
mTextPaint.setTextSize(mTextSize);
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//绘制矩形
RectF mArcRectF = new RectF((float) (w * 0.1), (float) (w * 0.1), (float) (w * 0.9), (float) (w * 0.9));
canvas.drawCircle(mCircleXY, mCircleXY, mRadius, mCirclePaint);
//绘制弧线
canvas.drawArc(mArcRectF, 270, mSweepAngle, false, mArcPaint);
//绘制文本
canvas.drawText(mShowText, 0, mShowText.length(), mCircleXY, mCircleXY + (mTextSize / 4), mTextPaint);
public void setSweepValues(float sweepValues) {
if (sweepValues != -0) {
mSweepAngle = sweepV
//如果没有,我们默认设置
mSweepAngle = 30;
//一定要刷新
invalidate();
这样,我们就实现了该节开头的那个效果图了,但是,大家应该会发现,还有一个public的方法,那么这个方法是做什么的呢?修改一下MainActivity.java的代码吧,修改如下:
public class MainActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.custom_cyrcle_view);
CustomCircleView customCircleView = (CustomCircleView) findViewById(R.id.cicle_view);
customCircleView.setSweepValues(180);
为了演示方便一些,我单独把我们的这个自定义的View放到一个新的布局中了,这里其实不是重要的,重要的是后面那个调用该public的代码,我们还是看下效果图吧,然后,你就懂了,如下图:
嘿嘿,相信大家都看懂了哈,调用该方法,并将参数设为180,那么外部的扇形角度就是180了,当然我们可以设任意角度哈!
实现音频条形图
我们来做一个小练习哈,就是一个最常见的一个动画——音频条形图,哈哈,对它都不陌生吧,高高低低的,由于我们只是演示下自定义View的用法,所以就不用真实的数据了哈,就随机模拟一些数据就好了,这个比上面那个还难一点,但是,万变不离其宗啊,只要心中有图,那么就是写写算算,对对坐标了,也不难,我们先来看下最终效果,如下图:
可以看到,每个线条都在变化,想实现这个View其实也不太难,新建一个CustomMusicRectangularFigure类,代码如下:
package com.llx.lenovo.
import android.content.C
import android.graphics.C
import android.graphics.C
import android.graphics.LinearG
import android.graphics.P
import android.graphics.S
import android.util.AttributeS
import android.view.V
import static android.R.attr.
CustomView
com.llx.lenovo.customview
CustomMusicRectangularFigure
实现音频条形图
public class CustomMusicRectangularFigure extends View {
private int mW
//矩形的宽度
private int mRectW
//矩形的高度
private int mRectH
private Paint mP
//矩形的数量
private int mRectC
private int offset = 5;
private double mR
//线性渐变效果
private LinearGradient mLinearG
public CustomMusicRectangularFigure(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
private void initView() {
mPaint = new Paint();
mPaint.setColor(Color.BLUE);
mPaint.setStyle(Paint.Style.FILL);
mRectCount = 12;
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//遍历绘制矩形
for (int i = 0; i & mRectC i++) {
//获取随机数
mRandom = Math.random();
//当前矩形的高度,通过随机数决定
float currentHeight = (float) (mRectHeight * mRandom);
//开始绘制
canvas.drawRect(
(float) (mWidth * 0.4 / 2 + mRectWidth * i + offset), currentHeight,
(float) (mWidth * 0.4 / 2 + mRectWidth * (i + 1)), mRectHeight, mPaint);
//300毫秒后通知onDraw进行View重绘
postInvalidateDelayed(300);
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
//获取View的宽
mWidth = getWidth();
//获取View的高
mRectHeight = getHeight();
mRectWidth = (int) (mWidth * 0.6 / mRectCount);
//设置线性渐变的属性
mLinearGradient = new LinearGradient(0, 0, mRectWidth, mRectHeight, Color.YELLOW, Color.BLUE, Shader.TileMode.CLAMP);
//通过画笔将线性渐变绘制到每个小矩形上面
mPaint.setShader(mLinearGradient);
OK,其实注释已经写的很详细了,但是,还是简略的说一下吧,说一下整体的思路哈,首先我们要自定义一个View,那么该View的宽和高是一定要知道的,要不然我们怎么决定可以画多少个矩形?每个矩形的高度又是多少,所以,对View的测量是很有必要的,故源码中,用mWidth代表了View的宽,高的话就用mRectHeight代替了,当我们知道了这些后,那么就该规定要有多少个小矩形了,当然我们可以根据测量到的View的宽来/矩形的宽,决定共有多少个小矩形,这里我就用mRectCount代替了哈,毕竟逻辑都是相同的,画的时候都要遍历进行画的,很显然只拥有这些值我们是远远不能让它动起来的,毕竟现在高度是固定的,那么我们该怎么样让它动起来呢,若想动,那就就需要我们的随机数啦,所以就用到了Math.random()来获取随机数,这样大致框架就完成了,但是,这样它只画一次啊,虽然有随机数的加入了,但是只有当多次重绘的时候,随机数的作用才能体现出来呢,所以就用到了我们的另一个方法postInvalidateDelayed(300),这个方法可以喽,300ms后通知onDraw(),进行重绘,由于我们用了随机数,这样它就可以动起来呢,然后主要的方法,主要的代码是那些呢?如下:
//遍历绘制矩形
for (int i = 0; i & mRectC i++) {
//获取随机数
mRandom = Math.random();
//当前矩形的高度,通过随机数决定
float currentHeight = (float) (mRectHeight * mRandom);
//开始绘制
canvas.drawRect(
(float) (mWidth * 0.4 / 2 + mRectWidth * i + offset), currentHeight,
(float) (mWidth * 0.4 / 2 + mRectWidth * (i + 1)), mRectHeight, mPaint);
//300毫秒后通知onDraw进行View重绘
postInvalidateDelayed(300);
主要的代码就是这些了,其实一直是这些代码在起作用了,然后我们的线性渐变体现在哪里呢,我们都知道,一个矩形上,若是只有一种颜色,那就太单调了,所以线性渐变就是让这个自定义View更美观些吧,代码也不多,注释都有写哦~嘿嘿,大家去看注释吧,哈哈!
七、自定义ViewGroup
ViewGroup在前文中,我们已经说过了,它就相当于一个大人,管着下面的一群小孩,那么大人管小孩,那是吃喝拉撒都管的,那么ViewGroup相对于子View的话,也亦如此,大小、位置、监听,这是最基本的了,那么相对应的我们要重写的方法,也就是onMeasure()测量,onLayout()位置,onTouchEvent()响应事件,这里我们实现一个类似系统ScrollView的效果,首先,我们测量:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//获取子View的数量
int count = getChildCount();
for (int i = 0; i & ++i) {
View childView = getChildAt(i);
//测量子View
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
接下来我们要对子View的位置进行计算放置,首先我们应该保证每一个View放置的时候都是全屏,这样我们在滑动的时候,可以比较好地实现后面的效果,我们这样来设置ViewGroup的高度:
protected void onLayout(boolean b, int i, int i1, int i2, int i3) {
int childCount = getChildCount();
//布局参数
MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams();
//设置ViewGroup高度
mlp.height = mScreenHeight * childC
setLayoutParams(mlp);
for (int j = 0; j & childC j++) {
View child = getChildAt(i);
//如果子View是可显示状态
if (child.getVisibility() != View.GONE) {
//放置子View
child.layout(1, i * mScreenHeight, i2, (i + 1) * mScreenHeight);
若想完整的显示子View的内容,那么管理子View的ViewGroup就必须&=所有的子View的高度之和!另一个就是,空间不能浪费,毕竟我们的手机屏幕就那么大,所以在ViewGroup管理下的子View只有是显示状态的给分配位置,其他的不是显示状态的则不分配位置!
在代码中主要还是修改每个View的top和bottom属性,可以让它们有序的排列下来,然后我们就可以滑动它们啦,代码如下:
public boolean onTouchEvent(MotionEvent event) {
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mStart = getScrollY();
case MotionEvent.ACTION_MOVE:
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
int dy = mLastY -
if (getScrollY() & 0) {
if (getScrollY() & getHeight() - mScreenHeight) {
scrollBy(0, dy);
case MotionEvent.ACTION_UP:
mEnd = getScrollY();
int dScrollY = mEnd - mS
if (dScrollY & 0) {
if (dScrollY & mScreenHeight / 3) {
mScroller.startScroll(0, getScrollY(), 0, -dScrollY);
mScroller.startScroll(0, getScrollY(), 0, mScreenHeight - dScrollY);
if (-dScrollY & mScreenHeight / 3) {
mScroller.startScroll(0, getScrollY(), 0, -dScrollY);
mScroller.startScroll(0, getScrollY(), 0, -mScreenHeight - dScrollY);
postInvalidate();
其实别看代码这么多,但还是很好理解的,我们应该都知道,手在屏幕上划,有三个状态,从开始到结束,依次是:手指落下,手指移动,手指抬起。然后它们这些动作在onTouchEvent中就分别对应着:ACTION_DOWN,ACTION_MOVE,ACTION_UP;然后我们处理相应的逻辑就好了,就是当手指落下时做什么,手指移动时做什么,当手指离开了又该做什么!大家应该能看懂的哈,虽然代码多了些,但是,逻辑还是很好理解的!
最后别忘记加下面这个方法哈:
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()) {
scrollTo(0, mScroller.getCurrY());
这个方法其实就计算滚动的,不难理解!嘿嘿…
八、事件拦截机制分析
这章讲的是一个事件拦截机制的一些基本概念,当Android系统扑捉到用户的各种输入事件之后,如何准确的传递给真正需要这个事件的控件呢?其实Android提供了一套非常完善的事件传递,处理机制,来帮助开发者完成准确的事件分配和处理。
要想了解拦截机制,我们首先要知道什么事触摸事件,一般MotionEvent提供的手势,我们常用的几个DOWN,MOVE,UP…
在MotionEvent中封装了很多东西,比如获取坐标点event.getX()和getRawX()获取。
但是我们这次讲的是事件拦截,那什么才是事件拦截,打个比方View和ViewGroup都要这个事件,这里就产生了事件拦截,其实就是一层层递减的意义,一般ViewGroup我们需要重写三个方法:
public boolean dispatchTouchEvent(MotionEvent ev) {
return super.dispatchTouchEvent(ev);
public boolean onInterceptHoverEvent(MotionEvent event) {
return super.onInterceptHoverEvent(event);
public boolean onTouchEvent(MotionEvent event) {
return super.onTouchEvent(event);
而对于我们的View就只需要两个了:
public boolean dispatchTouchEvent(MotionEvent ev) {
return super.dispatchTouchEvent(ev);
public boolean onTouchEvent(MotionEvent event) {
return super.onTouchEvent(event);
额,这概念还是有点儿繁琐的,毕竟刚学Android不久,所以这个…推荐大家看一篇博客http://blog.csdn.net/chunqiuwei/article/details/,很详细的,我们用四张图来大致的了解一下哈(以下图片来自原上面的博主哈,尊重博主)
得到点击事件的log:
我们就可以很明确的知道他们的执行过程了:
还有两种情况,就是A拦截:
这里大致的简写了以下,因为我觉得上面那篇博客总结的比我好,我们通过前面的几种情况分析得到了大致的思想,在后面的学习,我们应该结合源码,你才会有更深的认识,如果是初学者,还是不建议看源码,会头大的。
奥~,第三章写完了,说实话,从第三章学了不少东西呢,首先我知道一个自定义View应该经过哪些步骤才能实现,希望以后真遇到自定义View的情况,不会着急吧,为以后找工作,还有工作做准备,打基础,虽然现在Android工作很难找了,但是,也不能气馁嘛,总不能因为找不到工作,市场的饱和就放弃自己喜欢的Android啊,对吧。
本篇博客有借鉴程序员刘某人的群英传笔记,嘿嘿,尤其是最后这一节,其实就是照抄了,嘿嘿。在这里推荐下我们伟大的群主几个初级的自定义的View哈,链接如下:
Android绘图机制(一)——自定义View的基础属性和方法
Android绘图机制(二) - 自定义查看绘制形,圆形,三角形,扇形,椭圆形,曲线,文字和图片的坐标
Android绘图机制(三)——自定义View的实现方式以及半弧圆新控件
好了,今天的任务圆满完成了,可以去睡觉了…嘿嘿
大家若是有什么不懂的,可以在下面评论区中留言哈,我看到后会回的,另外对android有兴趣的同学可以加我们程序员刘某人的群:,群里面有很多大神的,而且很热情,很热心的,大家不懂的可以问的。
优质网站模板

我要回帖

更多关于 自定义控件的使用 的文章

 

随机推荐