Android知识点总结(三)自定义View

标签: 自定义View  Android  自定义ViewGroup

Android开发艺术探索读书笔记系列

ViewRoot、DecorView和MeasureSpec

ViewRoot、DecorView

ViewRoot是连接WindowManager和DecorView的纽带,View的measure、layout和draw三大流程都是通过ViewRoot来完成的。
View的绘制流程是从ViewRoot的performTraversals方法开始的:
这里写图片描述

MeasureSpec

MeasureSpec代表一个32位int值,高两位代表SpecMode,低30位代表SpecSize,SpecMode代表测量模式,SpecSize指在某种测量模式下的规格大小!
SpecMode有三类:

  • UNSPECIFIED 父容器不对view有任何限制,要多大给多大
  • EXACTLY 对应match_parent和具体数值
  • AT_MOST 对应wrap_content

MeasureSpec的创建规则:
这里写图片描述

View的工作流程

View的工作流程主要指measure、layout和draw,其中measure确定View的测量宽高,layout确定View的最终宽高和四个定点的位置,draw将View绘制到屏幕上!

measure流程

1、view的messure过程
measure方法是final类型,不能被子类重写,自定义view的时候只能重写measure里面的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;
}

UNSPECIFIED模式主要用于系统内部,我们一般不需要,所以对我们来说getDefaultSize返回的大小就是measureSpec的specSize(View测量后的大小),一般这个测量大小和最终大小相等,但是最终大小是在layout阶段确定的!View的宽高取决于sepcSize,直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content时自身大小,否则布局中使用了wrap_content就相当于使用了match_parent!解决方案如下:


@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    if (widthMode == MeasureSpec.AT_MOST && heightMeasureSpec == MeasureSpec.AT_MOST){
        setMeasuredDimension(myWidhh,myHeight);//指定的默认宽高
    }else if (widthMode == MeasureSpec.AT_MOST){
        setMeasuredDimension(myWidth, heightSize);
    }else if (heightMode == MeasureSpec.AT_MOST){
        setMeasuredDimension(widSize, myHeight);
    }
}

查看TextView、ImageView等直接继承View的组件都针对wrap_content做了特殊处理如TextView

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    ...
     // Check against our minimum width
     width = Math.max(width, getSuggestedMinimumWidth());

     if (widthMode == MeasureSpec.AT_MOST) {
         width = Math.min(widthSize, width);
     }
    ...

2、ViewGroup的measure过程
ViewGroup是抽象类,没有重写View的onMeasure,但是他提供了一个叫measureChildren的方法:

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
    final int size = mChildrenCount;
    final View[] children = mChildren;
    for (int i = 0; i < size; ++i) {
        final View child = children[i];
        if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }
    }
}

protected void measureChild(View child, int parentWidthMeasureSpec,
        int parentHeightMeasureSpec) {
    final LayoutParams lp = child.getLayoutParams();

    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom, lp.height);

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

可以看到ViewGroup没有定义其测量的具体过程,因为它是一个抽象类,其测量过程的onMeasure需要各个子类去具体实现,如LinearLayout、RelativeLayout等,去读一下他们的具体实现就会知道,大体上依然是去判断其mode,来决定其宽高,或是具体数值、或是计算子类的宽高来确定等。
View的measure过程是三大流程中最复杂的一个,通过getMeasureWidh/Height方法可以获取View的测量宽高,由于某些情况下,系统可能多次measure才能确定宽高,所以在onLayout里面获取view的宽高是一个比较好的习惯!
由于测量宽高和生命周期不是同步的,在Activity获取View的宽高最好使用下面的方法:

  • onWindowFocusChanged 这个方法是用来判断窗口的焦点变化的,Activity得到失去焦点,该方法都会被调用!该方法调用时View已经初始化完毕,可以获取宽高:
    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        if (hasFocus){
            int widthSize = textView.getMeasuredWidth();
            int heightSize = textView.getMeasuredHeight();
        }
    }
  • view.post(runnable) 通过post可以将一个runnable投递到消息队列的尾部,然后等待Looper调用的时候,View已经初始化好了:
    @Override
    protected void onStart() {
        super.onStart();
        new TextView(this).post(new Runnable() {
            @Override
            public void run() {
                int widthSize = textView.getMeasuredWidth();
                int heightSize = textView.getMeasuredHeight(); 
            }
        });
    }
  • ViewTreeObserver 使用ViewTreeObserver的众多回调也可以,如当View树的状态发生改变或则View树内部的View的可见性发生改变时,会调用OnGlobalLayoutListener的onGlobalLayout方法,在这里也可以!
  • view.measure(int widthMeasureSpec, int heightMeasureSpec)

layout过程

和measure过程相比,layout过程就比较简单了,作用是用来确定子元素的位置!当ViewGroup的位置被确定后,它会在onLayout中会遍历所有子元素并调用其layout方法,layout又会调用onLayout!layout确定View本身的位置,onLayout会确定子元素的位置!
下面解释一下view的测量宽高和最终宽高的区别,这个问题可以具体 至于为:View的getMeasuredWidth肯getWidth的区别,在View的默认实现中两者是相等的,但是测量宽高和最终宽高的形成时机不同,如果在layout中对最终宽高赋值作出修改,如:

