Android进阶之自定义控件二
来源:互联网 发布:淘宝的网商银行 编辑:程序博客网 时间:2024/05/15 23:48
了解自定义控件的三大流程(measure、layout、draw)
在上一篇博客中我们大致介绍了一下View和ViewGroup,接下来我们就学习一下自定义控件的三大流程,为我们打下夯实的基础。(本博客主要参考《Android群英传》和《Android开发艺术探索》,大家也可以去阅读这两本书籍)
自定义控件三大流程简介
什么是自定义控件的三大流程,相信正在阅读这篇博客的你肯定接触过自定义控件,也见过onMeasure()、onLayout()和onDraw()这三个方法,自定义控件的三大流程就是这三个方法了,下面就让我们循序渐进的了解一下这三个方法。
measure
onMeasure方法的作用通俗点讲就是确认View位置,而对于ViewGroup来说,除了完成自己的measure过程以外,还会遍历去调用所有子元素的measure方法,各子元素再递归去执行这个过程。下面就依次来讲解:
1、View的测量过程
@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec);}
我们在onMeasure上按住ctrl后鼠标左击,进入 super.onMeasure的源码,如下所示:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));}
这里我们大致的可以明白onMeasure方法是通过setMeasuredDimension来控制控件大小的,我们不需要深入的去了解,我们在看一下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;}
这段代码相信大家很容易理解,当然我们首先需要了解一下MeasureSpec这个类,MeasureSpec其实是一个32位的int值,高2位为测量模式,低30位为测量的大小,内部封装了一些获取测量模式和测量大小的位运算。测量模式分一下三种:
UNSPECIFIED
中文翻译为未特别指定(规定)的,既父容器不对View有任何限制,View想要多大就多大
EXACTLY
中文翻译为精确地,即精确值模式,但我们将控件的layout_width或者layout_height指定为固定值,如“10dp”,或者为match_parent时使用该模式
AT_MOST
最大值模式,当控件宽高指定为wrap_content时,使用改模式
通过上面的分析我们可以很轻松的写出我们自己想要的测量方法,下面给出一个示例:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getMeasureSize(widthMeasureSpec), getMeasureSize(heightMeasureSpec)); } public static int getMeasureSize(int measureSpec) { int size = 200; int result = 0; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); switch (specMode) { case MeasureSpec.UNSPECIFIED: result = size; break; case MeasureSpec.AT_MOST: result = Math.min(size, specSize); break; case MeasureSpec.EXACTLY: result = specSize; break; } return result; }
2、ViewGroup的measure过程
ViewGroup是一个抽象类,因此它没有重写View的onMeasure方法,但它提供了一个measureChildren的方法,同样的我们依次地大致阅读以下源码:
@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec);}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); } } }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); }
相信大家也能很容易的读懂,原理就是遍历ViewGroup内所有的View去调用View的measure方法。
layout
Layout的作用是ViewGroup用来确定子元素的位置,当ViewGroup的位置被确定后,它在onLayout中会遍历所有的子元素并调用其layout方法,在layout方法中子view的onLayout方法有会被调用,这段话大家可能看的云里雾里,下面给出LinearLayout的onLayout的源码,相信大家就一目了然了:
1、写一个继承LinearLayout的类,重写onLayout方法
@Overrideprotected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); }
2、进入super.onLayout方法
@Override 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); } }
3、从源码很明显可以看出线性布局分垂直和水平方向,我们以垂直方向为例,进入layoutVertical方法:
这里代码量很大,大家没必要看的非常仔细,首先找到for (int i = 0; i < count; i++)这个for循环,大家肯定明白这是遍历LinearLayout中的子View,之后调用setChildFrame这个方法确定子View的位置
void layoutVertical(int left, int top, int right, int bottom) { final int paddingLeft = mPaddingLeft; int childTop; int childLeft; // Where right end of child should go final int width = right - left; int childRight = width - mPaddingRight; // Space available for child 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: // mTotalLength contains the padding already childTop = mPaddingTop + bottom - top - mTotalLength; break; // mTotalLength contains the padding already 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(child, childLeft, childTop + getLocationOffset(child), childWidth, childHeight); childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child); i += getChildrenSkipCount(child, i); } } }
4、接下来进入setChildFrame方法看如何实现确定子View的位置
private void setChildFrame(View child, int left, int top, int width, int height) { child.layout(left, top, left + width, top + height); }
到这里大家就稍微明白了,原来是调用子View的layout方法来确定子View的位置
5、进入layout方法一探究竟
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(changed, l, t, r, b); if (shouldDrawRoundScrollbar()) { if(mRoundScrollbarRenderer == null) { mRoundScrollbarRenderer = new RoundScrollbarRenderer(this); } } else { mRoundScrollbarRenderer = null; } 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; }
这里代码量也有些多,相信仔细看了的肯定也能读懂,没懂也没关系,不用看得那么仔细,首先找到boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
setOpticalFrame和setFrame从参数我们就大致能猜到是确定子View的位置的,之后再找到onLayout(changed, l, t, r, b);这个方法,这样就到了子View的onLayout方法中,若子View是一个ViewGroup的话又可以确定子View的位置了,这样就可以确定一个View树上所有View的位置。
相信看到这里大家都恍然大悟了,希望大家在看博客的时候也打开eclipse或者as简单的阅读以下源码,参考源码我相信大家聪慧的大脑,肯定能玩转自定义View的layout。
draw
当View的位置确定好之后我们就要开始绘制View了,这里我们就需要了解一下Paint和Canvas这两个对象了,相信大家已经非常熟悉了,这里稍微做一下总结:
1.Paint(画笔)类
要绘制图形,首先得调整画笔,按照自己的开发需要设置画笔的相关属性。Pain类的常用属性设置方法如下:
setAntiAlias(); //设置画笔的锯齿效果
setColor(); //设置画笔的颜色
setARGB(); //设置画笔的A、R、G、B值
setAlpha(); //设置画笔的Alpha值
setTextSize(); //设置字体的尺寸
setStyle(); //设置画笔的风格(空心或实心)
setStrokeWidth(); //设置空心边框的宽度
getColor(); //获取画笔的颜色
2.Canvas(画布)类
画笔属性设置好之后,还需要将图像绘制到画布上。Canvas类可以用来实现各种图形的绘制工作,如绘制直线、矩形、圆等等。Canvas绘制常用图形的方法如下:
绘制直线:canvas.drawLine(float startX, float startY, float stopX, float stopY, Paint paint);
绘制矩形:canvas.drawRect(float left, float top, float right, float bottom, Paint paint);
绘制圆形:canvas.drawCircle(float cx, float cy, float radius, Paint paint);
绘制字符:canvas.drawText(String text, float x, float y, Paint paint);
绘制图形:canvas.drawBirmap(Bitmap bitmap, float left, float top, Paint paint);
示例:简单的实现一下音频条效果
public class CustomView extends View { private Paint mPaint = null; private int count = 80; private int mRectWidth = 10; private int offset = 2; private float mRectHight = 400; private float mCurrentHight = 400; public customView(Context context) { super(context); init(); } public customView(Context context, AttributeSet attrs) { super(context, attrs); init(); } public customView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setColor(Color.RED); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getMeasureSize(widthMeasureSpec), getMeasureSize(heightMeasureSpec)); } public static int getMeasureSize(int measureSpec) { int size = 200; int result = 0; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); switch (specMode) { case MeasureSpec.UNSPECIFIED: result = size; break; case MeasureSpec.AT_MOST: result = Math.min(size, specSize); break; case MeasureSpec.EXACTLY: result = specSize; break; } return result; } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); LinearGradient mLinearGradient = new LinearGradient( 0, 0, mRectWidth, mRectHight, Color.YELLOW, Color.BLUE, Shader.TileMode.CLAMP ); mPaint.setShader(mLinearGradient); } @Override protected void onDraw(Canvas canvas) { for (int i = 0; i < count; i++){ double mRandom = Math.random(); mCurrentHight = (float) (mRectHight * mRandom); canvas.drawRect((float)(mRectWidth * i + offset), mRectHight-mCurrentHight, (float)(mRectWidth * (i+1)), mRectHight, mPaint); } postInvalidateDelayed(300); }}
这个示例博主这里就不做解释了,实现方法有很多种,我只是提供一种思路,更多的还是希望读者亲自去敲一遍试试效果,遇到不懂的baidu或者google,这样收获会更多,感谢您的阅读,下一篇将继续讨论自定义View的滑动和事件分发机制,欢迎大家进一步学习!自定义控件的时间分发、拦截、处理http://blog.csdn.net/u010083327/article/details/60874681
- Android进阶之自定义控件二
- Android小白进阶(二)--自定义控件之自定义ViewGroup
- android UI进阶之自定义组合控件二
- Android进阶-纯粹自定义控件二
- android UI进阶之自定义组合控件
- android UI进阶之自定义组合控件
- android UI进阶之自定义组合控件
- android UI进阶之自定义组合控件
- android UI进阶之自定义组合控件
- android UI进阶之自定义组合控件
- android UI进阶之自定义组合控件
- android UI进阶之自定义组合控件
- android UI进阶之自定义组合控件
- android UI进阶之自定义组合控件
- android UI进阶之自定义组合控件
- android UI进阶之自定义组合控件
- Android进阶自定义控件之滑动开关
- android UI进阶之自定义组合控件
- LeetCode: Invert Binary Tree
- magento模块实例开发,前后台开发,表单提交案例
- FFMpeg 实现从视频中提取音轨
- Android Sensor详解(3)porting drvier
- 变分自编码VAE(variational autoencoder)及Keras 实现
- Android进阶之自定义控件二
- Android studio Building gradle info 过久,无反应
- Android 三种解析Json分别 原生态 Goson FastJson
- Spring Boot,IntelliJ IDEA,JSP,启动报错java.lang.NoClassDefFoundError: javax/servlet/ServletContext
- 【录教程必备】推荐几款屏幕录制工具(可录制GIF)
- windows下caffe运行我的第二个程序
- 手机控制电脑
- shell基础之一
- 你真的会用Gson吗?Gson使用指南(一)