记一次意外的自定义控件
有時候,意外也許就會造成一個不經意間的成功。
【注意:本文章前兩節盡是吐槽,要看代碼,實現方案什么的,請直接看第三節】
【注意:本文章前兩節盡是吐槽,要看代碼,實現方案什么的,請直接看第三節】
【注意:本文章前兩節盡是吐槽,要看代碼,實現方案什么的,請直接看第三節】
重要的話要說三遍。。。
咳咳,,,咱們不是專業寫手,就不要那么裝文藝了,還是逗比點好。 不如咱們先上個圖?
咳咳,請忽略我豎屏錄制了啦。。。。還有,請忽略為啥那條線會在屏幕邊邊走,在下不拘束它的自由←_←
##起因 事情的起源是這樣滴,因為某種需求,咱們需要擼一個這樣子的控件(為了不泄露設計圖,咱們就拿MPAndroidChart的圖展示吧,反正需求都一樣):
拿到設計圖,第一想法,這有多難,直接上MP庫唄,于是把庫放到MethodsCount一查,哭了。。。2K多個方法欸,2K欸!!!!2K!!!!
遂放棄,,,還是自己開干吧
看到曲線什么的,第一時間**貝塞爾曲線**走起~ 于是,最為一個面向搜索引擎編程的程序員,當然谷歌一下貝塞爾。。。
隨便搜搜,于是就看到CSDN的一篇文章文章點我。
啊~好細致,好贊啊!!!可惜在下沒法短時間內理解啊TAT。然而,按照我平時的經驗,還是擼個初步的東西出來吧。。。
OMG....這神馬啊,這尖尖,都快能戳死人了好嗎。。。。 于是,選擇戰略性撤退,休息一晚再開干。
##意外 第二天,毫無疑問的繼續一臉蒙逼。。。 這時候,一位老朋友叫我幫他摳個圖,是的,你沒看錯,摳圖。。。。如果有看過我的一起擼個朋友圈系列文章的人,或許會知道,在下也會AE這個視頻后期軟件。。。
摳就摳吧。。。。但!!! 意外就這么來了。。。。摳圖的時候,為了邊緣平滑,我經常調節錨點,使曲線更加的平滑,然后居然讓我發現了一個規律0.0,大致原理如下吧:
如圖,如果多看幾遍,也許你會發現,當兩個控制點的x位置在前后兩個坐標內,而y分別與前后兩個坐標平齊的時候,轉折點的銜接最為平滑,否則妥妥的出現尖尖(嗯。。。我還特地用鼠標繞了幾圈標出尖尖位置)。
媽蛋,得來毫不費功夫啊。。。。真的想抱著我朋友親幾口,可惜在下不搞基- -
##實現
既然找到了突破口,那妥妥的開干啊。
于是興沖沖的繼承View,開始我們的偉業:
public class TestView extends View {// 最大值private final float maxValue = 100f;// 測試數據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};// 點記錄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;...構造器初始化以上的東西 復制代碼我們定義了一個最大值,和一組測試數據。這個最大值的作用是用來計算當前數據在屏幕的y位置,比如這樣:最大值100,我們的數值15,但我們的屏幕是720*1280,那么當然不可以只畫15像素了,這怎么看得到嘛,我們的y位置判定為:
屏幕高度*(1-(15/100))
為什么要用1減去百分比,因為原點不在左下角而在左上角,所以我們需要減掉。
接下來到measure初始化我們的點。
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是偏移量,其作用是使點在屏幕上的x位置是均分的,然后初始化一個線性漸變。
這時候我們的點是這樣的(為了更方便查看,我們設定為橫屏并給上線條):
點和點之間的x偏移都是一致的(最后一個除外)
然后我們在onDraw開始繪制():
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);// 控制點展示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);} 復制代碼這里解析一下: 當i==0,也就是畫第一個點的時候,我們需要把畫筆移到我們第一個點的位置,否則永遠都會從0,0開始,以后就不需要移動了,因為畫完一條線后,畫筆位置會停留在最后一個點。
我們可以看到兩個控制點的坐標,跟我們上面AE展示出來的是一樣的,x位置都是取兩個點的中間,y則是分別跟兩邊平齊,這樣的曲線最為圓滑
clicPath.cubicTo這個方法,前面4個參數分別代表著控制點1的xy,控制點2的xy,最后一個參數則是結束點的xy,在下一次循環到來之時,最后一個參數則會作為下一次繪制的起點。
最后別忘了在循環外面將path封閉起來,我們不可以直接用path.close(),因為close方法是最后一個點與第一個點直接連一條直線的,但我們需要填充曲線下方。
為了方便展示,我們添加了參考點以及將線條設置為stroke,先不填充:
可以看到,我們的控制點都很好的分布在兩點之間,曲線看起來十分平滑。
為了更清晰,我們將測試數據減少一點:
private float[] testDatas = { 60f, 30f, 57f, 41f ,88f,70f}; 復制代碼現在看起來更加的清晰,然后我們填充一下并取消掉輔助線條和輔助點。
現在初步達到我們的效果了。。
然而,程序員的冤家產品卻說:哎,這太單調了,給個動畫唄。。。。
媽蛋!!!!!
不過罵完還是得干啊-T-
于是這次我們需要借助PathMeasure這個類
這個類通常用于將某個path轉換為一個具體的position,更多情況下是用作路徑動畫。
還記得我們之前定義的變量里面有些什么嗎:
private PathMeasure mPathMeasure;private float[] mCurrentPosition = new float[2];private float[] mPrePosition = new float[2]; 復制代碼根據命名,也很清楚是干啥的。
接下來繼續開工:
首先定義一個公用方法給外部調用:
public void startAnima(long duration) {} 復制代碼我們通過這個方法來繪制線條
然后我們利用ValueAnimator來動態獲取我們path的坐標
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() {public void onAnimationUpdate(ValueAnimator animation) {float value = (Float) animation.getAnimatedValue();// 獲取當前點坐標封裝到mCurrentPositionmPathMeasure.getPosTan(value, mCurrentPosition, null);invalidate();if (value == mPathMeasure.getLength()) animaFirst = true;}});valueAnimator.start();} 復制代碼為了防止onDraw里面多次繪制,我們定義一個animaFirst。
然后補充我們的onDraw方法:
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);} 復制代碼如果動畫剛啟動,我們就把點移到第一個點的位置,同時記錄 如果動畫已經啟動了,我們就重復前面的步驟畫出貝塞爾,當然,你也可以直接lineTo,然后將當前點付給前一個點。
最后,我們在onDetachedFromWindow清掉各種信息,畢竟那啥,內存還是挺珍貴的對吧-V-
protected void onDetachedFromWindow() {super.onDetachedFromWindow();datas.clear();clicPath=null;controllPaintA=null;controllPaintB=null;mPathMeasure=null;} 復制代碼最終效果圖(未修復到屏幕邊邊繼續畫的問題。。。,以及貌似有些地方有點偏差):
【附】所有代碼(可以直接copy使用,因為是測試demo,所以并沒有封裝什么的,同時measure那里也沒有指定wrap_content時的大小,大家可以自行封裝或修復或擴展哈哈-V-):
/*** Created by 大燈泡 on 2016/2/29.*/ public class TestView extends View {// 最大值private final float maxValue = 100f;// 測試數據//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};// 點記錄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);}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;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);/**輔助點和線**///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() {public void onAnimationUpdate(ValueAnimator animation) {float value = (Float) animation.getAnimatedValue();// 獲取當前點坐標封裝到mCurrentPositionmPathMeasure.getPosTan(value, mCurrentPosition, null);Log.d("curX",""+mCurrentPosition[0]);invalidate();if (value == mPathMeasure.getLength())animaFirst = true;}});valueAnimator.start();}protected void onDetachedFromWindow() {super.onDetachedFromWindow();datas.clear();clicPath = null;controllPaintA = null;controllPaintB = null;mPathMeasure = null;} } 復制代碼總結
以上是生活随笔為你收集整理的记一次意外的自定义控件的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 代码:android崩溃日志收集和处理
- 下一篇: 8.3. 测试 opensips