View基础与自定义

自定义View基础

view树绘制起点

android中大多数情况下我们的界面都是使用view进行显示,(目前我知道的除过使用surface进行显示的视图,其余的都是使用View进行显示,如果有其他方式大家可以一起讨论~)
首先介绍两个类的主要作用:

  • ViewRootImpl:
    视图层次结构的顶层,实现了WindowManager和View之间的协议,包括View的绘制过程,包括measure、layout、draw过程;event事件分发,如按键/触屏等事件。
  • DecorView:
    是界面中最顶层的View

android中界面使用view进行显示的时候界面可以看作是一颗树,父节点会包含对应的孩子节点,而这棵树的根节点为DecorView,界面上的视图都在这个父节点中显示。而Activity只负责生命周期和事件处理,不进行ui和视图控制相关的处理,视图的绘制和控制是由ViewRoot完成的。
当activity创建完毕后,会将ViewRoot和DecorView进行关联,

		root = new ViewRootImpl(view.getContext(), display);
        ...
        root.setView(view, wparams, panelParentView);

源码见WindowManagerGlobal.java,Activity创建的时候如何调用到这里可以参见博客 链接
ViewRoot和DecorView进行关联的时候调用了ViewRoot的setVIew方法,(ViewRoot实现对应于ViewRootImpl类,具体源码可以在该类中进行查看),setView方法中调用了requestLayout()方法,在这里插入图片描述
而requestLayout会调用scheduleTraversal,在scheduleTraversal方法中会发送mTraversalRunnable事件,
在这里插入图片描述
mTraversalRunnable中会执行doTraversal方法,该方法中会调用performTraversal方法,从该方法开始,view树开始调用performMeasure(),performLayout(),performDraw()进行绘制。
performTraversal绘制流程
View绘制流程
performTraversal()方法调用后会首先调用父容器的measure,layout,draw方法,然后递归调用到子View的measure,layout,draw方法。

MeasureSpec介绍

查看MeasureSpec的注解:
在这里插入图片描述
MeasureSpec携带了父view对子view宽高的要求,而该要求是通过view自身的layoutParams和父容器施加的转换规则转换得到的。
MeasureSpec实现时为了减少创建对象的开销,使用了数字进行实现。使用高两位表示SpecMode,使用低30位表示SpecSize,MeasureSpec在实现的时候也提供了相应的方法进行Mode和Size的提取,以及Mode和Size的组合。
该方法进行mode和size的组合

public static int makeMeasureSpec(int size,int mode) {
    if (sUseBrokenMakeMeasureSpec) {
        return size + mode;
    } else {
        return (size & ~MODE_MASK) | (mode & MODE_MASK);
    }
}

下面的两个方法则分别获得相应的size和mode

public static int getMode(int measureSpec) {
    //noinspection ResourceType
    return (measureSpec & MODE_MASK);
}

public static int getSize(int measureSpec) {
    return (measureSpec & ~MODE_MASK);
}

MeasureSpec在实现的时候规定了三种mode,如下:
在这里插入图片描述
UNSPECIFIED :这种情况下父容器不对view的大小做任何限制。

EXACTLY :父容器已经给出view的大小,这个时候SpecSize大小就是View最后的大小。这种情况对应于layoutparam为match_parent和具体数值的这两种情况,给出view一个明确的大小。

AT_MOST :父容器指定了一个SpecSize,View的大小不能大于这个值,具体是什么值需要看不用view的具体实现。这种情况对应于layoutparams为wrap_content,限定了view的范围,但是没有明确要求view的大小。
View在进行绘制的时候是以自身的MeasureSpec为标准的,而View 的MeasureSpec的是根据父容器的MeasureSpec和View自身的Layoutparams得到的,具体的方法参见getChildMeasureSpec()方法,将该方法中的规则总结出来表格如下:
在这里插入图片描述
而对于顶级View(DecorView),其MeasureSpec由自身的LayoutParams决定,具体可以参见方法ViewRootImpl中的getRootMeasureSpec()方法。

