Android仿餘額寶實現七天年化收益率圖表

先看效果圖
這裏寫圖片描述
接着是源碼下載地址

本來是使用MPAndroidChartLib,但是有一個業務上的bug,後來就想着自己練練手自己寫一個。
因爲以前沒有真正自定義過涉及到onDraw()的控件,所以目前來看,這個控件的可擴展性還是很差的,留作以後有時間再做吧,主要就是抽象出來一些操作來實現其他樣式或者功能的操作。
水平有限,在此只是幫助一些初學者提供一些思路上的幫助吧
這個View沒什麼複雜的地方,就是比較麻煩,主要流程有:
1、先畫出表格中X軸和Y軸和表格裏的虛線;
2、計算Y軸的刻度
3、遍歷傳入的數據列表繪製各個點的位置到Path中,數據線和陰影區域的區別就是:線只需要畫到最後一個點就行了,而陰影則需要添加兩個點,形成閉合區域。
4、動畫的實現。
5、手勢操作。

View的具體實現步驟和方法:

自定義屬性(如果需要)

定義一個同名的declare-styleable。

<declare-styleable name="CashChart">
    <attr name="line_color" format="color"/>
    <attr name="shadow_color" format="color"/>
    <attr name="y_axis_rows" format="integer"/>
</declare-styleable>

獲取自定義屬性

public CashChart(Context context) {
this(context, null);
}

public CashChart(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
}

public CashChart(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    this.mContext = context;
    TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CashChart);
    mLineColor = ta.getColor(R.styleable.CashChart_line_color, LINE_COLOR_DEFAULT);
    mShadowColor = ta.getColor(R.styleable.CashChart_shadow_color, SHADOW_COLOR_DEFAULT);
    mLineWidth = ta.getDimension(R.styleable.CashChart_line_width, LINE_WIDTH_DEFAULT);
    mYAxisRows = ta.getInteger(R.styleable.CashChart_y_axis_rows, CHART_Y_AXIS_ROWS_DEFAULT);
    ta.recycle();
    init();
}

接着在init()方法中,初始化各種UI值、Paint、動畫,在這裏我爲每個操作都定義了一個的畫筆。動畫後面再說。
接着有一個setData(List< ChartBean > data)方法,在設置後,計算出來最大最小值和差值,

private Paint mLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);//畫線用的
private Paint mLineEffectPaint = new Paint(Paint.ANTI_ALIAS_FLAG);//畫虛線用的
private Paint mChartScaleTxtPaint = new Paint(Paint.ANTI_ALIAS_FLAG);//表格刻度文本
private Paint mCurPointPaint = new Paint(Paint.ANTI_ALIAS_FLAG);//選中的點
private Paint mBubbleTxtPaint = new Paint(Paint.ANTI_ALIAS_FLAG);//選中的點的文本Paint
private Paint mShadowPaint = new Paint(Paint.ANTI_ALIAS_FLAG);//陰影區域的畫筆

public void setData(List<ChartBean> data) {
    if (data == null) return;
    this.mDatas = data;
    //找出數據列表中最大值和最小值,來確定表格Y軸上的數值
    float[] values = new float[mDatas.size()];
    for (int i = 0; i < mDatas.size(); i++) {
        if (mDatas.get(i) == null) continue;
        values[i] = mDatas.get(i).getValue();
    }
    // 這裏使用了冒泡算法
    float[] sortValues = bubbleSort(values);
    mMinValue = sortValues[0];
    mMaxValue = sortValues[sortValues.length - 1];
    float offsetValue = (mMaxValue - mMinValue) / mYAxisRows;
    mPerValueY = (offsetValue < 0.02f) ? (offsetValue + 0.02f) : offsetValue;
    mCurrentSelectedIndex = data.size() - 1;
    //Y軸刻度的最大值,比數據中的最大值還大一個單位
    mMaxValueYAxis = mMaxValue + mPerValueY;
    //Y軸刻度的最小值,比數據中的最大值還小一個單位
    mMinValueYAxis = mMinValue - mPerValueY;
    reset();
}
// 該方法主要是爲了動態設置數據時,清空一下圖表上的數據
private void reset() {
    mPointsPath.reset();
    mPointsShadowPath.reset();
    mPathDst.reset();
    mAnimAlphaPercent = 0;
    invalidate();
    mLineValueAnimator.start();
}

接下來就是繪製onDraw()了,哦,還有一個確定表格的寬高問題:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    //這裏可以重新設置View的寬高
    setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(widthMeasureSpec) * 2 / 3);
}

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    //這裏獲取的是表格真正的寬高
    mWidth = w;
    mHeight = h;
}

拿到寬高,和UI給定的一系列的padding值,差不多就可以確定表格原點的位置了。接着進入onDraw()方法:

