Android控件:自定义View和ViewGroup相关

这篇文章将讨论几个问题:

  • px、dp、sp等尺寸相关
  • View测量相关
  • canvas的绘制方法
  • ViewGroup测量布局
  • padding与margin
  • 自定义View和ViewGroup的方法回调时机

尺寸问题

我们常说的分辨率,比如480*800,指的是屏幕横向有480个像素点,纵向有800个像素点,这里的像素点指的是Pixel,也即px。在同样物理尺寸的手机上,相同数量的像素在高分辨率手机上显然是显示得比较短的。比如说,两台相同尺寸手机,有两种分辨率,横向分别是400和800像素,如果某一控件layout_width都设为200px,则前者宽度为屏幕1/2,后者为1/4。
因此为了兼容性,便产生了另外两种单位,分别是dp和sp。假设一台手机是横向2英寸,纵向3英寸,其分辨率为320*480,则其dpi(dots per inch)为160。我们可以简单的理解dp这个单位是一个系数乘以px,不同的dpi会使这个系数不同。

我们在一个布局文件里面添加一个宽度为240dp的Button

DisplayMetrics dm = getResources().getDisplayMetrics();
Log.e("TAG", dm.toString());

button = (Button) findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Log.e("TAG", button.getWidth() + "");
    }
});

打印结果:

E/TAG﹕ DisplayMetrics{density=3.0, width=1080, height=1776, scaledDensity=3.0, xdpi=480.0, ydpi=480.0}

E/TAG﹕ 720

所以由此可知,根据dpi得出的系数正是density。
结论:由分辨率和手机尺寸可以得出dpi,不同dpi对应不同的density值,dp单位则为density * px。所以相同尺寸的手机,当分辨率越高时,dpi越高,每个dp对应的像素也越多。

sp主要用于文字,其原理和dp类似。


View测量问题

在View的绘制流程中,第一项便是测量,即调用measure方法,然后再调用onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法,最后在onMeasure方法里面调用setMeasuredDimension(int measuredWidth, int measuredHeight)方法则确定其最终的测量大小。
首先需要认识一下widthMeasureSpecheightMeasureSpec两个参数,这种参数封装了规格和父布局的参考大小两个值,可以通过MeasureSpec.getModeMeasureSpec.getSize得到这两个值。
模式有三种:(事实上需要父布局以及自身大小共同确定,后面会提到)

  1. EXACTLY
    精准模式,即View的布局文件设为match_parent或确认值。
  2. AT_MOST
    最大模式,即View的布局文件设为wrap_content。
  3. UNSPECIFIED
    未指定,这个不太会用得到。

我们来看下View的onMeasure方法

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
public static int getDefaultSize(int size, int measureSpec) {
   int result = size;
   int specMode = MeasureSpec.getMode(measureSpec);
   int specSize = MeasureSpec.getSize(measureSpec);

   switch (specMode) {
   case MeasureSpec.UNSPECIFIED:
       result = size;
       break;
   case MeasureSpec.AT_MOST:
   case MeasureSpec.EXACTLY:
       result = specSize;
       break;
   }
   return result;
}

由此可知,如果我们自定义View不重写onMeasure方法,当其设为wrap_content时,其测量大小也会是等同于match_parent的。

比较典型的写法为:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int widthResult = 0;
    int heightResult = 0;
    if (widthMode == MeasureSpec.EXACTLY) {
        widthResult = MeasureSpec.getSize(widthMeasureSpec);
    } else {
        // 根据实际情况来设定其最终大小,比如一个自定义View
        // 需要一张图片加左右10px空白,则widthResult =
        // bitmap.getWidth() + 20; 另外也可能需要考虑padding、
        // Math.min(200,MeasureSpec.getSize(withMeasureSpec))
        widthResult = 200;
    }

    if (heightMode == MeasureSpec.EXACTLY) {
        heightResult = MeasureSpec.getSize(heightMeasureSpec);
    } else {
        heightResult = 200;
    }

    // 设定最终的测量大小
    setMeasuredDimension(widthResult, heightResult);
}

最后唠嗑一句,这个widthMeasureSpec、heightMeasureSpec由父布局产生传递给子View。而widthMeasureSpec则需要父布局和子控件共同决定,详见ViewGroup#getChildMeasureSpec(int spec, int padding, int childDimension)。


绘制方法

图片:
canvas.drawBitmap();
指定左上角坐标,绘制图片本身;或指定绘制的矩形区域,缩放绘制

线:
canvas.drawLine();
指定开始坐标与终点坐标

点:
canvas.drwaPoint();
指定坐标,绘制的其实是个正方形。

矩形:
canvas.drawRect();
指定矩形区域

圆形:
canvas.drawCircle();
指定圆心和半径

弧形:
canvas.drawArc();
一般指定圆形的四个点,开始角度、扫过角度,是否绘出半径;

椭圆形:
canvas.drwaOval();
指定四个点

不规则连线:
canvas.drawPath();

文字:
canvas.drawText();

最后注意Paint对象的两个方法。
paint.setStrokeWidth();
paint.setStyle();
比如

Paint paint = new Paint();
paint.setStrokeWidth(30);
paint.setStyle(Paint.Style.STROKE);
canvas.drawCircle(getWidth() / 2, getHeight() / 2, 100, paint);

最终画出来的圆真实半径会是100+30/2,因为是从线的中间开始绘制的,所以要注意线宽的问题~~


ViewGroup测量布局

1、测量

