Android輕量的線性和百分比圖表實現

一.寫這篇文章的原由

目前github上有多個關於圖表的框架,好比MPAndroidChart很好,可是很大,不必由於一個小的圖標讓工程項目擴大不少,另外有些輕量級的框架,可是我的感受都很難知足本身的需求,再者就算很好的框架,那也是別人的,只有本身動手寫起來,瞭解前先後後的坑,本身才能成長,並且在寫的過程,咱們能發現更多的細節,好比繪製的時候內存分配的問題,Canvas直接繪製和經過Bitmap繪製等等,因此這篇文章的目的:git

  • 1.是給你們提供自定義view繪製的思路
  • 2.滑動自定義view的部分區域怎麼實現
  • 3.path動畫繪製的實現
  • 4.熟悉canvas的api,總之能直接動手了,那就自定義view就通關了,因此就寫這篇文章主要是鼓勵你們多去實現。

二.實現出來的效果圖

圖片描述

三.線性圖表實現的思路

因爲屏幕的寬度有限,因此咱們一屏通過計算,最好顯示的7個點,因此咱們首先須要對咱們的view寬度進行計算,首先拿到屏幕的寬度,而後再進行/7,獲得每一個間隔的寬度,而後乘以咱們x的座標點的個數,其中的onMeasure的方法:github

int widthParentMeasureMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthParentMeasureSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightParentMeasureMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightParentMeasureSize = MeasureSpec.getSize(heightMeasureSpec);
    int resultWidthSize = 0;
    int resultHeightSize = 0;
    int resultWidthMode = MeasureSpec.EXACTLY;//用來對childView進行計算的
    int resultHeightMode = MeasureSpec.EXACTLY;
    int paddingWidth = getPaddingLeft() + getPaddingRight();
    int paddingHeight = getPaddingTop() + getPaddingBottom();
    ViewGroup.LayoutParams thisLp = getLayoutParams();
    switch (widthParentMeasureMode) {
        //父類不加限制給子類
        case MeasureSpec.UNSPECIFIED:
            //這個表明在佈局寫死了寬度
            if (thisLp.width > 0) {
                resultWidthSize = thisLp.width;
                resultWidthMode = MeasureSpec.EXACTLY;
            } else {
                resultWidthSize = (int) (getYMaxTextWidth() + mXinterval * mXdots.length);
                resultWidthMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        case MeasureSpec.AT_MOST:
            //這個表明在佈局寫死了寬度
            if (thisLp.width > 0) {
                resultWidthSize = thisLp.width;
                resultWidthMode = MeasureSpec.EXACTLY;
            } else if (thisLp.width == ViewGroup.LayoutParams.MATCH_PARENT) {
                resultWidthSize = Math.max(0, widthParentMeasureSize - paddingWidth);
                resultWidthMode = MeasureSpec.AT_MOST;
            } else if (thisLp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
                resultWidthSize = (int) (getYMaxTextWidth() + mXinterval * mXdots.length);
                resultWidthMode = MeasureSpec.AT_MOST;
            }
            break;
        case MeasureSpec.EXACTLY:
            //這個表明在佈局寫死了寬度
            if (thisLp.width > 0) {
                resultWidthSize = Math.min(widthParentMeasureSize, thisLp.width);
                resultWidthMode = MeasureSpec.EXACTLY;
            } else if (thisLp.width == ViewGroup.LayoutParams.MATCH_PARENT) {
                resultWidthSize = widthParentMeasureSize;
                resultWidthMode = MeasureSpec.EXACTLY;
            } else if (thisLp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
                resultWidthSize = (int) (getYMaxTextWidth() + mXinterval * mXdots.length);
                resultWidthMode = MeasureSpec.AT_MOST;
            }
            break;
    }


    switch (heightParentMeasureMode) {
        //父view不加限制
        case MeasureSpec.UNSPECIFIED:
            //這個表明在佈局寫死了寬度
            if (thisLp.height > 0) {
                resultHeightSize = thisLp.height;
                resultHeightMode = MeasureSpec.EXACTLY;
            } else {
                resultHeightSize = (int) (getYMaxTextHeight() + mYvisibleNum * mYinterval + getXMaxTextHeight());
                resultHeightMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        case MeasureSpec.AT_MOST:
            if (thisLp.height > 0) {
                resultHeightSize = heightParentMeasureSize;
                resultHeightMode = MeasureSpec.EXACTLY;
            } else if (thisLp.height == ViewGroup.LayoutParams.MATCH_PARENT) {
                resultHeightSize = Math.max(0, heightParentMeasureSize - paddingHeight);
                resultHeightMode = MeasureSpec.AT_MOST;
            } else if (thisLp.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
                resultHeightSize = (int) (getYMaxTextHeight() + mYvisibleNum * mYinterval + getXMaxTextHeight());
                resultHeightMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        case MeasureSpec.EXACTLY:
            //這個表明在佈局寫死了寬度
            if (thisLp.height > 0) {
                resultHeightSize = Math.min(heightParentMeasureSize, getMeasuredWidth());
                resultHeightMode = MeasureSpec.EXACTLY;
            } else if (thisLp.width == ViewGroup.LayoutParams.MATCH_PARENT) {
                resultHeightSize = heightParentMeasureSize;
                resultHeightMode = MeasureSpec.EXACTLY;
            } else if (thisLp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
                resultHeightSize = (int) (getYMaxTextHeight() + mYvisibleNum * mYinterval + getXMaxTextHeight());
                resultHeightMode = MeasureSpec.AT_MOST;
            }
            break;
    }

    setMeasuredDimension(MeasureSpec.makeMeasureSpec(resultWidthSize, resultWidthMode),
            MeasureSpec.makeMeasureSpec(resultHeightSize, resultHeightMode));

設置好了尺寸,咱們就能夠繪製界面,這裏咱們onDraw的時候,就依次繪製橫線和豎線,在繪製橫線的時候,將Y座標的數字一塊兒繪製上去,同理繪製豎線的時候,把x座標的數字繪製上去,折線的畫根據數字計算出座標點,而後建立一個path,首先moveTo(firstX,firstY),而後lineTo下面的點就能夠了,最後繪製上path,然而這樣的話,咱們在滑動的時候,會發現這個view都會跟着一塊兒滾動了,那麼咱們怎樣才能實現view的部分pinned呢?在這個時候,咱們就須要先建立一個bitmap,將須要滑動的部分繪製到這個bitmap上去,而後bitmap在繪製到這個canvas上的時候,保持固定的位置就好了,好了再說就懵逼了,仍是上代碼吧:canvas

float tempTableLeftPadding = getYMaxTextWidth();
    if (mBitmap == null || mYNumCanvas == null) {
        mBitmap = Bitmap.createBitmap((int) (getMeasuredWidth() - getYMaxTextWidth()), getMeasuredHeight(), Bitmap.Config.ARGB_8888);
        mYNumCanvas = new Canvas(mBitmap);
    }

    mYNumCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
    mYNumCanvas.translate(mScrollPosX,0);//這段代碼就是來實現滑動的操做

    //繪製橫線
    for (int y = 0, size = mYdots.length; y < size; y++) {
        String tempText = String.valueOf(mYdots[mYdots.length - 1 - y]);
        mYNumCanvas.drawLine(0, (float) (mYinterval * y), (float) (mXdots.length * mXinterval), (float) (mYinterval * y), mXlinePaint);
        canvas.drawText(tempText, getYMaxTextWidth() - mYNumPaint.measureText(tempText), getYMaxTextHeight() + (float) (mYinterval * y), mYNumPaint);
    }
    //繪製豎線
    for (int x = 0, size = mXdots.length; x <= size; x++) {
        mYNumCanvas.drawLine((float) (mXinterval * x), 0, (float) (mXinterval * x), (float) (mYinterval * mYvisibleNum), mXlinePaint);
        if (x >= 1) {
            String tempText = mXdots[x - 1];
            mYNumCanvas.drawText(tempText, (float) (mXinterval * x) - mYNumPaint.measureText(tempText) / 2, (float) (mYvisibleNum * mYinterval + getYMaxTextHeight()), mYNumPaint);
        }
    }

    if (isAnimationOpen)//是否須要開啓動畫繪製,這個後面會解釋實現方式
        mYNumCanvas.drawPath(mLineDrawPath, mLinePaint);
    else
        mYNumCanvas.drawPath(mLinePath, mLinePaint);
    canvas.drawBitmap(mBitmap, tempTableLeftPadding, getYMaxTextHeight() / 2, null);

上面的mScrollPosX是根據手勢監聽類GestureDetector來獲取的:api

@Override
public boolean onTouchEvent(MotionEvent event) {
    if (!isAnimationOpen || isDrawOver)
        return mGestureDetector.onTouchEvent(event);
    return super.onTouchEvent(event);
}

然而繪製了,咱們感受還缺乏了什麼,嗯,沒錯就是動畫效果,這裏咱們用到經過的path繪製實現動畫的方案,就是先經過PathMeasure獲得path的長度,而後根據動畫時間,經過ValueAnimator計算它在某個時刻的座標,而後從新進行繪製path路徑:框架

private void startPathAnim(long duration) {
    ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, mLineLength);
    valueAnimator.setDuration(duration);
    valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            float value = (Float) animation.getAnimatedValue();
            // 獲取當前點座標封裝到mCurrentPosition
            mPathMeasure.getPosTan(value, mCurrentPosition, null);
            mLineDrawPath.lineTo(mCurrentPosition[0], mCurrentPosition[1]);
            invalidate();
        }
    });
    valueAnimator.start();
}