DecorView的测量规则
LayoutParamesSpecMode
MATCH_PARENTEXACTLY,大小就是窗口的大小
WRAP_CONTENTAT_MOST,大小不能超过屏幕大小
固定大小如(100px)精确模式,大小为指定的大小
将顶层view的测量可以总结成上表

View绘制流程介绍

measure

View的测量过程由measure方法开始,该方法为final方法,无法被继承,measure的过程主要由onMeasure方法完成,子类在使用的时候重写该方法即可,通过该方法我们可以使用viewgroup中传递过来的measureSpec来确定view测量出的大小:
android.view.View#onMeasure
在这里插入图片描述
查看上述代码,onMeasure()方法中会调用setMeasuredDimension方法保存view的宽和高,view的宽和高通过getDefaultSize和getSuggestedMinmumWidth方法获取。
ViewGroup的measure过程:
相比于view的measure过程,ViewGroup在measure的过程会调用子view的measure方法,进行子View的大小的测量,然后进行自身大小的测量
但是ViewGroup自身是一个抽象类,没有定义具体的测量过程,即没有实现onMeasure方法,因为不同布局的测量方式不同,比如说RelativeLayout和LinearLayout的测量方式必然不同,所以onMeasure的具体过程需要子类去自己实现。以LinearLayout为例

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    if (mOrientation == VERTICAL) {
        measureVertical(widthMeasureSpec, heightMeasureSpec);
    } else {
        measureHorizontal(widthMeasureSpec, heightMeasureSpec);
    }
}

onMeasure方法会区分当前配置,进行垂直方向或者竖直方向的测量

void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
    mTotalLength = 0;
    ...
    // 循环遍历所有child,调用View#measure进行测量
    // See how tall everyone is. Also remember max width.
    for (int i = 0; i < count; ++i) {
        final View child = getVirtualChildAt(i);
        ...
        if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
            ...
        } else {
            ...
            // 测量child的宽高
            measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                    heightMeasureSpec, usedHeight);
            // child测量完成后,获取测量后的高度
            final int childHeight = child.getMeasuredHeight();
            ...

            // 将child的高度累加到mTotalLength中
            mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                   lp.bottomMargin + getNextLocationOffset(child));
        }
        ...
    }

    ...
    
    // 调用View#setMeasuredDimension方法设置LinearLayout的的测量宽高
    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
            heightSizeAndState);
    ...
}

上面的测量流程中

  1. 先for循环遍历子View,测量子View的大小
  2. 子View测量完毕然后进行自身大小的测量。

其它博客:measure流程介绍:
https://blog.csdn.net/a553181867/article/details/51494058

layout

Layout方法用来确定view自身的位置,而onLayout方法用来确认所有的子元素的位置,在layout方法中会调用setFrame方法首先确定自身的位置,

public void layout(int l, int t, int r, int b) {
    ...
    boolean changed = isLayoutModeOptical(mParent) ?
            setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
...
    onLayout(changed, l, t, r, b);

然后调用onLayout方法布局孩子的位置,

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {}

而onLayout方法可以看到view自身并没有实现,因为每一个布局自己对自己的孩子的布局方式不同,具体的实现需要相应的子类去重写。
例如在LinearLayout的onLayout中,会根据当前布局样式显示对应的布局:

    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 和 layoutHorizontal方法中会调用子View的Layout方法进行布局。

void layoutVertical(int left, int top, int right, int bottom) {
    ...
    for (int i = 0; i < count; i++) {
        final View child = getVirtualChildAt(i);
        if (child == null) {
            childTop += measureNullChild(i);
        } else if (child.getVisibility() != GONE) {
            ...
            // 这里的setChildFrame方法中调用的就是child.layout方法
            setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                    childWidth, childHeight);
            ...
        }
    }
}

setChildFrame方法中调用的就是child.layout方法

其他博客:layout详细过程:
https://blog.csdn.net/a553181867/article/details/51524527
https://blog.csdn.net/qq_26287435/article/details/94402563

draw

View在绘制的时候分为如下几步:

...
    drawBackground(canvas);  // 1.绘制背景
