Android-控件架构

Android-控件架构

Android的控件是Android的血与肉;本篇会讲解Android的View架构,view的测量与绘制,自定义view和控件的事件分发拦截机制

控件架构

1.View的测量

在OnMeasure()方法中进行,Android提供了一个短小但强大的类MeasureSpec(),通过它来帮助测量View。MeasureSpec是一个32位的Int值,高2位是测量的模式,低30位是测量的大小,使用这种位运算是为了提高测量的效率。

测量模式

  • EXACTLY
    表示父视图希望子视图的大小应该是由specSize的值来决定的,系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。

  • AT_MOST
    表示子视图最多只能是specSize中指定的大小,开发人员应该尽可能小得去设置这个视图,并且保证不会超过specSize。系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。

  • UNSPECIFIED
    表示开发人员可以将视图按照自己的意愿设置成任意的大小,没有任何限制。这种情况比较少见,不太会用到。

View默认的onMeasure只支持EXACTLY模式

直接继承View,就不能用wrap_content属性;想用就必须重写onMeasure方法

通过MeasureSpec这个类,我们就获取到了View的测量模式和绘制的大小,有了这些信息我们就可以控制View最后显示的大小了

首先要知道重写OnMeasure方法还是调用的父类的方法,而父类又是调用setMeasuredDimension方法

//继承View,重写OnMeasure方法
@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

//点进上面OnMeasure中,View类的OnMeasure方法就是又调用setMeasuredDimension方法
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

例子,自定义View,继承View,实现使用wrap_content属性:


public class MyView extends View {
    public MyView(Context context) {
        super(context);
    }

    public MyView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //由于我们直接调用setMeasuredDimension方法,就不需要调用父类的方法了,可以注释掉
        setMeasuredDimension(measrueWidth(widthMeasureSpec), measrueHeight(heightMeasureSpec));
    }

    //测量宽度,先判断模式为EXACTLY
    private int measrueWidth(int widthMeasureSpec) {
        int result = 0;
        //获取模式是EXACTLY还是其他模式
        int specMode = MeasureSpec.getMode(widthMeasureSpec);
        //获取控件大小
        int specSize = MeasureSpec.getSize(widthMeasureSpec);

        if (specMode == MeasureSpec.EXACTLY) {
            result = specSize;
        } else {
            //如果不是EXACTLY模式则需要设置一个默认大小
            result = 200;
            if (specMode == MeasureSpec.AT_MOST) {
                //取其中小的一个
                result = Math.min(result, specSize);
            }
        }
        return result;
    }

    //测量高度,类似宽度,如上
    private int measrueHeight(int heightMeasureSpec) {
        int result = 0;
        int specMode = MeasureSpec.getMode(heightMeasureSpec);
        int specSize = MeasureSpec.getSize(heightMeasureSpec);

        if (specMode == MeasureSpec.EXACTLY) {
            result = specSize;
        } else {
            result = 200;
            if (specMode == MeasureSpec.AT_MOST) {
                result = Math.min(result, specSize);
            }
        }
        return result;
    }

}

2.View的绘制

重写View的OnDraw()方法,介绍一下Canvas,就是一个画布,有了它就可以绘图了。

Canvas mCanvas = new Canvas(bitmap);

传递一个bitmap是为了记录保存下绘图的像素信息,有了这些信息才能进行绘制,两者紧紧联系在一起的。也就是说传的bitmap值不同,画的就不同;传递的是同一个bitmap,画的也是相同的。

3.ViewGroup的测量和绘制

  1. 测量: 这个是在面试的时候被坑过了的,如果属性是wrap_content主要就是调用OnLayout方法遍历子View,调用子View的Measure方法;不是则设定指定值。
    如果是自定义ViewGroup的话,跟View一样;想用wrap_content属性就必须重写OnMeasure方法。

  2. 绘制: 使用dispatchDraw()方法来调用子View的OnDraw()方法 进行绘制。或者如果设置了背景颜色或图片的情况下也会调用自己的OnDraw()方法

4.自定义View

大家最喜欢的也是最重点内容终于来了,初学时一直觉得感觉很深奥的样子,其实也很简单的啦

在View中比较重要的回调方法:

  • OnfinishInflate:从XML加载组件后回调
  • OnSizeChanged:组件大小改变时回调
  • OnMeasure:回调该方法来进行测量
  • OnLayout:回调该方法确定显示的位置
  • OnTouchEvent:监听触摸事件回调

一般来说可以对现有控件进行拓展:例如继承TextView来实现具有外框的TextView

    @Override
    protected void onDraw(Canvas canvas) {
        //回调之前实现自己的逻辑,即在绘制文本之前
        super.onDraw(canvas);
        //回调之后实现自己的逻辑,即在绘制文本之后
    }

自定义属性

在res中的values文件夹下,创建名为attrs的Values resouce File的文件,<declare-styleable name="TopBar">用来确定是作用于那个View的,下面的attr name就是每个属性的名称。

