请尊重个人劳动成果,转载注明出处,谢谢!
http://blog.csdn.net/xiaxiazaizai01/article/details/52355558
这是一个一言不合就手撸一个自定义View的任性时代,因此最近一段时间一直在学习自定义View相关的知识,也看了很多与此相关的博客,有句话叫做不要重复造轮子,别人写好的直接拿过来改吧改吧,能用就行,但是,要想像那些任性的大牛一样,分分钟撸一个自定义View,就得不断的重复造轮子,学习大神们的设计思路, 站在牛人的肩膀上不断前行,每篇开篇之前都要啰嗦半天,急性子的童鞋可以直接跳过。看到yissan大牛写了一篇自定义圆形进度条,思路很清晰,就照着也撸了一遍,果然是酸爽啊,在这里非常感谢yissan大牛,哈哈。。。为了让大家能一遍就看懂,我会把注释写的非常非常详细,秒懂哦,,什么??你不能秒懂。。注释都写的辣么详细了,面壁思过去。。哈哈
下面看下效果图:
1、首先创建View
(1)设置自定义View属性,通常做法是在res/values里面创建一个attrs文件夹,来写我们的自定义属性,一般我们设置属性的name时,一般习惯性的将我们自定义的类名作为name
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- 自定义圆形进度条,属性设置 -->
<declare-styleable name="CustomCircleProgress">
<!-- 默认圆的颜色 -->
<attr name="progress_default_color" format="color"/>
<!-- 进度圆的颜色 -->
<attr name="progress_reached_color" format="color"/>
<!-- 进度条的高度 -->
<attr name="progress_reached_height" format="dimension"/>
<!-- 无进度时(默认圆)的边框高 -->
<attr name="progress_default_height" format="dimension"/>
<!-- 圆的半径 -->
<attr name="circle_radius" format="dimension"/>
</declare-styleable>
</resources>
(2)设置完了自定义属性,下一步当然是在我们的自定义View类中去获取。(我们都习惯在参数多的构造方法中去获取自定义属性,其他构造方法则去通过this去调用,注意这里是this而不是super,super的话则指向的是父类,这里我犯了一个常识性错误,一键生成几个构造方法,忘了将super改成this,导致获取属性的方法没有被调用执行,大家在调用的时候可以打断点试试)
public CustomCircleProgress(Context context) {
this(context,null);
}
public CustomCircleProgress(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public CustomCircleProgress(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//获取自定义属性的值
TypedArray array = getContext().obtainStyledAttributes(attrs, R.styleable.CustomCircleProgress);
//默认圆的颜色
mDefaultColor = array.getColor(R.styleable.CustomCircleProgress_progress_default_color, PROGRESS_DEFAULT_COLOR);
//进度条的颜色
mReachedColor = array.getColor(R.styleable.CustomCircleProgress_progress_reached_color, PROGRESS_REACHED_COLOR);
//默认圆的高度
mDefaultHeight = (int) array.getDimension(R.styleable.CustomCircleProgress_progress_default_height, mDefaultHeight);
//进度条的高度
mReachedHeight = (int) array.getDimension(R.styleable.CustomCircleProgress_progress_reached_height, mReachedHeight);
//圆的半径
mRadius = (int) array.getDimension(R.styleable.CustomCircleProgress_circle_radius, mRadius);
//最后不要忘了回收 TypedArray
array.recycle();
//设置画笔(new画笔的操作一般不要放在onDraw方法中,因为在绘制的过程中onDraw方法会被多次调用)
setPaint();
我们在new我们的画笔时一般不要在onDraw()方法中去new,因为view在不断的绘制过程中onDraw()方法会不断的被调用,这样就会造成不停的new我们的画笔实例。
//设置画笔
private void setPaint() {
mPaint = new Paint();
//下面是设置画笔的一些属性
mPaint.setAntiAlias(true);//抗锯齿
mPaint.setDither(true);//防抖动,绘制出来的图要更加柔和清晰
mPaint.setStyle(Paint.Style.STROKE);//设置填充样式
/**
* Paint.Style.FILL :填充内部
* Paint.Style.FILL_AND_STROKE :填充内部和描边
* Paint.Style.STROKE :仅描边
*/
mPaint.setStrokeCap(Paint.Cap.ROUND);//设置画笔笔刷类型
}
2、处理View的布局,即测量onMeasure( )
当我们在xml文件中给这个view设置android:layout_width=”“android:layout_height=”“属性为固定值、wrap_parent、match_parent 时,表明开发者向ViewGroup沟通表明我需要的空间。ViewGroup收到了开发者对View大小的说明,然后ViewGroup会综合考虑自己的空间大小以及开发者的请求,然后生成两个MeasureSpec对象(width与height)传给View,这两个对象是ViewGroup向子View提出的要求,就相当于告诉子View:“我已经与你的使用者(开发者)商量过了,现在把我们商量确定的结果告诉你,你的宽度不能违反width MeasureSpec对象的要求,你的高度不能违反height MeasureSpec对象的要求,现在,你赶紧根据这个要求确定下自己要多大空间,只许少,不许多哦。”对于超过ViewGroup为我们分配的空间时,就需要进行测量处理,然后再将处理后的结果反馈给ViewGroup,如果不是很了解的话可以点击查看上一篇博客,有详细的说明
/**
* 使用onMeasure方法是因为我们的自定义圆形View的一些属性(如:进度条宽度等)都交给用户自己去自定义了,所以我们需要去测量下
* 看是否符合要求
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int paintHeight = Math.max(mReachedHeight, mDefaultHeight);//比较两数,取最大值
if(heightMode != MeasureSpec.EXACTLY){
//如果用户没有精确指出宽高时,我们就要测量整个View所需要分配的高度了,测量自定义圆形View设置的上下内边距+圆形view的直径+圆形描边边框的高度
int exceptHeight = getPaddingTop() + getPaddingBottom() + mRadius*2 + paintHeight;
//然后再将测量后的值作为精确值传给父类,告诉他我需要这么大的空间,你给我分配吧
heightMeasureSpec = MeasureSpec.makeMeasureSpec(exceptHeight, MeasureSpec.EXACTLY);
}
if(widthMode != MeasureSpec.EXACTLY){
//这里在自定义属性中没有设置圆形边框的宽度,所以这里直接用高度代替
int exceptWidth = getPaddingLeft() + getPaddingRight() + mRadius*2 + paintHeight;
widthMeasureSpec = MeasureSpec.makeMeasureSpec(exceptWidth, MeasureSpec.EXACTLY);
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
我们需要考虑开发者有时会给View设置一些padding属性
3、绘制View,即onDraw()
(1)这里我们需要绘制默认的内部圆以及表示进度的外层圆弧,根据进度值的变化来绘制圆弧。在绘制外层表示进度的圆弧时,需要首先确定圆弧的外接矩形(进度也就成了内切圆)的坐标,如下图所示
@Override
protected synchronized void onDraw(Canvas canvas) {
super.onDraw(canvas);
/**
* 这里canvas.save();和canvas.restore();是两个相互匹配出现的,作用是用来保存画布的状态和取出保存的状态的
* 当我们对画布进行旋转,缩放,平移等操作的时候其实我们是想对特定的元素进行操作,但是当你用canvas的方法来进行这些操作的时候,其实是对整个画布进行了操作,
* 那么之后在画布上的元素都会受到影响,所以我们在操作之前调用canvas.save()来保存画布当前的状态,当操作之后取出之前保存过的状态,
* (比如:前面元素设置了平移或旋转的操作后,下一个元素在进行绘制之前执行了canvas.save();和canvas.restore()操作)这样后面的元素就不会受到(平移或旋转的)影响
*/
canvas.save();
//为了保证最外层的圆弧全部显示,我们通常会设置自定义view的padding属性,这样就有了内边距,所以画笔应该平移到内边距的位置,这样画笔才会刚好在最外层的圆弧上
//画笔平移到指定paddingLeft, getPaddingTop()位置
canvas.translate(getPaddingLeft(),getPaddingTop());
mPaint.setStyle(Paint.Style.STROKE);
//画默认圆(边框)的一些设置
mPaint.setColor(mDefaultColor);
mPaint.setStrokeWidth(mDefaultHeight);
canvas.drawCircle(mRadius,mRadius,mRadius,mPaint);
//画进度条的一些设置
mPaint.setColor(mReachedColor);
mPaint.setStrokeWidth(mReachedHeight);
//根据进度绘制圆弧
float sweepAngle = getProgress() * 1.0f / getMax() * 360;
canvas.drawArc(new RectF(0, 0, mRadius * 2, mRadius *2), 0, sweepAngle, false, mPaint);//drawArc:绘制圆弧
canvas.restore();
}
我们做个定时器,让进度条动起来
public class MainActivity extends AppCompatActivity {
private CustomCircleProgress circleProgress;
private int progress;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
circleProgress = (CustomCircleProgress) findViewById(R.id.circleProgress);
Timer timer = new Timer();
TimerTask task = new TimerTask() {
@Override
public void run() {
if(progress >= 100){
progress = 0;
circleProgress.setProgress(0);
}else{
progress = circleProgress.getProgress();
circleProgress.setProgress(++progress);
}
}
};
timer.schedule(task,0,100);
}
}
这样得到的效果图是这样的
有的伙计该说了,为什么进度条的起始位置不是从最上面开始的,因为这里我设置的canvas.drawArc(new RectF(0, 0, mRadius * 2, mRadius 2), 0, sweepAngle, false, mPaint);中我设置的参数为0,表示圆弧的起始位置从0开始,即X轴的正方向。这里我们只需将圆弧的起始位置设置成-90度即可,canvas.drawArc(new RectF(0, 0, mRadius 2, mRadius *2), -90, sweepAngle, false, mPaint);我们再来看下效果图
完美,,哈哈,,,,
//绘制圆
public void drawCircle (float cx, float cy, float radius, Paint paint)
//参数说明
/**
* cx:圆心的x坐标。
cy:圆心的y坐标。
radius:圆的半径。
paint:绘制时所使用的画笔。
*/
(2)接下来,我们开始绘制里面的暂停(完成)状态时的三角形,以及开启状态时的两条竖线,首先我们通过枚举的方式定义这两种状态,并提供set/get方法供外界调用。首先我们需要Path mPath = new Path();然后通过mPath.moveTo()确定三角形的第一个点的坐标,然后通过mPath.lineTo()链接其他几个点的坐标,如果当我们设置画笔的样式为mPaint.setStyle(Paint.Style.STROKE);则我们需要执行close形成封闭的三角形,或者你也可以直接再来一条mPath.lineTo()再将第一个点的坐标给连接起来,这样也形成了一个封闭的三角形。
//通过path路径绘制三角形
mPath = new Path();
//让三角形的长度等于圆的半径(等边三角形)
triangleLength = mRadius;
//绘制三角形,首先我们需要确定三个点的坐标
float firstX = (float) ((mRadius*2 - Math.sqrt(3.0) / 2 * mRadius) / 2);//左上角第一个点的横坐标,根据勾三股四弦五定律,Math.sqrt(3.0)表示开方
//为了显示的好看些,这里微调下firstX横坐标
float mFirstX = (float)(firstX + firstX*0.2);
float firstY = mRadius - triangleLength / 2;
//同理,依次可得出第二个点(左下角)第三个点的坐标
float secondX = mFirstX;
float secondY = (float) (mRadius + triangleLength / 2);
float thirdX = (float) (mFirstX + Math.sqrt(3.0) / 2 * mRadius);
float thirdY = mRadius;
mPath.moveTo(mFirstX,firstY);
mPath.lineTo(secondX,secondY);
mPath.lineTo(thirdX,thirdY);
mPath.lineTo(mFirstX,firstY);
然后我们在onDraw()方法中去判断绘制不同状态下的view
//有了path之后就可以在onDraw中绘制三角形的End和Starting状态了
if(mStatus == Status.End){//未开始状态,画笔填充三角形
mPaint.setStyle(Paint.Style.FILL);
//设置颜色
mPaint.setColor(Color.parseColor("#01A1EB"));
//画三角形
canvas.drawPath(mPath,mPaint);
}else{//正在进行状态,画两条竖线
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(dp2px(5));
mPaint.setColor(Color.parseColor("#01A1EB"));
canvas.drawLine(mRadius*2/3, mRadius*2/3, mRadius*2/3, 2*mRadius*2/3, mPaint);
canvas.drawLine(2*mRadius - (mRadius*2/3), mRadius*2/3, 2*mRadius - (mRadius*2/3), 2*mRadius*2/3, mPaint);
}
4、处理与用户的交互
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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"
tools:context="com.example.customview.MainActivity">
<com.example.customview.CustomCircleProgress
android:id="@+id/circleProgress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:padding="10dp"
/>
</RelativeLayout>
最后是我们的MainActivity类
public class MainActivity extends AppCompatActivity {
private CustomCircleProgress circleProgress;
private int progress;
private Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
switch (msg.what){
case PROGRESS_CIRCLE_STARTING:
progress = circleProgress.getProgress();
circleProgress.setProgress(++progress);
if(progress >= 100){
handler.removeMessages(PROGRESS_CIRCLE_STARTING);
progress = 0;
circleProgress.setProgress(0);
circleProgress.setStatus(CustomCircleProgress.Status.End);//修改显示状态为完成
}else{
//延迟100ms后继续发消息,实现循环,直到progress=100
handler.sendEmptyMessageDelayed(PROGRESS_CIRCLE_STARTING, 100);
}
break;
}
}
};
public static final int PROGRESS_CIRCLE_STARTING = 0x110;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
circleProgress = (CustomCircleProgress) findViewById(R.id.circleProgress);
circleProgress.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if(circleProgress.getStatus() == CustomCircleProgress.Status.Starting){//如果是开始状态
//点击则变成关闭暂停状态
circleProgress.setStatus(CustomCircleProgress.Status.End);
//注意,当我们暂停时,同时还要移除消息,不然的话进度不会被停止
handler.removeMessages(PROGRESS_CIRCLE_STARTING);
}else{
//点击则变成开启状态
circleProgress.setStatus(CustomCircleProgress.Status.Starting);
Message message = Message.obtain();
message.what = PROGRESS_CIRCLE_STARTING;
handler.sendMessage(message);
}
}
});
}
}