呃,什么是旋转小按钮?上图:
自定义这个View的原因是我需要一个能点击一下就能旋转显示正在刷新的小按钮,等刷新结束后在使它停止旋转并恢复到初始状态,并且这个View的字体大小,字体颜色,进度条的颜色等都可以自由配置。
自定义View包含以下几步:
1、自定义View的属性
2、在XML布局文件中使用自定义属性
2、在View的构造方法中获得我们配置的属性
3、重写onMesure
4、重写onDraw
1、自定义View的属性,首先在res/values/ 下建立一个attrs.xml ,并在其中添加需要的属性,本例如下:
<resources>
<declare-styleable name="RefreshView">
<attr name="textSize" format="dimension">attr>
<attr name="textColor" format="color">attr>
<attr name="text" format="string">attr>
<attr name="processColor" format="color">attr>
<attr name="processWidth" format="dimension">attr>
declare-styleable>
resources>
declare-stylable标签只是为了给自定义属性分类。一个项目中可能又多个自定义控件,但只能又一个attr.xml文件,因此我们需要对不同自定义控件中的自定义属性进行分类,这也是为什么declare-stylable标签中的name属性往往定义成自定义控件的名称(引用来自【Android - 自定义View】之自定义View浅析);
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:custom="http://schemas.android.com/apk/res-auto"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.sharenew.viewselfdefine.MainActivity">
<com.sharenew.viewselfdefine.RefreshView
android:layout_centerInParent="true"
android:id="@+id/refreshview"
android:layout_width="100dp"
android:layout_height="100dp"
android:padding="10dp"
custom:text="刷新"
custom:processWidth="3dp"
custom:processColor="@android:color/holo_orange_light"
custom:textColor="@android:color/holo_green_dark"
custom:textSize="22sp"
/>
RelativeLayout>
我们的布局文件只有自定义的一个组件,它位于Activity的中央。为了使用自定义的属性,我们首先要声明自定的名称空间,在Gradle构建的工程中,它总是这样的:xmlns:custom=”http://schemas.android.com/apk/res-auto”
有了名称空间,我们就可以在该xml中使用自定的属性了。
public RefreshView(Context context) {
this(context, null);
}
public RefreshView(Context context, AttributeSet attrs) {
this(context,attrs,0);
}
public RefreshView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
/**
* 获得我们所定义的自定义样式属性
*/
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RefreshView);
int n = a.getIndexCount();
for (int i = 0; i < n; i++)
{
int attr = a.getIndex(i);
switch (attr) {
case R.styleable.RefreshView_text:
mText = a.getString(attr);
break;
case R.styleable.RefreshView_textColor:
// 默认颜色设置为黑色
mTextColor = a.getColor(attr, Color.BLACK);
break;
case R.styleable.RefreshView_textSize:
// 默认设置为16sp,TypeValue也可以把sp转化为px
mTextSize = (int) a.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));
break;
case R.styleable.RefreshView_processColor:
mProcessColor = a.getColor(attr,Color.YELLOW);
break;
case R.styleable.RefreshView_processWidth:
mProcessWidth = (int) a.getDimensionPixelSize(attr,(int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP, 3, getResources().getDisplayMetrics()));
break;
}
}
a.recycle();
/**
* 获得绘制文本的宽和高
*/
mPaint = new Paint();
mPaint.setAntiAlias(true);
if(mText!=null){
mTextBound = new Rect();
mPaint.setStrokeWidth(1);
mPaint.setTextSize(mTextSize);
mPaint.getTextBounds(mText,0,mText.length(),mTextBound);
}else {
mTextBound = new Rect(0,0,30,30);
}
/**
* 计算view的大小
*/
mProcessBound = new RectF(0,0,mTextBound.width()+mProcessWidth,mTextBound.height()+mProcessWidth);
}
我们重写了3个构造方法。在java代码中new出来的对象,默认使用的是一个参数的构造方法,使用布局文件创造的对象,默认使用的是的是两个参数的构造方法。关于为什么这两个构造函数最终都要调用到三个参数的构造函数,呃,这似乎已经成为了一种习惯…
注意:不管有没有使用自定义属性,都会默认调用两个参数的构造方法,“使用了自定义属性就会默认调用三个参数的构造方法”的说法是错误的。
obtainStyledAttributes方法会从attrs中提取出自定义的属性,并将所有的属性存放在TypedArray 的一个对象中,这样通过简单的解析这个对象,就能得到所有的自定义属性的值。
一个View的绘制流程是,首先调用onMeasure,然后才会调用onDraw。onMeasure用于测量这个view的大小。
对于自定的View,一定要处理WRAP_CONTENT的情况,不然配置WRAP_CONTENT会得到MATCH_PATENT的效果。
我们知道在ViewGroup中,给View分配的空间大小并不是确定的,有可能随着具体的变化而变化,而这个变化的条件就是传到specMode中决定的,specMode一共有三种可能:
MeasureSpec.EXACTLY:父视图希望子视图的大小应该是specSize中指定的。
MeasureSpec.AT_MOST:子视图的大小最多是specSize中指定的值,也就是说不建议子视图的大小超过specSize中给定的值。
MeasureSpec.UNSPECIFIED:我们可以随意指定视图的大小。
通过以上这些分析,可以知道视图最终的大小由父视图,子视图以及程序员根据需要决定,良好的设计一般会根据子视图的measureSpec设置合适的布局大小。
本例实现onMeasure如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width;
int height ;
if (widthMode == MeasureSpec.EXACTLY) {
width = widthSize;
} else {
int maxTextSize = (mTextBound.width()>mTextBound.height())?mTextBound.width():mTextBound.height();
width = maxTextSize + mProcessWidth*2+20;
}
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else {
height = width;
}
viewWidth = width;
viewHeight = height;
mProcessBound.set(mProcessWidth+getPaddingLeft(),mProcessWidth+getPaddingTop(),width-mProcessWidth-getPaddingRight(),height-mProcessWidth-getPaddingBottom());
setMeasuredDimension(width, height);
}
最后一步就是重写onDraw了。onDraw()方法负责绘制,即如果我们希望得到的效果在Android原生控件中没有现成的支持,那么我们就需要自己绘制我们的自定义控件的显示效果。要学习onDraw()方法,我们就需要学习在onDraw()方法中使用最多的两个类:Paint和Canvas。
Paint类似一个画笔,Canvas类似一个画布。
画笔的粗细,实心还是空心等都是在Paint的实例中设置的,具体的绘制是在Canvas中绘制,因此Canvas中有很多的draw函数,比如画圆、画矩形等。
一下为本例实现的draw函数:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(Color.DKGRAY);
mPaint.setStrokeWidth(mProcessWidth);
canvas.drawArc(mProcessBound,0,360,false,mPaint);
mPaint.setColor(mProcessColor);
canvas.drawArc(mProcessBound,startAngle,swapArea,false,mPaint);
if(mText!=null && mText!=""){
mPaint.setColor(mTextColor);
mPaint.setTextSize(mTextSize);
mPaint.setStyle(Paint.Style.FILL);
canvas.drawText(mText,(int)(viewWidth/2-mTextBound.width()/2),(int)(viewHeight/2+mTextBound.height()/2),mPaint);
}
}
注意,本例中,圆圈中的文字一定是居中的,Paint中有相关的方法可以测量字符串所占的像素大小,比如getTextBounds,measureText等。
更新动画的参数是在子线程中完成的,子线程通知UI线程重新绘制使用的是postInvalidate方法。为了启动和停止动画,我们必须提供start和stop接口,其实现如下:
public void start(){
startRun = true;
startAngle = 0;
swapArea = 60;
new Thread(new Runnable() {
boolean isSwapAreaReverse = false;
@Override
public void run() {
while (startRun){
startAngle++;
if(isSwapAreaReverse){
swapArea--;
startAngle++;
}else {
swapArea++;
}
if(swapArea==360)isSwapAreaReverse = true;
if(swapArea==0)isSwapAreaReverse=false;
RefreshView.this.postInvalidate();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
public void stop(){
startRun = false;
startAngle = 0;
swapArea = 0;
invalidate();
}
以上便是自定义旋转小按钮的核心程序,需要全部代码可从这里下载:
Android 自定义View-旋转小按钮源码