Android View工作原理详解及源码分析(1)
转载请声明出处:http://blog.csdn.net/andrexpert/article/details/77511996
在Android开发中,当我们需要显示用户交互界面时,通常的做法是创建一个继承Activity的类并重写它的onCreate()方法,再在该方法中调用setContentView()方法将布局界面显示出来。那么问题来了,setContentView方法具体做了些什么呢?基于此,本文将详细讲解Activity窗口机制原理、View的绘制流程及其源码分析,然后实现一个CircleProgressView控件以剖析自定义view的基本思路。
一、Activity窗口机制原理1. UI界面架构
Activity是Android四大组件之一,是与用户交互的窗口。Activity类负责创建一个窗口,即Window对象,每个Activity都包含一个Windows对象,然后在该窗口中使用setContentView方法来放置需要显示的UI。Window类是一个抽象类,它封装了与顶层可见窗口和行为策略相关方法接口,其具体的实现交给PhoneWindow类来完成。PhoneWindow类是Window具体实现类,它内部包含了一个DecorView对象并且将该对象设置为整个应用窗口的根View。DecorView是PhoneWindow的内部类,继承于FrameLayout,它作为窗口界面的顶层视图,封装了一些窗口操作的通用方法,并将要显示的具体内容呈现在PhoneWindow上。
2. View树结构
在Android中,所有的视图控件都是View或ViewGroup的子类,ViewGroup是View的子类。每个ViewGroup作为父控件,可以包含多个View,但是View不能包含View或ViewGroup。在布局中,View和ViewGroup之间的关系可以用数据结构中的树来描述,即ViewGroup通常作为父结点存在,而View作为叶子节点存在,它们以树的形式构成最终的布局界面,并由上层控件负责下层子控件的测量和绘制,然后传递交互事件。在每棵树的顶部,都有一个ViewParent对象,这是整棵树的控制核心,所有的交互管理事件都由它来统一调度和分配,从而可以对整个视图进行整体控制。
二、View绘制流程分析View的绘制过程主要经历三个阶段,即测量(Measure)、布局(Layout)、绘制(draw),其中,Measure的作用是测量要绘制View的大小;Layout的作用是明确要绘制View的具体位置;draw的作用就是绘制View。
1. measure流程
由View的源码可知,View的测量过程通过onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法实现的,并在该方法中调用setMeasuredDimension()方法来最终确定View的具体大小。View的测量包括两部分内容,即测量模式和具体大小的确定,不同的测量模式,其数值的设置方式是不同的,这可以借助MeasureSpec类从widthMeasureSpec和heightMeasureSpec来提取测量模式和具体数值。widthMeasureSpec、heightMeasureSpec本身是被MeasureSpec类处理过的,是一个32位的int值,它的高2位为测量模式,低30位为测量的大小。
int specMode = MeasureSpec.getMode(widthMeasureSpec); int specSize = MeasureSpec.getSize(widthMeasureSpec);View测量模式:
(1) EXACTLY,精确模式
当控件的layout_width属性或layout_height属性指定为具体值时,系统使用的就是精确模式。在自定义View时,如果不重写onMeasure方法,系统默认使用的就是EXACTLY模式,如果我们将layout_width属性或layout_height属性设置值为wrap_content,那么系统就不知道到底该绘制多大。
(2) AT_MOST,最大模式
当View的layout_width属性或layout_height属性指定为wrap_content时,对于View来说,View的大小随着其内容的变化而变化,对于ViewGroup来说,ViewGroup的大小随着其子控件变化而变化。
(3) UNSPECIFIED
这个模式通常只有系统才会使用,可以无需理会。
2. layout流程
View绘制的layout过程通过调用onLayout(boolean changed,int l, int t, int r, int b)方法实现,调用该方法需要传入放置View的矩形空间左上角left、top和右下角right、bottom,它们均是相对父控件而言的。需要注意的是,对于View来说,onLayout方法没有做任何事情,所以可以不用理会;对于ViewGroup来说,onLayout())是一个抽象方法,它将由继承于ViewGroup的子类实现,用来实现获取所有子View的实例,然后调用子View的layout(int l, int t, int r, int b)方法决定子View在父布局中的位置,其中l、r、r、b参数均是相对父控件而言的。自定义ViewGroup举例:
public class CustomLayout extends ViewGroup { // 子View的垂直间隔 private final static int padding = 20; public CustomLayout (Context context, AttributeSet attrs) { super(context, attrs); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { for (int i = 0, size = getChildCount(); i < size; i++) { // 获取第i个子View实例 View view = getChildAt(i); // 放置子View,宽高都是50 // left=0 ; top=0 ; right=50 ; bottom=50 view.layout(l, t, l + 50, t + 50); t += 50+ padding; } } }3. draw流程
View绘制经历测量、布局过程后,接下来就是在指定的位置绘制指定大小的图形了,这个过程由onDraw(Canvas canvas)方法实现。对于ViewGroup来说,ViewGroup只是一个View收纳容器,根本不需要绘制具体的View,因此它没有onDraw()方法;对于View来说,onDraw()方法是个空方法,View的子类需要重写该方法才能完成最终的图形绘制。
三、实战:自定义圆形进度条控件-CircleProgressView
1. Android 坐标系与视图坐标系
(1) Android坐标系
所谓Android坐标系,是指在Android中将手机屏幕最左上角的顶点作为Android坐标系的原点(0,0),从这个点向右表示x轴的正方向,从这个点的向下表示y轴的正方向,如下图所示。常用方法有getRawX()、getRawY(),作用如下:
getRawX():获取点击事件距离整个屏幕左边的距离;
getRawY():获取点击事件距离整个屏幕顶边的距离;
注:getLocationOnScreen(int[] location)获取该视图左上角在Android坐标系中的坐标
(2) 视图坐标系
与Android坐标系不同的是,视图坐标系不再是描述视图在整个屏幕中的位置,而是描述子视图在父视图中的位置,它以父视图左上角为坐标原点,从这个点向右为x轴的正方向,从这个点向下为y轴的正方向。常用的方法有getX()、getY(),作用如下:
getX():获取点击事件距离控件左边的距离;
getY():获取点击事件距离控件顶边的距离;
(3) View坐标getLeft、getTop、getRight、getBottom
getLeft()、getTop()、getRight()、getBottom()均为View的坐标API,分别用于获取该View的左侧(left)位置、顶部(top)位置、右侧(right)位置、底部(bottom)位置,它们是针对其父视图的相对位置,作用如下:
getLeft():获取View的左边到其父视图左边的距离;
getRight():获取View的右边到其父视图左边的距离
getTop():获取View的顶边到其父视图顶边的距离;
getBottom():获取View的底边到其父视图顶边的距离;
Top RelativeLayout中:
mTop.getLeft()=0; mTop.getTop()=0;
mTop.getRight()=720; mTop.getBottom()=1120
Father RelativeLayout中:
mFather.getLeft()=60; mFather.getTop()=260;
mFather.getRight()=660; mFather.getBottom()=860
Child View中
mChild.getLeft()=200; mChild.getTop()=200;
mChild.getRight()=400; mChild.getBottom()=400
2. CircleProgressView代码讲解
(1) 创建values/attr.xml资源文件。
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="CircleProgressView"> <!--外部圆形颜色--> <attr name="outsideCircleBgColor" format="color|reference"/> <!--弧形进度条颜色--> <attr name="progressArcBgColor" format="color|reference"/> <!--内部圆形颜色--> <attr name="insideCircleBgColor" format="color|reference"/> <attr name="insideCircleTouchedBgColor" format="color|reference"/> <!--内部正方形颜色--> <attr name="insideRectangleBgColor" format="color|reference"/> <!--文本提示字体颜色--> <attr name="tipTextColor" format="color|reference"/> <!--文本提示字体大小--> <attr name="tipTextSize" format="dimension"/> </declare-styleable> </resources>(2) 重写构造方法。CircleProgressView继承于View,CircleProgressView控件的实例化通过需要重写两个构造方法,即CircleProgressView(Context context)和CircleProgressView(Context context, AttributeSet attrs),前者用于在Java代码中实例化一个CircleProgressView,后者用在xml布局文件中,它提供了一个AttributeSet 类型参数,允许我们通过Context的obtainStyledAttributes方法获得自定义属性的集合TypedArray,然后再调用其相关的方法获取对应的自定义属性值。代码如下:
public CircleProgressView(Context context) { super(context); } public CircleProgressView(Context context, AttributeSet attrs) { super(context, attrs); // 获取自定义属性 TypedArray ta = context.obtainStyledAttributes(attrs,R.styleable.CircleProgressView); outsideCircleBgColor = ta.getColor(R.styleable.CircleProgressView_outsideCircleBgColor,getResources().getColor(R.color.colorWhite)); progressArcBgColor = ta.getColor(R.styleable.CircleProgressView_progressArcBgColor,getResources().getColor(R.color.colorGray)); insideCircleBgColor = ta.getColor(R.styleable.CircleProgressView_insideCircleBgColor,getResources().getColor(R.color.colorRed)); insideCircleTouchedBgColor = ta.getColor(R.styleable.CircleProgressView_insideCircleTouchedBgColor,getResources().getColor(R.color.colorDeepRed)); insideRectangleBgColor = ta.getColor(R.styleable.CircleProgressView_insideRectangleBgColor,getResources().getColor(R.color.colorRed)); tipTextColor = ta.getColor(R.styleable.CircleProgressView_tipTextColor,getResources().getColor(R.color.colorWhite)); tipTextSize = ta.getDimension(R.styleable.CircleProgressView_tipTextSize,34); // 回收TypedArray资源,防止内存溢出 ta.recycle(); // 完成相关初始化操作 mPaint = new Paint(); }(2) 重写onMeasure方法。View的测量主要有三种模式:EXACTLY、AT_MOST和UNSPECIFIED。在自定义View中,重写onMeasure方法的目的是使自定义的View支持wrap_content属性(AT_MOST模式),因为其默认只支持精确值(EXACTLY模式)。通过查看onMeasure方法源码可知,该方法主要通过setMeasuredDimension方法来测量View的大小,我们在onMeasure方法中调用这个方法,然后解析出widthMeasureSpec、heightMeasureSpec两个参数所承载的测量模式和大小。当不为EXACTLY模式时,我们预设宽高均为200,再与测量得到的值进行对比,取最小值。
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); // 调用setMeasuredDimension // 测量View大小 setMeasuredDimension(measureWidth(widthMeasureSpec), // 获取width measureHeight(heightMeasureSpec));// 获取height } private int measureHeight(int widthMeasureSpec) { int width = 0; int specMode = MeasureSpec.getMode(widthMeasureSpec); int specSize = MeasureSpec.getSize(widthMeasureSpec); if(specMode == MeasureSpec.EXACTLY){ // 精度模式 width = specSize; }else { // 默认大小 width = 200; // wrap_content if(specMode == MeasureSpec.AT_MOST){ width = Math.min(width,specSize); } } return width; } private int measureWidth(int heightMeasureSpec) { int height = 0; int specMode = MeasureSpec.getMode(heightMeasureSpec); int specSize = MeasureSpec.getSize(heightMeasureSpec); if(specMode == MeasureSpec.EXACTLY){ // 精度模式 height = specSize; }else { // 默认大小 height = 200; // wrap_content if(specMode == MeasureSpec.AT_MOST){ height = Math.min(height,specSize); } } return height; }(3) 重写onSizeChanged方法。当CircleProgressView大小变化时,回调该方法,我们可以在这个方法中获取CircleProgressView的具体宽高和初始化相关绘图参数,因为在绘制图形的时候会用到。
@Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); // 当View大小变化时,获取其宽高 mWidth = getWidth(); mHeight = getHeight(); circleX = mWidth/2; circleY = mWidth/2; radius = mWidth / 2; // 设置默认状态 state = STATE_UNDONE; }(4) 重写onDraw方法。CircleProgressView经历测量、布局后,接下来就是绘制具体图形了,通过回调onDraw方法实现,绘图具体图形使用Canvas相关方法。CircleProgressView有三种状态,初始状态(STATE_UNDONE )、进行状态(STATE_DOING )、完成状态(STATE_DONE ),它们对应不同的图形效果。
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); drawOutSideCircle(canvas); if(STATE_DONE == state){ drawInternelRectangle(canvas); }else{ // 点击效果 if(isTouched){ drawInternelCircle(canvas,insideCircleTouchedBgColor); }else{ drawInternelCircle(canvas,insideCircleBgColor); } // 绘制弧形进度条 if(STATE_DOING == state){ drawProgressArc(canvas); } } } // 绘制背景圆形,调用Canvas的drawCircle方法实现 private void drawOutSideCircle(Canvas canvas){ mPaint.setStrokeWidth(2); mPaint.setColor(outsideCircleBgColor); mPaint.setStyle(Paint.Style.FILL); mPaint.setAntiAlias(true); canvas.drawColor(Color.TRANSPARENT); canvas.drawCircle(circleX,circleY,radius,mPaint); } // 绘制内部圆形 private void drawInternelCircle(Canvas canvas,int colorType){ mPaint.setStrokeWidth(2); mPaint.setStyle(Paint.Style.FILL); mPaint.setColor(colorType); mPaint.setAntiAlias(true); canvas.drawCircle(circleX,circleY,(float) (radius-radius*0.15),mPaint); } // 绘制内部矩形,调用Canvas的drawRect方法,当状态为STAE_DONE private void drawInternelRectangle(Canvas canvas){ mPaint.setStrokeWidth(2); mPaint.setColor(insideRectangleBgColor); mPaint.setAntiAlias(true); mPaint.setStyle(Paint.Style.FILL); canvas.drawRect((float) (mWidth*0.3),(float) (mWidth*0.3),(float)( mWidth-mWidth*0.3) ,(float) (mWidth-mWidth*0.3),mPaint); } // 绘制弧形,调用drawArc方法,当状态为STATE_DOING // 有两种风格:普通进度条,具体数据进度条 private void drawProgressArc(Canvas canvas){ mPaint.setStrokeWidth((int)(radius * 0.15)); mPaint.setStyle(Paint.Style.STROKE); mPaint.setAntiAlias(true); mPaint.setColor(progressArcBgColor); if(progress >= 0){ if(totalSize == 0) return; canvas.drawArc(new RectF((float) (radius*0.08),(float) (radius*0.08),2*radius-(float) (radius*0.08),2*radius-(float) (radius*0.08)) ,180,(int)(Float.parseFloat(new DecimalFormat("0.00") .format((float)progress/totalSize)) * 360),false,mPaint); if(isShowTextTip){ drawTextTip(canvas,(int)(Float.parseFloat(new DecimalFormat("0.00") .format((float)progress/totalSize)) * 100)+" %"); } }else if(progress == NONE){ if(isOddNumber){ canvas.drawArc(new RectF((float) (radius*0.08),(float) (radius*0.08),2*radius-(float) (radius*0.08),2*radius-(float) (radius*0.08)) ,180,mSweepAngle,false,mPaint); mSweepAngle ++; if(mSweepAngle >= 360) isOddNumber = false; }else{ canvas.drawArc(new RectF((float) (radius*0.08),(float) (radius*0.08),2*radius-(float) (radius*0.08),2*radius-(float) (radius*0.08)) ,180,-mSweepAngle,false,mPaint); mSweepAngle--; if(mSweepAngle == 0) isOddNumber = true; } this.postInvalidateDelayed(5); } } // 绘制文本,调用Canvas的drawText方法,当状态为STATE_DOING private void drawTextTip(Canvas canvas,String tipText){ mPaint.setStrokeWidth(2); mPaint.setStyle(Paint.Style.FILL); mPaint.setAntiAlias(true); mPaint.setTextSize(tipTextSize); mPaint.setColor(tipTextColor); //Paint.Align.CENTER , x表示字体中心位置; // Paint.Align.LEFT ,x表示文本左边位置; mPaint.setTextAlign(Paint.Align.CENTER); float xCenter = getMeasuredHeight()/2; float yBaseLine = (getMeasuredHeight() - mPaint.getFontMetrics().bottom + mPaint.getFontMetrics().top)/2 -mPaint.getFontMetrics().top; canvas.drawText(tipText,xCenter,yBaseLine,mPaint); }(5) 重写onTouchEvent方法。当用户触摸CircleProgressView时回调该方法,MotionEvent类封装了各种触摸事件,比如down、move、up等,这里我们通过对 MotionEvent.ACTION_DOWN和MotionEvent.ACTION_UP事件的监听,来绘制CircleProgressView事件点击效果和响应点击事件,并且事件响应处理通过接口来实现,即CircleProgressView只是声明,接口方法的具体实现由调用者实现。
@Override public boolean onTouchEvent(MotionEvent event) { if(listener == null) return super.onTouchEvent(event); if(event.getAction() == MotionEvent.ACTION_DOWN){ isTouched = true; }else if(event.getAction() == MotionEvent.ACTION_UP){ isTouched = false; // 松开手时,处理触摸事件 listener.onViewClick(); } // 重新绘制View,即回调onDraw()方法 this.invalidate(); return true; } // 事件回调接口 public interface OnViewClickListener{ void onViewClick(); } // 注册事件监听回调接口 public void setOnViewClickListener(OnViewClickListener listener){ this.listener = listener; }
(1) 在工程build.gradle中添加
allprojects { repositories { maven { url 'https://jitpack.io' } } }(2) 在module的gradle中添加
dependencies { compile 'com.github.jiangdongguo:CircleProgressView:v1.0.2' }(3) Java代码
没有具体数值的进度条
// 设置状态为连接中,此外, // CircleProgressView.STAE_UNDONE为失败恢复到默认 // CircleProgressView.STAE_DONE为成功执行完毕 mCircleView.setConnectState(CircleProgressView.STAE_DOING); // 设置风格为没有具体数值进度条 mCircleView.setProgressVaule(CircleProgressView.NONE);有具体数据的进度条
// 状态为进行中 mCircleView.setConnectState(CircleProgressView.STAE_DOING); // 进度条最大值,可设置其他具体值 mCircleView.setTotalSize(100); // 进度条当前值,可设置其他具体值 mCircleView.setProgressVaule(10); // 中间显示进度百分比文本 mCircleView.setShowTextTipFlag(true); // 状态为执行完毕 mCircleView.setConnectState(CircleProgressView.STAE_DONE); // 添加点击事件监听,点击动画 mProgressView1.setOnViewClickListener(new CircleProgressView.OnViewClickListener() { @Override public void onViewClick() { mProgressView1.setConnectState(CircleProgressView.STAE_DOING); mProgressView1.setTotalSize(100); mProgressView1.setShowTextTipFlag(true); mProgressView1.setProgressVaule(i); i++; } });(4) XML文件配置
<!--使用默认配置--> <com.jiangdg.circleprogressview.CircleProgressView android:layout_width="wrap_content" android:layout_height="wrap_content"/> <!--自定义配置--> <com.jiangdg.circleprogressview.CircleProgressView android:layout_width="wrap_content" android:layout_height="wrap_content" custom:outsideCircleBgColor="@color/white_color" // 外部圆形颜色 custom:insideRectangleBgColor="@color/red_deep_color"// 内部矩形颜色 custom:insideCircleBgColor="@color/red_deep_color" // 内部圆形颜色 custom:progressArcBgColor="@color/black_color" // 进度条颜色 custom:tipTextColor="@color/white_color" // 进度百分比字体颜色 custom:tipTextSize="14sp"/> // 进度百分比字体大小Github地址:https://github.com/jiangdongguo/CircleProgressView