Android View工作原理详解及源码分析(1)

来源:互联网 发布:安卓最快的数据库 编辑:程序博客网 时间:2024/06/05 20:09

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),      // 获取widthmeasureHeight(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;    }


3. CircleProgressView使用方法
(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
原创粉丝点击