本文原创,技术新手,如有问题,请多指教。
记得当初面试某公司的时候,面试官看我做Android的界面似乎做的很漂亮,有意将我引导做移动UI方面的工作,就问我关于有没有自定义过组件,那时候的我,刚刚知道有自定义组件这个似乎跟高深的知识领域,但晕晕乎乎就被onMeasure onLayout等函数给弄糊涂了,更别说自己用过了,于是错过了一个很好的机会。
机会永远留给有准备的人。
当我接触了更多的自定义View的资料,并且自己使用过后,自定义View其实没有想象中那么难。
我们为什么要自定义控件?
1.为了特定的显示风格,丰富界面。
2.使组件更够处理特有的用户交互。
3.可以优化界面的布局。
4.封装特定功能的组件。
自定义组件的六大步骤。
- 自定义属性的声明和获取。
- 测量onMeasure。
- 布局onLayout。
- 绘制onDraw。
- onTouchEvent。
- onInterceptTouchEvent。
其中这六大步骤中 3 和6 主要是用于自定义ViewGroup类型的组件的时候 用于子View的测量和定位的。
而1步骤主要是方便在xml中使用的。
因此最简单的不需要包装点击事件的一个自定义View其实只需要2,4。
是不是很简单?
下面我们详细地介绍一下上面所属的六大步骤。
自定义属性的声明与获取
- 分析需要的自定义属性
- 在res/values/attrs.xml中定义声明。
- 在layout.xml文件中进行使用。
- 在view的构造方法中进行获取。
比如我需要的自定义组件是一个简单的圆。
那么我需要圆的半径,圆的颜色等属性。
res/values/attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--声明自定义view的属性 -->
<attr name="circle_radius" format="dimension"></attr>
<attr name="circle_color" format="color"></attr>
<declare-styleable name="MyCircleView">
<attr name="circle_radius" ></attr>
<attr name="circle_color" ></attr>
</declare-styleable>
</resources>
其中比较常用的format分别有string ,color,dimension,reference.等
分别对应 字符串 颜色 尺寸 引用的文件 等格式。
自定义属性结束之后,我们就可以新建MyCircleView继承View了。
为了方便我们使用各种方式生成组件,我们需要实现三个构造函数都实现。
然后在构造函数中获取我们声明的属性的值。
public class MyCircleView extends View {
private static final int DEFAULT_SIZE = ;
private static final int DEFAULT_COLOR = ;
private int mCircleColor;
private int mCirCleRadius;
private Paint mPaint;
// 构造函数 三个
public MyCircleView(Context context) {
this(context, null);
}
public MyCircleView(Context context, AttributeSet attrs) {
this(context, attrs, );
}
public MyCircleView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
// 初始化 根据参数值初始化成员变量
obtainStyleAttrs(attrs);
}
// 添加参数 初始化参数
private void obtainStyleAttrs(AttributeSet attrs) {
TypedArray ta = getContext().obtainStyledAttributes(attrs,
R.styleable.MyCircleView);
// 获取自定义属性值
mCircleColor = ta.getColor(R.styleable.MyCircleView_circle_color,
DEFAULT_COLOR);
mCirCleRadius = dp2px((int) ta.getDimension(
R.styleable.MyCircleView_circle_radius, DEFAULT_SIZE));
ta.recycle();
}
// 单位转换函数
// dp就是dip 设备独立像素 与密度(dpi)无关 与一个位于像素密度为 160dpi 的屏幕上的像素是一致的
// px 像素 对应屏幕上的实际像素
// sp 与的dp类似 用于字体
// pt 屏幕物理长度单位 表示一个店 是屏幕的物理尺寸
// 可以用这样一个简单的公式: px=dp*(density/160)。举个例子,在 密度 为 240 的屏幕上,1 个 DIP 等 于 1.5
// 个物理像素。
private int dp2px(int dpVal) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
dpVal, getResources().getDisplayMetrics());
}
private int sp2px(int spVal) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
spVal, getResources().getDisplayMetrics());
}
关于屏幕单位的简单介绍。
dp就是dip 设备独立像素 与密度(dpi)无关
与一个位于像素密度为 160dpi 的屏幕上的像素是一致的
px 像素 对应屏幕上的实际像素
sp 与的dp类似 用于字体
pt 屏幕物理长度单位 表示一个点 是屏幕的物理尺寸
可以用这样一个简单的公式: px=dp*(density/160)。
举个例子,在 密度 为 240 的屏幕上,1 个 DIP 等 于 1.5 个物理像素。
到此为止,我们已经完成了第一个大步骤的进行了。
现在我们需要了解onMeaure函数了。
首先我们要先了解一下测量的三种模式。
测量的三种模式
exactly 自定义的精确大小 固定不变的
at_most 根据内容的大小来决定整体的大小 最大不超过父控件的大小
unspecified 未规定
// 重写函数
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 设置计算后的宽高
setMeasuredDimension(measureWidthSize(widthMeasureSpec),
measureHeightSize(heightMeasureSpec));
}
private int measureWidthSize(int widthMeasureSpec) {
// 获取分别获取宽的模式和尺寸
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
int result = ;
if (widthMode == MeasureSpec.EXACTLY) {
// 测量规格模式:父组件已确定子组件的确切尺寸。 view要被赋予这些限制,无论它想要多大。
result = width;
} else {
// 根据内容确切地计算需要占用的尺寸
result = mCirCleRadius * + getPaddingLeft() + getPaddingRight();
if (widthMode == MeasureSpec.AT_MOST) {
// 测量规格模式:孩子可以达到指定尺寸的大小。
result = Math.min(width, result);
}
}
return result;
}
private int measureHeightSize(int heightMeasureSpec) {
// 获取分别获取宽的模式和尺寸
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
int result = ;
if (heightMode == MeasureSpec.EXACTLY) {
// 测量规格模式:父组件已确定子组件的确切尺寸。 view要被赋予这些限制,无论它想要多大。
result = height;
} else {
// 根据内容确切地计算需要占用的尺寸
result = mCirCleRadius * + getPaddingTop() + getPaddingBottom();
if (heightMode == MeasureSpec.AT_MOST) {
// 测量规格模式:孩子可以达到指定尺寸的大小。
result = Math.min(height, result);
}
}
return result;
}
这里使用到的MeasureSpec是 一个封装类 用于提取mode 和 size 父控件向子空间传递的参数类型 用于表示子控件最大的size
Requestlayout()函数 用于重新测量
注意:OnMeasure() 方法比较经常被调用 最好不要将耗时操作放在这里
关于onLayout()的使用 由于此次组件不是ViewGroup 我会放在后面介绍。
因此接下来就是onDraw()函数,即在我们的组件上画一个圆。
这部分的内容需要大家了解一下画布和画笔的使用方法,这部分比较简单,我不再赘述。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 重写ondraw函数使用画布canvas来绘制需要的图形
// 设置画笔属性
mPaint = new Paint();
mPaint.setColor(mCircleColor);
canvas.drawCircle(getMeasuredWidth() / , getMeasuredHeight() / ,
mCirCleRadius, mPaint);
}
上面的代码就是在组件的中间画出指定大小和指定颜色的圆。
最后我也在代码里封装了简单的点击事件的封装。
这样更加利于代码重用和功能封装。
不需要在活动中另外实现监听器重写ontouch函数了。
不够这样的情况只适用于你在任何情况都需要这样的点击事件处理操作。
@Override
public boolean onTouchEvent(MotionEvent event) {
// 设置当用户点击组件的时候 圆圈变大 直到圆圈直径等于屏幕宽 圆圈缩小到默认大小。
//注意此动画 不适用于组件自适应大小的时候。
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
mCirCleRadius+=;
if(mCirCleRadius*>getMeasuredWidth())
{
mCirCleRadius=DEFAULT_SIZE;
}
break;
case MotionEvent.ACTION_UP:
break;
case MotionEvent.ACTION_MOVE:
break;
}
postInvalidate();
// 返回true 表示已经消费了此事件
return true;
}
到此,我们的自定义组件已经大功告成了。
由于我们功能已经十分完善,我们只需要在layout.xml中直接使用该组件,并指定半径和颜色,就可以使用该组件,并且不需要在活动中写任何代码了。
活动的layout布局
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
xmlns:app="http://schemas.android.com/apk/res-auto">
<com.example.myviewdemo.MyCircleView
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="#23232323"
app:circle_radius="100dp"
app:circle_color="#ffffffff"
/>
</LinearLayout>
我的活动中简洁地就想没写过任何代码一样,但我们的应用以及实现了所有功能。
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
这就是自定义view的魅力之处。
