Quantcast
Channel: CSDN博客移动开发推荐文章
Viewing all articles
Browse latest Browse all 5930

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

$
0
0

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;
    }


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
作者:AndrExpert 发表于2017/8/23 21:31:36 原文链接
阅读:47 评论:0 查看评论

Viewing all articles
Browse latest Browse all 5930

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>