四.百分比圓形圖表實現

圖片描述

其實這個的實現,相比上一個少了不少,大可能是集中在onDraw方法裏面,關鍵點是在百分比的數字,怎麼橫向顯示在扇形區域,這裏我就主要這個計算規則提出來:
private void drawText(Canvas canvas, float sweepAngle, float startAngle, ArcVo temp) {ide

float middleAngle;
    middleAngle = startAngle + sweepAngle / 2;
    float startX;
    float startY;
    float endX;
    float endY;
    String drawText = temp.getPercentInCircle() * 100 + "%";
    if (middleAngle <= 90) {
        //在第四象限
        double angle = middleAngle;
        angle = Math.toRadians(angle);
        startY = endY = (float) (Math.sin(angle) * mRaduis + mRaduis);
        endX = (float) (mRaduis + Math.cos(angle) * mRaduis);
        startX = endX - UiUtils.getTextWidth(drawText, mTextPaint);
    } else if (middleAngle <= 180) {
        //在第三象限
        double angle = 180 - middleAngle;
        angle = Math.toRadians(angle);
        startY = endY = (float) (Math.sin(angle) * mRaduis + mRaduis);
        startX = (float) (mRaduis - Math.cos(angle) * mRaduis);
        endX = startX + UiUtils.getTextWidth(drawText, mTextPaint);
    } else if (middleAngle <= 270) {
        //在第二象限
        double angle = 270 - middleAngle;
        angle = Math.toRadians(angle);
        startY = endY = (float) (mRaduis - Math.cos(angle) * mRaduis);
        startX = (float) (mRaduis - Math.sin(angle) * mRaduis);
        endX = startX + UiUtils.getTextWidth(drawText, mTextPaint);
    } else {
        //在第一象限
        double angle = 360 - middleAngle;
        angle = Math.toRadians(angle);
        startY = endY = (float) (mRaduis - Math.sin(angle) * mRaduis);
        endX = (float) (mRaduis + Math.cos(angle) * mRaduis);
        startX = endX - UiUtils.getTextWidth(drawText, mTextPaint);
    }

    mTextPath.reset();
    mTextPath.moveTo(startX, startY);
    mTextPath.lineTo(endX, endY);
    if (middleAngle > 180) {
        canvas.drawTextOnPath(drawText, mTextPath, 0, UiUtils.getTextHeight(drawText, mTextPaint), mTextPaint);
    } else {
        canvas.drawTextOnPath(drawText, mTextPath, 0, -UiUtils.getTextHeight(drawText, mTextPaint), mTextPaint);

    }
}

 @Override
protected void onDraw(Canvas canvas) {
    if (!canDraw()) return;
    float sweepAngle;
    float startAngle = 0;
    for (int i = 0, size = mDisArcList.size(); i < size; i++) {
        ArcVo temp = mDisArcList.get(i);
        mArcPaint.setColor(temp.getScanColor());
        sweepAngle = temp.getPercentInCircle() * 360;
        canvas.drawArc(mDrawCircleRect, startAngle, sweepAngle, true, mArcPaint);
        drawText(canvas, sweepAngle, startAngle, temp);
        startAngle = startAngle + sweepAngle;
    }
}

五.使用方式

若是你以爲大家的項目正好要用到相似的圖標,在項目的gradle文件中,增長compile 'wellijohn.org.simplelinechart:linechart:0.0.2'具體的方法,歡迎移步到github上去看,已經封裝成庫上傳至jcenter,上面有具體的使用方法(圖表地址),目前暴露的方法很少,能夠留言增長
github地址:https://github.com/WelliJohn/...
若是以爲項目對大家的自定義view有必定的啓發的話,麻煩幫忙star一下,若是有更好的實現方案,歡迎留言交流!!佈局

相關文章
相關標籤/搜索