三、View布局

View布局

前言

什么是layout布局?前面,我们通过measure测量得到了View的尺寸,那么View到底是放在哪个位置上的呢?这就是layout的功能,确定View在屏幕上的位置(通常是相对于其parent的位置)。

谁来布局

不同于measureView的布局并不是在View内部设置,而是在其parent内确定。这也是合理的,因为ViewGroup的作用就是管理View在其内部的布局。

明白了这个概念,下面我们来看看layout的一些相关方法。

layout相关的方法说明

1. layout(int l, int t, int r, int b)

Assign a size and position to a view and all of its descendants.

翻译:为View及其children指定尺寸和位置。

This is the second phase of the layout mechanism. (The first is measuring). In this phase, each parent calls layout on all of its children to position them. This is typically done using the child measurements that were stored in the measure pass().

翻译:这是布局机制的第二阶段(第一阶段是测量)。在这个阶段中,每个parent调用它所有childrenlayout方法,设置这些children的位置。

Derived classes should not override this method. Derived classes with children should override onLayout. In that method, they should call layout on each of their children.

翻译:子类不应该重写layout方法,而是重写onLayout方法。在onLayout方法中,它应该调用它所有的childrenlayout(int l, int t, int r, int b)方法

谁使用

Viewparent用来设置这个Viewparent中的位置。

用来做什么

parent设置Viewparent的位置。

2. onLayout(boolean changed, int left, int top, int right, int bottom)

Called from layout when this view should assign a size and position to each of its children.

翻译:在Viewlayout(int, int, int, int)中被调用,用于指定每个children的尺寸和位置。

谁使用

View自身的layout调用时,自动会被调用

用来做什么

通常是parent用来确定它的children在它里面的位置(相对位置)。

特别注意

第一个参数changed指的是:这个View的尺寸或(和)位置改变了,这通常表明可能进行了多次无意义的layout

其他参数都是child相对于parent的位置(相对位置)。

总结

  1. layout(int, int, int, int)parent调用,childparent中的位置
  2. parent则应该在onLayout(boolean, int, int, int, int)中调用其child.layout(int, int, int, int)

FrameLayout源码分析

自定义一个ViewGroup我也暂时没有好的想法,这里来分析一下Android自带的布局中最简单的FrameLayout

FrameLayout is designed to block out an area on the screen to display a single item. Generally, FrameLayout should be used to hold a single child view, because it can be difficult to organize child views in a way that’s scalable to different screen sizes without the children overlapping each other. You can, however, add multiple children to a FrameLayout and control their position within the FrameLayout by assigning gravity to each child, using the android:layout_gravity attribute.

翻译:FrameLayout被设计用来为单独的一个View划分出一个区域。通常FrameLayout应该用于包含单独的一个View,因为对于扩展到多个不同的屏幕尺寸来说,FrameLayout则很难组织它的child views以保证相互不重叠。当然一页可以添加多个childrenFrameLayout中,并且为每个child指定gravity属性来控制他们的位置。

Child views are drawn in a stack, with the most recently added child on top. The size of the FrameLayout is the size of its largest child (plus padding), visible or not (if the FrameLayout’s parent permits). Views that are android.view.View#GONE are used for sizing only if setMeasureAllChildren(boolean) setConsiderGoneChildrenWhenMeasuring() is set to true.

翻译:child views以栈形式绘制,最后添加的在最上层。FrameLayout和它尺寸最大的child尺寸相同(加上padding属性)。只有当setMeasureAllChildren(boolean) setConsiderGoneChildrenWhenMeasuring()被设置为true时,设置为GONEchild才会进行测量。

@RemoteView
public class FrameLayout extends ViewGroup {
    // 默认的gravity
    private static final int DEFAULT_CHILD_GRAVITY = Gravity.TOP | Gravity.START;

    // 是否强制测量所有的children,可以在xml中指定measureAllChildren
    boolean mMeasureAllChildren = false;

    // 下面四个属性,指的是foreground的padding,若foreground有padding,那么将影响最后的尺寸
    private int mForegroundPaddingLeft = 0;
    private int mForegroundPaddingTop = 0;
    private int mForegroundPaddingRight = 0;
    private int mForegroundPaddingBottom = 0;

    // 所有layout_width或layout_height指定了match_parent属性的children,用于2次计算尺寸
    private final ArrayList<View> mMatchParentChildren = new ArrayList<>(1);

