有時(shí)候,意外也許就會(huì)造成一個(gè)不經(jīng)意間的成功。
【注意:本文章前兩節(jié)盡是吐槽,要看代碼,實(shí)現(xiàn)方案什么的,請(qǐng)直接看第三節(jié)】
【注意:本文章前兩節(jié)盡是吐槽,要看代碼,實(shí)現(xiàn)方案什么的,請(qǐng)直接看第三節(jié)】
【注意:本文章前兩節(jié)盡是吐槽,要看代碼,實(shí)現(xiàn)方案什么的,請(qǐng)直接看第三節(jié)】
重要的話要說三遍。。。
咳咳,,,咱們不是專業(yè)寫手,就不要那么裝文藝了,還是逗比點(diǎn)好。
不如咱們先上個(gè)圖?
咳咳,請(qǐng)忽略我豎屏錄制了啦。。。。還有,請(qǐng)忽略為啥那條線會(huì)在屏幕邊邊走,在下不拘束它的自由←_←
起因
事情的起源是這樣滴,因?yàn)槟撤N需求,咱們需要擼一個(gè)這樣子的控件(為了不泄露設(shè)計(jì)圖,咱們就拿MPAndroidChart的圖展示吧,反正需求都一樣):
拿到設(shè)計(jì)圖,第一想法,這有多難,直接上MP庫(kù)唄,于是把庫(kù)放到MethodsCount一查,哭了。。。2K多個(gè)方法欸,2K欸!!!!2K!!!!
遂放棄,,,還是自己開干吧
看到曲線什么的,第一時(shí)間**貝塞爾曲線**
走起~ 于是,最為一個(gè)面向搜索引擎編程的程序員,當(dāng)然谷歌一下貝塞爾。。。
隨便搜搜,于是就看到CSDN的一篇文章文章點(diǎn)我。
啊~好細(xì)致,好贊啊!!!可惜在下沒法短時(shí)間內(nèi)理解啊TAT。然而,按照我平時(shí)的經(jīng)驗(yàn),還是擼個(gè)初步的東西出來吧。。。
OMG....這神馬啊,這尖尖,都快能戳死人了好嗎。。。。
于是,選擇戰(zhàn)略性撤退,休息一晚再開干。
意外
第二天,毫無疑問的繼續(xù)一臉蒙逼。。。
這時(shí)候,一位老朋友叫我?guī)退麚競(jìng)€(gè)圖,是的,你沒看錯(cuò),摳圖。。。。如果有看過我的一起擼個(gè)朋友圈系列文章的人,或許會(huì)知道,在下也會(huì)AE這個(gè)視頻后期軟件。。。
摳就摳吧。。。。但!!!
意外就這么來了。。。。摳圖的時(shí)候,為了邊緣平滑,我經(jīng)常調(diào)節(jié)錨點(diǎn),使曲線更加的平滑,然后居然讓我發(fā)現(xiàn)了一個(gè)規(guī)律0.0,大致原理如下吧:
如圖,如果多看幾遍,也許你會(huì)發(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;
// 測(cè)試數(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è)最大值,和一組測(cè)試數(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)是這樣的(為了更方便查看,我們?cè)O(shè)定為橫屏并給上線條):
點(diǎn)和點(diǎn)之間的x偏移都是一致的(最后一個(gè)除外)
然后我們?cè)趏nDraw開始繪制():
@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)都會(huì)從0,0開始,以后就不需要移動(dòng)了,因?yàn)楫嬐暌粭l線后,畫筆位置會(huì)停留在最后一個(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ù)則會(huì)作為下一次繪制的起點(diǎn)。
最后別忘了在循環(huán)外面將path封閉起來,我們不可以直接用path.close(),因?yàn)閏lose方法是最后一個(gè)點(diǎn)與第一個(gè)點(diǎn)直接連一條直線的,但我們需要填充曲線下方。
為了方便展示,我們添加了參考點(diǎn)以及將線條設(shè)置為stroke,先不填充:
可以看到,我們的控制點(diǎn)都很好的分布在兩點(diǎn)之間,曲線看起來十分平滑。
為了更清晰,我們將測(cè)試數(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è)動(dòng)畫唄。。。。
媽蛋!!!!!
不過罵完還是得干啊-T-
于是這次我們需要借助PathMeasure這個(gè)類
這個(gè)類通常用于將某個(gè)path轉(zhuǎn)換為一個(gè)具體的position,更多情況下是用作路徑動(dòng)畫。
還記得我們之前定義的變量里面有些什么嗎:
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來動(dòng)態(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);
}
如果動(dòng)畫剛啟動(dòng),我們就把點(diǎn)移到第一個(gè)點(diǎn)的位置,同時(shí)記錄
如果動(dòng)畫已經(jīng)啟動(dòng)了,我們就重復(fù)前面的步驟畫出貝塞爾,當(dāng)然,你也可以直接lineTo,然后將當(dāng)前點(diǎn)付給前一個(gè)點(diǎn)。
最后,我們?cè)趏nDetachedFromWindow清掉各種信息,畢竟那啥,內(nèi)存還是挺珍貴的對(duì)吧-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(cè)試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;
// 測(cè)試數(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;
}
}