设计良好的类总是相似的。它使用一个好用的接口来封装一个特定的功能(功能),它有效的使用CPU与内存(性能),等等。
1、创建自定义的View类
继承一个View
为了让Android Developer Tools能够识别你的view,你必须至少提供一个constructor,它包含一个Contenx与一个AttributeSet对象作为参数。这个constructor允许layout editor创建并编辑你的view的实例。
自定义View有四个构造函数。
// 如果View是在Java代码里面new的,则调用第一个构造函数 public CircleView(Context context) { super(context); Log.e(tag, "1...."); // 在构造函数里初始化画笔的操作 init(); } // 如果View是在.xml里声明的,则调用第二个构造函数 // 自定义属性是从AttributeSet参数传进来的 public CircleView(Context context, AttributeSet attrs) { super(context, attrs); Log.e(tag, "2...."); // 加载自定义属性集合CircleView TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleView); // 解析集合中的属性circle_color属性 // 该属性的id为:R.styleable.CircleView_circle_color // 将解析的属性传入到画圆的画笔颜色变量当中(本质上是自定义画圆画笔的颜色) // 第二个参数是默认设置颜色(即无指定circle_color情况下使用) mColor = a.getColor(R.styleable.CircleView_circle_color, Color.RED); // 解析后释放资源 a.recycle(); init(); } int mColor; // 不会自动调用 // 一般是在第二个构造函数里主动调用 // 如View有style属性时 public CircleView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); Log.e(tag, "3...."); // 加载自定义属性集合CircleView TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleView); // 解析集合中的属性circle_color属性 // 该属性的id为:R.styleable.CircleView_circle_color // 将解析的属性传入到画圆的画笔颜色变量当中(本质上是自定义画圆画笔的颜色) // 第二个参数是默认设置颜色(即无指定circle_color情况下使用) mColor = a.getColor(R.styleable.CircleView_circle_color, Color.RED); // 解析后释放资源 a.recycle(); // init(); } //API21之后才使用 // 不会自动调用 // 一般是在第二个构造函数里主动调用 // 如View有style属性时 public CircleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); Log.e(tag, "4...."); }
定义自定义属性
为了添加一个内置的View到你的UI上,你需要通过XML属性来指定它的样式与行为。良好的自定义views可以通过XML添加和改变样式,为了让你的自定义的view也有如此的行为,你应该:
- 为你的view在资源标签下定义自设的属性
- 在你的XML layout中指定属性值
- 在运行时获取属性值
- 把获取到的属性值应用在你的view上
<declare-styleable name="CircleView"> <!--在attr标签下设置需要的自定义属性--> <!--此处定义了一个设置图形的颜色:circle_color属性,格式是color,代表颜色--> <!--格式有很多种,如资源id(reference)等等--> <attr name="circle_color" format="color"/> </declare-styleable>
应用自定义属性
1、资源文件里直接配置
一旦你定义了自设的属性,你可以在layout XML文件中使用它们,就像内置属性一样。唯一不同的是你自设的属性是归属于不同的命名空间。不是属于http://schemas.android.com/apk/res/android
的命名空间,它们归属于http://schemas.android.com/apk/res/[your package name]
。
<?xml version="1.0" encoding="utf-8"?> <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" xmlns:CircleView="http://schemas.android.com/apk/res-auto" android:orientation="vertical" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" android:paddingBottom="@dimen/activity_vertical_margin" tools:context="com.hy.appui.userdefinedview.CircleViewActivity"> <com.hy.appui.userdefinedview.CircleView android:id = "@+id/c1" android:layout_width="match_parent" android:layout_height="100dp" android:background="#000000" android:padding="20dp" CircleView:circle_color = "#BDFCC9"/> <Button android:id = "@+id/b1" android:layout_width="match_parent" android:layout_height="wrap_content" android:textSize="20sp" android:text="change"/> </LinearLayout>
2、在代码里配置
(1)在自定义view里提供对应的getter/setter接口。
public class CircleView extends View { String tag = "060_CircleView"; // 自定义View有四个构造函数 // 如果View是在Java代码里面new的,则调用第一个构造函数 public CircleView(Context context) { super(context); Log.e(tag, "1...."); // 在构造函数里初始化画笔的操作 init(); } // 如果View是在.xml里声明的,则调用第二个构造函数 // 自定义属性是从AttributeSet参数传进来的 public CircleView(Context context, AttributeSet attrs) { super(context, attrs); Log.e(tag, "2...."); // 加载自定义属性集合CircleView TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleView); // 解析集合中的属性circle_color属性 // 该属性的id为:R.styleable.CircleView_circle_color // 将解析的属性传入到画圆的画笔颜色变量当中(本质上是自定义画圆画笔的颜色) // 第二个参数是默认设置颜色(即无指定circle_color情况下使用) mColor = a.getColor(R.styleable.CircleView_circle_color, Color.RED); // 解析后释放资源 a.recycle(); init(); } int mColor; // 不会自动调用 // 一般是在第二个构造函数里主动调用 // 如View有style属性时 public CircleView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); Log.e(tag, "3...."); // 加载自定义属性集合CircleView TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleView); // 解析集合中的属性circle_color属性 // 该属性的id为:R.styleable.CircleView_circle_color // 将解析的属性传入到画圆的画笔颜色变量当中(本质上是自定义画圆画笔的颜色) // 第二个参数是默认设置颜色(即无指定circle_color情况下使用) mColor = a.getColor(R.styleable.CircleView_circle_color, Color.RED); // 解析后释放资源 a.recycle(); // init(); } //API21之后才使用 // 不会自动调用 // 一般是在第二个构造函数里主动调用 // 如View有style属性时 public CircleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); Log.e(tag, "4...."); } // 设置画笔变量 Paint mPaint1; // 画笔初始化 private void init() { // 创建画笔 mPaint1 = new Paint(); // 设置画笔颜色为蓝色 mPaint1.setColor(mColor); // 设置画笔宽度为10px mPaint1.setStrokeWidth(5f); //设置画笔模式为填充 mPaint1.setStyle(Paint.Style.FILL); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec,heightMeasureSpec); Log.e(tag, "onMeasure >>>>>>>>>>>>widthMeasureSpec : " + widthMeasureSpec + " , " + " heightMeasureSpec : " + heightMeasureSpec); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed,left,top,right,bottom); Log.e(tag, "onLayout >>>>>>>>>>>>changed : " + changed + " , " + " left" + left + " , " + " top" + top + " , " + " right" + right + " , " + " bottom" + bottom); } // 复写onDraw()进行绘制 @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); Log.e(tag, "onDraw"); init(); // 获取传入的padding值 final int paddingLeft = getPaddingLeft(); final int paddingRight = getPaddingRight(); final int paddingTop = getPaddingTop(); final int paddingBottom = getPaddingBottom(); // 获取绘制内容的高度和宽度(考虑了四个方向的padding值) int width = getWidth() - paddingLeft - paddingRight; int height = getHeight() - paddingTop - paddingBottom; // 设置圆的半径 = 宽,高最小值的2分之1 int r = Math.min(width, height) / 2; // 画出圆(蓝色) // 圆心 = 控件的中央,半径 = 宽,高最小值的2分之1 canvas.drawCircle(paddingLeft + width / 2, paddingTop + height / 2, r, mPaint1); } public int getCircleColor(){ return mColor; } public void setCircleColor(int color){ mColor = color; this.postInvalidate(); } }
2、实现自定义View的绘制
Override onMeasure()
作用:测量子控件的大小,也就是width和heighth。
onMeasure()没有返回值。它通过调用setMeasuredDimension()来获取结果.
/** * specMode一共有三种类型,如下所示: 1. EXACTLY 表示父视图希望子视图的大小应该是由specSize的值来决定的,系统默认会按照这个规则来设置子视图的大小,简单的说(当设置width或height为match_parent时,模式为EXACTLY,因为子view会占据剩余容器的空间,所以它大小是确定的) 2. AT_MOST 表示子视图最多只能是specSize中指定的大小。(当设置为wrap_content时,模式为AT_MOST, 表示子view的大小最多是多少,这样子view会根据这个上限来设置自己的尺寸) 3. UNSPECIFIED 表示开发人员可以将视图按照自己的意愿设置成任意的大小,没有任何限制。这种情况比较少见,不太会用到。 */
int WRAP_WIDTH = 300; //设置width的上限 int WRAP_HEIGHT = 300; //设置heighth的上限 @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec,heightMeasureSpec); int width = MeasureSpec.getSize(widthMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int height = MeasureSpec.getSize(heightMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); Log.e(tag, "onMeasure >>>>>>>>>>>>width : " + width + " , " + " widthMode : " + widthMode + " , " + " height : " + height + " , " + " heightMode : " + heightMode); if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) { setMeasuredDimension(WRAP_WIDTH, WRAP_HEIGHT); } else if (widthMode == MeasureSpec.AT_MOST) { setMeasuredDimension(WRAP_WIDTH, height); } else if (heightMode == MeasureSpec.AT_MOST) { setMeasuredDimension(width, WRAP_HEIGHT); } }
Activity中需要view的宽高时,onCreate
、onStart
和onResume
中都是无法获取的。这是由于view的生命周期和Activity的生命周期不是同步的。解决方法如下:
Activity中在onWindowFocusChanged
中获取。这时View已经初始化完了,可以获取宽高。当Activity窗口获得焦点和失去焦点时均会被调用,因此该函数会被调用多次。
举个栗子
1、
public class CircleViewActivity extends Activity { String tag = "060_CircleViewActivity"; CircleView c1; Button b1; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_panelview); c1 = (CircleView)findViewById(R.id.c1); b1 = (Button) findViewById(R.id.b1); b1.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { c1.setCircleColor(Color.YELLOW); } }); } @Override public void onResume() { super.onResume(); } @Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); if (hasFocus) { int width = c1.getWidth(); int height = c1.getHeight(); Log.e(tag, "width: " + width); Log.e(tag, "height: " + height); Log.e(tag, "measuredWidth: " + c1.getMeasuredWidth()); Log.e(tag, "measuredHeight: " + c1.getMeasuredHeight()); } } }
activity_panelview.xml
<?xml version="1.0" encoding="utf-8"?> <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" xmlns:CircleView="http://schemas.android.com/apk/res-auto" android:orientation="vertical" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" android:paddingBottom="@dimen/activity_vertical_margin" tools:context="com.hy.appui.userdefinedview.CircleViewActivity"> <com.hy.appui.userdefinedview.CircleView android:id = "@+id/c1" android:layout_width = "match_parent" android:layout_height="match_parent" android:background="#000000" CircleView:circle_color = "#BDFCC9"/> <Button android:id = "@+id/b1" android:layout_width="match_parent" android:layout_height="wrap_content" android:textSize="20sp" android:text="change"/> </LinearLayout>
1、设置为match_parent
android:layout_width = "match_parent" android:layout_height="match_parent"
测量出控件大小:
06-28 04:25:37.851 514-514/com.zxcn.test E/060_CircleViewActivity: width: 680
06-28 04:25:37.852 514-514/com.zxcn.test E/060_CircleViewActivity: height: 112006-28 04:25:37.852 514-514/com.zxcn.test E/060_CircleViewActivity: measuredWidth: 680
06-28 04:25:37.852 514-514/com.zxcn.test E/060_CircleViewActivity: measuredHeight: 1120
2、设置为wrap_content
<com.hy.appui.userdefinedview.CircleView android:id = "@+id/c1" android:layout_width = "wrap_content" android:layout_height="wrap_content" android:background="#000000" CircleView:circle_color = "#BDFCC9"/>
06-28 04:32:08.883 1269-1269/com.zxcn.test E/060_CircleViewActivity: width: 300
06-28 04:32:08.883 1269-1269/com.zxcn.test E/060_CircleViewActivity: height: 300
06-28 04:32:08.883 1269-1269/com.zxcn.test E/060_CircleViewActivity: measuredWidth: 300
06-28 04:32:08.883 1269-1269/com.zxcn.test E/060_CircleViewActivity: measuredHeight: 300
3、设置为精确大小
Override onLayout()
设置view的左端、顶端、右端和底端距父控件坐标原点的距离
layout(0, 100, childWidth + 0, childHeigth+100);//设置子控件的位置,需要首先获得子控件的大小
Override onDraw()
1、创建绘图对象
Canvas提供绘制矩形的方法,Paint定义是否使用颜色填充。简单来说:Canvas定义你在屏幕上画的图形,而Paint定义颜色,样式,字体,
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); Log.e(tag, "onDraw"); init(); // 获取传入的padding值 final int paddingLeft = getPaddingLeft(); final int paddingRight = getPaddingRight(); final int paddingTop = getPaddingTop(); final int paddingBottom = getPaddingBottom(); // 获取绘制内容的高度和宽度(考虑了四个方向的padding值) int width = getWidth() - paddingLeft - paddingRight; int height = getHeight() - paddingTop - paddingBottom; // 设置圆的半径 = 宽,高最小值的2分之1 int r = Math.min(width, height) / 2; // 画出圆(蓝色) // 圆心 = 控件的中央,半径 = 宽,高最小值的2分之1 canvas.drawCircle(paddingLeft + width / 2, paddingTop + height / 2, r, mPaint1); }
// 设置画笔变量 Paint mPaint1; // 画笔初始化 private void init() { // 创建画笔 mPaint1 = new Paint(); // 设置画笔颜色为蓝色 mPaint1.setColor(mColor); // 设置画笔宽度为10px mPaint1.setStrokeWidth(5f); //设置画笔模式为填充 mPaint1.setStyle(Paint.Style.FILL); }
3、使得View可交互
处理输入的手势
实现随手指移动控件的效果。private int lastX; private int lastY; @Override public boolean onTouchEvent(MotionEvent event) { // 获取当前触摸的绝对坐标 int rawX = (int) event.getRawX(); int rawY = (int) event.getRawY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: // 上一次离开时的坐标 lastX = rawX; lastY = rawY; break; case MotionEvent.ACTION_MOVE: // 两次的偏移量 int offsetX = rawX - lastX; int offsetY = rawY - lastY; moveView(offsetX, offsetY); // 不断修改上次移动完成后坐标 lastX = rawX; lastY = rawY; break; default: break; } return true; } //跟随手指移动 private void moveView(int offsetX, int offsetY) { // 方法五 ((View) getParent()).scrollBy(-offsetX, -offsetY); }
4、优化自定义View
Do Less, Less Frequently
1、先从onDraw开始,需要特别注意不应该在这里做内存分配的事情,因为它会导致GC,从而导致卡顿。
2、尽量减少调用invaildate()的次数。
//重写调用onDraw() this.postInvalidate();