    // 下面四个构造器
    public FrameLayout(@NonNull Context context) &#123;
        super(context);
    &#125;
    public FrameLayout(@NonNull Context context, @Nullable AttributeSet attrs) &#123;
        this(context, attrs, 0);
    &#125;
    public FrameLayout(@NonNull Context context, @Nullable AttributeSet attrs,
            @AttrRes int defStyleAttr) &#123;
        this(context, attrs, defStyleAttr, 0);
    &#125;
    public FrameLayout(@NonNull Context context, @Nullable AttributeSet attrs,
            @AttrRes int defStyleAttr, @StyleRes int defStyleRes) &#123;
        super(context, attrs, defStyleAttr, defStyleRes);
        final TypedArray a = context.obtainStyledAttributes(
                attrs, R.styleable.FrameLayout, defStyleAttr, defStyleRes);
        // 获取并设置measureAllChildren属性
        if (a.getBoolean(R.styleable.FrameLayout_measureAllChildren, false)) &#123;
            setMeasureAllChildren(true);
        &#125;
        a.recycle();
    &#125;

    /**
     * 描述前景色的gravity,默认是START|TOP,可以通过foregroundGravity属性设置
     */
    @android.view.RemotableViewMethod
    public void setForegroundGravity(int foregroundGravity) &#123;
        if (getForegroundGravity() != foregroundGravity) &#123;
            super.setForegroundGravity(foregroundGravity);

            // 获取foreground的padding属性,用于布局
            final Drawable foreground = getForeground();
            if (getForegroundGravity() == Gravity.FILL && foreground != null) &#123;
                Rect padding = new Rect();
                if (foreground.getPadding(padding)) &#123;
                    mForegroundPaddingLeft = padding.left;
                    mForegroundPaddingTop = padding.top;
                    mForegroundPaddingRight = padding.right;
                    mForegroundPaddingBottom = padding.bottom;
                &#125;
            &#125; else &#123;
                mForegroundPaddingLeft = 0;
                mForegroundPaddingTop = 0;
                mForegroundPaddingRight = 0;
                mForegroundPaddingBottom = 0;
            &#125;

            // 重新布局
            requestLayout();
        &#125;
    &#125;

    /**
     * 返回一个默认MATCH_PARENT的FrameLayout.LayoutParams
     * child没有LayoutParams时使用这个
     */
    @Override
    protected LayoutParams generateDefaultLayoutParams() &#123;
        return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
    &#125;

    /**
     * 下面四个是获取FrameLayout的padding,将foreground计算在内
     * Android内部使用,只用于FrameLayout和内部的屏幕layout
     */
    int getPaddingLeftWithForeground() &#123;
        return isForegroundInsidePadding() ? Math.max(mPaddingLeft, mForegroundPaddingLeft) :
            mPaddingLeft + mForegroundPaddingLeft;
    &#125;

    int getPaddingRightWithForeground() &#123;
        return isForegroundInsidePadding() ? Math.max(mPaddingRight, mForegroundPaddingRight) :
            mPaddingRight + mForegroundPaddingRight;
    &#125;

    private int getPaddingTopWithForeground() &#123;
        return isForegroundInsidePadding() ? Math.max(mPaddingTop, mForegroundPaddingTop) :
            mPaddingTop + mForegroundPaddingTop;
    &#125;

    private int getPaddingBottomWithForeground() &#123;
        return isForegroundInsidePadding() ? Math.max(mPaddingBottom, mForegroundPaddingBottom) :
            mPaddingBottom + mForegroundPaddingBottom;
    &#125;