public void layout(int l, int t, int r, int b){
	super.layout(l, t, r+100, b+100);
}

这时测量宽高和最终宽高就不一样了!

draw过程

draw过程比较简单,它的作用是将View绘制到屏幕上!其绘制过程 如下:

  1. 绘制背景background.draw(canvas)
  2. 绘制自己(onDraw)
  3. 绘制children(dispatchDraw)
  4. 绘制装饰(onDrawScrollBars)

这点从其源码中可以很清晰的看到:

  @CallSuper
    public void draw(Canvas canvas) {
        ...

        /*
         * Draw traversal performs several drawing steps which must be executed
         * in the appropriate order:
         *
         *      1. Draw the background
         *      2. If necessary, save the canvas' layers to prepare for fading
         *      3. Draw view's content
         *      4. Draw children
         *      5. If necessary, draw the fading edges and restore layers
         *      6. Draw decorations (scrollbars for instance)
         */

        // Step 1, draw the background, if needed
        int saveCount;

        if (!dirtyOpaque) {
            drawBackground(canvas);
        }
        // skip step 2 & 5 if possible (common case)
        //step 4, draw the children
        dispatchDraw(canvas);
       ...
        // Step 6, draw decorations (foreground, scrollbars)
        onDrawForeground(canvas);

        if (debugDraw()) {
            debugDrawFocus(canvas);
        }
    }

View绘制过程的传递是通过dispatchDraw来实现的,dispatchDraw会遍历调用所有子元素的draw方法!

自定义View

自定义View的分类

1、继承View重写onDraw方法
这种方法主要用于实现不规则或特殊的视觉效果,采用这种方式需要自己支持wrap_content,并且需要处理padding!

2、继承ViewGroup派生的Layout
这种方法主要用于自定义布局,一般需要处理ViewGroup及其子View的测量、布局这两个过程!

3、继承特定的View
这种方法一般是扩展某种已有的View的功能!

4、继承特定的ViewGroup,如LinearLayout
区别于方式2直接继承ViewGroup,这种方法不需要重写测量和布局的过程!

自定义View注意事项

  • 让View支持wrap_content
    直接继承View或ViewGroup,需要在onMeasure中对wrap_content做特殊处理!
  • 如果有必要,让View支持padding
  • 尽量不要在View中使用Handler
    View本身有post系列方法,可以替代Handler的作用,除非你明确要使用Handler来发送消息!
  • View中如果有线程或者动画需要及时停止
    如果有线程或动画需要停止时,可以在onDetachedFromWindow方法处理,该方法会在包含该View的Activity退出或者当前View被remove时被调用!另外当View变得不可见时我们也需要停止线程和动画,否则有可能造成内存泄露!
  • View带有滑动嵌套情形时,需要处理好滑动冲突

示例

1、直接继承View, 无自定义属性

public class MyCustomCircle extends View {
    private int mColor = Color.RED;
    private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

    public MyCustomCircle(Context context) {
        super(context);
        init();
    }

    public MyCustomCircle(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public MyCustomCircle(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        mPaint.setColor(mColor);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        //下面是处理wrap_content的清空  否则直接继承View其作用就相当于match_parent
        if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(200,200);//指定的默认宽高
        }else if (widthMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(200, heightSize);
        }else if (heightMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(widthSize, 200);
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //处理padding 如果不处理 padding将无效
        int paddingLeft = getPaddingLeft();
        int paddingRight = getPaddingRight();
        int paddingTop = getPaddingTop();
        int paddingBottom = getPaddingBottom();
        int width = getWidth() - paddingLeft - paddingRight;
        int height = getHeight() - paddingTop - paddingBottom;
        int radio = Math.min(width,height)/2;
        canvas.drawCircle(width/2 + paddingLeft, height/2 + paddingTop, radio, mPaint);
    }
}

然后直接在布局文件中使用即可

 <com.example.scy.myapplication.custom.MyCustomCircle
     android:id="@+id/myCricle"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:layout_margin="20dp"
     android:background="#671165"
     android:padding="10dp" />

2、下面是一个带有自定义属性的示例
自定义属性大致分三个步骤
第一, 在res/values/下面创建attrs.xml文件, 在里面声明自定义属性集合"MyCustomCircle", 内容如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
   <declare-styleable name="MyCustomCircle">
       <attr name="mColor" format="color"/>
       <attr name="mText" format="string"/>
       <attr name="mTextSize" format="dimension"/>
   </declare-styleable>
</resources>

自定义属性声明非常简单,一个name是属性名,一个format是格式,常用的格式如下:

1)  string:字符串类型;
2)  integer:整数类型;
3)  float:浮点型;
4)  dimension:尺寸,后面必须跟dp、dip、px、sp等单位;
5)  Boolean:布尔值;
6)  reference:引用类型,传入的是某一资源的ID,必须以“@”符号开头;
7)  color:颜色,必须是“#”符号开头;
8)  fraction:百分比,必须是“%”符号结尾;
9)  enum:枚举类型