ViewGroup的测量通常与子View有关,子控件总共占据多大的空间,布局文件的测量大小就设为多大。所以我们首先需要测量出各子View的大小,然后根据子View来测量布局文件的大小。ViewGroup里面有个measureChildren(int widthMeasureSpec, int heightMeasureSpec)方法,系统即会完成子View的测量,而这个方法最终辗转也会调用到measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec)
getChildMeasureSpec(int parentMeasureSpec, int padding, int childDimension)child.measure(childWidthMeasureSpec, childHeightMeasureSpec)等方法,所以也和上文说的一致:子View的测量大小需要父布局和自身大小共同确定,这也是为什么控件宽高的设置不是width,而是layout_width。

2、布局

ViewGroup有个抽象方法,onLayout(boolean changed, int l, int t, int r, int b),当我们实现自定义布局时,必须自己来实现子View的布局,而子View的布局只需要调用其layout(l,t,r,b)方法即可。
自己实现一个简单的纵向线性布局

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int widthResult = 0;
    int heightResult = 0;
    int childCount = getChildCount();
    measureChildren(widthMeasureSpec, heightMeasureSpec);
    if (widthMode == MeasureSpec.EXACTLY) {
        widthResult = MeasureSpec.getSize(widthMeasureSpec);
    } else {
        int maxChildWidth = 0;
        for (int i = 0; i < childCount; i++) {
            if (getChildAt(i).getMeasuredWidth() > maxChildWidth) {
                maxChildWidth = getChildAt(i).getMeasuredWidth();
            }
        }
        widthResult = Math.min(maxChildWidth, MeasureSpec.getSize(widthMeasureSpec));
    }

    if (heightMode == MeasureSpec.EXACTLY) {
        heightResult = MeasureSpec.getSize(heightMeasureSpec);
    } else {
        int totalHeight = 0;
        for (int i = 0; i < childCount; i++) {

            totalHeight += getChildAt(i).getMeasuredHeight();

        }
        heightResult = Math.min(totalHeight, MeasureSpec.getSize(heightMeasureSpec));
    }
    setMeasuredDimension(widthResult, heightResult);
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    int childCount = getChildCount();
    int currentTop = 0;
    for (int i = 0; i < childCount; i++) {
        View childView = getChildAt(i);
        childView.layout(0, currentTop, childView.getMeasuredWidth(), currentTop + childView.getMeasuredHeight());
        currentTop += childView.getMeasuredHeight();
    }
}

布局与坐标系统小结:
以下子View布局代表ViewGroup中调用childView.layout(l,t,r,b)
1、子View布局只能在父布局的测量大小范围内,超过部分无法显示;
2、子View的绘制是以子View布局为坐标系的,超过布局无法显示;
3、子View的measureWidth等于其测量宽度,width等于子View布局的r - l;
4、测量高度和”真实”高度都是为了绘制和布局,具体效果还得看代码实现;
5、子View#getX/getY/getLeft/getTop/getRight/getBottom/getTranslationX均以父布局为坐标,与绘制时的坐标系不同。
6、子View#setTranslationX,会改变其位置,但是不会改变其left、top、right、bottom,x = left + translationX。scaleX同理。


padding和margin

padding主要用于自定义View,影响测量大小以及绘制内容的位置;
margin主要用于自定义ViewGroup,当子View有margin属性时影响布局的测量大小以及子View布局的位置。
比如说上诉的例子,如果加入第一个子View添加一个 android:layout_marginLeft=”50dp”属性时,并不会影响布局的测量大小以及子View的布局,因为我们根本就没在里面做相关处理。
处理一下:

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(), attrs);
    }

#onMeasure
...
for (int i = 0; i < childCount; i++) {
    if (getChildAt(i).getMeasuredWidth() + ((MarginLayoutParams) (getChildAt(i).getLayoutParams())).leftMargin > maxChildWidth) {
        maxChildWidth = getChildAt(i).getMeasuredWidth() + ((MarginLayoutParams) (getChildAt(i).getLayoutParams())).leftMargin;
    }
}
...

#onLayout
...
for (int i = 0; i < childCount; i++) {
    View childView = getChildAt(i);
    childView.layout(((MarginLayoutParams) (getChildAt(i).getLayoutParams())).leftMargin, currentTop, childView.getMeasuredWidth() + ((MarginLayoutParams) (getChildAt(i).getLayoutParams())).leftMargin, currentTop + childView.getMeasuredHeight());
    currentTop += childView.getMeasuredHeight();
}
...

这样margin属性的作用就能体现出来了。padding在自定义View的处理也是类似的。总而言之:如果有必要,自定义View需要考虑padding,确定其测量大小以及绘制内容的位置;自定义ViewGroup需要考虑margin,确定其测量大小以及子View布局的位置。


自定义View和ViewGroup的方法回调时机

经常重写的几个方法,比如onMeasure、onLayout、onDraw、onSizeChanged等方法究竟在什么时候会被系统回调?

首先看自定义View。
1、XML定义后
这里写图片描述
2、调用invalidate()方法时,只会回调onDraw()方法
3、调用requestLayout()
这里写图片描述
4、设置参数再调用requestLayout()

mMyView.getLayoutParams().width = 300;
mMyView.requestLayout();

这里写图片描述

结论:
1、注意初始化的完整流程;
2、调用invalidate时只会执行onDraw()重绘操作;
3、调用requestLayout方法会执行完整的measure->layout->draw流程,即使没有任何改变;
4、当View的大小改变时,会回调onSizeChanged;
5、onLayout的changed参数表示此View的大小或者位置是否改变,改变则为true。另外,大小改变时会回调onSizeChanged和onDraw,只是位置改变时则不会。

自定义ViewGroup其实和自定义View基本一致,因为ViewGroup本身就是继承View的。需要注意的是:
当ViewGroup调用invalidate或者requestLayout等方法时,并不会影响的子View,也即不会回调子View的各个绘制流程方法。但是反过来,当子View调用invalidate和requestLayout方法时,却会回调ViewGroup的。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值