[TOC]
安卓的ui元素全部都基于view或者是viewgroup。在一些app中我imenxuyao自定义view来满足我们的需求,这意味着对于现有的view的一些延伸创造view的子类以创造更加复杂的view。
自定义自己的view视图意味着扩展view或者一个存在的子类,然后能够重写view的某些行为例如onDraw
和onToutchEvent
然后在你的活动中使用。
创建完全自定义的组件
自定义组件我们主要着重于5个方面:
- Drawing: 控制市局上view的渲染,通过重写onDraw
方法
- Interaction: 控制用户和view的交互方式通过控制onTouchEvent
和手势
- Measurement: 控制view的内容区域通过重写onMeasure()
方法
- Attributes: 自定义vie的XML的属性,然后使用TypedArray控制相关的行为
- Persistence: 存储或者获得相关的状态以避免失去状态,方法:onSavedInstanceState
和onRestoreInstanceState
下面我们将自定义一个试图展示不同的图形。
定义view类
为了实现一个可切换的图形选择器我们定义一个ShapeSelectorView
继承View
视图。
1. 建立一个ShapeSelectorView
类继承View类,并写明构造方法
2. 在布局文件中添加我们定义的view
3. 定义了两个属性app:shapeColor
和app:displayShapeName
4. 新建values/attr.xml
文件定义视图的两个属性
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="ShapeSelectorView">
<attr name="shapeColor" format="color" />
<attr name="displayShapeName" format="boolean" />
</declare-styleable>
</resources>
5.提取视图的相关属性赋值给成员变量,使用TypedArray
和obtainStyledAttributes
在AttributeSet上
public class ShapeSelectorView extends View {
private int shapeColor;
private boolean displayShapeName;
public ShapeSelectorView(Context context, AttributeSet attrs) {
super(context, attrs);
setupAttributes(attrs);
}
private void setupAttributes(AttributeSet attrs) {
// Obtain a typed array of attributes
TypedArray a = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.ShapeSelectorView, 0, 0);
// Extract custom attributes into member variables
try {
shapeColor = a.getColor(R.styleable.ShapeSelectorView_shapeColor, Color.BLACK);
displayShapeName = a.getBoolean(R.styleable.ShapeSelectorView_displayShapeName, false);
} finally {
// TypedArray objects are shared and must be recycled.
a.recycle();
}
}
}
6绘制一个形状
public class ShapeSelectorView extends View {
// ...
private int shapeWidth = 100;
private int shapeHeight = 100;
private int textXOffset = 0;
private int textYOffset = 30;
private Paint paintShape;
// ...
public ShapeSelectorView(Context context, AttributeSet attrs) {
super(context, attrs);
setupAttributes(attrs);
setupPaint();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRect(0, 0, shapeWidth, shapeHeight, paintShape);
if (displayShapeName) {
canvas.drawText("Square", shapeWidth + textXOffset, shapeHeight + textXOffset, paintShape);
}
}
private void setupPaint() {
paintShape = new Paint();
paintShape.setStyle(Style.FILL);
paintShape.setColor(shapeColor);
paintShape.setTextSize(30);
}
}
整体的代码:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/tvPrompt"
>
<com.codepath.shapeselector.ShapeSelectorView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/shapeSelector"
app:shapeColor="#7f0000"
app:displayShapeName="true"/>
</RelativeLayout>
public class ShapeSelectorView extends View {
private int shapeColor;
private boolean displayShapeName;
private int shapeWidth = 100;
private int shapeHeight = 100;
private int textXOffset = 0;
private int textYOffset = 30;
private Paint paintShape;
public boolean isDisplayShapeName() {
return displayShapeName;
}
//状态改变时需要重新绘制
public void setDisplayShapeName(boolean displayShapeName) {
this.displayShapeName = displayShapeName;
invalidate();//状态改变时,使view无效?
requestLayout();//当布局改变时调用
}
public int getShapeColor() {
return shapeColor;
}
public void setShapeColor(int shapeColor) {
this.shapeColor = shapeColor;
invalidate();
requestLayout();
}
//创建一个构造方法,包含父构造方法和属性集合和笔画的设置
public ShapeSelectorView(Context context, AttributeSet attrs) {
super(context, attrs);
//设置属性
setupAttributes(attrs);
setupPaint();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//参数分别为左上右下
canvas.drawRect(0, 0, shapeWidth, shapeHeight, paintShape);
if (displayShapeName) {
canvas.drawText("Square", textXOffset, shapeHeight + textYOffset, paintShape);
}
}
private void setupPaint() {
paintShape = new Paint();
paintShape.setStyle(Paint.Style.FILL);
paintShape.setColor(shapeColor);
paintShape.setTextSize(30);
}
//参数为一些属性的集合,可以与xml文件连接起来的属性
private void setupAttributes(AttributeSet attrs) {
//获得属性类型数组
TypedArray a = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.ShapeSelectorView, 0, 0);
try {
//提取属性作为成员变量
shapeColor = a.getColor(R.styleable.ShapeSelectorView_shapeColor, Color.BLACK);
displayShapeName = a.getBoolean(R.styleable.ShapeSelectorView_displayShapeName, false);
} finally {
a.recycle();//回收typedArray供以后使用
}
}
}
计算大小
//测量view和它的内容来确定高度和宽度
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//给名称定义额外的间距
int textPadding = 10;
int contentWidth = shapeWidth;
//根据测量的最小值和测量规格确定宽度:内容宽度 + 左边距+右边距
int minw = contentWidth + getPaddingLeft() + getPaddingRight();
int w = resolveSizeAndState(minw, widthMeasureSpec, 0);
//让view的获得足够的高度
int minh = shapeHeight + getPaddingTop() + getPaddingBottom();
if (displayShapeName) {
//如果展示名称的话要加上文字高度
minh += textYOffset + textPadding;
}
int h = resolveSizeAndState(minh, heightMeasureSpec, 0);
//调用此方法确定测量的宽度和高度
//可以使用getMeasuredWidth和getMeasuredHeight获得值
setMeasuredDimension(w, h);
}
onMeasure
方法决定了基于内容的视图的高度和宽度,要记住计算包括了view的内间距和内容的大小,而且这个方法必须带哦用setMeasuredDimension
。MeasureSpec
包含了父布局对子布局的限制,resolveSizeAndState
通过两方面进行比较返回一个恰当的值。
切换视图
我们想要每次按钮被点击的时候就会使图形被改变,因此需要onTouchEvent
方法处理点击事件,每次点击都会改变下标
public class ShapeSelectorView extends View {
// ...
private String[] shapeValues = { "square", "circle", "triangle" };
private int currentShapeIndex = 0;
// Change the currentShapeIndex whenever the shape is clicked
@Override
public boolean onTouchEvent(MotionEvent event) {
boolean result = super.onTouchEvent(event);
if (event.getAction() == MotionEvent.ACTION_DOWN) {
currentShapeIndex = (++currentShapeIndex) % shapeValues.length;
postInvalidate();
return true;
}
return result;
}
}
下面我们重写onDraw
方法:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
String shapeSelected = shapeValues[currentShapeIndex];
if (shapeSelected.equals("square")) {
canvas.drawRect(0, 0, shapeWidth, shapeHeight, paintShape);
textXOffset = 0;
} else if (shapeSelected.equals("circle")) {
canvas.drawCircle(shapeWidth / 2, shapeHeight / 2, shapeWidth / 2, paintShape);
textXOffset = 12;
} else if (shapeSelected.equals("triangle")) {
canvas.drawPath(getTrianglePath(), paintShape);
textXOffset = 0;
}
if (displayShapeName) {
canvas.drawText(shapeSelected, 0 + textXOffset, shapeHeight + textYOffset, paintShape);
}
}
protected Path getTrianglePath() {
Point p1 = new Point(0, shapeHeight), p2 = null, p3 = null;
p2 = new Point(p1.x + shapeWidth, p1.y);
p3 = new Point(p1.x + (shapeWidth / 2), p1.y - shapeHeight);//原点为屏幕左上角
Path path = new Path();
path.moveTo(p1.x, p1.y);
path.lineTo(p2.x, p2.y);
path.lineTo(p3.x, p3.y);
return path;
}
// ...
}
然后就是对于在主视图中加入按钮点击后,弹出Toast,
保存view相应状态
@Override
protected Parcelable onSaveInstanceState() {
//创建Bundle对象
Bundle bundle = new Bundle();
//存储基本view的状态
bundle.putParcelable("instanceState", super.onSaveInstanceState());
//存储自定义view的状态
bundle.putInt("currentShapeIndex", this.currentShapeIndex);
//如果有的话还应该存储其他的状态
return bundle;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
//检测我们是否保存了状态
if (state instanceof Bundle) {
Bundle bundle = (Bundle) state;
this.currentShapeIndex = bundle.getInt("currentShapeIndex");
state = bundle.getParcelable("instanceState");
}
super.onRestoreInstanceState(state);
}