一直感觉会自定义view很牛掰,在毕设项目中多处用到自定义控件,但都是在别人的框架上造轮子,一直打算总结一下自定义控件的实现方式,今天就来总结一下吧。
回到主题,自定义View ,需要掌握的几个点是什么呢?
我们先把自定义View细分一下,分为两种
1) 自定义View
2) 自定义ViewGroup
今天先总结自定义view(看大牛们的博客,根据自己的理解和学习习惯总结,嘿嘿,我学习东西一般喜欢先框架再细节)
一、自定义控件需要考虑的点
根据Android Developers官网的介绍,自定义控件你需要以下的步骤。(根据你的需要,某些步骤可以省略)
先总结下自定义View的步骤:
1、自定义View的属性
2、在View的构造方法中获得我们自定义的属性
[ 3、重写onMesure ]
4、重写onDraw
我把3用[]标出了,所以说3不一定是必须的,当然了大部分情况下还是需要重写的。
1/1 、创建View
(1)继承View
在Android中自定义一个View类并定是直接继承View类或者View类的子类比如TextView、Button等等,这里呢我们也依葫芦画瓢直接继承View自定义一个View的子类CustomView:
public class CustomView extends View {
public CustomView(Context context) {
super(context);
}
public CustomView(Context context, AttributeSet attrs) {
super(context, attrs);
}
}
Context是什么你不用管,只管记住它包含了许多各种不同的信息穿梭于Android中各类组件、控件等等之间,说得不恰当点就是一个装满信息的信使,Android需要它从里面获取需要的信息。
我们在xml文件引用我们的CustomView类时为其指定了两个android自带的两个属性:layout_width和layout_height,当我们需要使用类似的属性(比如更多的什么id啊、padding啊、margin啊之类)时必须在自定义View的构造方法中添加一个AttributeSet类型的签名来解析这些属性:
(2)定义自定义属性
自定义属性通常写在在res/values/attrs.xml文件中 下面是自定义属性的标准写法
这个文件定义了自定义View的属性的信息,包括属于哪个控件属性的名称,属性的类型。format是值该属性的取值类型:
一共有:string,color,demension,integer,enum,reference,float,boolean,fraction,flag;不清楚的可以google一把。
<declare-styleable name="PieChart">
<attr name="showText" format="boolean" />
<attr name="labelPosition" format="enum">
<enum name="left" value="0"/>
<enum name="right" value="1"/>
attr>
declare-styleable>
这段代码声明了两个自定义属性,showText和labelPosition,它们都是属于styleable PieChart的,为了方便,一般styleable的name和我们自定义控件的类名一样。自定义控件定义好了之后就是使用了。
使用代码示例
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:custom="http://schemas.android.com/apk/res/com.example.customviews">
<com.example.customviews.charting.PieChart
custom:showText="true"
custom:labelPosition="left" />
LinearLayout>
使用自定义属性的时候需要指定命名空间,固定写法就是http://schemas.android.com/apk/res/你的包名。如果你是在android studio,也可以用http://schemas.android.com/apk/res/res-auto
(3)获取自定义属性
在xml中设置了控件自定义属性,我们就需要拿到属性做一些事情。否则定义自定义属性就没有意义了。
固定的获取自定义属性代码如下
public PieChart(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.getTheme().obtainStyledAttributes(
attrs,
R.styleable.PieChart,
0, 0);
try {
mShowText = a.getBoolean(R.styleable.PieChart_showText, false);
mTextPos = a.getInteger(R.styleable.PieChart_labelPosition, 0);
} finally {
a.recycle();
}
}
当我们在 xml中创建了一个view时,所有在xml中声明的属性都会被传入到view的构造方法中的AttributeSet类型的参数当中。
通过调用Context的obtainStyledAttributes()方法返回一个TypedArray对象。然后直接用TypedArray对象获取自定义属性的值。
由于TypedArray对象是共享的资源,所以在获取完值之后必须要调用recycle()方法来回收。
举例加深理解创建view
1、自定义View的属性,首先在res/values/ 下建立一个attrs.xml , 在里面定义我们的属性和声明我们的整个样式。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="titleText" format="string" />
<attr name="titleTextColor" format="color" />
<attr name="titleTextSize" format="dimension" />
<declare-styleable name="CustomTitleView">
<attr name="titleText" />
<attr name="titleTextColor" />
<attr name="titleTextSize" />
</declare-styleable>
</resources>
2、然后在布局中声明我们的自定义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/com.example.customview01"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<com.example.customview01.view.CustomTitleView
android:layout_width="200dp"
android:layout_height="100dp"
custom:titleText="3712"
custom:titleTextColor="#ff0000"
custom:titleTextSize="40sp" />
</RelativeLayout>
3、在View的构造方法中,获得我们的自定义的样式
/**
* 文本
*/
private String mTitleText;
/**
* 文本的颜色
*/
private int mTitleTextColor;
/**
* 文本的大小
*/
private int mTitleTextSize;
/**
* 绘制时控制文本绘制的范围
*/
private Rect mBound;
private Paint mPaint;
public CustomTitleView(Context context, AttributeSet attrs)
{
this(context, attrs, 0);
}
public CustomTitleView(Context context)
{
this(context, null);
}
/**
* 获得我自定义的样式属性
*
* @param context
* @param attrs
* @param defStyle
*/
public CustomTitleView(Context context, AttributeSet attrs, int defStyle)
{
super(context, attrs, defStyle);
/**
* 获得我们所定义的自定义样式属性
*/
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CustomTitleView, defStyle, 0);
int n = a.getIndexCount();
for (int i = 0; i < n; i++)
{
int attr = a.getIndex(i);
switch (attr)
{
case R.styleable.CustomTitleView_titleText:
mTitleText = a.getString(attr);
break;
case R.styleable.CustomTitleView_titleTextColor:
// 默认颜色设置为黑色
mTitleTextColor = a.getColor(attr, Color.BLACK);
break;
case R.styleable.CustomTitleView_titleTextSize:
// 默认设置为16sp,TypeValue也可以把sp转化为px
mTitleTextSize = a.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));
break;
}
}
a.recycle();
/**
* 获得绘制文本的宽和高
*/
mPaint = new Paint();
mPaint.setTextSize(mTitleTextSize);
// mPaint.setColor(mTitleTextColor);
mBound = new Rect();
mPaint.getTextBounds(mTitleText, 0, mTitleText.length(), mBound);
}
1/2、处理View的布局.
测量
一个View是在展示时总是有它的宽和高,测量View就是为了能够让自定义的控件能够根据各种不同的情况以合适的宽高去展示。提到测量就必须要提到onMeasure方法了。onMeasure方法是一个view确定它的宽高的地方。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
}
onMeasure方法里有两个重要的参数, widthMeasureSpec, heightMeasureSpec。
在这里你只需要记住它们包含了两个信息:mode和size
我们可以通过以下代码拿到mode和sizeint specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
那么获取到的mode和size又代表了什么呢?
mode代表了我们当前控件的父控件告诉我们控件,你应该按怎样的方式来布局。
mode有三个可选值:EXACTLY, AT_MOST, UNSPECIFIED。它们的含义是:
EXACTLY:父控件告诉我们子控件了一个确定的大小,你就按这个大小来布局。比如我们指定了确定的dp值和macth_parent的情况。
AT_MOST:当前控件不能超过一个固定的最大值,一般是wrap_content的情况。
UNSPECIFIED:当前控件没有限制,要多大就有多大,这种情况很少出现。
size其实就是父布局传递过来的一个大小,父布局希望当前布局的大小。
下面是一个重写onMeasure的固定伪代码写法:
if mode is EXACTLY{
父布局已经告诉了我们当前布局应该是多大的宽高, 所以我们直接返回从measureSpec中获取到的size
}else{
计算出希望的desiredSize
if mode is AT_MOST
返回desireSize和specSize当中的最小值
else:
返回计算出的desireSize
}
上面的代码虽然基本都是固定的,但是需要写的步骤还是有点多,如果你不想自己写,你也可以用android为我们提供的工具方法:resolveSizeAndState,该方法需要传入两个参数:我们测量的大小和父布局希望的大小,它会返回根据各种情况返回正确的大小。这样我们就可以不需要实现上面的模版,只需要计算出想要的大小然后调用resolveSizeAndState。之后在做自定义View的时候我会展示用这个方法来确定view的大小。
计算出height和width之后在onMeasure中别忘记调用setMeasuredDimension()方法。否则会出现运行时异常。
计算一些自定义控件需要的值 onSizeChange()
onSizeChange() 方法在view第一次被指定了大小值、或者view的大小发生改变时会被调用。所以一般用来计算一些位置和与view的size有关的值。
View类已经提供了一个基本的onMeasure实现,
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
换汤不换药
@Override
protected void onMeasure(int widthMeasureSpec, int 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
{
mPaint.setTextSize(mTitleTextSize);
mPaint.getTextBounds(mTitle, 0, mTitle.length(), mBounds);
float textWidth = mBounds.width();
int desired = (int) (getPaddingLeft() + textWidth + getPaddingRight());
width = desired;
}
if (heightMode == MeasureSpec.EXACTLY)
{
height = heightSize;
} else
{
mPaint.setTextSize(mTitleTextSize);
mPaint.getTextBounds(mTitle, 0, mTitle.length(), mBounds);
float textHeight = mBounds.height();
int desired = (int) (getPaddingTop() + textHeight + getPaddingBottom());
height = desired;
}
setMeasuredDimension(width, height);
}
de> @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//这里方法套路都是一样,不管三七 二十一,上来就先把mode 和 size 获取出来。
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
//View 真正需要显示的大小
int width = 0, height = 0;
//这里是去测量字体大小
measureText();
//字体宽度加图片宽度取最大宽度,这里因为字体和图片是上下排列
int contentWidth = Math.max(mBoundText.width(), mIconNormal.getWidth());
// 我们渴望得到的宽度
int desiredWidth = getPaddingLeft() + getPaddingRight() + contentWidth;
//重点来了,判断模式,这个模式哪里来的呢,就是在编写xml的时候,设置的layout_width
switch (widthMode) {
//如果是AT_MOST,不能超过父View的宽度
case MeasureSpec.AT_MOST:
width = Math.min(widthSize, desiredWidth);
break;
//如果是精确的,好说,是多少,就给多少;
case MeasureSpec.EXACTLY:
width = widthSize;
break;
//这种情况,纯属在这里打酱油的,可以不考虑
case MeasureSpec.UNSPECIFIED://我是路过的
width = desiredWidth;
break;
}
int contentHeight = mBoundText.height() + mIconNormal.getHeight();
int desiredHeight = getPaddingTop() + getPaddingBottom() + contentHeight;
switch (heightMode) {
case MeasureSpec.AT_MOST:
height = Math.min(heightSize, desiredHeight);
break;
case MeasureSpec.EXACTLY:
height = heightSize;
break;
case MeasureSpec.UNSPECIFIED:
height = contentHeight;
break;
}
//最后不要忘记了,调用父类的测量方法
setMeasuredDimension(width, height);
}
</code>
上面讲了太多理论,我们实际操作一下吧,感受一下onMeasure的使用,假设我们要实现这样一个效果:将当前的View以正方形的形式显示,即要宽高相等,并且默认的宽高值为100像素。就可以这些编写:
private int getMySize(int defaultSize, int measureSpec) {
int mySize = defaultSize;
int mode = MeasureSpec.getMode(measureSpec);
int size = MeasureSpec.getSize(measureSpec);
switch (mode) {
case MeasureSpec.UNSPECIFIED: {//如果没有指定大小,就设置为默认大小
mySize = defaultSize;
break;
}
case MeasureSpec.AT_MOST: {//如果测量模式是最大取值为size
//我们将大小取最大值,你也可以取其他值
mySize = size;
break;
}
case MeasureSpec.EXACTLY: {//如果是固定的大小,那就不要去改变它
mySize = size;
break;
}
}
return mySize;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = getMySize(100, widthMeasureSpec);
int height = getMySize(100, heightMeasureSpec);
if (width < height) {
height = width;
} else {
width = height;
}
setMeasuredDimension(width, height);
}
1/3 、绘制View(Draw)
一旦自定义控件被创建并且测量代码写好之后,接下来你就可以实现onDraw()来绘制View了,onDraw方法包含了一个Canvas叫做画布的参数,onDraw()简单来说就两点:
Canvas决定要去画什么
Paint决定怎么画
比如,Canvas提供了画线方法,Paint就来决定线的颜色。Canvas提供了画矩形,Paint又可以决定让矩形是空心还是实心。
在onDraw方法中开始绘制之前,你应该让画笔Paint对象的信息初始化完毕。这是因为View的重新绘制是比较频繁的,这就可能多次调用onDraw,所以初始化的代码不应该放在onDraw方法里。
Why?Why?说白了就是不建议你在draw或者layout的过程中去实例化对象!为啥?因为draw或layout的过程有可能是一个频繁重复执行的过程,我们知道new是需要分配内存空间的,如果在一个频繁重复的过程中去大量地new对象内存爆不爆我不知道,但是浪费内存那是肯定的!所以Android不建议我们在这两个过程中去实例化对象。既然都这样说了我们就改改呗:
public class CustomView extends View {
private Paint mPaint;
public CustomView(Context context) {
this(context, null);
}
public CustomView(Context context, AttributeSet attrs) {
super(context, attrs);
// 初始化画笔
initPaint();
}
/**
* 初始化画笔
*/
private void initPaint() {
// 实例化画笔并打开抗锯齿
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
}
在Android的画笔里,现实有的它也有,没有的它还有!我们可以用Paint的各种setter方法来设置各种不同的属性,
比如setColor()设置画笔颜色,setStrokeWidth()设置描边线条,setStyle()设置画笔的样式:
Paint集成了所有“画”的属性,而Canvas则定义了所有要画的东西,我们可以通过Canvas下的各类drawXXX方法绘制各种不同的东西,比如绘制一个圆drawCircle(),绘制一个圆弧drawArc(),绘制一张位图drawBitmap()等等等:
既然初步了解了Paint和Canvas,我们不妨就尝试在我们的画布上绘制一点东西,比如一个圆环?我们先来设置好画笔的属性:
/**
* 初始化画笔
*/
private void initPaint() {
// 实例化画笔并打开抗锯齿
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
/*
* 设置画笔样式为描边,圆环嘛……当然不能填充不然就么意思了
*
* 画笔样式分三种:
* 1.Paint.Style.STROKE:描边
* 2.Paint.Style.FILL_AND_STROKE:描边并填充
* 3.Paint.Style.FILL:填充
*/
mPaint.setStyle(Paint.Style.STROKE);
// 设置画笔颜色为浅灰色
mPaint.setColor(Color.LTGRAY);
/*
* 设置描边的粗细,单位:像素px
* 注意:当setStrokeWidth(0)的时候描边宽度并不为0而是只占一个像素
*/
mPaint.setStrokeWidth(10);
}
然后在我们的onDraw方法中绘制Cricle即可:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 绘制圆环
canvas.drawCircle(MeasureUtil.getScreenSize((Activity) mContext)[0] / 2, MeasureUtil.getScreenSize((Activity) mContext)[1] / 2, 200, mPaint);
}
这里要注意哦!drawCircle表示绘制的是圆形,但是在我们的画笔样式设置为描边后其绘制出来的就是一个圆环!其中drawCircle的前两个参数表示圆心的XY坐标,这里我们用到了一个工具类获取屏幕尺寸以便将其圆心设置在屏幕中心位置,第三个参数是圆的半径,第四个参数则为我们的画笔!
这里有一点要注意:在Android中设置数字类型的参数时如果没有特别的说明,参数的单位一般都为px像素。
假设我们需要实现的是,我们的View显示一个圆形,我们在上面已经实现了宽高尺寸相等的基础上,继续往下做:
@Override
protected void onDraw(Canvas canvas) {
//调用父View的onDraw函数,因为View这个类帮我们实现了一些
// 基本的而绘制功能,比如绘制背景颜色、背景图片等
super.onDraw(canvas);
int r = getMeasuredWidth() / 2;//也可以是getMeasuredHeight()/2,本例中我们已经将宽高设置相等了
//圆心的横坐标为当前的View的左边起始位置+半径
int centerX = getLeft() + r;
//圆心的纵坐标为当前的View的顶部起始位置+半径
int centerY = getTop() + r;
Paint paint = new Paint();
paint.setColor(Color.GREEN);
//开始绘制
canvas.drawCircle(centerX, centerY, r, paint);
}
View中还有三个比较重要的方法
requestLayout
View重新调用一次layout过程。
invalidate
View重新调用一次draw过程
forceLayout
标识View在下一次重绘,需要重新调用layout过程。
1/4、与用户进行交互
也许某些情况你的自定义控件不仅仅只是展示一个漂亮的内容,还需要支持用户点击,拖动等等操作,这时候我们的自定义控件就需要做用户交互这一步骤了。
在android系统中最常见的事件就是触摸事件了,它会调用view的onTouchEvent(android.view.MotionEvent).重写这个方法去处理我们的事件逻辑
@Override
public boolean onTouchEvent(MotionEvent event) {
return super.onTouchEvent(event);
}
对与onTouchEvent方法相信大家都有一定了解,如果不了解的话,你就先记住这是处理Touch的地方。
现在的触控有了更多的手势,比如轻点,快速滑动等等,所以在支持特殊用户交互的时候你需要用到android提供的GestureDetector.你只需要实现GestureDetector中相对应的接口,并且处理相应的回调方法。
除了手势之外,如果有移动之类的情况我们还需要让滑动的动画显示得比较平滑。动画应该是平滑的开始和结束,而不是突然消失突然开始。在这种情况下,我们需要用到属性动画 property animation framework
由于与用户进行交互中涉及到的知识举例子会比较多,所以我在之后的自定义控件文章中再讲解。
1/5、优化你的自定义View
在上面的步骤结束之后,其实一个完善的自定义控件已经出来了。接下来你要做的只是确保自定义控件运行得流畅,官方的说法是:为了避免你的控件看得来迟缓,确保动画始终保持每秒60帧.
下面是官网给出的优化建议:
1、避免不必要的代码
2、在onDraw()方法中不应该有会导致垃圾回收的代码。
3、尽可能少让onDraw()方法调用,大多数onDraw()方法调用都是手动调用了invalidate()的结果,所以如果不是必须,不要调用invalidate()方法。
二、举一反三加深理解
1、下面我们准备来自定义一个计数器View,这个View可以响应用户的点击事件,并自动记录一共点击了多少次。新建一个CounterView继承自View,代码如下所示:
public class CounterView extends View implements OnClickListener {
private Paint mPaint;
private Rect mBounds;
private int mCount;
public CounterView(Context context, AttributeSet attrs) {
super(context, attrs);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mBounds = new Rect();
setOnClickListener(this);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPaint.setColor(Color.BLUE);
canvas.drawRect(0, 0, getWidth(), getHeight(), mPaint);
mPaint.setColor(Color.YELLOW);
mPaint.setTextSize(30);
String text = String.valueOf(mCount);
mPaint.getTextBounds(text, 0, text.length(), mBounds);
float textWidth = mBounds.width();
float textHeight = mBounds.height();
canvas.drawText(text, getWidth() / 2 - textWidth / 2, getHeight() / 2
+ textHeight / 2, mPaint);
}
@Override
public void onClick(View v) {
mCount++;
invalidate();
}
}
在布局文件中加入如下代码:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<com.example.customview.CounterView
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_centerInParent="true" />
</RelativeLayout>
思路:
1、需要画一个背景圆,再需要一个覆盖在背景圆上面的进度圆。
2、使用线程让进度圆产生动画。
3、在进度圆达到圆满的时候回到原点,给个回调。
现在我们先画出一个空心圆。
/** 背景圆的画笔 */
private Paint paint;
/** 进度条圆的画笔 */
private Paint paint1;
/** 设置矩阵的坐标点 */
private RectF rectF;
/** 屏幕的高度 */
private int width = 0;
/** 园的半径 */
private int circleRadius = 0;
/** 园的y轴起始坐标 */
private int circleStartY = 20;
/** 园的y轴终点坐标 起始坐标加上园的半径*2 */
private int circleEndy = 0;
/** 初始进度 */
private float currentPorcent = 0;
/** 进度是多少 */
private float maxPorcent = 0;
/**满级回调*/
public RestoreCirclr rc;
/** 是否还原 */
public boolean isRestore = false;
/**我一般喜欢直接写3个构造方法,方便引用*/
public CircleView(Context context) {
super(context);
init(context);
}
public CircleView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
/**初始化画笔*/
private void init(Context context){
paint = new Paint();// 布局xml里面引用
paint1 = new Paint();// 布局xml里面引用
paint.setAntiAlias(true);// 设置抗锯齿
paint.setColor(getResources().getColor(R.color.char_circlebackground));
paint.setStyle(Style.STROKE);// 设置圆心掏空
paint.setStrokeWidth(dip2px(context, 10));
// 设置画笔形状 圆形,需要先设置画笔样式 SYROKE 或者 FILL_AND_STROKE
paint.setStrokeCap(Paint.Cap.ROUND);
paint1.setAntiAlias(true);// 设置抗锯齿
paint1.setColor(getResources().getColor(R.color.char_circleplan));
paint1.setStyle(Style.STROKE);
paint1.setStrokeWidth(dip2px(context, 10));
paint1.setStrokeCap(Paint.Cap.ROUND);
width = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getWidth();
circleRadius = width / 4;
circleEndy = circleStartY + circleRadius * 2;
rectF = new RectF(width / 2 - circleRadius, circleStartY, width / 2 + circleRadius, circleEndy);// 弧形
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 第一个参数是圆的大小,根据矩阵来控制。第二个参数是在哪个点起始,已顺时针方向走,所以说90为正下方。0为最右边。第三个参数是圆的度数360为一圈
canvas.drawArc(rectF, 90, 360, false, paint);
//这里等级为4/1等级,所以是90
canvas.drawArc(rectF, 90, currentPorcent, false, paint1);
if (currentPorcent == 0) {
handler.postDelayed(drawRunnable, 0);
}
}
/**启动动画刷新界面*/
public void invalidateView(){
handler.postDelayed(drawRunnable, 0);
}
private Handler handler = new Handler();
Runnable drawRunnable = new Runnable() {
@Override
public void run() {
if (!isRestore) {//有经验时动画
if (currentPorcent >= maxPorcent) {
currentPorcent = maxPorcent;
invalidate();
//移除当前Runnable
handler.removeCallbacks(drawRunnable);
} else {
currentPorcent += 5;//这里是动画速度,当前为5。可自己去调试经验值增长速度
handler.postDelayed(drawRunnable, (long) (1300 / maxPorcent));
invalidate();
}
if (currentPorcent == 360) {
if (rc != null) {
isRestore = rc.OnRestoreCirclr();
handler.postDelayed(drawRunnable, 0);
}
}
} else {//满级之后经验条动画返回0进度
if (currentPorcent <= 0) {
currentPorcent = 0;
invalidate();
handler.removeCallbacks(drawRunnable);
} else {
currentPorcent -= 3;//这里是动画速度,当前为3。可自己去调试经验值反0速度
handler.postDelayed(drawRunnable, (long) (1300 / maxPorcent));
invalidate();
}
}
}
};
public boolean isRestore() {
return isRestore;
}
public void setRestore(boolean isRestore) {
this.isRestore = isRestore;
}
/** 设置等级进度,传入升级经验,以及当前经验 maxPorcent就是当前经验在升级经验占的百分比*/
public void setCirclePlan(int max, int current) {
maxPorcent = (int) (((float)360 / (float)max) * current);
}
/**传入dp,返回px*/
public float dip2px(Context context, float dpValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (float) (dpValue * scale + 0.5f);
}
/** 设置园线的颜色 */
public void setCircleColor(int color) {
paint.setColor(color);
}
/** 设置进度线的颜色 */
public void setCirclePlanColor(int color) {
paint1.setColor(color);
}
public void setRc(RestoreCirclr rc) {
this.rc = rc;
}
public interface RestoreCirclr {
public boolean OnRestoreCirclr();
}
在mainActivity里面调用代码如下:
private CircleView circleView;
/** 升级经验为100 */
private int max ;
/** 目前经验值为10 */
private int current ;
/** 经验加10 */
private Button button_1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_activity);
circleView = (CircleView) findViewById(R.id.circleView);
button_1 = (Button) findViewById(R.id.button_1);
button_1.setOnClickListener(this);
max = 100;
current = 10;
circleView.setCirclePlan(max, current);// 进度条已满 升级数是100,当前经验数是10
circleView.setRc(new RestoreCirclr() {// 满级之后的回调
@Override
public boolean OnRestoreCirclr() {
//满级之后的操作 返回true就回到原点
current=0;
return true;
}
});
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.button_1:
current += 10;
circleView.setRestore(false); //false经验值增加
circleView.setCirclePlan(max, current);
circleView.invalidateView();//刷新view
break;
}
}
效果图
好了,基本概念框架搭好了,现在就剩敲码实践自己想要的效果。
参考文章连接
http://www.2cto.com/kf/201604/498828.html
http://www.myexception.cn/mobile/1844902.html
http://blog.youkuaiyun.com/column/details/androidcustomview.html
http://blog.youkuaiyun.com/aigestudio/article/details/41212583
http://www.jianshu.com/p/84cee705b0d3
http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0606/3001.html
http://blog.youkuaiyun.com/huachao1001/article/details/51577291
http://blog.youkuaiyun.com/lmj623565791/article/details/24252901/
http://blog.youkuaiyun.com/guolin_blog/article/details/17357967
http://blog.youkuaiyun.com/guolin_blog/article/details/17357967