自定义View
来源:互联网 发布:毛孔收缩精华液知乎 编辑:程序博客网 时间:2024/06/08 06:12
自定义View解析
我们很经常是会使用View,但是系统系统的View毕竟是有限的,可能无法满足我们的需求,所以自定义View就显得十分有必要了。那么如何实现一个自定义View呢?想想,我们在画图干什么?要绘制的图有多大?画的图画在哪里?然后画成怎么样?在Android自定义View中的步骤也是这样。凭借着onMeasure(),onLayout(),onDraw()这三种方法进行绘画。
先看看view的结构,可以发现它是继承自ViewGroup的
再来看看View的绘制流程
西面我们一步一步来分析
1. onMeasure()
measure就是测量的意思,该方法主要就是用来测量视图的大小。View绘制流程会从ViewRoot的performTraversals()方法开始(ViewRoot不是一个View,而是一个Handler)。然后在其内部调用View的Measure()方法。而Measure()方法接受两个参数,分别是widthMeasureSpec和heighMeasureSpec,这两个参数用来确定视图的宽度和高度的规格大小。
在测量中,我们还需要理解MeasureSpec,它是View的一个内部类。
MeasureSpec的值由记录大小的specSize和记录规格的specMode共同组成的,
specMode一共有三种类型,如下所示:
1. EXACTLY
表示父视图希望子视图的大小应该是由specSize的值来决定的。
2. AT_MOST
父视图指定一个可用大小的SpecSize,View大小不能大于这个值
3. UNSPECIFIED
父视图对View没有任何限制。这种情况一般用于系统内部。
而widthMeasureSpec和heighMeasureSpec的值是父视图通过计算然后传递给子视图的。说明父视图一定程度上决定子视图的大小。而根视图的specSize都等于windowSize,意味着会根视图会充满全屏。
介绍完了MeasureSpec相关内容,接下来来看看View的measure方法
public final void measure(int widthMeasureSpec, int heightMeasureSpec) { if ((mPrivateFlags & FORCE_LAYOUT) == FORCE_LAYOUT || widthMeasureSpec != mOldWidthMeasureSpec || heightMeasureSpec != mOldHeightMeasureSpec) { mPrivateFlags &= ~MEASURED_DIMENSION_SET; if (ViewDebug.TRACE_HIERARCHY) { ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_MEASURE); } // 这里调用了onMeasure方法 onMeasure(widthMeasureSpec, heightMeasureSpec); if ((mPrivateFlags & MEASURED_DIMENSION_SET) != MEASURED_DIMENSION_SET) { throw new IllegalStateException("onMeasure() did not set the" + " measured dimension by calling" + " setMeasuredDimension()"); } mPrivateFlags |= LAYOUT_REQUIRED; } mOldWidthMeasureSpec = widthMeasureSpec; mOldHeightMeasureSpec = heightMeasureSpec;}
可以发现measure方法是final的,我们没有办法改写这一个方法,但是我们发现在该方法中调用了onMeasure方法,这里才是设施View大小的地方
那么下面来看看View的onMeasure方法,
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }
代码很简单,通过getDefaultSize方法来或许大小。接下来看看该方法
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的高和宽都是又measureSpec决定的。这里可以发现,只要重写onMeasure方法就能确定View的大小。
当然,一个界面的展示会设计多次measure,因为一个布局中会包含多个子视图,每个视图都要经历一次measure过程。所以ViewGroup定义了一个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); } }}
从代码中可以看到,首先是获取所有子视图,然后在调用measureChild的方法测量
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);}
可以看到这里通过getChildMeasureSpec的方法计算大小,然后在最后调用measure方法,之后就是前面介绍的一样了。那么我们绘制View的第一步就解决了
2.onLayout()
measure过程后,视图的大小就已经测量好了。接下来就是确定视图的位置了。onLayout方法的作用就是在于这里。
先来看看View的layout方法
public void layout(int l, int t, int r, int b) { int oldL = mLeft; int oldT = mTop; int oldB = mBottom; int oldR = mRight; // boolean changed = setFrame(l, t, r, b); if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) { if (ViewDebug.TRACE_HIERARCHY) { ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT); } onLayout(changed, l, t, r, b); mPrivateFlags &= ~LAYOUT_REQUIRED; if (mOnLayoutChangeListeners != null) { ArrayList<OnLayoutChangeListener> listenersCopy = (ArrayList<OnLayoutChangeListener>) 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 &= ~FORCE_LAYOUT; }
这里重点是关注changed这个变量。setFrame方法是用来判断视图大小是否发生变化,用来确定当前视图是否要进行重绘。之后会调用onLayout这个方法。
这里的onLayout方法是一个空方法,因为它的实现和具体布局有关。那么我们通过FrameLayout的onLayout方法来看看是怎么实现的。
protected void onLayout(boolean changed, int left, int top, int right, int bottom) { layoutChildren(left, top, right, bottom, false /* no force left gravity */); } void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) { final int count = getChildCount(); // 获取父内边距的padding值 final int parentLeft = getPaddingLeftWithForeground(); final int parentRight = right - left - getPaddingRightWithForeground(); final int parentTop = getPaddingTopWithForeground(); final int parentBottom = bottom - top - getPaddingBottomWithForeground(); // 遍历子View,确定子View的属性。 for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (child.getVisibility() != GONE) { final LayoutParams lp = (LayoutParams) child.getLayoutParams(); final int width = child.getMeasuredWidth(); final int height = child.getMeasuredHeight(); int childLeft; int childTop; int gravity = lp.gravity; if (gravity == -1) { gravity = DEFAULT_CHILD_GRAVITY; } final int layoutDirection = getLayoutDirection(); final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection); final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK; switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { case Gravity.CENTER_HORIZONTAL: childLeft = parentLeft + (parentRight - parentLeft - width) / 2 + lp.leftMargin - lp.rightMargin; break; case Gravity.RIGHT: if (!forceLeftGravity) { childLeft = parentRight - width - lp.rightMargin; break; } case Gravity.LEFT: default: childLeft = parentLeft + lp.leftMargin; } switch (verticalGravity) { 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; } 对View进行布局 child.layout(childLeft, childTop, childLeft + width, childTop + height); } } }
步骤可以分为
1、获取父View的内边距padding的值
2、遍历子View,处理子View的layout_gravity属性、根据View测量后的宽和高、父View的padding值、来确定子View的布局参数,
3、调用child.layout方法,对子View进行布局
可以发现,View通过layout方法确认自己的在父视图中的位置,ViewGroup通过onLayout方法确定View在视图的位置。
在onLayout()过程结束后,我们就可以通过getWidth()和getHeight()方法来获取视图的宽高了。上面onLayout()中使用了getMeasureWidth()方法。那么他们有什么区别呢?
首先getMeasureWidth()方法在measure()过程结束后就可以获取到了,而getWidth()方法要在layout()过程结束后才能获取到。另外,getMeasureWidth()方法中的值是通过setMeasuredDimension()方法来进行设置的,而getWidth()方法中的值则是通过视图右边的坐标减去左边的坐标计算出来的。
绘制View的第二部也完成了。
3.onDraw()
measure和layout结束后,接下来就进入视图的绘制过程了。顾名思义Draw就是绘画的意思
那么我们来看看onDraw方法
public void draw(Canvas canvas) { final int privateFlags = mPrivateFlags; final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE && (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState); mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN; // Step 1, draw the background, if needed int saveCount; if (!dirtyOpaque) { drawBackground(canvas); } // skip step 2 & 5 if possible (common case) final int viewFlags = mViewFlags; boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0; boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0; if (!verticalEdges && !horizontalEdges) { // Step 3, draw the content if (!dirtyOpaque) onDraw(canvas); // Step 4, draw the children dispatchDraw(canvas); // Overlay is part of the content and draws beneath Foreground if (mOverlay != null && !mOverlay.isEmpty()) { mOverlay.getOverlayView().dispatchDraw(canvas); } // Step 6, draw decorations (foreground, scrollbars) onDrawForeground(canvas); // we're done... return; } ... ... }
可以看到,绘制过程一共分为6步。
1. 绘制背景,这里就是通过在XML设置background属性获取的颜色或者图片,或者是通过setBackgroundResource设置的颜色或者图片
3. 绘制内容,这里动用了onDraw方法,这是个空方法,每个视图的内容都不同,所以这功能交给子类完成
4. 绘制子视图,这里的dispatchDraw也是个空方法,不过在ViewGroup中就有该方法的绘制代码
6. 绘制滚动条等,任何一个视图都是有滚动条的,只是一般情况下我们都没有让它显示出来而已
那么只用重写onDraw方法,自定义view就完成了。
- 自定义view
- 自定义View
- 自定义view
- 自定义View
- 自定义View
- 自定义view
- 自定义View
- 自定义view
- 自定义view
- 自定义View
- 自定义View
- 自定义view
- 自定义view
- 自定义view
- 自定义view
- 自定义view
- 自定义View
- 自定义View
- STL之sort、priority_queue 排序
- oracle建立索引
- 计算机热点方向、历史、未来
- c++常用知识点5
- birt基础使用
- 自定义View
- 设计模式之模板方法模式
- Struts2教程之一基本使用
- [jzoj]3875. 【NOIP2014八校联考第4场第2试10.20】星球联盟(alliance)(图论题,构树+缩点+LCA+并查集)
- 使用VSCode调试.NetCore
- 连通图遍历策略之深度优先搜索(C语言)
- D
- matplotlib模块数据可视化-多图
- C++自定义插入操作符和提取操作符