...
    if (!dirtyOpaque) onDraw(canvas);// 2.绘制自身内容

    dispatchDraw(canvas);//3.绘制孩子(该方法需要子类重写,从而根据子类自身的规则去绘制相应的view)

    drawAutofilledHighlight(canvas);// 4.绘制相应的装饰

    if (mOverlay != null && !mOverlay.isEmpty()) {
        mOverlay.getOverlayView().dispatchDraw(canvas); // 4.绘制相应的装饰
    }

    onDrawForeground(canvas);// 4.绘制相应的装饰

    drawDefaultFocusHighlight(canvas);// 4.绘制相应的装饰

ViewGroup在绘制的时候setWillNotDraw()会在中设置为true,用来优化绘制,如果我们需要在ViewGroup中绘制内容时,需setWillNotDraw()为false来关闭优化。
View的绘制流程可以概括为如下:

  1. 先进行自身当前View的绘制调用draw方法,onDraw中对应实现
  2. 然后进行子View的绘制,对于View的话dispatchDraw方法为空实现,ViewGroup会实现该方法,dispatchDraw中调用子类的绘制。

自定义View

实现效果: 支持padding,支持自定义属性实现颜色的自定义,同时支持wrap_content。
在这里插入图片描述
通过重写onDraw()我们去实现一个圆形,代码实现比较简单,但是我们需要注意
1:对直接继承自View和ViewGroup的控件,padding属性是默认无法生效的,需要自己处理。
2:继承View和ViewGroup的控件,自己需要处理wrap_content属性
处理padding属性无效的问题:在我们确定宽和高的大小进行绘制的时候我们需要将padding计算在内

int width = getWidth() - getPaddingLeft() - getPaddingRight();
int height = getHeight() - getPaddingTop() - getPaddingBottom();

处理wrap_content无效的问题
通过上表:在这里插入图片描述我们可以看到当设置为wrap_content的时候specSize为parentSize,查看getDefaultSize的源码,mode为AT_MOST的时候view的大小就是specSize,所以设置wrap_content等价于match_parent:
在这里插入图片描述
因此当我们自定义view使用wrap_content属性的时候,我们需要自己处理,处理的代码如下:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);

        // 下面的过程处理view的书信给wrap_content
        if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(200, 200);
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(200, heightSpecSize);
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSpecSize, 200);
        }
    }

上面的代码中,当我们判断view的specMode为AT_MOST的时候,我们设置大小为200(这个值可以自己根据自己的需要设置),通过上面的处理方式我们解决wrap_content不生效的问题。
看到这里自己脑子里面突然有个疑问在这里插入图片描述
红色的圈出来的那一部分也是会让view的specMode为AT_MOST,那我们重写了view对AT_MOST的处理,当我们设置view为match_parent的时候那岂不match_parent没办法生效了?
自己想了想,什么时候父容器的specMode为AT_MOST呢,查看上表《DecorView的测量规则》,只有当我们设置为wrap_content属性的时候才是AT_MOST,那这就意味大小只要满足孩子的大小即可,即我们自己实现的wrap_content。

增加自定义属性

首先我们在values目录 下面新建一个自定义属性文件,文件名字可以随意起,在这里我新建一个attrs.xml文件,文件中我们自定义自己需要的属性,这里定义了color属性:

    <declare-styleable name="CircleView">
        <attr name="circle_color" format="color" />
    </declare-styleable>

上面的format中我们还可以填写如下值,

format含义
reference代表资源id
dimension尺寸
基本数据类型(string/integer/boolean)需使用者自己定义
代码中我们通过下面的方式使用
在xml跟布局上面增加一个app命名空间,然后我们在该命名空间下使用我们自定义的属性
<LinearLayout ...
    xmlns:app="http://schemas.android.com/apk/res-auto"
	...>
	<com.example.customizeview.CircleView
        ...
        app:circle_color="@color/colorAccent" />
</LinearLayout>

最后我们需要在代码中获取我们自定义的属性

 public CircleView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        Log.i(TAG, "CircleView: enter constructor with attrs");
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
        mColor = typedArray.getColor(R.styleable.CircleView_circle_color, Color.BLUE);
        typedArray.recycle();
        init();
    }