第二,在View的构造方法中解析自定义属性:

public MyCustomCircle(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    //加载自定义属性集合
	 TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MyCustomCircle);
	 //有的在这里使用for循环加条件遍历也可以,但是那样的话,通过下面的方式设置的默认值就无效了
     mColor = typedArray.getColor(R.styleable.MyCustomCircle_mColor, Color.RED);//默认红色
     mTextSize = typedArray.getDimensionPixelSize(R.styleable.MyCustomCircle_mTextSize, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 14, getResources().getDisplayMetrics()));//默认14sp
     mText = typedArray.getString(R.styleable.MyCustomCircle_mText);
     typedArray.recycle();
}

第三, 布局文件中引用

 <com.example.scy.myapplication.custom.MyCustomCircle
     android:id="@+id/myCricle"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:layout_margin="20dp"
     android:background="#671199"
     app:mText="自定义"
     app:mTextSize="16sp"
     android:padding="10dp" />

下面是MyCustomCircle的完整代码:

public class MyCustomCircle extends View {
    private int mColor;
    private int mTextSize;
    private String mText;
    /**
     * 绘制时控制文本绘制的范围
     */
    private Rect mBound;
    private Paint mPaint;

    public MyCustomCircle(Context context) {
        this(context, null);
    }

    public MyCustomCircle(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyCustomCircle(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MyCustomCircle);
        mColor = typedArray.getColor(R.styleable.MyCustomCircle_mColor, Color.RED);
        mTextSize = typedArray.getDimensionPixelSize(R.styleable.MyCustomCircle_mTextSize, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 14, getResources().getDisplayMetrics()));
        mText = typedArray.getString(R.styleable.MyCustomCircle_mText);
        typedArray.recycle();

        mPaint = new Paint();
        mPaint.setColor(mColor);
        mPaint.setTextSize(mTextSize);
        //下面两句 是获取text的绘制范围
        mBound = new Rect();
        mPaint.getTextBounds(mText, 0, mText.length(), mBound);
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
         //下面是处理wrap_content的清空  否则直接继承View其作用就相当于match_parent
        if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(200, 200);//指定的默认宽高
        } else if (widthMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(200, heightSize);
        } else if (heightMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSize, 200);
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //处理padding 如果不处理 padding将无效
        int paddingLeft = getPaddingLeft();
        int paddingRight = getPaddingRight();
        int paddingTop = getPaddingTop();
        int paddingBottom = getPaddingBottom();
        int width = getWidth() - paddingLeft - paddingRight;
        int height = getHeight() - paddingTop - paddingBottom;
        //width / 2 + paddingLeft 这句和前面画圆一样 找中心并考虑内边距
        //... -mBound.width() / 2 没有这句 文字的起点就是上面说的中心点 而我们想要的是上面的中心点也是文字的中心点 
        canvas.drawText(mText, width / 2 + paddingLeft - mBound.width() / 2, height / 2 + paddingTop + mBound.height() / 2, mPaint);
    }

3、继承ViewGroup派生特殊的Layout

public class HorizontalLayout extends ViewGroup {
    public HorizontalLayout(Context context) {
        this(context, null);
    }

    public HorizontalLayout(Context context, AttributeSet attrs) {
        this(context,attrs, 0);
    }

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

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //将所有子View进行测量
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int childCount = getChildCount();
        if (childCount == 0){
            setMeasuredDimension(0,0);
        }else {
            if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST){
                int width = getTotleWidth(childCount);
                int height = getMaxChildHeight(childCount);
                setMeasuredDimension(width, height);
            }else if (widthMode == MeasureSpec.AT_MOST){
                setMeasuredDimension(getTotleWidth(childCount), heightSize);
            }else if (heightMode == MeasureSpec.AT_MOST){
                setMeasuredDimension(widthSize, getMaxChildHeight(childCount));
            }
        }
    }

    /**
     * 将所有子View的宽度相加  事实上这里还应该考虑 子view的外边距
     * @param childCount
     * @return
     */
    private int getTotleWidth(int childCount) {
        int width = 0;
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            width += child.getMeasuredWidth();
        }
        return width;
    }

    /**
     * 以最大的子View的高度作为 父View的高度
     * @param childCount
     * @return
     */
    private int getMaxChildHeight(int childCount) {
        int maxHeight = 0;
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (child.getMeasuredHeight() > maxHeight){
                maxHeight = child.getMeasuredHeight();
            }
        }
        return maxHeight;
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childCount = getChildCount();
        int mWidth = l;//记录宽度 
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() != GONE){
                int height = child.getMeasuredHeight();
                int width = child.getMeasuredWidth();
                child.layout(mWidth, 0, mWidth + width, 0 + height);
                mWidth += width;
            }
        }
    }
}

上述代码有两点不太规范的地方,第一是没有子元素时,不应该直接将自己的宽高都设置为0,而应该根据LayoutParams中的宽高来处理,第二测量宽高时应该考虑子View的margin和自己的padding!

原文链接:加载失败,请重新获取