先看效果(原諒我的渣像素),進(jìn)度的刻度、寬度、顏色可以隨意設(shè)定:
【項(xiàng)目github地址: https://github.com/zhangke3016/CircleLoading]
實(shí)現(xiàn)起來并不難,通過本文,我們可以學(xué)到:
1、自定義屬性的使用。
2、shader的使用
3、自定義View中對onmeasure的處理
4、增深對PathMeasure工具類的了解
5、最主要的是對自定義View有個(gè)比較清晰的思路認(rèn)識(shí)
一、原理介紹
做這樣一個(gè)進(jìn)度效果,我們可以拆分如下步驟來實(shí)現(xiàn):
1、從外部圓環(huán)開始測量繪制;
2、再加入刻度條效果;
3、再加入刻度隨進(jìn)度增加而增加效果;
4、增加自定義屬性增加可定制性;
5、控件使用方法介紹
csdn地址:http://blog.csdn.net/zhangke3016/article/details/52035641
OK,有了這個(gè)思路,那我們開始吧:
1、測量繪制外部圓環(huán)
首先我們要開始繪制外部的圓環(huán),這步很簡單,主要是使用canvas的drawArc()方法,
/*
* @param oval 畫弧線矩形區(qū)域
* @param startAngle 開始的角度
* @param sweepAngle 劃過的角度
* @param useCenter 如果為true 為實(shí)心圓弧
* @param paint 畫筆
* /
public void drawArc(RectF oval, float startAngle, float sweepAngle,boolean useCenter,Paint paint)
這個(gè)相對簡單,主要是確定開始角度,并不斷增加繪制劃過角度,圓弧就出現(xiàn)在界面中了,這里需要注意的是RectF oval的大小確定:
在確定RectF oval之前,我們要先測量確定當(dāng)前控件的寬高,根據(jù)當(dāng)前控件的寬高來確定oval的合適大小。
測量當(dāng)前控件的大小一般我們在onmeasure()方法中處理,resolveMeasured()方法傳遞兩個(gè)參數(shù),第一各參數(shù)為widthMeasureSpec或者h(yuǎn)eightMeasureSpec,第二個(gè)參數(shù)為期望值也就是默認(rèn)值。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(resolveMeasured(widthMeasureSpec, nDesired), resolveMeasured(heightMeasureSpec, nDesired));
}
/**
*
* @param measureSpec
* @param desired
* @return
*/
private int resolveMeasured(int measureSpec, int desired)
{
int result = 0;
int specSize = MeasureSpec.getSize(measureSpec);
switch (MeasureSpec.getMode(measureSpec)) {
case MeasureSpec.UNSPECIFIED: //
result = desired;
break;
case MeasureSpec.AT_MOST: //wrap-content
result = Math.min(specSize, desired);
break;
case MeasureSpec.EXACTLY: //match-content
default:
result = specSize;
}
return result;
}
在測量之后我們就可以來求oval的具體大小 ,我把這步操作放在了onSizeChanged方法中,求出最小一邊的一半減去圓環(huán)的寬度,得到圓弧的半徑,然后根據(jù)半徑以控件中心為中心繪制我們所需要的矩形區(qū)域。
radiu = (int) ((Math.min(getWidth(), getHeight()))/2-mPaint.getStrokeWidth());
oval.left = getWidth()/2-radiu;
oval.top = getHeight()/2-radiu;
oval.right = getWidth()/2+radiu;
oval.bottom = getHeight()/2+radiu;
這樣下來,基本上就可以繪制比較理想的弧線了,在這里也把繪制中心文字也說下吧,主要通過getTextBounds()方法獲取文字區(qū)域的寬高,然后在drawText()方法中將坐標(biāo)進(jìn)行適當(dāng)偏移以使文字居中顯示。
String strProgressText = "";
if(mOnProgressListener !=null){//如果不為空 則為接口返回的值
strProgressText = mOnProgressListener.OnProgress(mMax, mProgress);
}else{
strProgressText = mProgress+"/"+mMax;
}
mTextPaint.getTextBounds(strProgressText, 0, strProgressText.length(), bounds);
canvas.drawText(strProgressText, oval.centerX()-bounds.width()/2, oval.centerY()+bounds.height()/2, mTextPaint);
最后還有一個(gè)小點(diǎn)就是漸變色的繪制,用的SweepGradient,我們可以看下Shader的子類,shader類是很強(qiáng)大的,類似與圓形圖片、漸變效果都可以用它來實(shí)現(xiàn),這里就不過多展開了:
SweepGradient sweepGradient = new SweepGradient(getWidth()/2, getHeight()/2, colors, null);
p.setShader(sweepGradient);
到這里為止,我們的圓環(huán)已經(jīng)繪制好了,包括中間的文字以及圓環(huán)的漸變效果都已經(jīng)實(shí)現(xiàn)了,就是這樣的:
2、加入刻度效果
接下來要加入刻度效果,實(shí)現(xiàn)思路是這樣的,我先默認(rèn)實(shí)現(xiàn)兩個(gè)圓弧(注意這兩個(gè)圓弧只是我們假定添加的,并不是真正加在控件中顯示),然后獲取相同角度,根據(jù)相對位置獲取兩個(gè)圓環(huán)上的點(diǎn)進(jìn)行連線,將這兩個(gè)點(diǎn)連起的刻度線封裝成對象添加在集合中,最后在onDraw方法中遍歷集合,進(jìn)行繪制。
oval2 = new RectF();//內(nèi)環(huán)
oval2.left = getWidth()/2-radiu/4f*3;
oval2.top = getHeight()/2-radiu/4f*3;
oval2.right = getWidth()/2+radiu/4f*3;
oval2.bottom = getHeight()/2+radiu/4f*3;
oval3 = new RectF();//外環(huán)
oval3.left = getWidth()/2-radiu/8f*7;
oval3.top = getHeight()/2-radiu/8f*7;
oval3.right = getWidth()/2+radiu/8f*7;
oval3.bottom = getHeight()/2+radiu/8f*7;
//然后初始化數(shù)據(jù)
/**
* 初始化數(shù)據(jù)
*/
private void initData() {
mLinesList.clear();
Path path = new Path();
Path path1 = new Path();
//從startAngle開始 繪制180角度
path.addArc(oval2, mStartAngle, mGraduationSweepAngle);
path1.addArc(oval3, mStartAngle, mGraduationSweepAngle);
PathMeasure pm = new PathMeasure(path, false);
float itemLength = pm.getLength()/(nGraduationCount-1);
PathMeasure pm1 = new PathMeasure(path1, false);
float[] pos = new float[2];
float[] postemp = new float[2];
for (int i = 0; i < nGraduationCount; i++) {
pm.getPosTan(itemLength*i, pos , null );
pm1.getPosTan(itemLength*i/pm.getLength()*pm1.getLength(), postemp , null);
Line line = new Line();
line.p1.x = pos[0];
line.p1.y = pos[1];
line.p2.x = postemp[0];
line.p2.y = postemp[1];
mLinesList.add(line);
}
}
//ondraw方法:
for (int i = 0; i < mLinesList.size(); i++) {
Line line = mLinesList.get(i);
canvas.drawLine(line.p1.x, line.p1.y, line.p2.x, line.p2.y, mRollPaint);
}
這里用到了PathMeasure這個(gè)輔助工具,這里簡單講一下:
public PathMeasure()
//path:需要測量的path forceClosed:是否關(guān)閉path
public PathMeasure(Path path, boolean forceClosed)
//指定需要測量的path
public void setPath(Path path, boolean forceClosed)
//返回當(dāng)前path的總長度。
getLength()
//返回值是boolean,如過path為空,則返回false 傳入?yún)?shù)有三個(gè):
//distance:傳入距離起點(diǎn)的距離。
//pos[]:意思是position,分別對應(yīng)點(diǎn)的x,y坐標(biāo)
//tan[]:獲取切線值,不常用。
public boolean getPosTan(float distance, float pos[], float tan[])
//返回一個(gè)處理好的matrix,但是這個(gè)matrix是以左上角作為旋轉(zhuǎn)點(diǎn),所以需要將這個(gè)點(diǎn)移動(dòng)到中心點(diǎn)。 其中一個(gè)參數(shù)flags,指這個(gè)martrix需要什么信息。flags的值有如下兩個(gè)
PathMeasure.POSITION_MATRIX_FLAG:位置信息
pathMeasure.TANGENT_MATRIX_FLAG:切邊信息,方位角信息
public boolean getMatrix(float distance, Matrix matrix, int flags)
//這個(gè)方法返回boolean,如果截取的長度為0則返回false,否則為true。參數(shù)如下
//startD:起始距離
//stopD:終點(diǎn)距離
//dst:接收截取的path
//startWithMoveTo:是否把截取的path,moveto到起始點(diǎn)。
public boolean getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo)
3、加入刻度隨進(jìn)度增加而增加效果,并增加進(jìn)度變化回調(diào)方便操作
加入刻度隨進(jìn)度增加而增加,我們可以這樣想,首先我總的刻度數(shù)是一定的,判斷劃過的角度占圓周的百分比,隨之就可以得到劃過刻度數(shù)占總刻度數(shù)的百分比,進(jìn)而就求出劃過的刻度數(shù)了。
for (int i = 0; i < Math.round(mSweepAngle*nGraduationCount/360f); i++) {
if(i<mLinesList.size()){
Line line = mLinesList.get(i);
canvas.drawLine(line.p1.x, line.p1.y, line.p2.x, line.p2.y, mRollDrawPaint);
}
}
將劃過的刻度數(shù)用畫筆再繪制一次,隨進(jìn)度增加的刻度效果就出現(xiàn)啦!
/**
* 設(shè)置進(jìn)度監(jiān)聽
* @param mOnProgressListener
*/
public void setOnProgressListener(OnProgressListener mOnProgressListener) {
this.mOnProgressListener = mOnProgressListener;
}
/**
* 用于外部判斷當(dāng)前進(jìn)度狀態(tài)
*/
interface OnProgressListener{
/**
* 返回中間部分文字內(nèi)容
* @param max
* @param progress
* @return
*/
String OnProgress(int max,int progress);
}
設(shè)置回調(diào)監(jiān)聽,這樣在每次進(jìn)度變化的時(shí)候,可以隨意變化中間部分文字顯示的內(nèi)容。
4、增加自定義屬性增加可定制性
attrs.xml:
<declare-styleable name="LoadingStyle">
<attr name="textSize" format="dimension|reference"/><!-- 字體大小 -->
<attr name="textColor" format="color|reference"/><!-- 字體顏色 -->
<attr name="strokeWidth" format="dimension|reference"/><!-- 圓環(huán)大小 -->
<attr name="isShowGraduationBackground" format="boolean"/><!-- 是否顯示背景刻度 -->
<attr name="isShowOutRoll" format="boolean"/><!-- 是否顯示外部進(jìn)度框 -->
<attr name="startAngle" format="integer|reference"/><!-- 開始的角度 -->
<attr name="max" format="integer|reference"/><!-- 最大值 -->
<attr name="progress" format="integer|reference"/><!-- 默認(rèn)進(jìn)度值 -->
<attr name="graduationBackgroundColor" format="color|reference"/><!-- 刻度的背景顏色 -->
<attr name="graduationWidth" format="dimension|reference"/><!-- 刻度的寬度 -->
<attr name="graduationCount" format="integer|reference"/><!-- 刻度的個(gè)數(shù) -->
</declare-styleable>
layout文件:
xmlns:app="http://schemas.android.com/apk/res-auto"<!--設(shè)置命名空間 -->
<com.mrzk.circleloadinglibrary.CircleLoadingView
android:id="@+id/lv_loading"
android:layout_width="250dp"
android:layout_height="250dp"
android:layout_centerInParent="true"
app:textSize="35sp"
app:textColor="#f60"
app:strokeWidth="10dp"
app:isShowGraduationBackground="true"
app:startAngle="0"
app:max="300"
app:progress="100"
app:graduationBackgroundColor="#ccc"
app:graduationWidth="5dp"
app:graduationCount="10"
app:isShowOutRoll="false"
/>
獲取自定義屬性值:
TypedArray typedArray = getResources().obtainAttributes(attrs, R.styleable.LoadingStyle);
mTextSize = (int) typedArray.getDimension(R.styleable.LoadingStyle_textSize, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 20, getResources().getDisplayMetrics()));
mStrokeWidth = (int) typedArray.getDimension(R.styleable.LoadingStyle_strokeWidth, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5, getResources().getDisplayMetrics()));
mGraduationWidth = (int) typedArray.getDimension(R.styleable.LoadingStyle_graduationWidth, mStrokeWidth/2);
mTextColor = (int) typedArray.getColor(R.styleable.LoadingStyle_textColor, Color.BLACK);
mGraduationBackgroundColor = (int) typedArray.getColor(R.styleable.LoadingStyle_graduationBackgroundColor, Color.BLACK);
mStartAngle = (int) typedArray.getInt(R.styleable.LoadingStyle_startAngle, 180);
mMax = (int) typedArray.getInt(R.styleable.LoadingStyle_max, 0);
mProgress = (int) typedArray.getInt(R.styleable.LoadingStyle_progress, 0);
nGraduationCount = (int) typedArray.getInt(R.styleable.LoadingStyle_graduationCount, 35);
isShowGraduationBackground = typedArray.getBoolean(R.styleable.LoadingStyle_isShowGraduationBackground, true);
isShowOutRoll = typedArray.getBoolean(R.styleable.LoadingStyle_isShowOutRoll, true);
typedArray.recycle();
5、使用方法
int[] colors = {0xFFE5BD7D, 0xFFFAAA64,
0xFFFFFFFF, 0xFF6AE2FD,
0xFF8CD0E5, 0xFFA3CBCB,
0xFFBDC7B3, 0xFFD1C299, 0xFFE5BD7D};
lv_loading.setTextColor(Color.BLACK);//設(shè)置中心文字顏色
lv_loading.setMax(500);//設(shè)置最大進(jìn)度
lv_loading.setShowGraduationBackgroundEnable(true);//是否顯示刻度背景
lv_loading.setGraduationBackgroundColor(Color.GRAY);//刻度的背景顏色
lv_loading.setStartAngle(180);//設(shè)置開始旋轉(zhuǎn)角度
lv_loading.setGraduationCount(10);//設(shè)置刻度數(shù)
lv_loading.setGraduationWidth(5);//設(shè)置刻度的寬度
lv_loading.setOutColors(colors);//設(shè)置外部圓環(huán)顏色
lv_loading.setInnerGraduationColors(colors);//設(shè)置內(nèi)部刻度進(jìn)度顏色
lv_loading.setTextSize(35);//設(shè)置內(nèi)部文字字體大小
lv_loading.setShowOutRollEnable(false);//設(shè)置是否顯示外部進(jìn)度框
lv_loading.setOnProgressListener(new OnProgressListener() {
@Override
public String OnProgress(int max, int progress) {
return progress*100f/max+"%";
}
});