初识View-FOUR

这里写图片描述

开篇短叙

做开发很久了,却一直没有真正的去面对自定义View这一块蛋糕。平时,在github或者博客上自学了很多这方面的知识,但是一直不是很系统,总是在真正使用的时候发现,自己在这方面的知识储备显得-很“笨拙”,只能应对一些简单的!!借着最近项目不紧张,再次进行View的系统学习,并在这里记录一下自己的学习过程。

摘要

  • layout()作用?layout()和onLayout()关系
  • onDraw()作用?onDraw()、dispatchDraw()和draw()关系
概括性描述:

对于自定义View来实现自己的需求控件,在一些关键方法的使用上,比如:onMeasure()、onLayout()和onDraw()。
onMeasure()功能作为测量。以ViewGroup作为父控件(容器)且具有子控件(非容器)的自定义的View来说,若是已确定摆放的方式,通常会在自定义的容器中重写onMeasure()方法。然后以measureChildren 、 measureChild()或者measureChildWithMargin()根据具体情况来实现具体的测量功能。

... onMeasure(..){
  ...
  //measureChildren();
  measureChild();
  //measureChildWithMargin();
  ...
}

layout()本质上就是为View设置四个坐标值,从而可以精确的绘制在指定区域。而在使用的过程中,我们经常会遇到另外一个方法onLayout()。并且onLayout()都是在ViewGroup(容器)中重写,其作用是来确定容器中的每个子控件的位置。然后具体的位置摆放需要方法layout()实现,通过其四个参数左、上、右、下。而不同的容器的布局策略不尽相同如,RelaytiveLayout和LinearLayout。从而他们在方法onLayout()中的实现方式也不尽相同。

... onLayout(..){
  ...
  view.layout();
  ...
}

draw()就是对View(非容器)的具体绘制。并在onDraw()方法中实现。平时自定义View的时候,也许会遇到一个方法dispatchDraw(),他的出现表示对当前容器中的子View进行绘制,且该子View是容器。也即是说为了进行层层绘制。

layout() 分析

简单说
View 的 layout() 方法用于 View 确定自己本身在其父 View 的位置
ViewGroup 的 onLayout() 方法用于确定子 View 的位置

先简单使用代码理解一下:

public class MyViewGroup extends ViewGroup {

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

//首先使用这个方法获取child的尺寸
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int childCount = getChildCount();
        if(childCount > 0){
            View child = getChildAt(0);

            measureChild(child,widthMeasureSpec,heightMeasureSpec);
        }
    }

//然后在这个方法中来确定view的位置
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childCount = getChildCount();
        if(childCount > 0){
            View child = getChildAt(0);
            int measuredWidth = child.getMeasuredWidth();
            int measuredHeight = child.getMeasuredHeight();

            //确定自身在父容器中的位置
            child.layout(0,0,measuredWidth,measuredHeight);
        }
    }

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

ViewGroup 首先调用 layout() 来确定自己本身在其父 View 中的位置,然后调用 onLayout() 确定每个子 View 的位置,每个子 View 又会调用 View 的 layout() 方法来确定自己在 ViewGroup 的位置。
看源码体会!

// LinearLyout 源码
protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (mOrientation == VERTICAL) {
            layoutVertical(l, t, r, b);
        } else {
            layoutHorizontal(l, t, r, b);
        }
    }
//layoutVertical
void layoutVertical(int left, int top, int right, int bottom) {
    final int paddingLeft = mPaddingLeft;
    int childTop;
    int childLeft;
    final int width = right - left;
    int childRight = width - mPaddingRight;

    int childSpace = width - paddingLeft - mPaddingRight;

    final int count = getVirtualChildCount();

    final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
    final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;

    switch (majorGravity) {
          case Gravity.BOTTOM:
              childTop = mPaddingTop + bottom - top - mTotalLength;
               break;

          case Gravity.CENTER_VERTICAL:
              childTop =mPaddingTop+(bottom-top-mTotalLength) / 2;
              break;

          case Gravity.TOP:
          default:
              childTop = mPaddingTop;
              break;
    }

    for (int i = 0; i < count; i++) {
        final View child = getVirtualChildAt(i);
        if (child == null) {
            childTop += measureNullChild(i);
        } else if (child.getVisibility() != GONE) {
            final int childWidth = child.getMeasuredWidth();
            final int childHeight = child.getMeasuredHeight();

            final LinearLayout.LayoutParams lp =
                        (LinearLayout.LayoutParams) child.getLayoutParams();

            int gravity = lp.gravity;
            if (gravity < 0) {
                gravity = minorGravity;
            }
            final int layoutDirection = getLayoutDirection();
            final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
            switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                case Gravity.CENTER_HORIZONTAL:
                    childLeft = paddingLeft + ((childSpace - childWidth) / 2)
                                + lp.leftMargin - lp.rightMargin;
                    break;

                case Gravity.RIGHT:
                    childLeft = childRight - childWidth - lp.rightMargin;
                    break;

                case Gravity.LEFT:
                default:
                    childLeft = paddingLeft + lp.leftMargin;
                    break;
            }

            if (hasDividerBeforeChildAt(i)) {
                childTop += mDividerHeight;
            }

            childTop += lp.topMargin;
            //在 setChildFrame() 中又调用了 View 的 layout() 方法来确定子 View自身在父容器中的位置
            setChildFrame(child,childLeft,childTop+ getLocationOffset(child),
                        childWidth, childHeight);
            childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

            i += getChildrenSkipCount(child, i);
        }
    }
}

