《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.xml就是设置在这样一个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
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
Android给我们提供了一个设计短小精悍的类——MeasureSpec类,通过它来帮助我们测量View, MeasureSpec是一个32位的int值,其中高2位为测量模式,低30为测量的大小,在计算中使用位运算是为了提高并且优化效率。
测量模式
- EXACTLY
精确值模式。当控件的layout_width属性或者layout_height属性指定为具体数值时,或,指定为match_parent属性时,系统会使用该模式。
- AT_MOST
最大值模式。当控件的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
*/
@Override
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 = specSize;
} else {
result = 200;
if (specMode == MeasureSpec.AT_MOST) {
result = Math.min(result, specSize);
}
}
return result;
}
宽高的设置方法是一样的,所以,当我们在布局中设置match_parent时它就铺满容器了,要是设置wrap_content它就是包裹内容,如果不设置的话,那他就只有200px的大小了。
三、View的绘制
当我们用onMeasure()方法测量完成之后,我们就该重写onDraw()方法来绘制了,这个应该大家都很熟悉吧,首先我们要知道2D绘制的一些相关API:
- Canvas
什么是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的绘制方法来完成绘制的。
@Override
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加载组件后回调
@Override
protected void onFinishInflate() {
super.onFinishInflate();
}
- onSizeChanged()
//组件大小改变时回调
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
}
- onMeasure()
//回调该方法进行测量
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
- onLayout()
//回调该方法来确定显示的位置
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
}
- onTouchEvent()
//监听到触摸事件时回调
@Override
public boolean onTouchEvent(MotionEvent event) {
return super.onTouchEvent(event);
}
- onDraw()
//绘图
@Override
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了,然后我们开始绘制
@Override
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.customview;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.widget.TextView;
/*
* 项目名: CustomView
* 包名: com.llx.lenovo.customview
* 文件名: CustomTextView
* 创建者: LLX
* 创建时间: 2017/2/23 20:25
* 描述: 自定义TextView
*/
public class CustomTextView extends TextView {
//声明画笔
private Paint blue_paint, yellow_paint;
public CustomTextView(Context context, AttributeSet attrs) {
super(context, attrs);
//初始化
init();
}
//初始化
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);
}
@Override
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()方法中完成一些初始化操作:
@Override
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属性,最后用矩阵不断平移渐变效果,就实现了这个效果,代码如下:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (matrix != null) {
mTranslate += mViewWidth + 5;
if (mTranslate > 2 * mViewWidth / 5) {
mTranslate = -mViewWidth;
}
matrix.setTranslate(mTranslate, 0);
mLinearGradient.setLocalMatrix(matrix);
//每隔100毫秒闪动一下
postInvalidateDelayed(100);
}
}
最后,完整代码如下:
package com.llx.lenovo.customview;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LinearGradient;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Shader;
import android.util.AttributeSet;
import android.widget.TextView;
/*
* 项目名: CustomView
* 包名: com.llx.lenovo.customview
* 文件名: CustomTextViewGradient
* 创建者: LLX
* 创建时间: 2017/2/23 21:48
* 描述: 自定义TextView,文字渐变
*/
public class CustomTextViewGradient extends TextView {
private int mViewWidth = 0;
private Paint mPaint;
//线性渐变
private LinearGradient mLinearGradient;
//矩阵
private Matrix matrix;
private int mTranslate;
public CustomTextViewGradient(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
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();
}
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (matrix != null) {
mTranslate += mViewWidth + 5;
if (mTranslate > 2 * mViewWidth / 5) {
mTranslate = -mViewWidth;
}
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.customview;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.RelativeLayout;
import android.widget.TextView;
/*
* 项目名: CustomView
* 包名: com.llx.lenovo.customview
* 文件名: CustomTopBarView
* 创建者: LLX
* 创建时间: 2017/2/23 22:39
* 描述: 自定义TopBarView
*/
public class CustomTopBarView extends RelativeLayout {
//左边文本颜色
private int mLeftTextColor;
//左边背景
private Drawable mLeftBackground;
//左边文本
private String mLeftText;
//右边文本颜色
private int mRightTextColor;
//右边背景
private Drawable mRightBackgroud;
//右边文本
private String mRightText;
//Title文字大小
private float mTitleSize;
//Title颜色
private int mTitleColor;
//Title文本
private String mTitle;
//TypedArray
private TypedArray ta;
//Button
private Button mLeftButton;
private Button mRightButton;
//TextView
private TextView mTitleView;
//LayoutParams
private LayoutParams mLeftParams;
private LayoutParams mRightParams;
private LayoutParams mTitlepParams;
//监听回调接口
private CustomTopBarClickListener mListener;
//带参构造方法
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() {
@Override
public void onClick(View view) {
mListener.rightClick();
}
});
mLeftButton.setOnClickListener(new OnClickListener() {
@Override
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);
//Tietle
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 = mListener;
}
public void setButtonVisable(int id, boolean flag) {
if (flag) {
if (id == 0) {
mLeftButton.setVisibility(View.VISIBLE);
} else {
mRightButton.setVisibility(View.VISIBLE);
}
} else {
if (id == 0) {
mLeftButton.setVisibility(View.VISIBLE);
} else {
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() {
@Override
public void onClick(View view) {
mListener.rightClick();
}
});
mLeftButton.setOnClickListener(new OnClickListener() {
@Override
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);
//Tietle
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.java如下:
public class MainActivity extends AppCompatActivity {
@Override
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">
<include
layout="@layout/topbar"
android:layout_width="match_parent"
android:layout_height="40dp" />
<TextView
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 {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
这样运行的效果就是第二种了,如下:
OK,这个就是实现自定义控件的第二种方式,通过组合的方式实现新的控件了。下面我们来看下第三种方式是怎么实现控件的,继续看书吧。
3、重写View来实现全新的控件
当Android系统原生的控件无法满足我们的需求时,我们可以继承原有的控件进行修改,也可以将几个控件进行组合,当然也可以继承View来创建一个新的View喽。先看下效果:
我们就来实现一下它哈,不难的,首先,新建一个CustomCircleView类,代码如下:
package com.llx.lenovo.customview;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.View;
import android.view.WindowManager;
/*
* 项目名: CustomView
* 包名: com.llx.lenovo.customview
* 文件名: CustomCircleView
* 创建者: LLX
* 创建时间: 2017/2/25 17:41
* 描述: 自定义半弧圆
*/
public class CustomCircleView extends View {
//圆的直径
private int mCircleXY;
//屏幕高宽
private int w, h;
//圆的半径
private float mRadius;
//圆的画笔
private Paint mCirclePaint;
//弧线的画笔
private Paint mArcPaint;
//文本画笔
private Paint mTextPaint;
//需要显示的文字
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.widthPixels;
h = dm.heightPixels;
//初始化
init();
}
//初始化
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);
}
@Override
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 = sweepValues;
} else {
//如果没有,我们默认设置
mSweepAngle = 30;
}
//一定要刷新
invalidate();
}
}
这样,我们就实现了该节开头的那个效果图了,但是,大家应该会发现,还有一个public的方法,那么这个方法是做什么的呢?修改一下MainActivity.java的代码吧,修改如下:
public class MainActivity extends AppCompatActivity {
@Override
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.customview;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Shader;
import android.util.AttributeSet;
import android.view.View;
import static android.R.attr.offset;
/*
* 项目名: CustomView
* 包名: com.llx.lenovo.customview
* 文件名: CustomMusicRectangularFigure
* 创建者: LLX
* 创建时间: 2017/2/26 2:42
* 描述: 实现音频条形图
*/
public class CustomMusicRectangularFigure extends View {
//View宽
private int mWidth;
//矩形的宽度
private int mRectWidth;
//矩形的高度
private int mRectHeight;
//画笔
private Paint mPaint;
//矩形的数量
private int mRectCount;
private int offset = 5;
//随机数
private double mRandom;
//线性渐变效果
private LinearGradient mLinearGradient;
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;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//遍历绘制矩形
for (int i = 0; i < mRectCount; 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);
}
@Override
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 < mRectCount; 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的效果,首先,我们测量:
//测量
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//获取子View的数量
int count = getChildCount();
for (int i = 0; i < count; ++i) {
View childView = getChildAt(i);
//测量子View
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
}
}
接下来我们要对子View的位置进行计算放置,首先我们应该保证每一个View放置的时候都是全屏,这样我们在滑动的时候,可以比较好地实现后面的效果,我们这样来设置ViewGroup的高度:
//位置
@Override
protected void onLayout(boolean b, int i, int i1, int i2, int i3) {
int childCount = getChildCount();
//布局参数
MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams();
//设置ViewGroup高度
mlp.height = mScreenHeight * childCount;
setLayoutParams(mlp);
for (int j = 0; j < childCount; 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属性,可以让它们有序的排列下来,然后我们就可以滑动它们啦,代码如下:
//滑动
@Override
public boolean onTouchEvent(MotionEvent event) {
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastY = y;
mStart = getScrollY();
break;
case MotionEvent.ACTION_MOVE:
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
int dy = mLastY - y;
if (getScrollY() < 0) {
dy = 0;
}
if (getScrollY() > getHeight() - mScreenHeight) {
dy = 0;
}
scrollBy(0, dy);
mLastY = y;
break;
case MotionEvent.ACTION_UP:
mEnd = getScrollY();
int dScrollY = mEnd - mStart;
if (dScrollY > 0) {
if (dScrollY < mScreenHeight / 3) {
mScroller.startScroll(0, getScrollY(), 0, -dScrollY);
} else {
mScroller.startScroll(0, getScrollY(), 0, mScreenHeight - dScrollY);
}
} else {
if (-dScrollY < mScreenHeight / 3) {
mScroller.startScroll(0, getScrollY(), 0, -dScrollY);
} else {
mScroller.startScroll(0, getScrollY(), 0, -mScreenHeight - dScrollY);
}
}
break;
}
postInvalidate();
return true;
}
其实别看代码这么多,但还是很好理解的,我们应该都知道,手在屏幕上划,有三个状态,从开始到结束,依次是:手指落下,手指移动,手指抬起。然后它们这些动作在onTouchEvent中就分别对应着:ACTION_DOWN,ACTION_MOVE,ACTION_UP;然后我们处理相应的逻辑就好了,就是当手指落下时做什么,手指移动时做什么,当手指离开了又该做什么!大家应该能看懂的哈,虽然代码多了些,但是,逻辑还是很好理解的!
最后别忘记加下面这个方法哈:
//滚动
@Override
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我们需要重写三个方法:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptHoverEvent(MotionEvent event) {
return super.onInterceptHoverEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return super.onTouchEvent(event);
}
而对于我们的View就只需要两个了:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return super.onTouchEvent(event);
}
额,这概念还是有点儿繁琐的,毕竟刚学Android不久,所以这个…推荐大家看一篇博客,很详细的,我们用四张图来大致的了解一下哈(以下图片来自原上面的博主哈,尊重博主)
得到点击事件的log:
我们就可以很明确的知道他们的执行过程了:
还有两种情况,就是A拦截:
B拦截:
这里大致的简写了以下,因为我觉得上面那篇博客总结的比我好,我们通过前面的几种情况分析得到了大致的思想,在后面的学习,我们应该结合源码,你才会有更深的认识,如果是初学者,还是不建议看源码,会头大的。
奥~,第三章写完了,说实话,从第三章学了不少东西呢,首先我知道一个自定义View应该经过哪些步骤才能实现,希望以后真遇到自定义View的情况,不会着急吧,为以后找工作,还有工作做准备,打基础,虽然现在Android工作很难找了,但是,也不能气馁嘛,总不能因为找不到工作,市场的饱