当前位置 博文首页 > 高远的博客:金融类自定义View(三)--股票分时图(关于细节和实
在上一节,我们已经绘制出了长按的十字,也可以左右滑动。现在的需求是仿照【天厚实盘】长按在View上面绘制出按下点(十字中央)对应点的报价详情以及涨跌幅情况。这个地方的处理,有两种:直接在View上绘制;在布局中设置布局,然后把数据回调给使用者,再设置上对应数据。很明显,第二种方式更简单,也更灵活,这里采用第二种。特别注意需要回调除了当前点,还有上一个点,便于计算涨跌幅。套路:定义接口,设置回调,设置布局。
//长按的绘制逻辑以及回调
protected void drawLongPress(Canvas canvas) {
if (!mDrawLongPressPaint) return;
//长按的逻辑
....
//在这里回调数据信息
if (mTimeSharingListener != null) {
int size = mQuotesList.size();
if ((0 <= finalIndex && finalIndex < size) &&
(0 <= finalIndex - 1 && finalIndex - 1 < size))
//回调,需要两个数据,便于计算涨跌百分比
mTimeSharingListener.onLongTouch(mQuotesList.get(finalIndex - 1),
mQuotesList.get(finalIndex));
}
}
对于缩放问题的处理,真是操碎了心,思考了好久好久。所谓缩放,我们要知道两手指缩放的距离占View有效宽度的百分比,然后根据百分比计算新的有效可视个数,然后重绘。真的有这么简单吗?我们一直绘制的依据是确定起始位置和结束位置。当两个手指缩小视图时,真正的中心点在两指中间,因此起始位置要变,结束位置也要变。最后采用的方案是采用系统的ScaleGestureDetector
监听手指,根据detector.getScaleFactor()
确定缩放因子。缩放思路:所谓缩放,也是计算新的起始位置和结束位置。这里根据缩放因子detector.getScaleFactor()计算新的可见个数(x缩放因子即可)。当放大时,可见的数据集合的个数(A)应该减少。detector.getScaleFactor()(B的范围[1,2)),这个时候可以新的可见数据集合(C)可以考虑采用C=A-A*(B-1);当然这样计算是否准确,还需要商榷。思路简单,但是这里细节比较多,具体可以参考代码。
//缩放手势监听
ScaleGestureDetector.OnScaleGestureListener mOnScaleGestureListener =
new ScaleGestureDetector.SimpleOnScaleGestureListener() {
@Override
public boolean onScale(ScaleGestureDetector detector) {
//没有缩放
if (detector.getScaleFactor() == 1) return true;
//是放大还是缩小
boolean isBigger = detector.getScaleFactor() > 1;
//变化的个数(缩小或者放大),必须向上取整,不然当mShownMaxCount过小时容易取到0。
int changeNum = (int) Math.ceil(mShownMaxCount * Math.abs(detector.getScaleFactor() - 1));
//容错处理,省略
...
//计算新的开始位置。这个地方比较难以理解:拉伸了起始点变大,并且是拉伸数量的一半,结束点变小,也是原来的一半。
// 收缩,相反。可以自己画一个图看看
mBeginIndex = isBigger ? mBeginIndex + helfChangeNum : mBeginIndex - helfChangeNum;
if (mBeginIndex < 0) {
mBeginIndex = 0;
} else if ((mBeginIndex + mShownMaxCount) > mQuotesList.size()) {
mBeginIndex = mQuotesList.size() - mShownMaxCount;
}
mEndIndex = mBeginIndex + mShownMaxCount;
//只要找好起始点和结束点就可以交给处理重绘的方法就好啦~
seekAndCalculateCellData();
return true;
}
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
Log.e(TAG, "onScaleBegin: " + detector.getFocusX());
//指头数量,过滤无用手势
if (mFingerPressedCount != 2) return true;
return true;
}
};
本身,加载更多是很简单的,只要判断移动的时候到最右端就去加载就好。可是,这里牵扯出来另外几个问题
这里的处理是定义滑动枚举类型,确认实时状态
enum PullType {
PULL_RIGHT,//向右滑动
PULL_LEFT,//向左滑动
PULL_RIGHT_STOP,//滑动到最右边
PULL_LEFT_STOP,//滑动到最左边
}
实时监听并记录状态,并且在触发加载更多时(最好设置一定阀值,比如剩余10个数据时就触发加载更多)
/**
* 移动K线图计算移动的单位和重新计算起始位置和结束位置
*
* @param moveLen
*/
protected void moveKView(float moveLen) {
//移动之前将右侧的内间距值为0
mInnerRightBlankPadding = 0;
mPullRight = moveLen > 0;
int moveCount = (int) Math.ceil(Math.abs(moveLen) / mPerX);
if (mPullRight) {
int len = mBeginIndex - moveCount;
//阀值
if (len < DEF_MINLEN_LOADMORE) {
//加载更多
if (mTimeSharingListener != null && mCanLoadMore) {
loadMoreIng();
mTimeSharingListener.needLoadMore();
}
}
//向右拉逻辑
...
} else {
//向左拉逻辑
...
}
//确认结束位置
mEndIndex = mBeginIndex + mShownMaxCount;
//开始位置和结束位置确认好,就可以重绘啦~
//Log.e(TAG, "moveKView: mPullRight:" + mPullRight + ",mBeginIndex:" + mBeginIndex + ",mEndIndex:" + mEndIndex);
seekAndCalculateCellData();
}
核心方法,核心方法全部都在onDraw(Canvas canvas)
中完成,但是为了逻辑清晰,我们会单独绘制每一个业务功能,保证业务逻辑的清晰,方便修改和扩展。其它的,手势监控在onTouchEvent(MotionEvent event)
和ScaleGestureDetector mScaleGestureDetector
中完成。加载数据直接由使用者传递。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//Log.e("TimeSharingView", "onDraw: ");
//默认加载loading界面
showLoadingPaint(canvas);
if (mQuotesList == null || mQuotesList.isEmpty()) {
return;
}
drawOuterLine(canvas);
drawInnerXy(canvas);
drawXyTxt(canvas);
drawBrokenLine(canvas);
//长按处理
drawLongPress(canvas);
//长按情况下的时间和数据框
drawLongPressTxt(canvas);
}
关于绘图,绘图全部相对于View的左上角开始,在View全局中定义了宽度和高度以及内边距等,基本上在View中绘制任何线和图全部会相对这几个值进行操作,更多的是对位置的准确把握以及对边界的准确控制。
//控件宽高,会在onMeasure中进行测量。
int mWidth;
int mHeight;
//上下左右padding,这里不再采用系统属性padding,因为用户容易忘记设置padding,直接在这里更改即可。
float mPaddingTop = 20;
float mPaddingBottom = 50;
float mPaddingLeft = 8;
float mPaddingRight = 90;
//默认情况下结束点距离右边边距
float mInnerRightBlankPadding = DEF_INNER_RIGHT_BLANK_PADDING;
//为了美观,容器内(边框内部的折线图距离外边框线的上下距离)上面有一定间距,下面也有一定的间距。
float mInnerTopBlankPadding = 60;
float mInnerBottomBlankPadding = 60;
关于y轴最大最小值的确认,这个比较重要。如何保证分时图准确绘制不绘制出边界?如果数据的范围本来是[1,100],突然来了一个10000的数据怎么办?处理的手段是,每次拿到数据之后遍历,找到分时图可视范围内的最大值和最小值和View有效高度,算出来单位高度,然后根据每个点的值和最小值(最大值也可以)的差值计算应有的高度坐标即可。如果出现上述中的特别特别大的数据怎么办?那可能就绘制出一条直线咯(生产环境中有遇到)。
关于错误和调试,有阅读代码的同学可以在代码中看到大量的Log日志。写的过程中遇到了很多问题,由于有大量的数据还实时推数据并且实时刷新绘制,这个时候可能debug就很难发现问题,直接打Log是有效的手段,可以实时观察到数据的异常。当然,大部分问题直接卡断点就能定位到问题。
https://github.com/scsfwgy/FinancialCustomerView