这里说明一下属性:

  • diemnsion: 一般为文字大小
  • color:颜色
  • String:字符串
  • reference:引用,一般为background
  • 还可以两者结合,用|分隔开
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="TopBar">

        <attr name="top_bar_title" format="string"/>
        <attr name="title_text_size" format="dimension"/>
        <attr name="title_text_color" format="color"/>

        <attr name="left_text_color" format="color"/>
        <attr name="left_btn_back" format="reference|color"/>
        <attr name="left_text" format="string"/>

        <attr name="right_text_color" format="color"/>
        <attr name="right_btn_back" format="reference|color"/>
        <attr name="right_text" format="string"/>

    </declare-styleable>

</resources>

何如在代码中获取定义好的属性呢?

private TypedArray mTypedArray;

public void initTypedArray(Context context){
        //指定是从哪读取的属性
        mTypedArray = context.obtainStyledAttributes(R.styleable.TopBar);

        mTxtTile = mTypedArray.getString(R.styleable.TopBar_top_bar_title);
        mTxtLeft = mTypedArray.getString(R.styleable.TopBar_left_text);
        mTxtRight = mTypedArray.getString(R.styleable.TopBar_right_text);

        mTitleColor = mTypedArray.getColor(R.styleable.TopBar_title_text_color, Color.WHITE);
        mLeftColor = mTypedArray.getColor(R.styleable.TopBar_left_text_color,Color.WHITE);
        mRightColor = mTypedArray.getColor(R.styleable.TopBar_right_text_color,Color.WHITE);

        mLeftBackground = mTypedArray.getDrawable(R.styleable.TopBar_left_btn_back);
        mRightBackground = mTypedArray.getDrawable(R.styleable.TopBar_right_btn_back);

        mTextSize = mTypedArray.getDimension(R.styleable.TopBar_title_text_size,10);
        //获取完值后一般都会调用recycle方法,来避免重新创建时候的错误
        mTypedArray.recycle();

    }

XML中能用吗?怎么用呢?
既然是自定义的控件属性,当然也可以在XML中使用


看到上图的topbar属性了吗?都是自定义的属性,那么怎么才能这样定义呢?看看Android原本的控件如何定义的吧

这是使用自定义控件的根布局LinearLaout,看其中的xmlns:android就是系统的,而我们的就是xmlns:topbarxmlns就是命名空间;这个名字可以随意更改,直接在根布局中改就可以了

组合控件

组合控件一般都是动态添加子View的,所以所有控件都是在Java代码中添加,与xml无关。这时控件大家都会定义,属性也会设置如setTextColor();但是Layout的位置呢?应该如下所示:


private ViewGroup.LayoutParams layoutParams;

layoutParams = new ViewGroup.LayoutParams(
                ViewGroup.LayoutParams.WRAP_CONTENT,
                ViewGroup.LayoutParams.MATCH_PARENT
        );

//然后就可以用addView方法添加到组合控件中了
//例如定义了一个mLeftButton
addView(mLeftButton,layoutParams);

//查看addView源码
public void addView(View view, ViewGroup.LayoutParams params);

可以看到第四个方法有view和layoutParams的属性,就选这个

自定义点击事件接口

一般一个控件都会有点击事件吧,但是一个组合控件里面有多个控件,这时你就要自己写一个点击事件的接口让别人用了。

public interface OnTopBarClickListener {
        void onLeftClick();
        void onRightClick();
}

定义完后只是空的接口,需要有里面的点击响应:

OnTopBarClickListener mListener;

mLeftButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                //自行在外定义接口对象
                mListener.onLeftClick();
            }
    });

最后暴露出一个方法让调用者注册接口回调

public void setOnTopBarClickListener(OnTopBarClickListener listener){
    this.mListener = listener;
}

这样就ok了,是不是跟用系统的setOnTouchListener一样用了?就是这么简单

事件拦截机制简单分析

首先要知道什么是触摸事件;比如说按下一个按钮,按下按钮是事件一,不小心滑动是时间二,抬起手是事件三;

Android为触摸事件封装了一个类——MotionEvent;基本上所有与事件相关的操作都需要这个类

  • 事件传递dispatchTouchEvent:父控件--->子控件
    返回值:true拦截;false不拦截

  • 事件处理OnInterceptTouchEvent:子控件--->父控件
    返回值:true处理了;false给上级处理

初始情况下返回值都是false。

父控件想拦截事件就给dispatchTouchEvent设为true,这样就不会传递给子控件

子控件不想让父控件知道自己干了什么,就把OnInterceptTouchEvent置为true;这样就不会向父控件报告

是不是非常浅显易懂?这里就不过多阐述,只要知道是这样的就够了,深入的以后再说,或者可以看源码了解

该笔记源于《Android群英传》,加之自己的实践与理解,其中话语可能粗糙简陋,但可能更通俗直观,由于每个人知识水平不同,我只记录我认为不熟的,重点难点,想看全还请购买医生的书

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

推荐阅读更多精彩内容