有時(shí)候因妙,意外也許就會造成一個(gè)不經(jīng)意間的成功。
【注意:本文章前兩節(jié)盡是吐槽票髓,要看代碼攀涵,實(shí)現(xiàn)方案什么的,請直接看第三節(jié)】
【注意:本文章前兩節(jié)盡是吐槽洽沟,要看代碼以故,實(shí)現(xiàn)方案什么的,請直接看第三節(jié)】
【注意:本文章前兩節(jié)盡是吐槽裆操,要看代碼怒详,實(shí)現(xiàn)方案什么的,請直接看第三節(jié)】
重要的話要說三遍踪区。昆烁。。
咳咳缎岗,静尼,,咱們不是專業(yè)寫手传泊,就不要那么裝文藝了鼠渺,還是逗比點(diǎn)好。
不如咱們先上個(gè)圖眷细?
咳咳拦盹,請忽略我豎屏錄制了啦。溪椎。普舆。。還有池磁,請忽略為啥那條線會在屏幕邊邊走奔害,在下不拘束它的自由←_←
起因
事情的起源是這樣滴,因?yàn)槟撤N需求地熄,咱們需要擼一個(gè)這樣子的控件(為了不泄露設(shè)計(jì)圖华临,咱們就拿MPAndroidChart的圖展示吧,反正需求都一樣):
拿到設(shè)計(jì)圖端考,第一想法雅潭,這有多難揭厚,直接上MP庫唄,于是把庫放到MethodsCount一查扶供,哭了筛圆。。椿浓。2K多個(gè)方法欸太援,2K欸!0獍L岵怼!2KK癯ā<蠲伞!夯巷!
遂放棄赛惩,,趁餐,還是自己開干吧
看到曲線什么的喷兼,第一時(shí)間**貝塞爾曲線**
走起~ 于是,最為一個(gè)面向搜索引擎編程的程序員后雷,當(dāng)然谷歌一下貝塞爾褒搔。。喷面。
隨便搜搜,于是就看到CSDN的一篇文章文章點(diǎn)我走孽。
啊~好細(xì)致惧辈,好贊啊?拇伞:谐荨!可惜在下沒法短時(shí)間內(nèi)理解啊TAT困食。然而边翁,按照我平時(shí)的經(jīng)驗(yàn),還是擼個(gè)初步的東西出來吧硕盹。符匾。。
OMG....這神馬啊瘩例,這尖尖啊胶,都快能戳死人了好嗎甸各。。焰坪。趣倾。
于是,選擇戰(zhàn)略性撤退某饰,休息一晚再開干儒恋。
意外
第二天,毫無疑問的繼續(xù)一臉蒙逼黔漂。诫尽。。
這時(shí)候瘟仿,一位老朋友叫我?guī)退麚競€(gè)圖箱锐,是的,你沒看錯(cuò)劳较,摳圖驹止。。观蜗。臊恋。如果有看過我的一起擼個(gè)朋友圈系列文章的人,或許會知道墓捻,在下也會AE這個(gè)視頻后期軟件抖仅。。砖第。
摳就摳吧撤卢。。梧兼。放吩。但!S鸾堋渡紫!
意外就這么來了。考赛。惕澎。。摳圖的時(shí)候颜骤,為了邊緣平滑唧喉,我經(jīng)常調(diào)節(jié)錨點(diǎn),使曲線更加的平滑,然后居然讓我發(fā)現(xiàn)了一個(gè)規(guī)律0.0欣喧,大致原理如下吧:
如圖腌零,如果多看幾遍,也許你會發(fā)現(xiàn)唆阿,當(dāng)兩個(gè)控制點(diǎn)的x位置在前后兩個(gè)坐標(biāo)內(nèi)益涧,而y分別與前后兩個(gè)坐標(biāo)平齊的時(shí)候,轉(zhuǎn)折點(diǎn)的銜接最為平滑驯鳖,否則妥妥的出現(xiàn)尖尖(嗯闲询。。浅辙。我還特地用鼠標(biāo)繞了幾圈標(biāo)出尖尖位置)扭弧。
媽蛋,得來毫不費(fèi)功夫啊记舆。鸽捻。。泽腮。真的想抱著我朋友親幾口御蒲,可惜在下不搞基- -
實(shí)現(xiàn)
既然找到了突破口,那妥妥的開干啊诊赊。
于是興沖沖的繼承View厚满,開始我們的偉業(yè):
public class TestView extends View {
// 最大值
private final float maxValue = 100f;
// 測試數(shù)據(jù)
private float[] testDatas = { 55f, 38f, 50f, 44f, 31f, 22f, 9f, 19f, 50f, 78f, 62f, 51f, 45f, 66f, 79f, 50f, 33f,
24f, 26f, 58f };
//private float[] testDatas = { 60f, 55f, 57f, 50f ,56f,70f};
//private float[] testDatas = { 60f, 55f};
// 點(diǎn)記錄
private List<Point> datas;
private final int num = 12;
// 路徑
private Path clicPath;
// 漸變填充
private Paint mPaint;
// 輔助性畫筆
private Paint controllPaintA;
private Paint controllPaintB;
private Path linePath;
private PathMeasure mPathMeasure;
private float[] mCurrentPosition = new float[2];
private float[] mPrePosition = new float[2];
LinearGradient mGradient;
int width;
int height;
int offSet;
...構(gòu)造器初始化以上的東西
我們定義了一個(gè)最大值,和一組測試數(shù)據(jù)碧磅。這個(gè)最大值的作用是用來計(jì)算當(dāng)前數(shù)據(jù)在屏幕的y位置碘箍,比如這樣:最大值100,我們的數(shù)值15鲸郊,但我們的屏幕是720*1280丰榴,那么當(dāng)然不可以只畫15像素了,這怎么看得到嘛秆撮,我們的y位置判定為:
屏幕高度*(1-(15/100))
為什么要用1減去百分比多艇,因?yàn)樵c(diǎn)不在左下角而在左上角,所以我們需要減掉像吻。
接下來到measure初始化我們的點(diǎn)。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
width = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
height = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
offSet = width / testDatas.length;
if (datas.size() == 0) {
for (int i = 0; i < testDatas.length; i++) {
float ratio = testDatas[i] / maxValue;
Point point;
if (i == 0) {
point = new Point(0, (int) (height * (1 - ratio)));
}
else if (i == testDatas.length - 1) {
point = new Point(width, (int) (height * (1 - ratio)));
}
else {
point = new Point(i * offSet, (int) (height * (1 - ratio)));
}
datas.add(point);
}
}
if (mGradient == null) {
mGradient = new LinearGradient(getMeasuredWidth() >> 1, getMeasuredHeight() >> 1, getMeasuredWidth() >> 1,
getMeasuredHeight(), Color.parseColor("#e0cab3"), Color.parseColor("#ffffff"),
Shader.TileMode.CLAMP);
mPaint.setShader(mGradient);
}
}
其中我們的offSet是偏移量复隆,其作用是使點(diǎn)在屏幕上的x位置是均分的拨匆,然后初始化一個(gè)線性漸變。
這時(shí)候我們的點(diǎn)是這樣的(為了更方便查看挽拂,我們設(shè)定為橫屏并給上線條):
點(diǎn)和點(diǎn)之間的x偏移都是一致的(最后一個(gè)除外)
然后我們在onDraw開始繪制():
@Override
protected void onDraw(Canvas canvas) {
clicPath.reset();
super.onDraw(canvas);
//clicPath.moveTo(datas.get(0).x, datas.get(0).y);
for (int i = 0; i < datas.size() - 1; i++) {
Point startPoint = datas.get(i);
Point endPoint = datas.get(i + 1);
if (i == 0) clicPath.moveTo(startPoint.x, startPoint.y);
int controllA_X = (startPoint.x + endPoint.x) >>1;
int controllA_Y = startPoint.y;
int controllB_X = (startPoint.x + endPoint.x) >>1;
int controllB_Y = endPoint.y;
clicPath.cubicTo(controllA_X, controllA_Y, controllB_X, controllB_Y, endPoint.x, endPoint.y);
// 控制點(diǎn)展示
canvas.drawCircle(controllA_X,controllA_Y,5,controllPaintA);
canvas.drawCircle(controllB_X,controllB_Y,5,controllPaintB);
canvas.drawCircle(startPoint.x,startPoint.y,5,mPaint);
//控制點(diǎn)展示
canvas.drawLine(startPoint.x,startPoint.y,controllA_X,controllA_Y,mPaint);
canvas.drawLine(endPoint.x,endPoint.y,controllB_X,controllB_Y,mPaint);
}
clicPath.lineTo(datas.get(datas.size() - 1).x, height);
clicPath.lineTo(datas.get(0).x, height);
clicPath.lineTo(datas.get(0).x, datas.get(0).y);
canvas.drawPath(clicPath, mPaint);
}
這里解析一下:
當(dāng)i==0惭每,也就是畫第一個(gè)點(diǎn)的時(shí)候,我們需要把畫筆移到我們第一個(gè)點(diǎn)的位置,否則永遠(yuǎn)都會從0台腥,0開始宏赘,以后就不需要移動了,因?yàn)楫嬐暌粭l線后黎侈,畫筆位置會停留在最后一個(gè)點(diǎn)察署。
我們可以看到兩個(gè)控制點(diǎn)的坐標(biāo),跟我們上面AE展示出來的是一樣的峻汉,x位置都是取兩個(gè)點(diǎn)的中間贴汪,y則是分別跟兩邊平齊,這樣的曲線最為圓滑
clicPath.cubicTo這個(gè)方法休吠,前面4個(gè)參數(shù)分別代表著控制點(diǎn)1的xy扳埂,控制點(diǎn)2的xy,最后一個(gè)參數(shù)則是結(jié)束點(diǎn)的xy瘤礁,在下一次循環(huán)到來之時(shí)阳懂,最后一個(gè)參數(shù)則會作為下一次繪制的起點(diǎn)。
最后別忘了在循環(huán)外面將path封閉起來柜思,我們不可以直接用path.close()岩调,因?yàn)閏lose方法是最后一個(gè)點(diǎn)與第一個(gè)點(diǎn)直接連一條直線的,但我們需要填充曲線下方酝蜒。
為了方便展示誊辉,我們添加了參考點(diǎn)以及將線條設(shè)置為stroke,先不填充:
可以看到亡脑,我們的控制點(diǎn)都很好的分布在兩點(diǎn)之間堕澄,曲線看起來十分平滑。
為了更清晰霉咨,我們將測試數(shù)據(jù)減少一點(diǎn):
private float[] testDatas = { 60f, 30f, 57f, 41f ,88f,70f};
現(xiàn)在看起來更加的清晰蛙紫,然后我們填充一下并取消掉輔助線條和輔助點(diǎn)。
現(xiàn)在初步達(dá)到我們的效果了途戒。坑傅。
然而,程序員的冤家產(chǎn)品卻說:哎喷斋,這太單調(diào)了唁毒,給個(gè)動畫唄。星爪。浆西。。
媽蛋M缣凇=恪!!久信!
不過罵完還是得干啊-T-
于是這次我們需要借助PathMeasure這個(gè)類
這個(gè)類通常用于將某個(gè)path轉(zhuǎn)換為一個(gè)具體的position窖杀,更多情況下是用作路徑動畫。
還記得我們之前定義的變量里面有些什么嗎:
private PathMeasure mPathMeasure;
private float[] mCurrentPosition = new float[2];
private float[] mPrePosition = new float[2];
根據(jù)命名裙士,也很清楚是干啥的入客。
接下來繼續(xù)開工:
首先定義一個(gè)公用方法給外部調(diào)用:
public void startAnima(long duration) {}
我們通過這個(gè)方法來繪制線條
然后我們利用ValueAnimator來動態(tài)獲取我們path的坐標(biāo)
public void startAnima(long duration) {
if (mPathMeasure == null) mPathMeasure = new PathMeasure(clicPath, true);
ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, mPathMeasure.getLength());
valueAnimator.setDuration(duration);
// 減速插值器
valueAnimator.setInterpolator(new DecelerateInterpolator());
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float value = (Float) animation.getAnimatedValue();
// 獲取當(dāng)前點(diǎn)坐標(biāo)封裝到mCurrentPosition
mPathMeasure.getPosTan(value, mCurrentPosition, null);
invalidate();
if (value == mPathMeasure.getLength()) animaFirst = true;
}
});
valueAnimator.start();
}
為了防止onDraw里面多次繪制,我們定義一個(gè)animaFirst潮售。
然后補(bǔ)充我們的onDraw方法:
@Override
protected void onDraw(Canvas canvas) {
...
if (animaFirst) {
linePath.moveTo(datas.get(0).x, datas.get(0).y);
mPrePosition[0] = datas.get(0).x;
mPrePosition[1] = datas.get(0).y;
animaFirst = false;
}
else {
int controllA_X = (int) ((mPrePosition[0] + mCurrentPosition[0]) /2);
int controllA_Y = (int) mPrePosition[1];
int controllB_X = (int) ((mPrePosition[0] + mCurrentPosition[0]) /2);
int controllB_Y = (int) mCurrentPosition[1];
linePath.cubicTo(controllA_X, controllA_Y, controllB_X, controllB_Y, mCurrentPosition[0],
mCurrentPosition[1]);
mPrePosition[0] = mCurrentPosition[0];
mPrePosition[1] = mCurrentPosition[1];
}
canvas.drawPath(linePath, controllPaintA);
}
如果動畫剛啟動痊项,我們就把點(diǎn)移到第一個(gè)點(diǎn)的位置,同時(shí)記錄
如果動畫已經(jīng)啟動了酥诽,我們就重復(fù)前面的步驟畫出貝塞爾鞍泉,當(dāng)然,你也可以直接lineTo肮帐,然后將當(dāng)前點(diǎn)付給前一個(gè)點(diǎn)咖驮。
最后,我們在onDetachedFromWindow清掉各種信息训枢,畢竟那啥托修,內(nèi)存還是挺珍貴的對吧-V-
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
datas.clear();
clicPath=null;
controllPaintA=null;
controllPaintB=null;
mPathMeasure=null;
}
最終效果圖(未修復(fù)到屏幕邊邊繼續(xù)畫的問題。恒界。睦刃。,以及貌似有些地方有點(diǎn)偏差):
【附】所有代碼(可以直接copy使用十酣,因?yàn)槭菧y試demo涩拙,所以并沒有封裝什么的,同時(shí)measure那里也沒有指定wrap_content時(shí)的大小耸采,大家可以自行封裝或修復(fù)或擴(kuò)展哈哈-V-):
/**
* Created by 大燈泡 on 2016/2/29.
*/
public class TestView extends View {
// 最大值
private final float maxValue = 100f;
// 測試數(shù)據(jù)
//private float[] testDatas = { 55f, 38f, 50f, 44f, 31f, 22f, 9f, 19f, 50f, 78f, 62f, 51f, 45f, 66f, 79f, 50f, 33f,
// 24f, 26f, 58f };
private float[] testDatas = { 60f, 30f, 57f, 41f, 88f, 70f };
//private float[] testDatas = { 60f, 55f};
// 點(diǎn)記錄
private List<Point> datas;
// 路徑
private Path clicPath;
// 漸變填充
private Paint mPaint;
// 輔助性畫筆
private Paint controllPaintA;
private Paint controllPaintB;
private Path linePath;
private PathMeasure mPathMeasure;
private float[] mCurrentPosition = new float[2];
private float[] mPrePosition = new float[2];
LinearGradient mGradient;
int width;
int height;
int offSet;
public TestView(Context context) {
this(context, null);
}
public TestView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public TestView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
clicPath = new Path();
linePath = new Path();
datas = new ArrayList<>();
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
//mPaint.setStyle(Paint.Style.STROKE);
controllPaintA = new Paint(Paint.ANTI_ALIAS_FLAG);
controllPaintA.setStyle(Paint.Style.STROKE);
controllPaintA.setStrokeWidth(5);
controllPaintA.setColor(0xffff0000);
controllPaintB = new Paint(Paint.ANTI_ALIAS_FLAG);
controllPaintB.setStyle(Paint.Style.STROKE);
controllPaintB.setColor(0xff00ff00);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
width = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
height = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
offSet = width / testDatas.length;
if (datas.size() == 0) {
for (int i = 0; i < testDatas.length; i++) {
float ratio = testDatas[i] / maxValue;
Point point;
if (i == 0) {
point = new Point(0, (int) (height * (1 - ratio)));
}
else if (i == testDatas.length - 1) {
point = new Point(width, (int) (height * (1 - ratio)));
}
else {
point = new Point(i * offSet, (int) (height * (1 - ratio)));
}
datas.add(point);
}
}
if (mGradient == null) {
mGradient = new LinearGradient(getMeasuredWidth() >> 1, getMeasuredHeight() >> 1, getMeasuredWidth() >> 1,
getMeasuredHeight(), Color.parseColor("#e0cab3"), Color.parseColor("#ffffff"),
Shader.TileMode.CLAMP);
mPaint.setShader(mGradient);
}
}
private boolean animaFirst = true;
@Override
protected void onDraw(Canvas canvas) {
clicPath.reset();
super.onDraw(canvas);
for (int i = 0; i < datas.size() - 1; i++) {
Point startPoint = datas.get(i);
Point endPoint = datas.get(i + 1);
if (i == 0) clicPath.moveTo(startPoint.x, startPoint.y);
int controllA_X = (startPoint.x + endPoint.x) >> 1;
int controllA_Y = startPoint.y;
int controllB_X = (startPoint.x + endPoint.x) >> 1;
int controllB_Y = endPoint.y;
clicPath.cubicTo(controllA_X, controllA_Y, controllB_X, controllB_Y, endPoint.x, endPoint.y);
/**輔助點(diǎn)和線**/
//canvas.drawCircle(controllA_X,controllA_Y,5,controllPaintA);
//canvas.drawCircle(controllB_X,controllB_Y,5,controllPaintB);
//canvas.drawCircle(startPoint.x,startPoint.y,5,mPaint);
//canvas.drawLine(startPoint.x,startPoint.y,controllA_X,controllA_Y,mPaint);
//canvas.drawLine(endPoint.x,endPoint.y,controllB_X,controllB_Y,mPaint);
}
clicPath.lineTo(datas.get(datas.size() - 1).x, height);
clicPath.lineTo(datas.get(0).x, height);
clicPath.lineTo(datas.get(0).x, datas.get(0).y);
canvas.drawPath(clicPath, mPaint);
if (animaFirst) {
linePath.moveTo(datas.get(0).x, datas.get(0).y);
mPrePosition[0] = datas.get(0).x;
mPrePosition[1] = datas.get(0).y;
animaFirst = false;
}
else {
int controllA_X = (int) ((mPrePosition[0] + mCurrentPosition[0]) / 2);
int controllA_Y = (int) mPrePosition[1];
int controllB_X = (int) ((mPrePosition[0] + mCurrentPosition[0]) / 2);
int controllB_Y = (int) mCurrentPosition[1];
linePath.cubicTo(controllA_X, controllA_Y, controllB_X, controllB_Y, mCurrentPosition[0],
mCurrentPosition[1]);
mPrePosition[0] = mCurrentPosition[0];
mPrePosition[1] = mCurrentPosition[1];
}
canvas.drawPath(linePath, controllPaintA);
}
public void startAnima(long duration) {
if (mPathMeasure == null) mPathMeasure = new PathMeasure(clicPath, true);
ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, mPathMeasure.getLength());
valueAnimator.setDuration(duration);
// 減速插值器
valueAnimator.setInterpolator(new DecelerateInterpolator());
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float value = (Float) animation.getAnimatedValue();
// 獲取當(dāng)前點(diǎn)坐標(biāo)封裝到mCurrentPosition
mPathMeasure.getPosTan(value, mCurrentPosition, null);
Log.d("curX",""+mCurrentPosition[0]);
invalidate();
if (value == mPathMeasure.getLength())
animaFirst = true;
}
});
valueAnimator.start();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
datas.clear();
clicPath = null;
controllPaintA = null;
controllPaintB = null;
mPathMeasure = null;
}
}