    /**
     * 真正的measure过程
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) &#123;
        int count = getChildCount();

        // 这句话的意思是:FrameLayout的尺寸需要根据children的尺寸确定(长或宽不是确定的)
        // 那么就需要测量两次:
        // 1. 找出最大的child,FrameLayout尺寸根据最大的child得出
        // 2. 重新测量所有match_parent的children
        final boolean measureMatchParentChildren =
                MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
                MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
        // 用于存储需要重新测量的children(match_parent)
        mMatchParentChildren.clear();

        int maxHeight = 0;
        int maxWidth = 0;
        int childState = 0;

        for (int i = 0; i < count; i++) &#123;
            final View child = getChildAt(i);
            // View不是GONE,或者强制测量所有的children时才进行测量
            if (mMeasureAllChildren || child.getVisibility() != GONE) &#123;
                // 测量一个child
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                // 获取LayoutParams
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                // 计算得到maxWidth
                maxWidth = Math.max(maxWidth,
                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                // 计算得到maxHeight
                maxHeight = Math.max(maxHeight,
                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
                // 计算childState
                childState = combineMeasuredStates(childState, child.getMeasuredState());
                // 找出需要测量两次的children
                if (measureMatchParentChildren) &#123;
                    if (lp.width == LayoutParams.MATCH_PARENT ||
                            lp.height == LayoutParams.MATCH_PARENT) &#123;
                        mMatchParentChildren.add(child);
                    &#125;
                &#125;
            &#125;
        &#125;

        // 在宽高上加上FrameLayout本身的padding
        maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
        maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();

        // 宽高不能小于最小值(background和minHeight检测)
        maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

        // 宽高不能小于最小值(foreground检测)
        final Drawable drawable = getForeground();
        if (drawable != null) &#123;
            maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
            maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
        &#125;

        // 确定FrameLayout的尺寸
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));

        // 二次测量match_parent的children,下面的一些就不说了,说一下这个count
        count = mMatchParentChildren.size();
        // 这里判断count>1并不是count>0,是因为
        // 1.如果没有MATCH_PARENT,那么count==0
        // 2.如果有MATCH_PARENT,那么count=1的话,那么child就是尺寸最大的,不需要再次测量
        if (count > 1) &#123;
            for (int i = 0; i < count; i++) &#123;
                final View child = mMatchParentChildren.get(i);
                final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

                final int childWidthMeasureSpec;
                if (lp.width == LayoutParams.MATCH_PARENT) &#123;
                    final int width = Math.max(0, getMeasuredWidth()
                            - getPaddingLeftWithForeground() - getPaddingRightWithForeground()
                            - lp.leftMargin - lp.rightMargin);
                    childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                            width, MeasureSpec.EXACTLY);
                &#125; else &#123;
                    childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                            getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
                            lp.leftMargin + lp.rightMargin,
                            lp.width);
                &#125;

                final int childHeightMeasureSpec;
                if (lp.height == LayoutParams.MATCH_PARENT) &#123;
                    final int height = Math.max(0, getMeasuredHeight()
                            - getPaddingTopWithForeground() - getPaddingBottomWithForeground()
                            - lp.topMargin - lp.bottomMargin);
                    childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                            height, MeasureSpec.EXACTLY);
                &#125; else &#123;
                    childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
                            getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
                            lp.topMargin + lp.bottomMargin,
                            lp.height);
                &#125;

                child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            &#125;
        &#125;
    &#125;

    /**
     * 我们真正关心的地方
     */
    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) &#123;
        layoutChildren(left, top, right, bottom, false /* no force left gravity */);
    &#125;

    // 真正的逻辑执行地方
    void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) &#123;
        final int count = getChildCount();

        // 获取padding用于layout
        final int parentLeft = getPaddingLeftWithForeground();
        final int parentRight = right - left - getPaddingRightWithForeground();

        final int parentTop = getPaddingTopWithForeground();
        final int parentBottom = bottom - top - getPaddingBottomWithForeground();

        for (int i = 0; i < count; i++) &#123;
            final View child = getChildAt(i);
            // 只layout非GONE的View
            if (child.getVisibility() != GONE) &#123;
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();

                final int width = child.getMeasuredWidth();
                final int height = child.getMeasuredHeight();

                int childLeft;
                int childTop;

                int gravity = lp.gravity;
                // 若无gravity,则设置默认的gravity
                if (gravity == -1) &#123;
                    gravity = DEFAULT_CHILD_GRAVITY;
                &#125;

                // 布局方向,一般是从左至右
                final int layoutDirection = getLayoutDirection();
                // absoluteGravity,忽略布局方向,可以认为是horizontalGravity
                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;

                // 计算horizontal方向,即childLeft
                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) &#123;
                    // centerHorizontal,计算出在中间位置是的childLeft
                    case Gravity.CENTER_HORIZONTAL:
                        childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
                        lp.leftMargin - lp.rightMargin;
                        break;
                    // right/end,注意,实际上一定会执行下面的if
                    case Gravity.RIGHT:
                        if (!forceLeftGravity) &#123;
                            childLeft = parentRight - width - lp.rightMargin;
                            break;
                        &#125;
                    // 默认是start|top
                    case Gravity.LEFT:
                    default:
                        childLeft = parentLeft + lp.leftMargin;
                &#125;

                // 计算vertical方向,即childTop
                switch (verticalGravity) &#123;
                    case Gravity.TOP:
                        childTop = parentTop + lp.topMargin;
                        break;
                    case Gravity.CENTER_VERTICAL:
                        childTop = parentTop + (parentBottom - parentTop - height) / 2 +
                        lp.topMargin - lp.bottomMargin;
                        break;
                    case Gravity.BOTTOM:
                        childTop = parentBottom - height - lp.bottomMargin;
                        break;
                    default:
                        childTop = parentTop + lp.topMargin;
                &#125;

                // 进行layout
                child.layout(childLeft, childTop, childLeft + width, childTop + height);
            &#125;
        &#125;
    &#125;

    /**
     * 设置是否measure所有的children,默认false,不measure设置GONE的View
     */
    @android.view.RemotableViewMethod
    public void setMeasureAllChildren(boolean measureAll) &#123;
        mMeasureAllChildren = measureAll;
    &#125;

    /**
     * 返回是否measure所有的children,deprecated
     */
    @Deprecated
    public boolean getConsiderGoneChildrenWhenMeasuring() &#123;
        return getMeasureAllChildren();
    &#125;

    /**
     * 返回是否measure所有的children
     */
    public boolean getMeasureAllChildren() &#123;
        return mMeasureAllChildren;
    &#125;

    /**
     * 根据xml配置,获取LayoutParams
     */
    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) &#123;
        return new FrameLayout.LayoutParams(getContext(), attrs);
    &#125;

    /**
     * 不应该拦截press显示状态
     */
    @Override
    public boolean shouldDelayChildPressedState() &#123;
        return false;
    &#125;

    // 检查LayoutParams是否是FrameLayout.LayoutParams
    @Override
    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) &#123;
        return p instanceof LayoutParams;
    &#125;

    // 转换LayoutParams
    @Override
    protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) &#123;
        if (sPreserveMarginParamsInLayoutParamConversion) &#123;
            if (lp instanceof LayoutParams) &#123;
                return new LayoutParams((LayoutParams) lp);
            &#125; else if (lp instanceof MarginLayoutParams) &#123;
                return new LayoutParams((MarginLayoutParams) lp);
            &#125;
        &#125;
        return new LayoutParams(lp);
    &#125;

    // 用于accessibility
    @Override
    public CharSequence getAccessibilityClassName() &#123;
        return FrameLayout.class.getName();
    &#125;

    // 内部的方法
    @Override
    protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) &#123;
        super.encodeProperties(encoder);

        encoder.addProperty("measurement:measureAllChildren", mMeasureAllChildren);
        encoder.addProperty("padding:foregroundPaddingLeft", mForegroundPaddingLeft);
        encoder.addProperty("padding:foregroundPaddingTop", mForegroundPaddingTop);
        encoder.addProperty("padding:foregroundPaddingRight", mForegroundPaddingRight);
        encoder.addProperty("padding:foregroundPaddingBottom", mForegroundPaddingBottom);
    &#125;

    /**
     * children的LayoutParams,包含了布局属性margin和layout_gravity
     */
    public static class LayoutParams extends MarginLayoutParams &#123;
        /**
         * Value for &#123;@link #gravity&#125; indicating that a gravity has not been
         * explicitly specified.
         */
        public static final int UNSPECIFIED_GRAVITY = -1;

        public int gravity = UNSPECIFIED_GRAVITY;

        public LayoutParams(@NonNull Context c, @Nullable AttributeSet attrs) &#123;
            super(c, attrs);

            final TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.FrameLayout_Layout);
            gravity = a.getInt(R.styleable.FrameLayout_Layout_layout_gravity, UNSPECIFIED_GRAVITY);
            a.recycle();
        &#125;

        public LayoutParams(int width, int height) &#123;
            super(width, height);
        &#125;

        public LayoutParams(int width, int height, int gravity) &#123;
            super(width, height);
            this.gravity = gravity;
        &#125;

        public LayoutParams(@NonNull ViewGroup.LayoutParams source) &#123;
            super(source);
        &#125;

        public LayoutParams(@NonNull ViewGroup.MarginLayoutParams source) &#123;
            super(source);
        &#125;

        public LayoutParams(@NonNull LayoutParams source) &#123;
            super(source);

            this.gravity = source.gravity;
        &#125;
    &#125;
