3.1 Android控件架构
Android中,控件分为两类,ViewGroup和View。ViewGroup作为父控件可包含多个View。控件树如下:
上层控件负责下层控件的测量和绘制,并传递交互事件。
每个Activity都包含一个Window对象(通常由PhoneWindow实现)。PhoneWindow将一个DecorView(decor
布置,装饰; 布景;)设置为根View。DecorView封装了窗口操作的通用方法。DecorView把所有要现实的内容呈现在PhoneWindow,这里面所有View的监听事件,都听过WindowManagerService来进行接收,并通过Activity来回调相应的onClickListener。DecorView将显示器分为TitleView和ContentView(id为content的FrameLayout,activity.xml就是设置在这样一个FrameLayout里)。视图树如下:
视图树的第二层装载了一个LinearLayout,常见的上面显示TitleBar下面显示Content。标准视图树如下:
在代码中,setContentView()方法后,ActivityManagerService会回调onResume()方法,此时系统才会把DecorView添加到PhoneWindow中,并让其显示出来,完成整个界面的绘制。
3.2 View的测量
要画一个图形,就要知道它的大小和位置。系统在绘制View前,通过onMeasure()方法测量View
MeasureSpec类可帮助测量View。它是一个32位的int值,高两位代表测量模式,低三十位为测量的大小,使用位运算为了提高效率。
测量模式如下:
- EXACTLY:当View的layout_width和layout_height为固定数值或者match_parent时
- AT_MOST:当View的layout_width和layout_height为wrap-content时
- UNSPECIFIED(未指定的):它不指定其大小测量模式,View想多大就多大,通常情况下在绘制自定义View是才会使用
View类默认的onMeasure()方法只支持EXACTLY模式,如果要自定义View支持wrap_content属性,就必须重写onMeasure()方法来制定wrap_content时的大小。
测量步骤:
在自定义View中重写onMeasure()方法,初步写法如下:
@Override
protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec){
super.omMeasure(widthMeasureSpec,heightMeasureSpec);
}
进入super.onMeasure(),发现系统最终调用的setMeasuredDimension(int measuredwidth,int measuredHeight)。所以重写onMeasure()的目的就是把宽高值赋给终极父类的setMeasureDimension()方法。正式写法如下:
@Override
protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec){
setMeasureDimension(measureWidth(widthMeasureSpec),measureHeight(heightMeasureSpec));
}
其中,measureWidth和measureHeight为我们自定义方法。
以MeasureWidth为例:
private int measureWidth(int widthMeasureSpec){
int result=0;
int specMode=MeasureSpec.getMode(widthMeasureSpec);
int specSize=MeasureSpec.getSize(widthmeasureSpec);
if(specMode==MeasureSpec.EXACTLY){
result=specSize;
}else{
result=200;//自定义默认值
if(specMode==MeasureSpec.AT_MOST){
result=Math.min(result,specSize);
}
}
return result;
}
当指定wrap_content时,View就获得一个默认大小200。
3.3 View的绘制
通常需要继承View并重写其onDraw(canvas)方法,在onDraw(canvas)的参数canvas(画布)上用paint绘制。
首先创建一个canvas
Canvas canvas=new Canvas(bitmap);
通过传入bitmap的方式创建canvas,传入的bitmap与canvas是紧密相连的,此过程称为装载画布。这个bitmap用来存储所有绘制在canvas上的像素信息。后面调用的所有的Canvas.drawXXX方法都发生在这个bitmap上。
可以再View类的onDraw()方法中绘制两个bitmap
canvas.drawBitmap(bitmap1,0,0,null);
canvas.drawBitmap(bitmap2,0,0,null);
对于bitmap2,将它装载在另一个对象中
Canvas canvas=new Canvas(bitmap2);
在其他地方使用Canvas类绘图方法在装载bitmap2的Canvas对象上进行绘图
mCanvas.drawXXX;
通过mCanvas将挥之效果作用在了bitmap2上,刷新View的时候,通过onDraw()方法就改变了bitmap2,。并不是直接绘制在onDraw()指定的canvas上,而是通过改变bitmap,让View重绘,显示改变之后的bitmap。
(PS:有点不明白,感觉作者的意思是,canvas可装载bitmap,通过drawXXX改变自己的bitmap,也可通过drawBitmap绘制别人的bitmap。drawXXX可在其他地方使用,drawBitmap必须在onDraw()使用来绘制。先挖个坑,以后彻底明白了,回来填坑)
3.4 ViewGroup的测量
当ViewGroup的measureMode为EXACTLY或AT_MOST时,按指定值绘制。
当ViewGroup的measureMode为UNSPECIFIED时,需要对子View进行遍历,一遍获得所有子View的大小,从而决定自己的大小。
ViewGroup遍历子View,调用子View的measure方法。子View的measure方法如下:
public final void measure(int widthmeasureSpec,int heightmeasureSpec){
...
onMeasure(widthmeasureSpec,heightmeasureSpec);//子View重写的测量方法
...
}
大小确定后,需要知道位置。这就是View的Layout过程。ViewGroup在执行Layout时,同样适用遍历调用子View的Layout方法,并指定其具体的显示位置。
自定义ViewGroup时,通常重写onLayout来控制字View的显示位置。如果需要支持wrap_content,同样要重写onMeasure。
3.5 ViewGroup的绘制
ViewGroup除非指定了背景色,会调用onDraw(),否则不需要绘制。但是,ViewGroup会使用dispatchDraw()方法来绘制其子View,其过程同样是遍历所有子View,并调用子View的o绘制方法完成绘制。
3.6 自定义View
在自定义View时,通常重写onDraw()方法来绘制View的显示内容。如果该View需要wrap_content属性,还要重写onMeasure()。通过自定义attrs属性,可以设置新的属性配置值。
在View中通常由以下比较重要的回调方法:
- onFinishInflated():从XML加载组件后回调
- onSizeChanged():组件大小改变时回调
- onMeasure():回调该方法来进行测量
- onLayout():回调该方法来确定显示的位置
- onTouchView():监听到触摸事件时的回调
通常由以下三种方式来实现在定义控件:
- 对现有控件进行拓展
- 通过组合实现新的控件
- 重写View来实现全新的控件
3.6.1 对现有控件进行拓展
例:给TextView多绘制几层背景
自定义View继承TextView,重写onDraw()
@Override
protected void onDraw(Canvas canvas){
//在TextView绘制文本前,可加入自己的逻辑
super.onDraw(canvas);//TextView的onDraw(),实现了绘制文字
//在TextView绘制文本后,可加入自己的逻辑
}
在自定义View的构造方法中完成画笔等的初始化操作
mPaint1=new Paint();
mPaint1.setColor(getResources().getColor(android.R.color.holo_blue_light));
mPaint1.setStyle(Paint.Style.FILL);
mPaint2=new Paint();
mPaint2.setColor(Color.YELLOW);
mPaint2.setStyle(Paint.Style.FILL);
重写onDraw(canvas)
@Override
protected void onDraw(canvas){
//在绘制文字前县绘制两个矩形
//绘制外层矩形
canvas.drawRect(0,0,getMeasureWidth,getMeasureHeiget,mPaint1);
//绘制内层矩形
canvas.drawRect(10,10,getMeasuredWidth-10,getMeasureHeight-10.mPaint2);
//用来保存Canvas的状态。save之后,可以调用Canvas的平移、放缩、旋转、错切、裁剪等操作。
canvas.save();
//绘制文字前平移10像素
canvas.tranlate(10,0);
//父类绘制文本
super.onDraw();
//用来恢复Canvas之前保存的状态。防止save后对Canvas执行的操作对后续的绘制有影响。
canvas.restore();
//当我们对画布进行旋转,缩放,平移等操作的时候其实我们是想对特定的元素进行操作,比如图片,一个
//矩形等,但是当你用canvas的方法来进行这些操作的时候,其实是对整个画布进行了操作,那么之后在画
//布上的元素都会受到影响,所以我们在操作之前调用canvas.save()来保存画布当前的状态,当操作之后
//取出之前保存过的状态,这样就不会对其他的元素进行影响
}
例:利用LinearGradient Shader和Matrix实现由动态文字闪动效果的TextView
可充分利用Paint对象的Shader渲染器。通过设置一个不断变化的LinearGradient,并使用带有该属性的Paint对象来绘制现实的文字。首先,在onSizeChanged方法中进行一些对象的初始化工作,并根据View的宽度设置一个LinearLayoutGradient渐变渲染器。
int mViewWidth=0;
Paint mPaint;
LinearGradient mLinearGradient;
Matrix mGradientMatrix;
@Override
protected void onSizeChanged(int w,int h,int oldw,int oldh){
super.onSizeChanged(w,h,oldw,oldh);
if(mViewWidth==0){
mViewWidth=getMeasureWidth();
if(mViewWidth>0){
mPaint=getPaint();
mLinnearGradient=new LinearGradient(0,
0,
mViewWidth,
0,
new int[]{Color.BLUE,0xffffffff,Color.BLUE},
null,
Shader.TileMode.CLAMP);
mPaint.setShader(mLinearGradient);
mGradientMatrix=new Matrix();
}
}
}
最关键的是getPaint获取Paint对象并设置原生TextView没有的LinearGradient属性。在onDraw中,通过矩阵方式不断平移渐变效果,从而在绘制文字时,产生动态的渐变效果。
@Override
protected void onDraw(Canvas canvas){
super.onDraw(canvas);
if(mGradientMatrix!=null){
mTranslate+=mViewWidth/5;
if(mTranslate>2*mViewWidth){
mTranslate-=mViewWidth;
}
mGradientMatrix.setTranslate(mTranslate,0);
mLinearGradient.setLocalMatrix(mGradientMatrix);
postInvalidateDelayed(100);
}
}
3.6.2 创建复合控件
这种方式需要集成一个合适的ViewGroup,再给它添加指定的控件。一般会给它指定一些可配置的属性。
例:TopBar
这种UI模板应该具有通用性和可定制性,需要给调用者以丰富的接口,让他们可以更改模板中的文字、颜色、行为等信息。
3.6.2.1 定义属性
在values下的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="rightBackgroundColor" format="reference|color"/>
<attr name="rightText" format="string"/>
</declare-styleable>
</resources>
通过declare-styleable声明使用自定义属性,通过name确定引用的名称。通过attr声明具体的自定义属性。format的reference属性代表引用属性,比如leftBackground可以设置为一张图片。
TopBar继承RelativeLayout,在构造方法中,使用如下代码获取自定义属性
TypedArray ta=context.obtainStyledAttributes(attr,R.styleable.TopBar);
TypedArray为系统提供的获取自定义属性集的自定义结构,获取自定义属性
//通过这个方法,将attrs.xml中定义的declare-styleable的所有属性的值存储到TypedArray中
TypedArray ta=context.obtaibstyledAttributes(attrs,R.styleable.TopBar);
//从TypedArray中取出对应的值来用于后续绘制
mLeftTextColor=ta.getColor(R.styleable.TopBar_leftTextColor,0);
mLeftBackground=ta.getDrawable(R.styleable.TopBar_lefBackground);
mLeftText=ta.getString(R.styleable.leftText);
mRightTextColor=ta.getColor(R.styleable.TopBar_rightTextColor,0);
mRightBackground=ta.getDrawable(R.styleable.TopBar_rightBackGround);
mRightText=ta.getText(R.styleable.TopBar_rightText);
mTitleTextSize=ta.getDimension(R.styleable.TopBar_titleSize,10);
mTitleTextColor=ta.getColor(R.styleable.TopBar_titleTextColor,0);
mTitle=ta.getString(R.styleable.TopBar_title);
//获取完TypeArray的值后,一般需要调用recycle方法完成资源的回收
ta.recycle();
3.6.2.2 组合控件
通过动态添加addView()的方式,并设置前面获取到的属性值
mLeftButton=new Button(context);
mRightButton=new Button(context);
mTitleView=new TextView(context);
//为创建的组件元素赋值
//值就来源于引用xml文件中对应该出的值
mLeftButton.setTextColor(mLeftTextColor);
mLeftButton.setBackGround(mLeftBackground);
mLeftButton.setText(mLeftText);
mRightButton.setTextColor(mLeftTextColor);
mRightButton.setBackGround(mRightBackground);
mRightButton.setText(mRightText);
mTitleView.setText(mTitle);
mTitleView.setTextColor(mTitleTextColor);
mTitleView.setTextSize(mTitleTextSize);
mTitleView.setGravity(Gravity.CENTER);
//为组件设置相应的布局元素
mLeftParams=new LayoutParams(LayoutParams.WRAP_CONTENT,LayoutParams.MATCH_PARENT);
mLwftParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT,TRUE);
//添加到ViewGroup
addView(mLeftButton,mLeftParams);
mRightParams=new LayoutParams(LayoutParams.WRAP_CONTENT,LayoutParams.MATCH_PARENT);
mLeftParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT,TRUE);
addView(mRightButton,mRightParams);
mTitleParams=new LayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.WRAP_CONTENT);
mTitleParams.addRule(RelativeLayout.CENTER_IN_PARENT,TRUE);
addView(mTitleView,mTitleParams);
定义接口
//报漏一个方法给调用者注册接口回调
//通过接口来获得回调者对接口方法的实现
public void setOnTopBarClickListener(topBarClickListener mListener){
this.mListener=mListener;
}
//接口对象,实现回调机制,在回调方法中
//通过映射的接口对象调用接口中的方法
//而不用去考虑如何实现,具体的实现由调用者去创建
public interface topBarClickListener{
//左侧按钮点击事件
void leftClick();
//右侧按钮点击事件
void rightClick();
}
//按钮的点击事件,只需要调用接口的方法,会掉的时候,会有具体的实现
mLeftButton.setOnClickListener(new onClickListener(){
@Override
public void onClick(View v){
mListener.liftClick();
}
});
mRightButton.setOnClickListener(new onClickListener(){
@Override
public void onClick(View v){
mListener.rightClick();
}
});
除了通过接口会掉的方式动态的控制模板,还可以使用公共方法动态的修改UI
//id区分按钮,flag区分显示
public void setButtonVisible(int id,boolean flag){
if(flag){
if(id==0){
mLeftButton.setVisible(View.VISIBLE);
}else{
mRightButton.setVisible(View.VISIBLE);
}
}else{
if(id==0){
mLeftButton.setVisible(View.INVISIBLE);
}else{
mRightButton.setVisible(View.INVISIBLE);
}
}
}
3.6.2.3 引用UI模板
在引用前,需要制定第三方控件的命名空间。
xmlns:android="http://schemas.android.com/apk/res/android"
这行代码指定引用的命名空间为xmlns,即xml namespace。这里制定了命名空间为"android",接下来使用系统属性的时候,才可以使用“android”。第三方控件使用如下代码来引入名字空间,这里我们把第三方控件的命名空间指定为custom。
xmlns:custom="http://schemas.android.com/apk/res-auto"
实例代码如下:
<com.inooc.systemwidget.TopBar
android:id="@+id/topBar"
android:layout_width="match_parent"
android:layout+height="40dp"
custom:leftBackGround="@drawable/blue_button"
custom:leftText="Back"
custom:leftTextColor="#FFFFFF"
custom:leftBackGround="@drawable/blue_button"
custom:leftText="More"
custom:leftTextColor="#FFFFFF"
custom:title="自定义标题"
custom:titleTextColor="#123456"
custom:titleTextSize="10sp"/>
使用自定义控件时需要指定完整的包名。
如果将其写成一个独立的布局文件以供调用
<com.inooc.systemwidget.TopBar
xmlns:android="https://schemas.android.com/apk/res/android"
xmlns:custom="http://schemas.android.com/apk/res-auto"
android:id="@+id/topBar"
android:layout_width="match_parent"
android:layout+height="40dp"
custom:leftBackGround="@drawable/blue_button"
custom:leftText="Back"
custom:leftTextColor="#FFFFFF"
custom:leftBackGround="@drawable/blue_button"
custom:leftText="More"
custom:leftTextColor="#FFFFFF"
custom:title="自定义标题"
custom:titleTextColor="#123456"
custom:titleTextSize="10sp">
<com.inooc.systemwidget.TopBar/>
可在其他布局文件中,通过<include>来调用
<include layout="@layout/topbar"/>
3.6.3 重写View实现全新的控件
3.6.3.1 弧线展示图
这个自定义View分为中间的圆形,显示的文字,已经最外的环形。只要在onDraw中一个个去绘制就可以了。
初始化一些值
//圆环半径,length为控件宽度
mCircleXY=length/2;
//圆环内外径之差
mRadius=(float)(length*0.5/2);
//圆环外接矩形
mArcRectF=new RectF((float)length*0.1,
(float)length*0.1,
(float)length*0.9,
(float)length*0.9);
在onDraw方法中调用
//绘制圆,参数圆心XY坐标,半径和画笔
canvas.drawCircle(mCircleXY,mCircleXY,nRadius,mCirclePaint);
//绘制弧线,参数外接矩形,起始角度,扫过角度,是否经过圆心(经过为扇形),画笔
canvas.drawArc(mArcRectF,270,mSweepAngle,false,mArcPaint);
//绘制文字
canvas.drawText(mShowText,0,mShowText.Length(),mCircleXY,mCircleXY+(mShowTextSize/4),mPaintText);
3.6.3.2 音频条形图
核心是绘制一个个矩形
@Override
protected void onDraw(Canvas canvas){
super.onDraw(canvas);
for(int i=0;i<nRectCount;i++){
canvas.drawRect(
(float)(mWidth*0.4/2+mRectWidth*i+offset),
currentHeight,
(float)(mWidth*0.4/2+mRectWidth*(i+1)),
mRectHeight,
mPaint
);
}
}
高度动态随机变化
mRandom=Math.Random();
float currentHeight=(float)(mRectHeight*mRandom);
在onDraw()中调用invalidate()方法通知View重绘。使用postInvalidateDelayed(300)进行延迟,避免刷新过频。
给Paint增加一个LinearGradient的渐变效果
@Override
protected void onSizeChanged(int w,int h,int oldw,int oldh){
super.onSizeChanged(w,h,oldw,oldh);
mWidth=getWidth();
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);
}
3.7 自定义ViewGroup
ViewGroup存在的意义就是对子View进行管理。通常需要重写onMeasure,onLayout,onTouchEvent。
例:一个有惯性的ScrollView,滑动超过一定距离会惯性画出下一个View,小于一定距离会弹回原位置
重写onMeasure
@Override
protected void onMeasure(int WidthMeasureSpec,int HeightMeasureSpec){
super.onMeasure(WidthMeasureSpec,HeightMasureSpec);
int count=getChildCount();
for(int i=0;i<count;i++){
View childView=getChildAt(i);
measureChild(WidthMeasureSpec,HeightMeasureSpec);
}
}
设定View大小为完整的一屏以达到明显滑动效果,确定ViewGroup高度
//设置ViewGroup的高度
MarginLayoutParams mpl=(MarginLayoutParams)getLayoutParams();
mpl.geight=mScreenHeight*childCount;
setLayoutParams(mpl);
重写onLayout
@Override
protected void onLayout(boolean changed,int l,int t,int r,int b){
int childCount=getChildCount();
//设置ViewGroup的高度
MarginLayoutParams mlp=(MarginLayoutParams)getLayoutParams();
mpl.height=mScreenHeight*childCount;
setLayoutParams(mlp);
for(int i=0;i<childCount;i++){
View childView=getChildAt(i);
childView.layout(l,i*mScreenLayout,r,(i+1)*mScreenLayout);
}
}
计算滑动距离dy
case MotionEvent.ACTION_DOWN:
mLastY=y;
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;
//getScrollY()的详解参考
//https://blog.csdn.net/linmiansheng/article/details/17767795
//讲的很简单透彻
//scrollto scrollby 参考
//https://www.jianshu.com/p/2e60448ac44c
判断手指离开后的惯性效果
case MotionEvent.ACTION_DOWN:
//记录触摸起点
mStart=getScrollY();
case MotionEvent.ACTION_UP:
//记录触摸终点
mEnd=getScrollY();
int dScrollY=mEnd-mStart;
if(dScrollY>0){
if(dScroll<mScreenHeight/3){
mScroller.startScroll(0,getScrollY,0,-dScrollY);
}else{
mScroller.startScroll(0,getScrollY,0,mScreenHeight-dScrollY);
}
}else{
if(-dScroll<mScreenHeight/3){
mScroller.startScroll(0,getScrollY,0,-dScrollY);
}else{
mScroller.startScroll(0,getScrollY,0,-mScreenHeight-dScrollY);
}
}
break;
整个onTouchEvent的代码如下
加上computeScroll代码
@Override
public void computeScroll(){
super.computeScroll();
if(mScroller.computeScrollOffset()){
scrollTo(0,mScroller.getCurrentY());
postInValitate();
}
}
3.8 事件拦截机制分析
ViewGroup重写三个方法
//事件分发方法
@Override
public boolean dispatchTouchEvent(MotionEvent ev){
return super.dispatchTouchEvent(ev);
}
//事件拦截方法
@Override
public boolean onInterceptTouchEvent(MotionEvent ev){
return super.onInterceptYouchEvent(ev);
}
//触摸事件
@Override
public boolean onTouchEvent(MotionEvent ev){
return super.onTouchEvent(ev);
}
View重写两个方法
@Override
public boolean dispatchTouchEvent(MotionEvent ev){
super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent ev){
super.onMotionEvent(ev);
}
事件流程:以ViewGroupA嵌套ViewGroupB嵌套MyView为例,不做改动下log为
分发拦截事件向下传递,触摸处理向上传递。
一般不需要改dispatchTouchEvent
onInterceptTouchEvent:事件传递的返回值:True,拦截,False,不拦截,继续流程
onTouchEvent:事件处理:True,处理了,不用向上传递,False,向上传递