@Override
protected void onDraw(Canvas canvas) {
    mChartHeight = mHeight - dp2px(34);//表格高度 = View的高度 - 底部文本區域的高度
    // 表格寬度 = View的寬度 - 右邊距14dp - 表格X軸起點座標
    mChartWidth = mWidth - dp2px(14) - mChartAxisX;
    //先畫表格基線
    drawChartLine(canvas);
    //再畫數據的刻度和座標點
    drawChartText(canvas);
    //最後再畫氣泡
    drawBubble(canvas);
}

繪製應該是最繁瑣的一步,不過不用着急,慢慢分析,一步一步來,無非就是一些UI上的計算。

表格基線繪製

// 繪製表格基線
private void drawChartLine(Canvas canvas) {
    int perHeight = mChartHeight / mYAxisRows;
    mLineEffectPaint.setPathEffect(null);
    mLineEffectPaint.setColor(mLineColor);
    mLineEffectPaint.setStrokeWidth(2);
    mLineEffectPaint.setColor(Color.parseColor("#e0e0e0"));
    //左邊豎線
    canvas.drawLine(mChartAxisX, 0, mChartAxisX, mChartHeight, mLineEffectPaint);
    //底部橫線
    canvas.drawLine(mChartAxisX, mChartHeight, mChartAxisX + mChartWidth, mChartHeight, mLineEffectPaint);
    //中間虛線
    PathEffect pathEffect = new DashPathEffect(new float[]{10, 4}, 1);
    mLineEffectPaint.setPathEffect(pathEffect);
    for (int i = 1; i < mYAxisRows + 1; i++) {
        canvas.drawLine(mChartAxisX, mChartHeight - perHeight * i, mChartAxisX + mChartWidth, mChartHeight - perHeight * i, mLineEffectPaint);
    }
}

其中有需要說明的是虛線部分的繪製,在Android3.0以後默認開啓了硬件加速,但是這個可能引起一些繪製上的問題,比如這個虛線,但是可以給當前自定義View關閉硬件加速就行了,代碼如下:

// 關閉硬件加速
@Override
public int getLayerType() {
    return LAYER_TYPE_SOFTWARE;
}

表格刻度實現

代碼有點多,因爲X軸方向的數據點包括了X軸刻度、數據點、陰影區域,Y軸上倒是隻有刻度的繪製:

// 繪製表格刻度
private void drawChartText(Canvas canvas) {
    if (mDatas == null || mDatas.size() == 0) {
        return;
    }
    //跟表格基線的Paint共用
    mLinePaint.setPathEffect(null);
    mLinePaint.setStrokeWidth(LINE_WIDTH_DEFAULT);
    mLinePaint.setColor(LINE_COLOR_DEFAULT);

    mChartPerWidth = mChartWidth / (mDatas.size() - 1);

    // 繪製X軸刻度 & 數據值點的位置
    // 因爲X軸刻度和數據點是一致的,所以在此放在同一個for循環中
    for (int i = 0; i < mDatas.size(); i++) {
        ChartBean bean = mDatas.get(i);
        if (i == 0) {
            canvas.drawText(bean.getDate(), mChartAxisX + mChartPerWidth * i, mChartHeight + dp2px(14), mChartScaleTxtPaint);
            mPointsPath.moveTo(mChartAxisX, mChartHeight - ((bean.getValue() - mMinValueYAxis) / (mMaxValueYAxis - mMinValueYAxis) * mChartHeight));
            mPointsShadowPath.moveTo(mChartAxisX, mChartHeight - ((bean.getValue() - mMinValueYAxis) / (mMaxValueYAxis - mMinValueYAxis) * mChartHeight));
        } else if (i == mDatas.size() - 1) {
            canvas.drawLine(mChartAxisX + mChartPerWidth * i, 0, mChartAxisX + mChartPerWidth * i, mChartHeight, mLineEffectPaint);
            canvas.drawText(bean.getDate(), mChartAxisX + mChartPerWidth * i - mBottomTxtWidth, mChartHeight + dp2px(14), mChartScaleTxtPaint);
            mPointsPath.lineTo(mChartAxisX + mChartPerWidth * i, mChartHeight - ((bean.getValue() - mMinValueYAxis) / (mMaxValueYAxis - mMinValueYAxis) * mChartHeight));
            mPointsShadowPath.lineTo(mChartAxisX + mChartPerWidth * i, mChartHeight - ((bean.getValue() - mMinValueYAxis) / (mMaxValueYAxis - mMinValueYAxis) * mChartHeight));
        } else {
            canvas.drawLine(mChartAxisX + mChartPerWidth * i, 0, mChartAxisX + mChartPerWidth * i, mChartHeight, mLineEffectPaint);
            canvas.drawText(bean.getDate(), mChartAxisX + mChartPerWidth * i - mBottomTxtWidth / 2, mChartHeight + dp2px(14), mChartScaleTxtPaint);
            mPointsPath.lineTo(mChartAxisX + mChartPerWidth * i, mChartHeight - ((bean.getValue() - mMinValueYAxis) / (mMaxValueYAxis - mMinValueYAxis) * mChartHeight));
            mPointsShadowPath.lineTo(mChartAxisX + mChartPerWidth * i, mChartHeight - ((bean.getValue() - mMinValueYAxis) / (mMaxValueYAxis - mMinValueYAxis) * mChartHeight));
        }
    }

    //繪製值線的動畫效果
    mPathMeasure.setPath(mPointsPath, false);
    mPathMeasure.getSegment(0, mAnimValuePercent * mPathMeasure.getLength(), mPathDst, true);
    canvas.drawPath(mPathDst, mLinePaint);

    //繪製陰影區域
    mPointsShadowPath.lineTo(mChartAxisX + mChartWidth, mChartHeight);
    mPointsShadowPath.lineTo(mChartAxisX, mChartHeight);
    mShadowPaint.setAlpha((int) (mAnimAlphaPercent * 0x19));//漸隱效果
    canvas.drawPath(mPointsShadowPath, mShadowPaint);

    int chartPerHeight = mChartHeight / mYAxisRows;
    // 繪製Y軸刻度
    for (int i = 0; i < mYAxisRows + 1; i++) {
        if (i == 0) {
            canvas.drawText(MathUtil.getFormat4DecimalNoSeperator().format((mMaxValueYAxis - mMinValueYAxis) / mYAxisRows * (mYAxisRows - i) + mMinValueYAxis), mViewPadding, mChartScaleSize, mChartScaleTxtPaint);
        } else if (i == mYAxisRows) {
            canvas.drawText(MathUtil.getFormat4DecimalNoSeperator().format((mMaxValueYAxis - mMinValueYAxis) / mYAxisRows * (mYAxisRows - i) + mMinValueYAxis), mViewPadding, chartPerHeight * i, mChartScaleTxtPaint);
        } else {
            canvas.drawText(MathUtil.getFormat4DecimalNoSeperator().format((mMaxValueYAxis - mMinValueYAxis) / mYAxisRows * (mYAxisRows - i) + mMinValueYAxis), mViewPadding, chartPerHeight * i + mChartScaleSize / 2, mChartScaleTxtPaint);
        }
    }
}

