首先按照老規矩,無圖無真相嘛,先看看先:
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsICMyYTMvw1dvwlMvwlM3VWaWV2Zh1Wa-YWan5yYohDZwFWcrFnNvwVO0EzNzkjMtUGall3LcVmdhNXLwRHdo9CXt92YucWbpRWdvx2Yx5yazF2Lc9CX6MHc0RHaiojIsJye.gif)
效果圖.gif
是不是很像呢,那具體是實作是怎樣的呢,即使概括的來說就是
1.計算各個變量的值(記得是會随整個View的大小變化而變化)。
2其次利用好canvas.translate()這個方法,計算好大小移動canvas的原點。
3最後就是調用api提供的各種方法畫圖就是了。這麼說是不是太過于簡略了呢,好,現在就來
看看那具體的吧。首先看看xml有什麼參數吧
<com.example.jack.besselcurve.BesselCurveView
android:id="@+id/besselCurveView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffffff"
android:layout_centerHorizontal="true"
app:besselColor="@color/besselColor"
app:besselColorText="@color/besselColorText"
app:friendAverageStep="6752"
app:averageStep="2603"
app:champion="Jack"
app:allStep="8765"
app:time="17:26"
app:ranking="15">
</com.example.jack.besselcurve.BesselCurveView>
複制
各參數對應的解釋如下:
//時間
private String time;
//所有步數
private int allStop;
//還有平均步數
private int friendAverageStep;
//平均步數
private int averageStep;
//排名
private String ranking;
//頭像
private Bitmap champion_icon;
//冠軍名字
private String champion;
複制
接着代碼段初始化所有參數:
TypedArray mTypedArray=context.getTheme().obtainStyledAttributes(attrs,R.styleable.BesselCurveView,defStyleAttr,0);
int numCount=mTypedArray.getIndexCount();
for(int i=0;i<numCount;i++){
int attr=mTypedArray.getIndex(i);
switch(attr){
case R.styleable.BesselCurveView_allStep:
allStop=mTypedArray.getInt(attr,0);
break;
case R.styleable.BesselCurveView_averageStep:
averageStep=mTypedArray.getInt(attr,0);
break;
case R.styleable.BesselCurveView_friendAverageStep:
friendAverageStep = mTypedArray.getInt(attr,0);
break;
case R.styleable.BesselCurveView_time:
time=mTypedArray.getString(attr);
break;
case R.styleable.BesselCurveView_ranking:
ranking=mTypedArray.getString(attr);
break;
case R.styleable.BesselCurveView_champion:
champion=mTypedArray.getString(attr);
break;
case R.styleable.BesselCurveView_besselColor:
mBesselCurveColor=mTypedArray.getColor(attr,Color.BLUE);
break;
case R.styleable.BesselCurveView_besselColorText:
besselColorText=mTypedArray.getColor(attr,Color.GRAY); break;
}
}
複制
這些都是每個自定義都有的相當于模闆,來初始化參數,都看的明白吧。接下來也很簡單,就是初始化畫筆等變量,以便于後面看畫圖更簡單:
public void initValue(){
animSet=new AnimatorSet();
//外圓的畫筆
mCirclePaint=new Paint(Paint.ANTI_ALIAS_FLAG);
mCirclePaint.setStyle(Paint.Style.STROKE);
mCirclePaint.setStrokeWidth(radius/10);
mCirclePaint.setStrokeJoin(Paint.Join.ROUND);
mCirclePaint.setStrokeCap(Paint.Cap.ROUND);
mCirclePaint.setAntiAlias(true);
//中間的文字的畫筆
mCenterTextPaint=new Paint();
mCenterTextPaint.setColor(mBesselCurveColor);
mCenterTextPaint.setTextSize(radius/5);
mCenterTextPaint.setAntiAlias(true);
//除中間之外的文字的畫筆
mTextPaint=new Paint();
mTextPaint.setAntiAlias(true);
//最低下的矩形
mBottomRectPaint=new Paint(Paint.ANTI_ALIAS_FLAG);
mBottomRectPaint.setColor(mBesselCurveColor);
mBottomRectPaint.setAntiAlias(true);
//虛線的畫筆
mDottedLinePaint = new Paint();
mDottedLinePaint.setAntiAlias(true);
mDottedLinePaint.setStyle(Paint.Style.STROKE);
mDottedLinePaint.setStrokeWidth(2); mDottedLinePaint.setColor(mBesselCurveColor); mDottedLinePaint.setPathEffect(new DashPathEffect(new float[]{5,5},1)); //畫波浪線畫筆 WavylinesPaint=new Paint(); WavylinesPaint = new Paint(Paint.ANTI_ALIAS_FLAG); WavylinesPaint.setColor(wavyColor); WavylinesPaint.setStyle(Paint.Style.FILL_AND_STROKE); //虛線的畫線 mDottedLinePath=new Path();
//畫波浪線畫線
WavyLinePath=new Path();
//底下更多的畫線
morePath=new Path();
mWaveCount = (int) Math.round(widthView / mWaveLength + 1.5);
marginBottomText=radius/4;
}
複制
好了,最重要的初始化都差不多了,現在就來畫圖(畫畫)吧先貼出所有畫的代碼然後再逐一講解吧:
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.save();
canvas.translate(widthView/2,(heightView*((float)2/3))/2);
//畫内圓圈
mCirclePaint.setColor(besselColorText);
RectF mCircleRectF=new RectF(-radius,-radius,radius,radius);
canvas.drawArc(mCircleRectF,120,300,false,mCirclePaint);
//畫外圓圈
mCirclePaint.setColor(mBesselCurveColor);
canvas.drawArc(mCircleRectF,120,mCircleNum,false,mCirclePaint);
//畫中間的文字
Rect mCenterRect=new Rect(); String tempAllStop=mCenterNum+"";
mCenterTextPaint.getTextBounds(tempAllStop,0,tempAllStop.length(),mCenterRect);
int halfWidthText=(mCenterRect.right-mCenterRect.left)/2;
int halfHeightText=(mCenterRect.bottom-mCenterRect.top)/2;
canvas.drawText(tempAllStop,-halfWidthText,halfHeightText,mCenterTextPaint);
//畫上邊的文字
mTextPaint.setColor(besselColorText); mTextPaint.setTextSize(radius/6);
String tempFriendAverageStep=stringTemplate(R.string.besselTime,time);
Rect mTopRect=new Rect(); mTextPaint.getTextBounds(tempFriendAverageStep,0,tempFriendAverageStep.length(),mTopRect);
int halfTopWidthText=(mTopRect.right-mTopRect.left)/2;
canvas.drawText(tempFriendAverageStep,-halfTopWidthText,-(halfHeightText+marginText),mTextPaint);
//畫下邊的文字 String
tempAverageStep=stringTemplate(R.string.friendAverageStep,friendAverageStep+"");
Rect mBottomRect=new Rect();
mTextPaint.getTextBounds(tempAverageStep,0,tempAverageStep.length(),mBottomRect);
int halfBottomWidthText=(mBottomRect.right-mBottomRect.left)/2;
int mBottomHeightText=(mBottomRect.bottom-mBottomRect.top);
canvas.drawText(tempAverageStep,- halfBottomWidthText,mBottomHeightText+halfHeightText+marginText,mTextPaint);
//畫排名 Rect mNumRect=new Rect();
mCenterTextPaint.getTextBounds(ranking,0,ranking.length(),mNumRect);
int halfNum=(mNumRect.right-mNumRect.left)/2;
mCenterTextPaint.setTextSize(40); canvas.drawText(ranking,- halfNum,radius,mCenterTextPaint);
String rankingLeft=getContext().getResources().getString(R.string.ranking_left);
mTextPaint.getTextBounds(rankingLeft,0,rankingLeft.length(),mNumRect);
canvas.drawText(rankingLeft,-halfNum-(mNumRect.right- mNumRect.left)/2-20,radius,mTextPaint);
canvas.drawText(getContext().getResources().getString(R.string.ranking_right),halfNum+10,radius,mTextPaint);
canvas.restore();
//畫最近七天和平均運動
mTextPaint.setTextSize(radius/9); canvas.save(); canvas.translate(0,heightView*((float)2/3));
canvas.drawText(getContext().getResources().getString(R.string.nextSevenDay),marginLi neChart,0,mTextPaint);
Rect mPercentRect=new Rect();
String mPercentText=stringTemplate(R.string.averageStep,averageStep+"");
mTextPaint.getTextBounds(mPercentText,0,mPercentText.length(),mPercentRect);
canvas.drawText(mPercentText,widthView-marginLineChart-(mPercentRect.right- mPercentRect.left),0,mTextPaint);
//畫虛線
mDottedLinePath.moveTo(marginLineChart,marginBottomText);
mDottedLinePath.lineTo(widthView-marginLineChart,marginBottomText);
canvas.drawPath(mDottedLinePath,mDottedLinePaint);
//畫7天資料柱狀圖 mTextPaint.setTextSize(radius/9);
int lineWidth=(widthView-marginLineChart*2)/8;
mCalendar.setTime(new Date());
RectF mRecf=null;
if(mListStep.size()>0){
for(int i=mListStep.size();i>=1;i--){
if(mListStep.get(i-1)!=0){
int startX=marginLineChart+lineWidth*i-radius/23;
int endX=marginLineChart+lineWidth*i+radius/23;
if(mListStep.get(i-1)>mStandardStop){
//達标 mTextPaint.setColor(mBesselCurveColor);
int exceed=mListStep.get(i-1)-mStandardStop;
float standard=(float)
(mCircleRectHeight*Double.valueOf(exceed/Double.valueOf(mStandardStop)));
mRecf=new RectF(startX,marginBottomText-(standard>mCircleRectHeight?mCircleRectHeight:standard) ,endX,marginBottomText+mCircleRectHeight);
canvas.drawRoundRect(mRecf,50,50,mTextPaint);
}else{
//不達标
mTextPaint.setColor(besselColorText);
float noStandard=(float)(mCircleRectHeight*Double.valueOf(mListStep.get(i-1)/Double.valueOf(mStandardStop)));
mRecf=new RectF(startX,marginBottomText,endX,marginBottomText+( noStandard>mCircleRectHeight?mCircleRectHeight:noStandard));
canvas.drawRoundRect(mRecf,50,50,mTextPaint);
}
}
//畫底下的日期
mTextPaint.setColor(besselColorText);
mCalendar.set(Calendar.DAY_OF_MONTH,mCalendar.get(Calendar.DAY_OF_MONTH)-1);
Rect rect =new Rect();
String number=stringTemplate(R.string.day,mCalendar.get(Calendar.DAY_OF_MONTH)+"");
mTextPaint.getTextBounds(number,0,number.length(),rect);
canvas.drawText(number,(marginLineChart+lineWidth*i)-(rect.right-rect.left)/2,marginBottomText+70,mTextPaint);
}
}
canvas.restore();
//畫波浪圖形
canvas.save();
float mWavyHeight=heightView*((float)4/5)+50;
canvas.translate(0,mWavyHeight);
WavyLinePath.reset();
WavyLinePath.moveTo(-mWaveLength+ mOffset,0);
int wHeight=radius/5;
for(int i=0;i<mWaveCount;i++){
WavyLinePath.quadTo((-mWaveLength*3/4)+(i*mWaveLength)+mOffset,wHeight,(-mWaveLength/2)+(i*mWaveLength)+mOffset,0);
WavyLinePath.quadTo((-mWaveLength/4)+(i * mWaveLength)+mOffset,- wHeight,i*mWaveLength+mOffset,0);
}
WavyLinePath.lineTo(widthView,heightView-mWavyHeight);
WavyLinePath.lineTo(0,heightView-mWavyHeight);
WavyLinePath.close();
canvas.drawPath(WavyLinePath,WavylinesPaint);
canvas.restore();
//畫最低的資訊
float removeHeight=mWavyHeight+(radius/5);
canvas.translate(0,removeHeight);
float rectHeight=heightView-removeHeight;
//畫底下的矩形
RectF rect = new RectF(0,0,widthView,rectHeight);
canvas.drawRect(rect,mBottomRectPaint);
//畫頭像
int bitmap_icon_x=radius/5;
float centerHeight=rectHeight/2;
Bitmap bitmap_icon=getRoundCornerImage(champion_icon,50,radius/5,radius/5);
canvas.drawBitmap(bitmap_icon,bitmap_icon_x,centerHeight- bitmap_icon.getHeight()/2,null);
mTextPaint.setColor(Color.WHITE); mTextPaint.setTextSize(radius/8);
//畫冠軍文字
int champion_x=radius/2; Rect mNameRect=new Rect();
String championMame=stringTemplate(R.string.champion,champion);
mTextPaint.getTextBounds(championMame,0,championMame.length(),mNameRect);
canvas.drawText(championMame,champion_x,(rectHeight+(mNameRect.bottom-mNameRect.top))/2,mTextPaint);
//畫檢視
String look=getContext().getResources().getString(R.string.check);
mTextPaint.getTextBounds(look,0,look.length(),mNameRect);
canvas.drawText(look,widthView-(radius*(float)2/3),(rectHeight+(mNameRect.bottom-mNameRect.top))/2,mTextPaint);
//畫更多圖像
float morePoint=(radius*(float)2/3)/2;
canvas.drawLine(widthView-morePoint,centerHeight-(mNameRect.bottom- mNameRect.top)/2, widthView-morePoint+15,centerHeight,mTextPaint);
canvas.drawLine(widthView-morePoint+15,centerHeight,widthView-morePoint, centerHeight+(mNameRect.bottom-mNameRect.top)/2,mTextPaint);
}
複制
代碼是不是有點多呢,沒辦法畫的東西本身就有點多了。好了剛開始我說要移動canvas的原點是不是,你看剛開始就移動了吧:
super.onDraw(canvas);
canvas.save();
canvas.translate(widthView/2,(heightView*((float)2/3))/2);
複制
1、移動原點到整個圓弧的中心,其中widthView是整個view的寬,heightView是整個view的高,如下圖:
center.PNG
就在上圖的藍色點就是現在的原點。
然後在這原點裡畫圓弧呗,代碼如下
//畫内圓圈
mCirclePaint.setColor(besselColorText);
RectF mCircleRectF=new RectF(-radius,-radius,radius,radius);
canvas.drawArc(mCircleRectF,120,300,false,mCirclePaint);
//畫外圓圈
mCirclePaint.setColor(mBesselCurveColor);
canvas.drawArc(mCircleRectF,120,mCircleNum,false,mCirclePaint);
複制
mCircleNum是為了實作動畫效果的,這後面會講,這樣圓弧就畫完了。效果也是如上圖。
2.在中心點再畫今天的走的總路程,代碼如下:
//畫中間的文字
Rect mCenterRect=new Rect();
String tempAllStop=mCenterNum+"";
mCenterTextPaint.getTextBounds(tempAllStop,0,tempAllStop.length(),mCenterRect);
int halfWidthText=(mCenterRect.right-mCenterRect.left)/2;
int halfHeightText=(mCenterRect.bottom-mCenterRect.top)/2;
canvas.drawText(tempAllStop,-halfWidthText,halfHeightText,mCenterTextPaint);
複制
基本的實作思路是用Rect在這個類計算出你要畫文字的大小,然後在原點畫,不過,記得這裡的x,y點是在原點的左下,具體詳解看這裡寫連結内容
接這就是畫時間和好友平均步數,其實實作原理也是一樣的,隻不過在上面的高度是
canvas.drawText(tempFriendAverageStep,-halfTopWidthText,-(halfHeightText+marginText),mTextPaint);
複制
是中心總步數高度的一半再加間隔,而下面的是:
canvas.drawText(tempAverageStep,-halfBottomWidthText,mBottomHeightText+halfHeightText+marginText,mTextPaint);
複制
是下面文字總的高度再加上中心總步數高度的一半再加間隔。現在效果如下圖:
img1.PNG
接着就是畫排名,首先還是套路:
Rect mNumRect=new Rect();
mCenterTextPaint.getTextBounds(ranking,0,ranking.length(),mNumRect);
int halfNum=(mNumRect.right-mNumRect.left)/2;
mCenterTextPaint.setTextSize(40);
canvas.drawText(ranking,-halfNum,radius,mCenterTextPaint);
複制
計算出排名文字的大小,然後在中心原點x軸為排名文字的一半,y軸問為半徑畫出排名,效果圖如下:
img2.PNG
接着就在排名的兩端畫文字就行了,帶代碼如下:
String rankingLeft=getContext().getResources().getString(R.string.ranking_left);
mTextPaint.getTextBounds(rankingLeft,0,rankingLeft.length(),mNumRect);
canvas.drawText(rankingLeft,-halfNum-(mNumRect.right-mNumRect.left)/2-20,radius,mTextPaint);
canvas.drawText(getContext().getResources().getString(R.string.ranking_right),halfNum+10,radius,mTextPaint);
複制
思路還是一樣,就不說了。此時效果
img3.PNG
畫底下柱狀圖是,首先用canvas.restore();恢複原點到(0,0)的狀态,再用canvas.translate(0,heightView*((float)2/3));把原點移動到圓弧的下面,接着又可以繼續畫,實作思路和前面一樣:
//畫最近七天和平均運動
mTextPaint.setTextSize(radius/9);
canvas.save(); canvas.translate(0,heightView*((float)2/3));
canvas.drawText(getContext().getResources().getString(R.string.nextSevenDay),marginLineChart,0,mTextPaint);
Rect mPercentRect=new Rect();
String mPercentText=stringTemplate(R.string.averageStep,averageStep+"");
mTextPaint.getTextBounds(mPercentText,0,mPercentText.length(),mPercentRect);
canvas.drawText(mPercentText,widthView-marginLineChart-(mPercentRect.right-mPercentRect.left),0,mTextPaint);
//畫虛線
mDottedLinePath.moveTo(marginLineChart,marginBottomText);
mDottedLinePath.lineTo(widthView-marginLineChart,marginBottomText);
canvas.drawPath(mDottedLinePath,mDottedLinePaint);
複制
此時效果如下:
img4.PNG
接下來畫柱狀圖,首先
int lineWidth=(widthView-marginLineChart*2)/8;
計算出每個點之間的間隔
img5.PNG
if(mListStep.size()>0){
for(int i=mListStep.size();i>=1;i--){
if(mListStep.get(i-1)!=0){
//計算出起始點X和終點X的值
int startX=marginLineChart+lineWidth*i-radius/23;
int endX=marginLineChart+lineWidth*i+radius/23;
if(mListStep.get(i-1)>mStandardStop){
//達标 mTextPaint.setColor(mBesselCurveColor);
//超出的部分
int exceed=mListStep.get(i-1)-mStandardStop;
//算出柱體大小 float standard=(float) (mCircleRectHeight*Double.valueOf(exceed/Double.valueOf(mStandardStop)));
mRecf=new RectF(startX,marginBottomText-(standard>mCircleRectHeight?mCircleRectHeight:standard) ,endX,marginBottomText+mCircleRectHeight);
canvas.drawRoundRect(mRecf,50,50,mTextPaint);
}else{
//不達标
mTextPaint.setColor(besselColorText);
//算出不達标柱體的大小
float noStandard=(float)(mCircleRectHeight*Double.valueOf(mListStep.get(i-1)/Double.valueOf(mStandardStop)));
mRecf=new RectF(startX,marginBottomText,endX,marginBottomText+( noStandard>mCircleRectHeight?mCircleRectHeight:noStandard));
canvas.drawRoundRect(mRecf,50,50,mTextPaint);
}
}
//畫底下的日期
mTextPaint.setColor(besselColorText);
mCalendar.set(Calendar.DAY_OF_MONTH,mCalendar.get(Calendar.DAY_OF_MONTH)-1);
Rect rect =new Rect();
String number=stringTemplate(R.string.day,mCalendar.get(Calendar.DAY_OF_MONTH)+"");
mTextPaint.getTextBounds(number,0,number.length(),rect);
canvas.drawText(number,(marginLineChart+lineWidth*i)-(rect.right-rect.left)/2,marginBottomText+70,mTextPaint);
}
}
複制
mStandardStop是達标的資料,當資料小于mStandardStop就是不達标,是以柱狀圖就要畫在虛線的下面,mCircleRectHeight是柱狀圖一半的高
float standard=(float)(mCircleRectHeight*Double.valueOf(exceed/Double.valueOf(mStandardStop)));
這句代碼是計算出下面圓柱體的具體大小,
noStandard>mCircleRectHeight?mCircleRectHeight:noStandard
當,但柱狀圖大于mCircleRectHeight時就用mCircleRectHeight不然就根據計算的數值來。當資料大于mStandardStop時,
int exceed=mListStep.get(i-1)-mStandardStop;float standard=(float)(mCircleRectHeight*Double.valueOf(exceed/Double.valueOf(mStandardStop)));
exceed是計算出超出的部分,再拿超出的部分算出具體的大小,剩下的和小于的一樣,當standard大于最大的mCircleRectHeight是就用mCircleRectHeight否則就用standard。底下日期是用Calendar得到前7天的日期再循環的畫上去,思路和上面一樣不再贅述。此時效果如下:
img6.PNG
接下來是畫波浪,畫波浪是用了貝塞爾曲線的方法畫的,如果不懂貝塞爾曲線請參考這裡寫連結内容,這也是我學貝塞爾曲線參考的内容。首先我們又把canvas恢複到原點
canvas.restore();
再用
float mWavyHeight=heightView*((float)4/5)+50; canvas.translate(0,mWavyHeight);
移動這個位置,是為了适配。
WavyLinePath.reset();
WavyLinePath.moveTo(-mWaveLength+ mOffset,0);
int wHeight=radius/5; for(int i=0;i<mWaveCount;i++){
WavyLinePath.quadTo((-mWaveLength*3/4)+(i*mWaveLength)+mOffset,wHeight,(-mWaveLength/2)+(i*mWaveLength)+mOffset,0);
WavyLinePath.quadTo((-mWaveLength/4)+(i * mWaveLength)+mOffset,-wHeight,i*mWaveLength+mOffset,0);
}
WavyLinePath.lineTo(widthView,heightView-mWavyHeight);
WavyLinePath.lineTo(0,heightView-mWavyHeight);
WavyLinePath.close();
canvas.drawPath(WavyLinePath,WavylinesPaint);
複制
WavyLinePath.quadTo就是貝塞爾曲線調的方法,for循環幾次使之形成波浪圖形,記得一樣要WavyLinePath.lineTo().不讓會出現底下有些地方會畫不到。原理是向上定一個控制點有向下定一個控制點使之形成一個sin函數圖形。具體請學貝塞爾曲線。此時效果圖:
img7.PNG
最後就是畫底下的矩形和頭像和文字了。最值得講的是頭像我一開始的設想的傳Url的,不過這樣子又要做網絡方面的代碼工作,這樣子會破懷類的功能單一性原則,是以最後我實在外部傳一個位圖,在位圖進行處理使其圓角。剩下的隻是畫文字而已,上面已經講夠多了,就不在講了。
對了,最後還有一個剛開始的動畫效果。
public void startAnimator(){
ValueAnimator mCircleAminator=ValueAnimator.ofFloat(0f,300f);
mCircleAminator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) {
mCircleNum=(float)animation.getAnimatedValue(); postInvalidate();
}
});
ValueAnimator mCenterText=ValueAnimator.ofInt(0,allStop);
mCenterText.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) {
mCenterNum=(int)animation.getAnimatedValue(); postInvalidate();
} });
ValueAnimator mWavyAnimator = ValueAnimator.ofInt(0, mWaveLength);
mWavyAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) {
mOffset = (int) animation.getAnimatedValue(); postInvalidate();
} });
animSet.setDuration(2000);
animSet.playTogether(mCircleAminator,mCenterText,mWavyAnimator);
animSet.start();
}
//字元串拼接
public String stringTemplate(int template,String content){
return String.format(getContext().getResources().getString(template),content);
}
複制
其實也簡單通過設定ValueAnimator讓它在規定的時間内産生數值的變化,再調用postInvalidate().對View的界面進行重新整理即可實作動畫效果。
最後給源碼好好研究吧源碼隻有好好看源碼才能學到更多東西。
如果對你有幫助就請給我給星星或喜歡吧。