Android 控件架构与自定义控件详解
架构:
- PhoneWindow 将一个 DecorView 设置为整个应用窗口的根 View,这里面所有 View 的监听事件,都通过 WindowManagerService 来接收。DecorView 分为 TitleView 和 ContentView,ContentView 是一个 ID 为 content 的 FrameLayout
- 在 onCreate()方法中调用setContentView()方法后,ActivityManagerService 会回调onResume()方法,此时系统才会把整个 DecorView 添加到 PhoneWindow 中,并让其显示出来,从而完成最终的界面绘制。
View 的测量:
测量 View 的类:MeasureSpec 类,它是一个32位的 int 值,高两位为测量模式,低30位为测量大小,使用位运算提高并优化效率。
重写 onMeasure() 后,最终要做的是把测量后的宽高值作为参数设置给 setMeasureDimension() 方法。
[代码]java代码:
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 | @OverrideprotectedvoidonMeasure(intwidthMeasureSpec, intheightMeasureSpec){    setMeasureDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec));}//可作为模板代码!privateintmeasureWidth(intmeasureSpec){    intresult = 0;    intspecMode = MeasureSpec.getMode(measureSpec);    intspecSize = MeasureSpec.getSize(measureSpec);    if(specMode == MeasureSpec.EXACTLY){//精确值模式,指定具体数值        result = specSize;    }else{        result = 200;//先设置一个默认大小        //最大值模式,layout_width 或 layout_height 为 wrap_content 时,控件大小随控件的内容变化而变化,此时控件尺寸只要不超过父控件允许的最大尺寸即可。        if(specMode == MeasureSpec.AT_MOST){            result = Math.min(result, specSize);//取出我们指定的大小和 specSize 中最小的一个来作为最后的测量值        }        //MeasureSpec.UNSPECIFIED 不指定其大小,View 想多大就多大    }    returnresult;} | 
即,如果不重写 onMeasure() 方法,系统则会不知道该默认多大尺寸,就会默认填充整个父布局,所以,重写 onMeasure() 方法的目的,就是为了能够给 View 一个 wrap_content 属性下的默认大小。
View 的绘制
onDraw() 中的参数,就是 Canvas 对象,使用该对象进行绘图,而在其他地方,则需要 new 出该对象:
[代码]java代码:
| 1 | Canvas canvas = newCanvas(bitmap); | 
传进去的 bitmap 是与这个 bitmap 创建的 Canvas 画布紧密联系的,这个过程称为装载画布。该 bitmap 用来存储所有绘制在 Canvas 上的像素信息。所有的 Canvas.drawXXX 方法都发生在这个 bitmap 上。
[代码]java代码:
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 | @OverrideprotectedvoidonDraw(Canvas canvas){    //...    //在 onDraw 方法中绘制两个 bitmap    canvas.drawBitmap(bitmap1, 0, 0, null);    canvas.drawBitmap(bitmap2, 0, 0, null);    //...}privatevoidotherMethod(){    //将 bitmap2 装载到另一个 Canvas 对象中    Canvas mCanvas = newCanvas(bitmap2);    //其他地方使用 Canvas 对象的绘图方法在装载 bitmap2 的 Canvas 对象上进行绘图    mCanvas.drawXXX} | 
通过 mCanvas 将绘制效果作用在了 bitmap2 上,再刷新 View 的时候,就会发现通过 onDraw()方法画出来的 bitmap2 已经改变,因为 bitmap2 承载了在 mCanvas 上所进行的绘图操作。我们没有将图形直接绘制在 onDraw() 方法制定的那块画布上,而是通过改变 bitmap,让 View 重绘,从而显示改变之后的 bitmap。
ViewGroup 的测量
当 ViewGroup 的大小为 wrap_content 时,ViewGroup 需要对子 View 进行遍历,以便获得所有子 View 大小从而决定自己的大小,即调用子 View 的 Measure 方法来获得每一个子 View 的测量结果。
子 View 测量完毕后,ViewGroup 执行 Layout 过程时,同样是遍历调用子 View 的 Layout 方法,并指定其具体显示的位置,从而来决定其布局位置。
自定义 View
[代码]java代码:
| 1 2 3 4 5 6 | @OverrideprotectedvoidonDraw(Canvas canvas){    //在回调父类方法前,对 TextView 来说是在绘制文本内容之前,实现逻辑    super.onDraw(canvas);    //之后,绘制文本之后} | 
在 attrs.xml 中通过使用<declar-styleable>标签声明使用了自定义属性,使用如下代码获得在布局文件中自定义的那些属性
[代码]java代码:
| 1 2 3 4 | TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.TopBar);mLeftColor = ta.getColor(R.styleable.TopBar_leftTextColor, 0);//完成资源回收,避免重新创建的时候的错误ta.recycle(); | 
自定义 ViewGroup
重写 onMeasure() 来对子 View 进行测量,重写 onLayout() 确定子 View 位置,重写onTouchEvent() 增加响应事件。
实例需求:自定义 ViewGroup 实现类似 ScrollView 上下滑动,同时增加粘性效果。即,当一个子 View 向上滑动大于一定距离后,松开将自动上滑,显示下一个子 View,否则回到原始位置。
步骤一:先实现类似 ScrollView 功能:
[代码]java代码:
| 01 02 03 04 05 06 07 08 09 10 | @overrideprotectedvoidonMeasure(intwidthMeasureSpec, intheightMeasureSpec){    super.onMeasure(widthMeasureSpec, heightMeasureSpec);    intcount = getChildCount();    //遍历通知子 View 对其自身进行测量    for(inti = 0;i < count; ++i){        View childView = getChildAt(i);        measureChild(childView, widthMeasureSpec, heightMeasureSpec);    }} | 
步骤二:再对子 View 进行放置位置设定,让每个子 View 都显示完整的一屏。所以,本例中ViewGroup 的高度就是子 View 的个数乘以屏幕高度,然后遍历设定每个子 View 放置的位置
[代码]java代码:
| 1 2 3 4 5 6 7 8 9 | @OverrideprotectedvoidonLayout(booleanchanged, intl, intt, intr, intb){    intchildCount = getChildCount();    //设置 ViewGroup 高度    MarginLayoutParams mlp = (MarginLayoutParams)getLayoutParams();    mlp.height = mScreenHeight.childCount;    setLayoutParams(mlp);    //修改子 View 的 top 和 bottom 属性,使它们依次排列    for(inti=0; i<childcount; i++){=""view=""child="getChildAt(t);"if(child.getvisibility()=""!="View.GONE){"child.layout(l,=""i*mscreenheight,=""r,=""(i+1)*mscreenheight);=""}=""}<=""pre=""></childcount;> | 
步骤三:
[代码]java代码:
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | @OverridepublicbooleanonTouchEvent(MotionEvent event){    inty = (int)event.getY();    switch(event.getAction()){        caseMotionEvent.ACTION_DOWN:            mLastY = y;            mStart = getScrollY();//记录按下位置            break;        caseMotionEvent.ACTION_MOVE:            if(!mScroller.isFinished()){                mScroller.abortAnimation();            }            intdy = mLastY - y;            if(getScrollY() < 0){                dy = 0;            }            if(getScrollY() >getHeight - mScreenHeight){                dy = 0;            }            scrollBy(0, dy);//随手指滚动 dy            mLastY = y;            break;        caseMotionEvent.ACTION_UP:            mEnd = getScrollY();            intdScrollY = 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;    }    postInvalidata();    returntrue;}/***Called by a parent to request that a child update its values for mScrollX and mScrollY if necessary. This will typically be done if the child is animating a scroll using a Scroller object.**/@OverridepublicvoidcomputeScroll(){    super.computeScroll();    if(mScroller.computeScrollOffset()){        scrollTo(0, mScroller.getCurrY());        postInvalidate();    }} | 
事件拦截机制

点击 View 的 log:
[代码]java代码:
| 1 2 3 4 5 6 7 8 | ViewGroupA dispatchTouchEventViewGroupA onInterceptTouchEventViewGroupB dispatchTouchEventViewGroupB onInterceptTouchEventView dispatchTouchEventView onTouchEvent //last event, will back to parentViewGroupB onTouchEventViewGroupA onTouchEvent | 
所以事件传递顺序是:先执行 dispatchTouchEvent() 然后是 onInterceptTouchEvent()。返回值:True,拦截,不继续;False,不拦截,继续流程。初始返回是 false。
事件处理顺序是:onTouchEvent()。返回值:True,处理了,不审核;False,给上级处理。初始返回是 false。
即:
- 
分发、拦截:如果某个 ViewGroup 直接使用 dispatchTouchEvent()返回了 true ,则分发拦截结束,不再向其子 View 传递,则,直接执行该 ViewGroup 的onTouchEvent(),然后继续向上处理对应 ViewGroup 的onTouchEvent()。
- 
处理:如果某个 View 直接在 onTouchEvent()中返回了 true。则上级不再执行onTouchEvent()。所有的处理在此结束。
 
                     
                    
                 
                    
                 
                
            
         
 
         浙公网安备 33010602011771号
浙公网安备 33010602011771号