動畫的實現原理

通過ValueAnimator來計算出0->1之前的數值,設置duration,這樣在指定的時間內,可以拿到一個百分比的數值。代碼如下:

// 初始化動畫
private void initAnimator() {
    mLineValueAnimator = ValueAnimator.ofFloat(0, 1);
    mLineValueAnimator.setDuration(1000);
    mLineValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            mAnimValuePercent = (float) animation.getAnimatedValue();
            // 當線條繪製結束時,開始陰影面積的繪製。
            if (mAnimValuePercent == 1) {
                mAlphaValueAnimator.start();
            }
            invalidate();
        }
    });

    mAlphaValueAnimator = ValueAnimator.ofFloat(0, 1);
    mAlphaValueAnimator.setDuration(600);
    mAlphaValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            mAnimAlphaPercent = (float) animation.getAnimatedValue();
            invalidate();
        }
    });
}

onAnimationUpdate()方法中不停的去調用invalidate()實現重繪,然後在onDraw()的方法中使用PathMeasure類來實現部分區域的繪製,請看下面代碼,其實在繪製線條的時候,並不是使用mPointsPath(雖然mPointsPath是完整的路徑),而是先給setPath()mPathMeasure類中,然後通過PathMeasuregetSegment獲取到路徑的片段,並賦值給mPathDst,最後使用mPathDst在canvas上繪圖。

mPathMeasure.setPath(mPointsPath, false);
    mPathMeasure.getSegment(0, mAnimValuePercent * mPathMeasure.getLength(), mPathDst, true);
    canvas.drawPath(mPathDst, mLinePaint);

繪製完線條,再來繪製陰影區域。首先得讓陰影區域的Path完善最後的兩個點,然後調用如下代碼實現陰影的漸隱出現。

mShadowPaint.setAlpha((int) (mAnimAlphaPercent * 0x19));//漸隱效果
canvas.drawPath(mPointsShadowPath, mShadowPaint);

注:如果是靜態的線條,可以採用mPointsShadowPath.addPath(mPointsPath),現在做的是動態添加的,只能老老實實的給mPointsShadowPath 挨個兒賦值。

Y軸的刻度實現自行閱讀代碼,不再贅述。

氣泡繪製

