背景
金融軟件現(xiàn)在的k線圖功能強大,支持各種各樣的指標(biāo)。但是指數(shù)的計算和繪制其實是有點復(fù)雜的。我不炒股也不炒幣,所以對指標(biāo)背后蘊含的市場含義不懂,也不太感興趣。前不久做了一個相關(guān)的需求,今天總結(jié)一下。
MPAndroidChart
https://github.com/PhilJay/MPAndroidChart 這是github地址。筆者使用的版本為v2.2.5,做了很多定制。 MPAndroidChart功能很強大,使用者一些特殊的需求也可以自己定制實現(xiàn),因為該庫的設(shè)計很靈活,性能也不錯。其有付費版本,但我覺得基于開源版本就可以在性能和功能上滿足開發(fā)者的需求。下面我想先捋一下它的結(jié)構(gòu)設(shè)計,然后結(jié)合實際例子講一下它的使用。
MPAndroidChart的設(shè)計
根據(jù)上圖簡明扼要敘述一下各包的作用:
-
animation -- 動畫
image buffer 數(shù)據(jù)類,用來提高繪制效率,可以看成是緩存。 舉個例子,我們要繪制柱狀圖,其中每個數(shù)據(jù)從點轉(zhuǎn)換成柱狀(四個點,矩形柱狀每個角對應(yīng)一個點),
BarBuffer類是怎么做的呢?根據(jù)Entry數(shù)據(jù)的value(點)轉(zhuǎn)成矩形圖像。
... start fori
float left = x - barWidth + barSpaceHalf;
float right = x + barWidth - barSpaceHalf;
float bottom, top;
if (mInverted) {
bottom = y >= 0 ? y : 0;
top = y <= 0 ? y : 0;
} else {
top = y >= 0 ? y : 0;
bottom = y <= 0 ? y : 0;
}
// multiply the height of the rect with the phase
if (top > 0)
top *= phaseY;
else
bottom *= phaseY;
addBar(left, top, right, bottom);
... end fori
protected void addBar(float left, float top, float right, float bottom) {
if (index >= buffer.length - 1) {
return;
}
buffer[index++] = left;
buffer[index++] = top;
buffer[index++] = right;
buffer[index++] = bottom;
}
- chart -- 包里面包含各種圖表類
- components -- 圖表的其它組件,例如描述/軸/限制線/legend 等等
- data -- 原始數(shù)據(jù)類,與
buffer有所不同,這個包里根據(jù)不同圖表封裝了不同的數(shù)據(jù)類型。 - formatter -- 要繪制的文字的格式(例如x軸的刻度值的格式)
- highlight -- 高亮線(選中圖表上某個點時出現(xiàn)的高亮狀態(tài))
- interfaces -- 項目中全局的接口定義(主要是數(shù)據(jù)相關(guān)的的接口定義)
- jobs -- chart的滑動縮放處理工作
- listener -- 各種監(jiān)聽器
- render -- 渲染器, 所有的繪制工作(各種圖表的繪制,軸的繪制,背景分割線,legend/highlight等等)
- util -- 工具類,最重要的有
ViewPortHandler,Transformer
uml類圖結(jié)構(gòu)
項目支持的圖表很多,uml全部呈現(xiàn)顯得比較繁雜,我們就以LineChart(線圖)為例。盡量用最少的信息來理解該庫的設(shè)計,所以我們只包含線圖,不包含x軸/y軸/legend/markview等。
[圖片上傳失敗...(image-eb4cc9-1617104235932)]
chart
chart包里面都是圖表相關(guān)的類,這里以LineChart為例,剖析它的繼承層次,以及每一個父類的職責(zé)。
首先是Chart.class這個最基礎(chǔ)的基類。
public abstract class Chart<T extends ChartData<? extends IDataSet<? extends Entry>>> extends
ViewGroup
implements ChartInterface {
...
}
Chart繼承自ViewGroup,其重寫了onMeasure以及onLayout方法。還定義了一些抽象的模版方法。Chart的主要職責(zé)就是進行measure和layout,以及一些公共行為定義,其它相關(guān)工作交給了對應(yīng)的對象。例如繪制交給了#mRender,手勢監(jiān)聽交給了#mChartTouchListener,縮放/移動處理交給了#mViewPortHandler。
BarLineChartBase繼承自Chart,從類名就可看出它抽象了線圖和柱狀圖的一些公共行為。
LineChart繼承自BarLineChartBase,初始化#mRender
render
渲染器,繪制職責(zé)。LineChartRenderer實現(xiàn)的drawData方法如下:
@Override
public synchronized void drawData(Canvas c) {
int width = (int) mViewPortHandler.getChartWidth();
int height = (int) mViewPortHandler.getChartHeight();
if (mDrawBitmap == null
|| (mDrawBitmap.get().getWidth() != width)
|| (mDrawBitmap.get().getHeight() != height)) {
if (width > 0 && height > 0) {
mDrawBitmap = new WeakReference<>(Bitmap.createBitmap(width, height, mBitmapConfig));
if (mDrawBitmap.get() == null) {
return;
}
mBitmapCanvas = new Canvas(mDrawBitmap.get());
} else
return;
}
mDrawBitmap.get().eraseColor(Color.TRANSPARENT);
LineData lineData = mChart.getLineData();
for (ILineDataSet set : lineData.getDataSets()) {
if (set.isVisible() && set.getEntryCount() > 0)
drawDataSet(c, set);
}
c.drawBitmap(mDrawBitmap.get(), 0, 0, mRenderPaint);
}
drawDataSet方法就不深究了。
Transformer
把數(shù)據(jù)值映射成屏幕上的像素點,用matrix實現(xiàn), path的映射主要用到下面?zhèn)z個方法。
public void prepareMatrixValuePx(float xChartMin, float deltaX, float deltaY, float yChartMin) {
float scaleX = (float) (mViewPortHandler.contentWidth() / deltaX);
float scaleY = (float) (mViewPortHandler.contentHeight() / deltaY);
if (Float.isInfinite(scaleX)) {
scaleX = 0;
}
if (Float.isInfinite(scaleY)) {
scaleY = 0;
}
// setup all matrices
mMatrixValueToPx.reset();
mMatrixValueToPx.postTranslate(-xChartMin, -yChartMin);
mMatrixValueToPx.postScale(scaleX, -scaleY);
}
public void pathValueToPixel(Path path) {
path.transform(mMatrixValueToPx);
path.transform(mViewPortHandler.getMatrixTouch());
path.transform(mMatrixOffset);
}
手勢處理
上圖是手勢move操作的時序圖,然后修改matrix, ViewPortHandler根據(jù)matrix更新視圖,Chart再重新繪制;
下面是修改matrix的代碼:
private void performDrag(MotionEvent event) {
mLastGesture = ChartGesture.DRAG;
mMatrix.set(mSavedMatrix);
OnChartGestureListener l = mChart.getOnChartGestureListener();
float dX, dY;
// check if axis is inverted
if (mChart.isAnyAxisInverted() && mClosestDataSetToTouch != null
&& mChart.getAxis(mClosestDataSetToTouch.getAxisDependency()).isInverted()) {
// if there is an inverted horizontalbarchart
if (mChart instanceof HorizontalBarChart) {
dX = -(event.getX() - mTouchStartPoint.x);
dY = event.getY() - mTouchStartPoint.y;
} else {
dX = event.getX() - mTouchStartPoint.x;
dY = -(event.getY() - mTouchStartPoint.y);
}
} else {
dX = event.getX() - mTouchStartPoint.x;
dY = event.getY() - mTouchStartPoint.y;
}
mMatrix.postTranslate(dX, dY);
if (l != null)
l.onChartTranslate(event, dX, dY);
}
動畫
動畫的實現(xiàn)是使用屬性動畫進行實現(xiàn)的,以線圖為例,屬性動畫的值從0到1,每個繪制周期根據(jù)屬性動畫的值計算要繪制的線圖范圍,這樣就實現(xiàn)了動畫效果。以包分析時的gif圖片所展示的動畫為例,時序圖如下:
MPAndroidChart的優(yōu)化
這個庫性能優(yōu)化方便后續(xù)再補。
上面從這個庫的各個方面分析了它的設(shè)計,實現(xiàn)原理。下一篇會介紹基于該庫實現(xiàn)不同的指標(biāo),有的指標(biāo)只是純粹的線圖,只做數(shù)值計算即可,有的指標(biāo)的展現(xiàn)形式有點特殊,就需要對這個庫進行定制化。