&#125;

FrameLayout代码分析完了,其实非常简单,需要注意的是,FrameLayout可能会对match_parentchildren进行两次测量。

我们这里着重需要关心的还是layout

  1. 考虑到了padding,所以我们在定义View时,注意padding是我们在定义时决定的,所以不要忘记
  2. 考虑marginmarginchild相对于在parent的,需要在ViewGroup控制
  3. 考虑childvisibility属性,如果是GONE,那么ViewGroup则不应该显示
  4. 计算出child的左上角的位置,自然就可以得到右下角的位置,调用child.layout(int, int, int, int),如此循环

自定义View需要注意的点

FrameLayout除了实现onMeasure(int, int)onLayout(boolean, int, int, int, int)之外,还重写了一些方法。FrameLayout本身是一个非常简单的ViewGroup,所以它的实现可以作为定义初级ViewGroup的参考。

LayoutParams generateDefaultLayoutParams()

Returns a set of default layout parameters. These parameters are requested when the View passed to {@link #addView(View)} has no layout parameters already set. If null is returned, an exception is thrown from addView.

翻译:返回默认的LayoutParams。其中的属性要求在addView(View)时传递,因为这时没有指定LayoutParams。如果为null,那么在调用addView(View)时会抛出异常。

LayoutParams generateLayoutParams(AttributeSet attrs)

Returns a new set of layout parameters based on the supplied attributes set.

翻译:返回读取AttributeSet属性的LayoutParams

这样可以在xml配置一些布局相关的属性,这些属性封装在自定义的LayoutParams中。

boolean shouldDelayChildPressedState()

Return true if the pressed state should be delayed for children or descendants of this ViewGroup. Generally, this should be done for containers that can scroll, such as a List. This prevents the pressed state from appearing when the user is actually trying to scroll the content.

翻译:如果ViewGroupchildren或子代(可能child是一个ViewGroup,其内又有View),如果返回true,表示自身将优先处理press状态。通常,这应该是在可以滑动的容器中返回true,例如一个ListView。这避免了press状态在用户实际上想滑动内容时提前出现。

boolean checkLayoutParams(ViewGroup.LayoutParams p)

检查LayoutParams是否是我们需要的类型。如果我们自定义了一个LayoutParams,那么我们应该应该实现这个方法,保证会配合generateLayoutParams(LayoutParams)生成我们自定义的LayoutParams

ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp)