這部分需要說的是,使用Matrix實現bitmap的旋轉控制,當然,Matrix的功能是很強大的,這裏只使用到了背景圖的旋轉,就是爲了讓氣泡的小箭頭指向所選擇的點。matrix.postScale(X, Y);可以實現在X軸和Y軸上的反轉。還使用到drawCircle來畫圓,這裏是先畫了個橙色的圓圈,再畫一個同心圓(半徑略小,實心)。

//繪製選中的點
private void drawBubble(Canvas canvas) {
    if (mDatas == null || mDatas.size() == 0) {
        return;
    }
    ChartBean bean = mDatas.get(mCurrentSelectedIndex);
    if (bean == null) return;
    float curPointX = mChartAxisX + mChartPerWidth * mCurrentSelectedIndex;
    float curPointY = mChartHeight - ((bean.getValue() - mMinValueYAxis) / (mMaxValueYAxis - mMinValueYAxis) * mChartHeight);
    mCurPointPaint.setColor(Color.parseColor("#ff6200"));
    mCurPointPaint.setStrokeWidth(dp2px(0.7f));
    mCurPointPaint.setStyle(Paint.Style.STROKE);
    canvas.drawCircle(curPointX, curPointY, dp2px(4.8f), mCurPointPaint);
    mCurPointPaint.setStyle(Paint.Style.FILL);
    mCurPointPaint.setColor(Color.WHITE);
    canvas.drawCircle(curPointX, curPointY, dp2px(4.1f), mCurPointPaint);

    Matrix matrix = new Matrix();
    int offsetX = 0;
    int offsetY = 0;
    int offsetTxtY = mBubbleBitmap.getHeight();
    int offsetTxtX = 0;
    if (mCurrentSelectedIndex <= mDatas.size() / 2 && curPointY < mChartHeight / 2) {
        // 左上
        matrix.postScale(1, -1);
        offsetTxtY = mBubbleBitmap.getHeight() * 3 / 4 + 4;
        offsetTxtX = dp2px(5);
    } else if (mCurrentSelectedIndex >= mDatas.size() / 2 && curPointY < mChartHeight / 2) {
        // 右上
        matrix.postRotate(180);
        offsetX = -mBubbleBitmap.getWidth();
        offsetTxtY = mBubbleBitmap.getHeight() * 3 / 4 + 4;
        offsetTxtX = -dp2px(6) - getTextWidth(mBubbleTxtPaint, "0,0000");
    } else if (mCurrentSelectedIndex <= mDatas.size() / 2 && curPointY > mChartHeight / 2) {
        // 左下
        offsetY = -mBubbleBitmap.getHeight();
        offsetTxtY = -mBubbleBitmap.getHeight() / 2;
        offsetTxtX = dp2px(5);
    } else if (mCurrentSelectedIndex >= mDatas.size() / 2 && curPointY > mChartHeight / 2) {
        // 右下
        offsetX = -mBubbleBitmap.getWidth();
        offsetY = -mBubbleBitmap.getHeight();
        matrix.postScale(-1, 1);
        offsetTxtY = -mBubbleBitmap.getHeight() / 2;
        offsetTxtX = -dp2px(6) - getTextWidth(mBubbleTxtPaint, "0,0000");
    } else {
        matrix.postScale(1, -1);
    }
    Bitmap dstBmp = Bitmap.createBitmap(mBubbleBitmap, 0, 0, mBubbleBitmap.getWidth(), mBubbleBitmap.getHeight(), matrix, true);
    canvas.drawBitmap(dstBmp, curPointX + offsetX, curPointY + offsetY, null);
    canvas.drawText(bean.getValueString(), curPointX + offsetTxtX, curPointY + offsetTxtY, mBubbleTxtPaint);
}

至此,繪製方面的工作差不多完工了。

手勢操作

@Override
public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
        case MotionEvent.ACTION_MOVE:
            checkInside(event);
            break;
    }
    return true;
}

private void checkInside(MotionEvent event) {
    int x = (int) event.getX();
    for (int i = 0; i < mDatas.size(); i++) {
        int resultMin = mChartAxisX + mChartPerWidth * i - mChartPerWidth / 2;
        int resultMax = mChartAxisX + mChartPerWidth * i + mChartPerWidth / 2;
        if (x >= resultMin && x < resultMax) {
            mCurrentSelectedIndex = i;
            invalidate();
            break;
        }
    }
}

onTouchEvent()中的MotionEvent.ACTION_DOWN & MotionEvent.ACTION_MOVE事件中 return true; 聲明View需要做一些操作。然後就是判斷手指滑動到了哪個區域裏,見checkInside()方法。

OK,大致就這麼多。怎麼樣?應該不難理解吧,只要把那些需要計算的搞定就夠了。
還是那句話,此篇博客僅僅是提供了一個思路,還有很多需要優化的地方,比如進入頁面時,內存會陡增,又瞬間減下去,還不知道該怎麼優化……等有空再考慮吧,也希望有大神能夠指點一二,謝謝。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章