mColor就是我们自己定义的颜色。
完整代码见 CircleView

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Android自定义View圆形刻度在实现上相对简单,主要步骤如下: 1. 创建一个继承自View自定义View类,命名为CircleScaleView。 2. 在该自定义View的构造方法中完成必要的初始化工作,例如设置画笔、设置View的宽高、设置绘制模式等。 3. 重写onMeasure()方法,设置View的尺寸大小。可以根据自定义的需求来决定View的宽高。 4. 重写onDraw()方法,完成绘制整个圆形刻度的逻辑。 5. 在onDraw()方法中,首先通过getMeasuredWidth()和getMeasuredHeight()方法获取到View的宽高,然后计算圆心的坐标。 6. 接着,使用Canvas对象的drawArc()方法来绘制圆弧,根据需求设置圆弧的起始角度和扫描角度。 7. 再然后,通过循环绘制每个刻度线,可以使用Canvas对象的drawLine()方法来绘制。 8. 最后,根据需要绘制刻度值或其他其他附加元素,例如圆心的标记。 9. 至此,整个圆形刻度的绘制逻辑就完成了。 10. 在使用该自定义View的时候,可以通过添加该View到布局文件中或者在代码中动态添加,并按需设置相应的属性。 需要注意的是,自定义圆形刻度的具体样式和行为取决于项目需求,上述步骤仅为基础实现框架,具体细节需要根据实际情况进行相应的调整。 ### 回答2: 在Android中实现一个圆形刻度的自定义View有几个步骤。 首先,我们需要创建一个自定义View类,继承自View或者它的子类(如ImageView)。 接下来,在自定义View的构造方法中,初始化一些必要的属性,比如画笔的颜色、宽度等。我们可以使用Paint类来设置这些属性。 然后,我们需要在自定义View的onMeasure方法中设置View的宽度和高度,确保View在屏幕上正常显示。一种常见的实现方式是将宽度和高度设置为相同的值,使得View呈现出圆形的形状。 接着,在自定义View的onDraw方法中,我们可以利用画笔来绘制圆形刻度。可以使用canvas.drawCircle方法来绘制一个圆形,使用canvas.drawLine方法绘制刻度线。我们可以根据需要,定义不同的刻度颜色和宽度。 最后,我们可以在自定义View的其他方法中,添加一些额外的逻辑。比如,在onTouchEvent方法中处理触摸事件,以实现拖动刻度的功能;在onSizeChanged方法中根据View的尺寸调整刻度的大小等等。 当我们完成了自定义View的代码编写后,我们可以在布局文件中使用这个自定义View。通过设置布局文件中的属性,可以进一步自定义View的外观和行为。 总之,实现一个圆形刻度的自定义View,我们需要定义一个自定义View类,并在其中使用画笔来绘制圆形和刻度。通过处理一些事件和属性,我们可以实现更多的功能和样式。以上就是简单的步骤,可以根据需要进行更加详细的实现。 ### 回答3: Android自定义View圆形刻度可以通过以下步骤实现。 首先,我们需要创建一个自定义View,继承自View类,并重写onDraw方法。在该方法中,我们可以自定义绘制的内容。 其次,我们需要定义一个圆形的刻度尺背景,可以使用Canvas类提供的drawCircle方法来绘制实心圆或空心圆。 接着,我们可以通过Canvas类的drawLine方法来绘制刻度线。根据刻度的数量,可以计算出每个刻度之间的角度,然后循环绘制出所有的刻度线。 然后,我们可以通过Canvas类的drawText方法来绘制刻度的值。根据刻度线的角度和半径,可以计算出刻度的坐标,然后将刻度的值绘制在指定的位置上。 最后,我们可以通过在自定义View的构造方法中获取相关的参数,如刻度的最大值、最小值、当前值等,然后根据这些参数来计算刻度的位置和值。 在使用自定义View时,可以通过设置相关的属性来改变刻度的样式和位置。例如,可以设置刻度线的颜色、粗细、长度等,也可以设置刻度值的颜色、大小等。 通过以上步骤,我们就可以实现一个圆形刻度尺的自定义View。在使用时,可以根据需要自行调整绘制的样式和逻辑。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值