然后简单看下 onLayout源码:

//确定自己在父容器中的位置
public void layout(int l, int t, int r, int b) {
        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }

        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;

        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        //此方法是一个空方法,因为onLayout在layout方法中被调用,onLayout应该确定并为其子View分配位置。
        //而只有ViewGroup(容器)才有子View。但是ViewGroup 的 onLayout()是一个抽象方法,说明ViewGroup 的子类都必须重写这个方法,这样才能实现自己特有的逻辑功能:如,RelativeLayout和LinearLayout。
        //只有具有子View的派生类(容器)才具备调用此方法(此时不会为空的)来确定并分配子view位置功能。
            onLayout(changed, l, t, r, b);
            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnLayoutChangeListeners != null) {
                ArrayList<OnLayoutChangeListener> listenersCopy =
                        (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
                int numListeners = listenersCopy.size();
                for (int i = 0; i < numListeners; ++i) {
                    listenersCopy.get(i).onLayoutChange(this,l,t,r,b,oldL,oldT,oldR,oldB);
                }
            }
        }

        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
    }

onDraw() & Canvas()分析

首先简单表示一下。因为我之前在自定义view时候,在用到onDraw方法,其中有两个经常用到的方法,canvas.save() 和 canvas.restore()。当时很恍然,不清楚。知道后来才知道,canvas.save()表示对当前已经绘制的图形的锁定。而所谓锁定,就是指之后的任何操作不会对锁定前的图形产生影响,并且随之便会产生一个透明的图层(layer),使得之后的draw操作都会执行在这个新的透明图层上。canvas.restore()表示将图像最外层的图层和图层下已经形成的图像进行糅合,生成一个新的图像。当然,该操作之后依然可以继续在该新图像上进行绘制,表示当前操作的所有操作已经被解锁定!请留心:save() 和 restore() 最好配对使用,若 调用次数restore() > save() ,可能会造成异常。如下代码:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawColor(Color.GREEN);
    Paint paint=new Paint();
    paint.setTextSize(70);
    paint.setColor(Color.BLUE);
    canvas.drawText("Before clip", 20, 80, paint);
    canvas.save();
    Rect rect=new Rect(20,200,900,1000);
    canvas.clipRect(rect);
    canvas.drawColor(Color.YELLOW);
    paint.setColor(Color.BLACK);
    canvas.drawText("After clip", 10, 310, paint);
    canvas.restore();
    paint.setColor(Color.RED);
    canvas.drawText("Test", 20, 170, paint);
}

我们在进行图或者控件的绘制的时候,会用到 canvas ,那么如果我们自己进行创建对象而非使用系统的canvas对象,这时会有两个构造方法需要我们进行选择。看一下,

/**
*官方不推荐通过该无参的构造方法生成一个canvas。如果要这么做那就需要调用setBitmap( )为其设置一个 
* Bitmap。为什么Canvas非要一个Bitmap对象呢?原因很简单:Canvas需要一个Bitmap对象来保存像素,如果画
* 的东西没有地方可以保存,又还有什么意义呢?
*/
public Canvas() {
    if (!isHardwareAccelerated()) {
        mNativeCanvasWrapper = initRaster(null);
        mFinalizer = new CanvasFinalizer(mNativeCanvasWrapper);
    } else {
        mFinalizer = null;
    }
}

既然不推荐这么做,那就我们接着看有参的构造方法,

public Canvas(Bitmap bitmap) {
    if (!bitmap.isMutable()) {
        throw new IllegalStateException("Immutable bitmap passed to Canvas constructor");
    }
    throwIfCannotDraw(bitmap);
    mNativeCanvasWrapper = initRaster(bitmap);
    mFinalizer = new CanvasFinalizer(mNativeCanvasWrapper);
    mBitmap = bitmap;
    mDensity = bitmap.mDensity;
}

方法传入了一个 Bitmap 作为参数来保持绘制图像的信息。使用 canvas 来进行绘制,

protected void drawOnBitmap(){
    Bitmap bitmap = Bitmap.createBitmap(800, 400, Bitmap.Config.ARGB_8888);
    Canvas canvas = new Canvas(bitmap);
    canvas.drawColor(Color.Blue);
    Paint paint = new Paint();
    paint.setColor(Color.RED);
    paint.setTextSize(60);
    canvas.drawText("This is my view", 150, 200, paint);
    mImageView.setImageBitmap(bitmap);
}

我们知道 onDraw( ) 的输入参数是一个 canvas,它与我们自己创建的 canvas 不同。这个系统传递给我们的 canvas 来自于 ViewRootImpl 的 Surface,在绘图时系统将会 SkBitmap 设置到 SkCanvas 中并返回与之对应 Canvas。所以,在 onDraw() 中也是有一个 Bitmap 的,只是这个 Bitmap 是由系统创建的罢了。
既然已经提到了 onDraw( ) 我们就利用 onDraw( )方法来画一些常见的图形。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值