Android 控件架构与自定义控件详解

链接 Android 控件架构与自定义控件详解

这是我重读《Android 群英传》的时候做的读书笔记,在 View 这块,医生讲解真的非常深入浅出,非常值得一读,并且多次重读。

架构:

  • 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() 方法。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
    setMeasureDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec));
}

//可作为模板代码!
private int measureWidth(int measureSpec){
    int result = 0;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = 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 想多大就多大
    }
    return result;
}

即,如果不重写 onMeasure() 方法,系统则会不知道该默认多大尺寸,就会默认填充整个父布局,所以,重写 onMeasure() 方法的目的,就是为了能够给 View 一个 wrap_content 属性下的默认大小

View 的绘制

onDraw() 中的参数,就是 Canvas 对象,使用该对象进行绘图,而在其他地方,则需要 new 出该对象:

Canvas canvas = new Canvas(bitmap);

传进去的 bitmap 是与这个 bitmap 创建的 Canvas 画布紧密联系的,这个过程称为装载画布。该 bitmap 用来存储所有绘制在 Canvas 上的像素信息。所有的 Canvas.drawXXX 方法都发生在这个 bitmap 上。

@Override
protected void onDraw(Canvas canvas){
    //...
    //在 onDraw 方法中绘制两个 bitmap
    canvas.drawBitmap(bitmap1, 0, 0, null);
    canvas.drawBitmap(bitmap2, 0, 0, null);
    //...
}
private void otherMethod(){
    //将 bitmap2 装载到另一个 Canvas 对象中
    Canvas mCanvas = new Canvas(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

@Override
protected void onDraw(Canvas canvas){
    //在回调父类方法前,对 TextView 来说是在绘制文本内容之前,实现逻辑
    super.onDraw(canvas);
    //之后,绘制文本之后
}

在 attrs.xml 中通过使用<declar-styleable>标签声明使用了自定义属性,使用如下代码获得在布局文件中自定义的那些属性

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 功能:

@override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int count = getChildCount();
    //遍历通知子 View 对其自身进行测量
    for(int i = 0;i < count; ++i){
        View childView = getChildAt(i);
        measureChild(childView, widthMeasureSpec, heightMeasureSpec);
    }
}

步骤二:再对子 View 进行放置位置设定,让每个子 View 都显示完整的一屏。所以,本例中ViewGroup 的高度就是子 View 的个数乘以屏幕高度,然后遍历设定每个子 View 放置的位置

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b){
    int childCount = getChildCount();
    //设置 ViewGroup 高度
    MarginLayoutParams mlp = (MarginLayoutParams)getLayoutParams();
    mlp.height = mScreenHeight.childCount;
    setLayoutParams(mlp);
    //修改子 View 的 top 和 bottom 属性,使它们依次排列
    for(int i=0; i<childCount; i++){
        View child = getChildAt(t);
        if(child.getVisibility() != View.GONE){
            child.layout(l, i*mScreenHeight, r, (i+1)*mScreenHeight);
        }
    }
}

步骤三:

@Override
public boolean onTouchEvent(MotionEvent event){
    int y = (int)event.getY();
    switch(event.getAction()){
        case MotionEvent.ACTION_DOWN:
            mLastY = y;
            mStart = getScrollY();//记录按下位置
            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);//随手指滚动 dy
            mLastY = y;
            break;
        case MotionEvent.ACTION_UP:
            mEnd = getScrollY();
            int dScrollY = 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();
    return true;
}

/**
*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.
**/
@Override
public void computeScroll(){
    super.computeScroll();
    if(mScroller.computeScrollOffset()){
        scrollTo(0, mScroller.getCurrY());
        postInvalidate();
    }
}

事件拦截机制

custom_view_viewgroup

点击 View 的 log:

ViewGroupA dispatchTouchEvent
ViewGroupA onInterceptTouchEvent
ViewGroupB dispatchTouchEvent
ViewGroupB onInterceptTouchEvent
View dispatchTouchEvent
View onTouchEvent //last event, will back to parent
ViewGroupB onTouchEvent
ViewGroupA onTouchEvent

所以事件传递顺序是:先执行 dispatchTouchEvent() 然后是 onInterceptTouchEvent()返回值:True,拦截,不继续;False,不拦截,继续流程。初始返回是 false。

事件处理顺序是:onTouchEvent()返回值:True,处理了,不审核;False,给上级处理。初始返回是 false。

即:

  • 分发、拦截:如果某个 ViewGroup 直接使用 dispatchTouchEvent() 返回了 true ,则分发拦截结束,不再向其子 View 传递,则,直接执行该 ViewGroup 的 onTouchEvent(),然后继续向上处理对应 ViewGroup 的 onTouchEvent()

  • 处理:如果某个 View 直接在 onTouchEvent() 中返回了 true。则上级不再执行 onTouchEvent()。所有的处理在此结束。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容