Returns a safe set of layout parameters based on the supplied layout params. When a ViewGroup is passed a View whose layout params do not pass the test of {@link #checkLayoutParams(android.view.ViewGroup.LayoutParams)}, this method is invoked. This method should return a new set of layout params suitable for this ViewGroup, possibly by copying the appropriate attributes from the specified set of layout params.

翻译:基于提供的ViewGroup.LayoutParams返回一个类型安全的LayoutParams。当一个View添加到ViewGroup中,但是ViewLayoutParams并不能通过checkLayoutParams(ViewGroup.LayoutParams)检查,那么这个方法将会被调用。这个方法返回了一个新的LayoutParams(通常是自定义的),并且可能会从提供的ViewGroup.LayoutParams中复制一些合适的属性。

CharSequence getAccessibilityClassName()

Return the class name of this object to be used for accessibility purposes. Subclasses should only override this if they are implementing something that should be seen as a completely new class of view when used by accessibility, unrelated to the class it is deriving from. This is used to fill in AccessibilityNodeInfo#setClassName AccessibilityNodeInfo.setClassName.

大致翻译:返回用于accessibility目的当前的类名。如果需要提供这样的功能,那么子类应该实现这个方法。

LayoutParams

LayoutParams are used by views to tell their parents how they want to be laid out.

翻译:LayoutParamsview用于告诉他们的parent他们想要的布局信息。

我们知道LayoutParams包含了很多布局信息,在xml中通常是以android:layout_xxx的形式存在,如android:layout_gravity:start|top。在Android读取xml生成对应的View对象时,将一些属性赋予LayoutParams,那么Viewparent就可以根据LayoutParams来对它进行布局。

总结

其实layout是一个相对而言比较繁琐的工作。因为要考虑到各个方面,paddingmargingravity等等。更重要的是,通常LayoutLinearLayoutConstraintLayout会有自己的一套布局逻辑,这个逻辑可能非常的繁琐,如ConstraintLayout。逻辑相对简单的LinearLayout也有2000多行的代码。

layout的相关方法使用很简单,难的是需要实现自己的逻辑。这篇文章分析了简单的FrameLayout,我们在定义时,可以参考FrameLayout的实现,同时如果想要更好的定义ViewGroup,建议多阅读其他优秀实现的源码。

参考文章

小Demo小知识-android:foreground与android:background


   转载规则


《三、View布局》 Mycroft Wong 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
FileProvider FileProvider
FileProvider前言Android开发始终脱离不了图片处理,特别是Android 7.0开始,无法通过file:///的URI来进行在应用之间共享文件,取而代之的是content uri。这样必然增加了开发难度,如必须生成conte
2019-09-05
下一篇 
二、View测量 二、View测量
View测量前言自定义View实际上是Android给我们定下了一些规则,我们需要遵循这些规则去定义一个View,符合这个规则的View才会更好的显示。实际上,它并没有如Java的强类型般的限制我们怎么做,我们在使用中可能时长在破坏这些规则
2019-08-24
  目录