Android 自定义View
来源:互联网 发布:传奇怪物补丁算法 编辑:程序博客网 时间:2024/06/07 10:48
自定义View
代码
public class MyView extends View { private final static int REQUEST_DRAW = 0; private final static int REQUEST_LAYOUT = 1; private Paint mPaint; private int mCount = 0; private Rect mBounds; //不能这样定义,因为此时Thread.sleep(1000);休眠的是主线程,所以很可能导致ANR,就算没有导致ANR, //比如此时主界面有一个button,那么该button永远也接受不到点击事件,因为点击事件的处理也是在main线程,但是main线程一直在这里阻塞了 /*private Handler anoHandler=new Handler(){ @Override public void handleMessage(Message msg) { while (true) { try { mCount++; if (mCount == 100) { break; } Thread.sleep(1000); invalidate(); } catch (InterruptedException e) { e.printStackTrace(); } } } };*/ private Handler handler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case REQUEST_DRAW: invalidate();//只会调用onDraw break; case REQUEST_LAYOUT: requestLayout();//会重新调用整个流程,onMeasure,onLayout,onDraw break; default: break; } } }; public MyView(Context context) { this(context, null); } public MyView(Context context, AttributeSet attrs) { super(context, attrs); mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setTextSize(30); mBounds = new Rect(); new Thread() { @Override public void run() { while (true) { try { mCount++; if (mCount == 100) { break; } Thread.sleep(1000); if (mCount == 10) { handler.sendEmptyMessage(REQUEST_LAYOUT);//此时变成两位数,所以需要重新测量布局 } else { //注意这里不能直接调用invalidate,控件在main线程,同时控件是线程不安全的,所以不能在子线程中刷新view,所以只能通过handler发消息到main线程 //或者可以直接使用postInvalidate();实际上postInvalidate()内部实现使用了handler,原理还是发消息到main线程,让main线程去刷新view handler.sendEmptyMessage(REQUEST_DRAW);//否则只是重绘view,不需要重新测量布局 } } catch (InterruptedException e) { e.printStackTrace(); } } } }.start(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthSize = MeasureSpec.getSize(widthMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); String text = String.valueOf(mCount); mPaint.getTextBounds(text, 0, text.length(), mBounds); //文字的宽度,为了支持padding,必须加上-左右padding //为什么不用mBounds.width()测量文字宽,而是用measureText方法,通过实际发现,mBounds.width()测量宽度的结果并不准确 int textWidth = (int) mPaint.measureText(text) + getPaddingLeft() + getPaddingRight(); //文字的高度,为了支持padding,必须加上-上下padding int textHeigth = mBounds.height() + getPaddingTop() + getPaddingBottom(); //如果设置了背景图片或者view中设置了android:minWidth,android:minHeight属性,那么defWidth等于getSuggestedMinimumWidth/getSuggestedMinimumHeight //否则就是文字的宽度/高度,注意为了支持padding,宽度必须加上上下左右padding int defWidth = getSuggestedMinimumWidth() == 0 ? textWidth : getSuggestedMinimumWidth(); int defHeight = getSuggestedMinimumHeight() == 0 ? textHeigth : getSuggestedMinimumHeight(); //如果是EXACTLY,设置了具体数值/match_parent,就使用父view传递下来的widthSize/heightSize,否则如果是wrap_content,AT_MOST就使用defWidth/defHeight int width = (widthMode == MeasureSpec.EXACTLY) ? widthSize : defWidth; int heigth = (heightMode == MeasureSpec.EXACTLY) ? heightSize : defHeight; setMeasuredDimension(width, heigth); //实际上以上的实现,基本跟这句等效,只是如果view设置的是wrap_content,模式为AT_MOST,那么最后view的大小得到的将是父view传递下来的大小 //而上面的代码中对wrap_content,模式为AT_MOST进行了自己的实现 //super.onMeasure(widthMeasureSpec, heightMeasureSpec); } @Override protected void onDraw(Canvas canvas) { //super.onDraw(canvas); 因为父类view的onDraw没有任何实现,如果继承自button等,就必须调用super.onDraw(canvas) mPaint.setColor(Color.BLUE); mPaint.setStyle(Paint.Style.FILL); //画一个长方新蓝色背景 canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint); String text = String.valueOf(mCount); mPaint.setColor(Color.YELLOW); mPaint.getTextBounds(text, 0, text.length(), mBounds); int textWidth = (int) mPaint.measureText(text); int textHeigth = mBounds.height(); //在view的中间画数字 canvas.drawText(text, getMeasuredWidth() / 2 - textWidth / 2, getMeasuredHeight() / 2 + textHeigth / 2, mPaint); }}
效果
<lbb.mytest.demo.MyView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:minHeight="50dp" android:minWidth="50dp"/>实际显示:中间的数字一直在更新。
此时width设置为wrap_content,但是设置了minWidth。所以该view最后的大小就是50dp
<lbb.mytest.demo.MyView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center"/>实际显示:中间的数字一直在更新
此时width设置为wrap_content,但是没有设置背景图片也没有设置minWidth,同时没有设置padding,所以最后宽度就是文字的实际宽度,在数字累加到10的时候,会重新测量一次。
invalidate: 刷新view,调用OnDraw,不可以在子线程中调用
postInvalidate:刷新view,调用OnDraw,可以在子线程中直接刷新,内部使用handler发消息到main线程
requestLayout: 重新测量布局绘制view,调用过程onMeasure,onLayout,onDraw
自定义ViewGroup
代码
对《Android 手把手教您自定义ViewGroup》 中的例子做了优化,让它支持margin,同时增加了自身的绘制过程。
package lbb.mytest.demo;import android.content.Context;import android.graphics.Canvas;import android.graphics.Paint;import android.util.AttributeSet;import android.view.View;import android.view.ViewGroup;/** * Created by liaobinbin on 2015/10/19. */public class MyViewGroup extends ViewGroup { private Paint paint; public MyViewGroup(Context context) { this(context, null); } public MyViewGroup(Context context, AttributeSet attrs) { super(context, attrs); paint = new Paint(); //setPadding(0, 0, 0, 0);//忽略padding的影响 } //为了支持margin,默认情况下ViewGroup的generateLayoutParams方法返回的是LayoutParams对象,只能获取layout_width和layout_height。 //但是MarginLayoutParams(context,attrs)会额外获取margin参数,padding默认是支持的。 //generateLayoutParams在inflate过程中被调用,未每个子view生成layoutparam。setContentView内部其实也是调用inflate函数的 @Override public LayoutParams generateLayoutParams(AttributeSet attrs) { return new MarginLayoutParams(getContext(), attrs); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthSize = MeasureSpec.getSize(widthMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); //必须先手动测量一次viewgroup内所有子view,否则子view的getMeasuredWidth/getMeasuredHeight都是0 measureChildren(widthMeasureSpec, heightMeasureSpec); int width = 0; if (widthMode == MeasureSpec.EXACTLY) { width = widthSize;//如果设置了具体数值或者使用了match_parent,那么使用父控件传下来的size } else { //如果是wrap_content(AT_MOST),那么就自己计算width int tWidth = 0, bWidth = 0; for (int i = 0; i < getChildCount(); i++) { View view = getChildAt(i); MarginLayoutParams lp = (MarginLayoutParams) view.getLayoutParams(); if (i == 0 || i == 1) { tWidth += view.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; } else { bWidth += view.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; } } width += getPaddingLeft() + getPaddingRight() + Math.max(tWidth, bWidth);//上下排最大值+padding作为width if (width > widthSize) {//如果计算出来的值比父控件给的size还要大,那就说明已经超过了最大值。 width = widthSize; } } int height = 0; if (heightMode == MeasureSpec.EXACTLY) { height = heightSize; } else { int lHeight = 0, rHeight = 0; for (int i = 0; i < getChildCount(); i++) { View view = getChildAt(i); MarginLayoutParams lp = (MarginLayoutParams) view.getLayoutParams(); if (i == 0 || i == 2) { lHeight += view.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; } else { rHeight += view.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; } } height += getPaddingTop() + getPaddingBottom() + Math.max(lHeight, rHeight); if (height > heightSize) { height = heightSize; } } setMeasuredDimension(width, height); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int startX = 0, startY = 0; for (int i = 0; i < getChildCount(); i++) { View view = getChildAt(i); MarginLayoutParams lp = (MarginLayoutParams) view.getLayoutParams();//取得子view的layoutparam参数 switch (i) { case 0: startX = getPaddingLeft() + lp.leftMargin; startY = getPaddingTop() + lp.topMargin; break; case 1: startX = getMeasuredWidth() - getPaddingRight() - lp.rightMargin - view.getMeasuredWidth(); startY = getPaddingTop() + lp.topMargin; break; case 2: startX = getPaddingLeft() + lp.leftMargin; startY = getMeasuredHeight() - getPaddingBottom() - lp.bottomMargin - view.getMeasuredHeight(); break; case 3: startX = getMeasuredWidth() - getPaddingRight() - lp.rightMargin - view.getMeasuredWidth(); startY = getMeasuredHeight() - getPaddingBottom() - lp.bottomMargin - view.getMeasuredHeight(); break; default: break; } view.layout(startX, startY, startX + view.getMeasuredWidth(), startY + view.getMeasuredHeight()); } } //测量,布局完毕之后,就到了绘制过程 @Override protected void onDraw(Canvas canvas) { //super.onDraw(canvas); paint.setColor(getResources().getColor(android.R.color.darker_gray)); paint.setStyle(Paint.Style.FILL); canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), paint);//在viewgroup的最外层绘制一个长方形作为背景 paint.setColor(getResources().getColor(R.color.background));//#FF00FFE1 paint.setStyle(Paint.Style.FILL); int left = getPaddingLeft(); int right = getMeasuredWidth() - getPaddingRight(); int top = getPaddingTop(); int buttom = getMeasuredHeight() - getPaddingBottom(); canvas.drawRect(left, top, right, buttom, paint);//viewgroup去掉padding绘制一个长方形作为背景 }}主界面
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:padding="5dp"> <lbb.mytest.demo.MyViewGroup android:layout_width="match_parent" android:layout_height="300dp" android:layout_marginTop="10dp" android:background="#00000000" android:padding="30dp"> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="10dp" android:background="#fff944" android:text="11"/> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="#00ff00" android:text="22"/> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="#ff0000" android:text="11"/> <lbb.mytest.demo.MyView android:layout_width="wrap_content" android:layout_height="wrap_content" android:padding="10dp"/> </lbb.mytest.demo.MyViewGroup></LinearLayout>
实际显示,MyView支持padding,MyViewGroup完美支持margin
MyViewGroup中measureChildren(widthMeasureSpec, heightMeasureSpec);会去测量每个子view,所以当测量myview的时候,自然会去执行myView的onMeasure,这个时候测量出来的结果自然是文字的宽高加上padding了。
分析
1、ViewGroup的职责是啥?
ViewGroup相当于一个放置View的容器,并且我们在写布局xml的时候,会告诉容器(凡是以layout为开头的属性,都是为用于告诉容器的),我们的宽度(layout_width)、高度(layout_height)、对齐方式(layout_gravity)等;当然还有margin等;于是乎,ViewGroup的职能为:给childView计算出建议的宽和高和测量模式 ;决定childView的位置;为什么只是建议的宽和高,而不是直接确定呢,别忘了childView宽和高可以设置为wrap_content,这样只有childView才能计算出自己的宽和高。
2、View的职责是啥?
View的职责,根据测量模式和ViewGroup给出的建议的宽和高,计算出自己的宽和高;同时还有个更重要的职责是:在ViewGroup为其指定的区域内绘制自己的形态。
3、ViewGroup和LayoutParams之间的关系?
大家可以回忆一下,当在LinearLayout中写childView的时候,可以写layout_gravity,layout_weight属性;在RelativeLayout中的childView有layout_centerInParent属性,却没有layout_gravity,layout_weight,这是为什么呢?这是因为每个ViewGroup需要指定一个LayoutParams,用于确定支持childView支持哪些属性,比如LinearLayout指定LinearLayout.LayoutParams等。如果大家去看LinearLayout的源码,会发现其内部定义了LinearLayout.LayoutParams,在此类中,你可以发现weight和gravity的身影。
首先可以去参考《Android View绘制流程》《Android LayoutInflater原理分析》
1. 为什么要去重写generateLayoutParams方法才能支持margin?
因为不管viewgroup通过inflate加载还是setcontentview(内部其实还是通过inflate)加载,都会去解析xml中的资源,然后会为每个view设置它的layoutparam,这个layoutparam是通过它的父viewgroup调用generateLayoutParams方法得到的,默认情况下这个方法返回的是LayoutParams对象,只能获取layout_width,layout_height。如果要支持margin,那么generateLayoutParams方法就必须返回MarginLayoutParams对象。
ViewGroup.LayoutParams params = null;params = root.generateLayoutParams(attrs);if (!attachToRoot) {temp.setLayoutParams(params); }//inflate的是根View,也是generateLayoutParams这个带attrs参数的重载方法。
final View view = createViewFromTag(parent, name, context, attrs); final ViewGroup viewGroup = (ViewGroup) parent; final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs); rInflateChildren(parser, view, attrs, true); viewGroup.addView(view, params);在inflate视图的时候,递归解析子视图时,调用了viewgroup的generateLayoutParams(attrs)方法,并把该layoutparam绑定给了该view
public LayoutParams generateLayoutParams(AttributeSet attrs) {return new LayoutParams(getContext(), attrs);}public LayoutParams(Context c, AttributeSet attrs) { TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout); setBaseAttributes(a,R.styleable.ViewGroup_Layout_layout_width,R.styleable.ViewGroup_Layout_layout_height); a.recycle(); }默认情况下LayoutParams对象只会获取,layout_width和layout_height这两个参数。
@Override public LayoutParams generateLayoutParams(AttributeSet attrs) { return new MarginLayoutParams(getContext(), attrs); } public MarginLayoutParams(Context c, AttributeSet attrs) { super(); TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_MarginLayout); setBaseAttributes(a, R.styleable.ViewGroup_MarginLayout_layout_width, R.styleable.ViewGroup_MarginLayout_layout_height); int margin = a.getDimensionPixelSize( com.android.internal.R.styleable.ViewGroup_MarginLayout_layout_margin, -1); if (margin >= 0) { leftMargin = margin; topMargin = margin; rightMargin= margin; bottomMargin = margin; }.............}所以如果要能获取margin参数,那么必须重写generateLayoutParams方法
另一方面为什么LinearLayout等支持,因为他们实现了自己的测量过程,已经在测量过程中计算了margin。可以看到LinearLayout的内部类LayoutParams
public static class LayoutParams extends ViewGroup.MarginLayoutParams。继承了MarginLayoutParams所以可以获取margin
2. 为什么说默认是支持padding的?
viewgroup测量循环测量子view大小,measureChildren(widthMeasureSpec, heightMeasureSpec);可以看到里面把padding考虑进去了
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); } } } /** * Ask one of the children of this view to measure itself, taking into * account both the MeasureSpec requirements for this view and its padding. * The heavy lifting is done in getChildMeasureSpec. * * @param child The child to measure * @param parentWidthMeasureSpec The width requirements for this view * @param parentHeightMeasureSpec The height requirements for this view */ 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); }
可以看到measureChild中使用了padding,其实也是用了layoutparam,但是layoutparam只取了了layout_width和layout_height,当然不支持margin了。
public static int getChildMeasureSpec(int spec, int padding, int childDimension) { int specMode = MeasureSpec.getMode(spec); int specSize = MeasureSpec.getSize(spec); int size = Math.max(0, specSize - padding); int resultSize = 0; int resultMode = 0; switch (specMode) { // Parent has imposed an exact size on us case MeasureSpec.EXACTLY://如果viewgroup是EXACTLY if (childDimension >= 0) { //如果子view设置的是具体数值 resultSize = childDimension; //具体数值 resultMode = MeasureSpec.EXACTLY; //EXACTLY } else if (childDimension == LayoutParams.MATCH_PARENT) { //如果子view设置的是MATCH_PARENT // Child wants to be our size. So be it. resultSize = size; //父view的大小 resultMode = MeasureSpec.EXACTLY; //EXACTLY } else if (childDimension == LayoutParams.WRAP_CONTENT) { //如果子view设置的是WRAP_CONTENT // Child wants to determine its own size. It can't be // bigger than us. resultSize = size; //父view的大小 resultMode = MeasureSpec.AT_MOST; //AT_MOST } break; // Parent has imposed a maximum size on us case MeasureSpec.AT_MOST: //如果viewgroup是AT_MOST if (childDimension >= 0) { //如果子view设置的是具体数值 // Child wants a specific size... so be it resultSize = childDimension; //具体数值 resultMode = MeasureSpec.EXACTLY; //EXACTLY } else if (childDimension == LayoutParams.MATCH_PARENT) { //如果子view设置的是MATCH_PARENT // Child wants to be our size, but our size is not fixed. // Constrain child to not be bigger than us. resultSize = size; //父view的大小 resultMode = MeasureSpec.AT_MOST; //AT_MOST } else if (childDimension == LayoutParams.WRAP_CONTENT) { //如果子view设置的是WRAP_CONTENT // Child wants to determine its own size. It can't be // bigger than us. resultSize = size; //父view的大小 resultMode = MeasureSpec.AT_MOST; //AT_MOST } break;} protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); } //现在测量子view了,其实viewgroup自身的大小也是这里的。 protected int getSuggestedMinimumWidth() { return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());//背景图片大小 } 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; //如果是UNSPECIFIED,那么使用的getSuggestedMinimumWidth,很可能就是背景图片大小了。 break; case MeasureSpec.AT_MOST: case MeasureSpec.EXACTLY: result = specSize; //说明如果是AT_MOST,EXACTLY那么最终的结果是从measureSpec中拿到的。 break; } return result; }
可以看到如果是UNSPECIFIED,那么View最终的大小将是背景图片宽高和android:minWidth/android:minHeight之间的最大值
protected int getSuggestedMinimumWidth() { return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth()); }
case R.styleable.View_minWidth: mMinWidth = a.getDimensionPixelSize(attr, 0); break; case R.styleable.View_minHeight: mMinHeight = a.getDimensionPixelSize(attr, 0);
- Android View---自定义View
- Android View---自定义View
- Android 自定义View 之 自定义View属性
- 【自定义View系列】android自定义View概述
- Android自定义view自定义属性
- Android自定义控件 -- 自定义View
- android自定义view(自定义数字键盘)
- Android自定义View-自定义属性
- Android自定义View-自定义属性
- Android 自定义View
- Android 自定义 View
- android自定义View
- Android 中自定义 view
- android 自定义view组件
- Android 自定义 View
- android 自定义view
- Android:如何自定义View
- android 自定义View
- Uniyt 异步加载关卡(Application.LoadLevelAsync)
- NSTimer
- C++ 文件读写
- 自定义异常类
- PHP7新特性
- Android 自定义View
- 【POJ2152】Fire——树形DP
- 顺序表的应用实例
- extjs panel layoutconfig属性
- POJ 3280 Cheapest Palindrome(区间DP)
- 定位UNIX上常见问题的经验总结
- Swift学习day2之Tuple
- [Android高级知识][1] 如何调用支付宝接口
- MySQL Cluster 7.4.8集群安装及遇到的问题