内容简介:如何自定义View
首先奉上 AndroodDeveloper的教程 。
假设我们以自定义一个View,实现圆形的按钮功能。
说一下简单的流程:
- 继承View
- 重写构造函数
- 重写OnMeasure()方法
- 重写OnDraw()方法
- 配置XML
我把配置XML放到最后不是因为需要最后去处理,而是它相对来说比较独立。
继承View,重写构造函数
首先重写View的构造函数
private CustomView (Context context){ this(context,null); } private CustomView(Context context, AttributeSet attrs){ this(context,attrs,0); } private CustomView(Contxt context, AttributeSet attrs, int defStyleAttr){ super(context,attrs,defStyleAttr); }
重写OnMeasure()函数
这一块着重的讲一下,之前我这里也不是特别的理解。 我觉得在XML文件中其实已经将View 的尺寸宽高已经固定好了,何必在View中再次测量并设置么 。同理可以再往上层分析下,若Google在父View中直接获取XML里面的尺寸岂不更好。
在我们布局XML的时候,有两个属性 wrap_content 和 match_parent 。可以看到这两个属性并没有去告诉系统,我要多少尺寸的大小,而是描述了一种关系,即 内容包裹 和 填充父空间 ,因此在我们绘制到屏幕的过程中,就必须知道View的具体宽高,所以我们必须去处理尺寸。当View默认处理,无法满足我们需求的时候,就需要重写OnMeasure()函数了。
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int width = measure(widthMeasureSpec, 100); int height = measure(heightMeasureSpec, 100); if (width < height){ height = width; }else{ width = height; } setMeasuredDimension(width, height); }
在这个函数中传了两个参数,这里需要注意的是参数是int型的,却包含了两个重要的信息: 测量的模式 以及 测量的大小 。Google将int数据的前两个bit用于区分不同的布局模式,后面三十个bit存放的是尺寸的数据。一般我们需要通过移位操作来获取数据,Android中的MeasureSpec中有两个函数 getMode() 和 getSize() 就可以很方便的获取测量的模式和大小。
这样恐怕你会有疑问, 既然已经获取了View的Size了,那要Mode有何用? 其实这里的Size只是父级View提供的参考大小而已。Mode分为下面三种:
| 测量模式 | 英文 | 中文 | | UNSPECIFIED | The parent has not imposed any constraint on the child. It can be whatever size it wants | 父容器对当前View没有任何限制,当前View可以取任意尺寸 | EXACTLY | The parent has determined an exact size for the child. The child is going to be given those bounds regardless of how big it wants to be | 父容器给子容器了确定的大小,无论子容器想要多大,它只能接收父容器给的大小 | AT_MOST | The child can be as large as it wants up to the specified size | 子容器可以获得它想要的尺寸大小
简单点来说,和 warp_content 和 match_parten 做下对比不难发现。
match_parent--->EXACTLY。match_parent就是要利用父View给我们提供的所有剩余空间,而父View剩余空间是确定的,即Size。
wrap_content--->AT_MOST。怎么理解:就是我们想要将大小设置为包裹我们的view内容,那么尺寸大小就是父View给我们作为参考的尺寸,只要不超过这个尺寸就可以啦,具体尺寸就根据我们的需求去设定。
固定尺寸(如100dp)--->EXACTLY。用户自己指定了尺寸大小,我们就不用再去干涉了,当然是以指定的大小为主啦。
private int measure(int measureSpec, int defaultSize) { int result = defaultSize; int mode = MeasureSpec.getMode(measureSpec); int size = MeasureSpec.getSize(measureSpec); switch(mode){ case MeasureSpec.EXACTLY: result = size; break; case MeasureSpec.AT_MOST: result = size; break; case MeasureSpec.UNSPECIFIED: result = defaultSize; break; default: break; } return result; }
假设我们在XML中设置该控件的长宽属性都是 match_parent ,则效果如下
重写OnDraw()方法
上面我们通过OnMeasure()来设定了View的大小,接下来需要通过OnDraw()来绘制这个View的样子。
这里需要注意下Canvas和Paint的区别,下面一段话说明 Canvas确定了你在屏幕中所能展现的形状,而Paint用来定义具体的颜色,样式,字体等。
Simply put, Canvasdefines shapes that you can draw on the screen, while Paint defines the color, style, font, and so forth of each shape you draw.
OK,假设我们去绘制一个原谅色的原型,代码如下:
@override protected void OnDraw(Canvas canvas){ Super.onDraw(canvas); int r = getMeasureWidth() / 2; int x = getLeft() + r; int y = getTop() + r; Paint paint = new Paint(); paint.setColor(Color.Green); canvas.drawCircle(x, y, r, paint); }
效果如下:
设置监听事件
自定义XML属性
如果我们需要给用户一些更加灵活的设置,就需设置一些属性。首先我们在 res/values/styles.xml 中声明我们自己的属性:
<resources> <declare-styleable name="CostomView"> <attr name="default_size" format="dimension"/> </declare-styleable> </resources>
接着在布局文件中使用我们的声明的属性:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context="domon.cn.coustomerview.MainActivity"> <domon.cn.coustomerview.View.RectView android:id="@+id/my_rv" android:layout_width="match_parent" android:background="#f2e" app:default_size="100dp" android:layout_height="100dp" /> </LinearLayout>
在引用自己的属性的时候,需要注意一下命名空间的问题,基本上我们的自定义View就已经好了。
案例分析
下面我 代码家 的一个自定义控件 NumberProgressBar 来简单分析一下。
- 构造函数
public NumberProgressBar(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); default_reached_bar_height = dp2px(1.5f); default_unreached_bar_height = dp2px(1.0f); default_text_size = sp2px(10); default_progress_text_offset = dp2px(3.0f); //load styled attributes. final TypedArray attributes = context.getTheme().obtainStyledAttributes(attrs, R.styleable.NumberProgressBar, defStyleAttr, 0); mReachedBarColor = attributes.getColor(R.styleable.NumberProgressBar_progress_reached_color, default_reached_color); mUnreachedBarColor = attributes.getColor(R.styleable.NumberProgressBar_progress_unreached_color, default_unreached_color); mTextColor = attributes.getColor(R.styleable.NumberProgressBar_progress_text_color, default_text_color); mTextSize = attributes.getDimension(R.styleable.NumberProgressBar_progress_text_size, default_text_size); mReachedBarHeight = attributes.getDimension(R.styleable.NumberProgressBar_progress_reached_bar_height, default_reached_bar_height); mUnreachedBarHeight = attributes.getDimension(R.styleable.NumberProgressBar_progress_unreached_bar_height, default_unreached_bar_height); mOffset = attributes.getDimension(R.styleable.NumberProgressBar_progress_text_offset, default_progress_text_offset); int textVisible = attributes.getInt(R.styleable.NumberProgressBar_progress_text_visibility, PROGRESS_TEXT_VISIBLE); if (textVisible != PROGRESS_TEXT_VISIBLE) { mIfDrawText = false; } setProgress(attributes.getInt(R.styleable.NumberProgressBar_progress_current, 0)); setMax(attributes.getInt(R.styleable.NumberProgressBar_progress_max, 100)); attributes.recycle(); initializePainters(); }
通过在构造函数中,获取在XML中设置的属性。
- 重写OnMeasure()方法
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(measure(widthMeasureSpec, true), measure(heightMeasureSpec, false)); } private int measure(int measureSpec, boolean isWidth) { int result; int mode = MeasureSpec.getMode(measureSpec); int size = MeasureSpec.getSize(measureSpec); int padding = isWidth ? getPaddingLeft() + getPaddingRight() : getPaddingTop() + getPaddingBottom(); if (mode == MeasureSpec.EXACTLY) { result = size; } else { result = isWidth ? getSuggestedMinimumWidth() : getSuggestedMinimumHeight(); result += padding; if (mode == MeasureSpec.AT_MOST) { if (isWidth) { result = Math.max(result, size); } else { result = Math.min(result, size); } } } return result; }
根据不同模式测量不同的尺寸
- 重写OnDraw()方法
@Override protected void onDraw(Canvas canvas) { if (mIfDrawText) { calculateDrawRectF(); } else { calculateDrawRectFWithoutProgressText(); } if (mDrawReachedBar) { canvas.drawRect(mReachedRectF, mReachedBarPaint); } if (mDrawUnreachedBar) { canvas.drawRect(mUnreachedRectF, mUnreachedBarPaint); } if (mIfDrawText) canvas.drawText(mCurrentDrawText, mDrawTextStart, mDrawTextEnd, mTextPaint); }
在OnDraw()中根据条件判断不同的绘制对象。我们就来看看 calculateDrawRectFWithoutProgressText() 这个方法。
private void calculateDrawRectFWithoutProgressText() { mReachedRectF.left = getPaddingLeft(); mReachedRectF.top = getHeight() / 2.0f - mReachedBarHeight / 2.0f; mReachedRectF.right = (getWidth() - getPaddingLeft() - getPaddingRight()) / (getMax() * 1.0f) * getProgress() + getPaddingLeft(); mReachedRectF.bottom = getHeight() / 2.0f + mReachedBarHeight / 2.0f; mUnreachedRectF.left = mReachedRectF.right; mUnreachedRectF.right = getWidth() - getPaddingRight(); mUnreachedRectF.top = getHeight() / 2.0f + -mUnreachedBarHeight / 2.0f; mUnreachedRectF.bottom = getHeight() / 2.0f + mUnreachedBarHeight / 2.0f; }
ReachedRectf是已经完成部分的矩形,UnReachedRectF是未完成的矩形。
因此在绘制的时候,我们需要通过上下左右的位置坐标,将这个View画出来。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- Android 自定义 View (04自定义属性)
- Vue自定义组件(简单实现一个自定义组件)
- Android 自定义View:深入理解自定义属性(七)
- Qt编写自定义控件20-自定义饼图 原 荐
- SpringBoot2 | SpringBoot自定义AutoConfiguration | SpringBoot自定义starter(五)
- 『互联网架构』软件架构-springboot自定义视图和自定义Starter(90)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。