記一次意外的自定義控件

有時(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ì)圖

拿到設(shè)計(jì)圖,第一想法,這有多難,直接上MP庫(kù)唄,于是把庫(kù)放到MethodsCount一查,哭了。。。2K多個(gè)方法欸,2K欸!!!!2K!!!!

方法統(tǒng)計(jì)

遂放棄,,,還是自己開干吧

看到曲線什么的,第一時(shí)間**貝塞爾曲線**走起~ 于是,最為一個(gè)面向搜索引擎編程的程序員,當(dāng)然谷歌一下貝塞爾。。。

隨便搜搜,于是就看到CSDN的一篇文章文章點(diǎn)我

啊~好細(xì)致,好贊啊!!!可惜在下沒法短時(shí)間內(nèi)理解啊TAT。然而,按照我平時(shí)的經(jīng)驗(yàn),還是擼個(gè)初步的東西出來吧。。。

OMG

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,大致原理如下吧:

AE

如圖,如果多看幾遍,也許你會(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,先不填充:

預(yù)覽圖

可以看到,我們的控制點(diǎn)都很好的分布在兩點(diǎn)之間,曲線看起來十分平滑。

為了更清晰,我們將測(cè)試數(shù)據(jù)減少一點(diǎn):

private float[] testDatas = { 60f, 30f, 57f, 41f ,88f,70f};
預(yù)覽圖2

現(xiàn)在看起來更加的清晰,然后我們填充一下并取消掉輔助線條和輔助點(diǎn)。

預(yù)覽圖3

現(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)偏差):

preview

【附】所有代碼(可以直接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;
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容