android view
控件树
- activity->PhoneWindow->DecorView(根view,LinearLayout)->TitleView+ContentView(FrameLayout)
LayoutInflater 加载布局
- 创建:
LayoutInflater.from(context);
即(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - 加载xml布局:layoutInflater.inflate(resourceId, root);
pull方式解析xml,根据节点反射创建view - inflate(int resource, ViewGroup root, boolean attachToRoot)
如果root为null,attachToRoot将失去作用,设置任何值都没有意义。
如果root不为null,attachToRoot为true,则会给加载的布局文件的指定父布局root。
如果root不为null,attachToRoot为false,则会将布局文件最外层的所有layout属性进行设置,当该view被添加到父view当中时,这些layout属性会自动生效。
在不设置attachToRoot参数的情况下,如果root不为null,attachToRoot参数默认为true。
view绘制流程 ViewRootImpl
- ActivityThread中,Activity创建完成后,将DecorView添加到Window中,同时创建ViewRootImpl对象,并建立两者的关联
- View的绘制流程从ViewRoot的performTraversals方法开始,经过measure、layout和draw三大流程
private void performTraversals() { ...... //最外层的根视图的widthMeasureSpec和heightMeasureSpec由来 //lp.width和lp.height在创建ViewGroup实例时等于MATCH_PARENT int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height); ...... mView.measure(childWidthMeasureSpec, childHeightMeasureSpec); ...... mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight()); ...... mView.draw(canvas); ...... } - measure过程决定了view的宽高,在几乎所有的情况下这个宽高都等同于view最终的宽高,通过getMeasuredHeight()获取
layout过程决定了view的四个顶点的坐标和view实际的宽高,通过getWidth和getHeight方法可以得到最终的宽高
draw过程决定了view的显示
测量
- MeasureSpec
- specMode:
exactly精确模式:父视图希望子视图的大小应该是由specSize的值来决定的
at_most最大模式:子视图最多只能是specSize中指定的大小
unspecified:可以将视图按照自己的意愿设置成任意的大小,没有任何限制 - 父容器的MeasureSpec和自身的LayoutParams-->MeasureSpec-->确定View测量后的宽高
固定宽高:精确模式,大小是LayoutParams中的大小
match_parent+父容器精确模式:精确模式,大小是父容器的剩余空间
match_parent+父容器最大模式:最大模式,大小不会超过父容器的剩余空间
wrap_content:最大模式,大小不超过父容器的剩余空间
- specMode:
- 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(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; } - onMeasure() 设置大小setMeasuredDimension()
layout中会遍历视图树调用layout.measureChild(childView, widthMeasureSpec, heightMeasureSpec);//View.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; } - 自定义的view原有的onMeasure不支持wrap_content
//重写 支持wrap_content //match_parent,wrap_content 由系统测量并传入match_parent值 //exactly 设置明确的宽度和高度时,传入设置的值 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int width; int height; if (widthMode == MeasureSpec.EXACTLY) { //传入准确值 width = widthSize; } else { mPaint.setTextSize(mTitleTextSize); mPaint.getTextBounds(mTitle, 0, mTitle.length(), mBounds); //内容大小 float textWidth = mBounds.width(); //内容+边距 int desired = (int) (getPaddingLeft() + textWidth + getPaddingRight()); width = desired; } if (heightMode == MeasureSpec.EXACTLY) { //传入准确值 height = heightSize; } else { mPaint.setTextSize(mTitleTextSize); mPaint.getTextBounds(mTitle, 0, mTitle.length(), mBounds); float textHeight = mBounds.height(); int desired = (int) (getPaddingTop() + textHeight + getPaddingBottom()); height = desired; } //调用设置大小的接口 setMeasuredDimension(width, height); }
布局
- 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; } - onLayout() 由ViewGroup的子类根据各自的规则重写
遍历视图树调用childView.layout(childLeft, childTop, childLeft + childView.getMeasuredWidth(), childTop + childHeight)@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { //遍历childView.layout() if (getChildCount() > 0) { View childView = getChildAt(0); childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight()); } } - getWidth()与getMeasureWidth()
setMeasuredDimension()-->getMeasureWidth()
childView.layout()------>getWidth()视图右边的坐标减去左边的坐标
绘制
- draw()
//绘制背景 //绘制内容 onDraw(canvas)由子类实现 //绘制子视图 dispatchDraw(canvas)由ViewGroup实现 //绘制滚动条 onDrawScrollBars(canvas) public void draw(Canvas canvas) { if (ViewDebug.TRACE_HIERARCHY) { ViewDebug.trace(this, ViewDebug.HierarchyTraceType.DRAW); } final int privateFlags = mPrivateFlags; final boolean dirtyOpaque = (privateFlags & DIRTY_MASK) == DIRTY_OPAQUE && (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState); mPrivateFlags = (privateFlags & ~DIRTY_MASK) | DRAWN; //对视图的背景进行绘制 int saveCount; if (!dirtyOpaque) { final Drawable background = mBGDrawable; if (background != null) { final int scrollX = mScrollX; final int scrollY = mScrollY; if (mBackgroundSizeChanged) { background.setBounds(0, 0, mRight - mLeft, mBottom - mTop); mBackgroundSizeChanged = false; } if ((scrollX | scrollY) == 0) { background.draw(canvas); } else { canvas.translate(scrollX, scrollY); background.draw(canvas); canvas.translate(-scrollX, -scrollY); } } } final int viewFlags = mViewFlags; boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0; boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0; if (!verticalEdges && !horizontalEdges) { //对视图的内容进行绘制 if (!dirtyOpaque) onDraw(canvas); // draw the children dispatchDraw(canvas); // 对视图的滚动条进行绘制 onDrawScrollBars(canvas); return; } }
视图重绘
- 视图状态变化 setVisibility()... -->invalidate()
- enabled表示当前视图是否可用,不可用的视图是无法响应onTouch事件的
- focused表示当前视图是否获得到焦点
通常情况下有两种方法可以让视图获得焦点,即通过键盘的上下左右键切换视图,以及调用requestFocus()方法
一般只有视图在focusable和focusable in touch mode同时成立的情况下才能成功获取焦点 - window_focused表示当前视图是否处于正在交互的窗口中,由系统自动决定,应用程序不能进行改变。
- selected表示当前视图是否处于选中状态。一个界面当中可以有多个视图处于选中状态
- pressed表示当前视图是否处于按下状态。通常情况下由系统自动赋值
- invalidate()-->performTraversals()只进行draw流程(可以requestLayout()强制重新走一遍)
void invalidate(boolean invalidateCache) { if (ViewDebug.TRACE_HIERARCHY) { ViewDebug.trace(this, ViewDebug.HierarchyTraceType.INVALIDATE); } //skipInvalidate()判断当前View是否需要重绘 //如果View是不可见的且没有执行任何动画,就认为不需要重绘 if (skipInvalidate()) { return; } if ((mPrivateFlags & (DRAWN | HAS_BOUNDS)) == (DRAWN | HAS_BOUNDS) || (invalidateCache && (mPrivateFlags & DRAWING_CACHE_VALID) == DRAWING_CACHE_VALID) || (mPrivateFlags & INVALIDATED) != INVALIDATED || isOpaque() != mLastIsOpaque) { mLastIsOpaque = isOpaque(); mPrivateFlags &= ~DRAWN; mPrivateFlags |= DIRTY; if (invalidateCache) { mPrivateFlags |= INVALIDATED; mPrivateFlags &= ~DRAWING_CACHE_VALID; } final AttachInfo ai = mAttachInfo; final ViewParent p = mParent; if (!HardwareRenderer.RENDER_DIRTY_REGIONS) { if (p != null && ai != null && ai.mHardwareAccelerated) { p.invalidateChild(this, null); return; } } if (p != null && ai != null) { final Rect r = ai.mTmpInvalRect; r.set(0, 0, mRight - mLeft, mBottom - mTop); p.invalidateChild(this, r); } } }
自定义控件
- 绘制视图
重写onDraw(canvas) - 布局
重写onMeasure() 测量子控件measureChild() 设置大小setMeasuredDimension()
onLayout() 设置子控件位置childView.layout() - 组合控件
定义xml布局
在构造器中加载布局进父布局 LayoutInflater.from(context).inflate(R.layout.layout_name, this); - 自定义属性
定义命名空间 xmlns:app="http://schemas.android.com/apk/res-auto"
获取属性集合 TypedArray typeArray = context.obtainStyledAttributes(attrs,R.styleable.MyView);
获取属性 int textColor = typeArray.getColor(R.styleable.MyView_textColor,defaultValue);
关闭资源 typeArray.recycle();//定义命名空间 xmlns:app="http://schemas.android.com/apk/res-auto" //使用 <com.test.MyView app:textColor="#ff0000" /> //自定义View public MyView(Context context,AttributeSet attrs, int defStyle) { super(context,attrs); /*这里取得declare-styleable集合*/ TypedArray typeArray = context. obtainStyledAttributes(attrs,R.styleable.MyView); //TypedArray typeArray = context.getTheme(). // obtainStyledAttributes(attrs,R.styleable.MyView, defStyle, 0); /*这里从集合里取出相对应的属性值,第二参数是如果使用者没用配置该属性时所用的默认值*/ int textColor = typeArray.getColor(R.styleable.MyView_textColor,0XFFFFFFFF); float textSize = typeArray.getDimension(R.styleable.MyView_textSize, 36); // int n = typeArray.getIndexCount(); for (int i = 0; i < n; i++) { int attr = typeArray.getIndex(i); switch (attr) { case R.styleable.MyView_textSize: { textSize = a.Dimension(attr); break; } } } /*关闭资源*/ typeArray.recycle(); }
Implementing a Custom View
- Creation 创建
- Constructors 构造器
There is a form of the constructor that are called when the view is created from code and a form that is called when the view is inflated from a layout file. The second form should parse and apply any attributes defined in the layout file.
- onFinishInflate() XML加载完毕
Called after a view and all of its children has been inflated from XML.
- Constructors 构造器
- Layout 布局
- onMeasure(int, int) 测量
Called to determine the size requirements for this view and all of its children.
- onLayout(boolean, int, int, int, int) 布局
Called when this view should assign a size and position to all of its children.
- onSizeChanged(int, int, int, int) 视图大小改变
Called when the size of this view has changed.
- onMeasure(int, int) 测量
- Drawing 绘制
- onDraw(android.graphics.Canvas)
Called when the view should render its content.
- onDraw(android.graphics.Canvas)
- Event 事件响应
- processing onKeyDown(int, KeyEvent) 手机键
Called when a new hardware key event occurs.
- onKeyUp(int, KeyEvent) 手机键
Called when a hardware key up event occurs.
- onTrackballEvent(MotionEvent) 轨迹球
Called when a trackball motion event occurs.
- onTouchEvent(MotionEvent) 触摸
Called when a touch screen motion event occurs.
- processing onKeyDown(int, KeyEvent) 手机键
- Focus 焦点改变
- onFocusChanged(boolean, int, android.graphics.Rect)
Called when the view gains or loses focus.
- onWindowFocusChanged(boolean)
Called when the window containing the view gains or loses focus.
- onFocusChanged(boolean, int, android.graphics.Rect)
- Attaching View关联/解除关联Window window可见性
- onAttachedToWindow()
Called when the view is attached to a window.
- onDetachedFromWindow()
Called when the view is detached from its window.
- onWindowVisibilityChanged(int)
Called when the visibility of the window containing the view has changed.
- onAttachedToWindow()
IDs
-
android:id="@+id/my_button" // Button myButton = (Button) findViewById(R.id.my_button);
Position Size padding margins
- Position
getLeft() and getTop() getRight() and getBottom()
- Size
- 测量时getMeasuredWidth() and getMeasuredHeight()
- 实际getWidth() and getHeight()
- padding
- setPadding(int, int, int, int) or setPaddingRelative(int, int, int, int)
- getPaddingLeft(), getPaddingTop(), getPaddingRight(), getPaddingBottom(), getPaddingStart(), getPaddingEnd()
- margins
- ViewGroup.MarginLayoutParams
Scrolling
- scrollBy(int, int), scrollTo(int, int), and awakenScrollBars()
Focus TouchMode
- Touch Mode
- 当用户开始通过键盘与设备交互的时候,设备就退出Touch Mode模式;当用户开始通过触摸屏与设备交互的时候,设备就进入Touch Mode模式。可以通过调用View的isInTouchMode来判断设备当前是否处于Touch Mode模式。
- Android规定,某些元素,即使是在Touch Mode模式下,也可以获得焦点。调用View的setFocusableInTouchMode(true)可以使View在Touch Mode模式之下仍然可获得焦点(像Edittext就是在内部设置了这个属性),调用isFocusableInTouchMode可以判断View是否可在Touch Mode模式下聚焦。
- 焦点(光标) focus
- 焦点改变监听
view.setOnFocusChangeListener(new View.OnFocusChangeListener(){ @Override public void onFocusChange(View v, boolean hasFocus) { if(hasFocus){ //获得焦点 }else{ } } }); - 获取焦点
// if(view.isInTouchMode()){ //View支持Focus,但是不支持在Touch模式下的Focus view.requestFocusFromTouch(); }else{ view.requestFocus(); } //焦点移动 android:nextFocusDown android:nextFocusLeft android:nextFocusRight android:nextFocusUp - 使用/禁用 焦点
// isFocusable() setFocusable() android:focusable //在TouchMode下 isFocusableInTouchMode() setFocusableInTouchMode() android:focusableInTouchMode="true"
- 焦点改变监听
states
- enabled
表示当前视图是否可用,不可用的视图是无法响应onTouch事件的
- focused
- window_focused
- selected
- pressed
XML属性
- 标记/描述
- android:id 唯一编号 View.findViewById() , Activity.findViewById()
- android:tag 文本标签 View.getTag() , View.findViewWithTag()
- android:contentDescription 为一些没有文字描述的View提供说明
- 大小 边距 背景
- android:minHeight
- android:minWidth
- android:padding
- android:paddingBottom
- android:paddingLeft
- android:paddingRight
- android:paddingTop
- android:background 透明:"@android:color/transparent"和"@null"
- 显示/唤醒
- android:visibility visible,invisible,gone(不显示,不占用空间)
- android:keepScreenOn View在可见的情况下是否保持唤醒状态
- 事件响应
- android:clickable 是否响应点击事件
- android:longClickable
- android:onClick 从上下文中调用指定的方法public void onClickXxx(View view)
- 焦点focus 触摸方式TouchMode
- android:focusable 是否获得焦点
- android:focusableInTouchMode 在Touch模式下View是否能取得焦点
- android:nextFocusDown
设置下方指定视图获得下一个焦点。焦点移动是基于一个 在给定方向查找最近邻居的算法。如果指定视图不存在, 移动焦点时将报运行时错误。可以设置 imeOptions= actionDone,这样输入完即跳到下一个焦点。
- android:nextFocusLeft
设置左边指定视图获得下一个焦点。
- android:nextFocusRight
设置右边指定视图获得下一个焦点。
- android:nextFocusUp
设置上方指定视图获得下一个焦点。
- 滚动
- 允许滚动
- android:isScrollContainer 设置当前View为滚动容器
- 边框渐变
- android:fadingEdge 边框渐变的方向 none,horizontal,vertical,
- android:fadingEdgeLength 边框渐变的长度
- 显示滚动条
- android:scrollbars none,horizontal,vertical
- android:scrollbarAlwaysDrawHorizontalTrack 始终显示
- android:scrollbarAlwaysDrawVerticalTrack 始终显示
- 滚动条样式
- android:scrollbarSize
- android:scrollbarStyle 风格和位置
insideOverlay、 insideInset、outsideOverlay、outsideInset
- android:scrollbarThumbHorizontal drawable
- android:scrollbarThumbVertical drawable
- android:scrollbarTrackHorizontal 轨迹 drawable
- android:scrollbarTrackVertical 轨迹 drawable
- 初始偏移
- android:scrollX
- android:scrollY
- 滚动条淡化
- android:scrollbarDefaultDelayBeforeFade N毫秒后开始淡化
- android:scrollbarFadeDuration 淡出毫秒时间
- 其他
- android:drawingCacheQuality
设置绘图时半透明质量。有以下值可设置:auto(默认, 由框架决定)/high(高质量,使用较高的颜色深度,消耗 更多的内存)/low(低质量,使用较低的颜色深度,但是 用更少的内存)
- android:duplicateParentState
如果设置此属性,将直接从父容器中获取绘图状态(光标, 按下等)。 见下面代码部分,注意根据目前测试情况仅仅是 获取绘图状态,而没有获取事件,也就是你点一下 LinearLayout时Button有被点击的效果,但是不执行点击事 件。
- android:fitsSystemWindows
设置布局调整时是否考虑系统窗口(如状态栏)
- android:hapticFeedbackEnabled
设置触感反馈。(译者注:按软键以及进行某些UI交互时振动,暂时不知道用法,大家可以找找performHapticFeedback或HapticFeedback这个关键字的资料看看。)
- android:saveEnabled
设置是否在窗口冻结时(如旋转屏幕)保存View的数据, 默认为true,但是前提是你需要设置id才能自动保存,参 见这里。
- android:soundEffectsEnabled
设置点击或触摸时是否有声音效果
- android:drawingCacheQuality