概述:
这个控件难点在于绘图时候的一些坐标计算,大小计算。
自定义一个View来绘制折线图,外面套一层自定义的HorizontalScrollView来实现横向的滚动...
效果图:
代码讲解:
初始化部分代码,初始化一些参数,画笔对象,因为只是个demo所以把高度之类的参数都写死了,你们可以自己改改。
public Today24HourView(Context context) {
this(context, null);
}
public Today24HourView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public Today24HourView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mWidth = MARGIN_LEFT_ITEM + MARGIN_RIGHT_ITEM + ITEM_SIZE * ITEM_WIDTH;
mHeight = 500; //暂时先写死
tempBaseTop = (500 - bottomTextHeight)/4;
tempBaseBottom = (500 - bottomTextHeight)*2/3;
initHourItems();
initPaint();
}
private void initPaint() {
pointPaint = new Paint();
pointPaint.setColor(new Color().WHITE);
pointPaint.setAntiAlias(true);
pointPaint.setTextSize(8);
linePaint = new Paint();
linePaint.setColor(new Color().WHITE);
linePaint.setAntiAlias(true);
linePaint.setStyle(Paint.Style.STROKE);
linePaint.setStrokeWidth(5);
dashLinePaint = new Paint();
dashLinePaint.setColor(new Color().WHITE);
PathEffect effect = new DashPathEffect(new float[]{5, 5, 5, 5}, 1);
dashLinePaint.setPathEffect(effect);
dashLinePaint.setStrokeWidth(3);
dashLinePaint.setAntiAlias(true);
dashLinePaint.setStyle(Paint.Style.STROKE);
windyBoxPaint = new Paint();
windyBoxPaint.setTextSize(1);
windyBoxPaint.setColor(new Color().WHITE);
windyBoxPaint.setAlpha(windyBoxAlpha);
windyBoxPaint.setAntiAlias(true);
textPaint = new TextPaint();
textPaint.setTextSize(DisplayUtil.sp2px(getContext(), 12));
textPaint.setColor(new Color().WHITE);
textPaint.setAntiAlias(true);
bitmapPaint = new Paint();
bitmapPaint.setAntiAlias(true);
}
//简单初始化下,后续改为由外部传入
private void initHourItems(){
listItems = new ArrayList<>();
for(int i=0; i<ITEM_SIZE; i++){
String time;
if(i<10){
time = "0" + i + ":00";
} else {
time = i + ":00";
}
int left =MARGIN_LEFT_ITEM + i * ITEM_WIDTH;
int right = left + ITEM_WIDTH - 1;
int top = (int)(mHeight -bottomTextHeight +
(maxWindy - WINDY[i])*1.0/(maxWindy - minWindy)*windyBoxSubHight
- windyBoxMaxHeight);
int bottom = mHeight - bottomTextHeight;
Rect rect = new Rect(left, top, right, bottom);
Point point = calculateTempPoint(left, right, TEMP[i]);
HourItem hourItem = new HourItem();
hourItem.windyBoxRect = rect;
hourItem.time = time;
hourItem.windy = WINDY[i];
hourItem.temperature = TEMP[i];
hourItem.tempPoint = point;
hourItem.res = WEATHER_RES[i];
listItems.add(hourItem);
}
}
绘制部分的代码:
里面的循环是为了画出24个时刻的温度,风力和天气的图片。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for(int i=0; i<listItems.size(); i++){
Rect rect = listItems.get(i).windyBoxRect;
Point point = listItems.get(i).tempPoint;
//画风力的box和提示文字
onDrawBox(canvas, rect, i);
//画温度的点
onDrawTemp(canvas, i);
//画表示天气图片
if(listItems.get(i).res != -1 && i != currentItemIndex){
Drawable drawable = ContextCompat.getDrawable(getContext(), listItems.get(i).res);
drawable.setBounds(point.x - DisplayUtil.dip2px(getContext(), 10),
point.y - DisplayUtil.dip2px(getContext(), 25),
point.x + DisplayUtil.dip2px(getContext(), 10),
point.y - DisplayUtil.dip2px(getContext(), 5));
drawable.draw(canvas);
}
onDrawLine(canvas, i);
onDrawText(canvas, i);
}
//底部水平的白线
linePaint.setColor(new Color().WHITE);
canvas.drawLine(0, mHeight - bottomTextHeight, mWidth, mHeight - bottomTextHeight, linePaint);
}
onDrawBox代码片段:
1.通过drawRoundRect画下面的矩形,如果是选中的那个时刻,那么将透明度设置成255
2.画文字为了让文字在box上面并居中对齐,需要将画笔改为居中模式,然后算出一块矩形,表示在该矩形水平居中。其次baseLine是为了高度的居中。
3.里面的getScrollBarX()方法是计算偏移量,因为文字会随着滑动而移动,移动的水平位置就是由它决定。
//画底部风力的BOX
private void onDrawBox(Canvas canvas, Rect rect, int i) {
// 新建一个矩形
RectF boxRect = new RectF(rect);
HourItem item = listItems.get(i);
if(i == currentItemIndex) {
windyBoxPaint.setAlpha(255);
canvas.drawRoundRect(boxRect, 4, 4, windyBoxPaint);
//画出box上面的风力提示文字
Rect targetRect = new Rect(getScrollBarX(), rect.top - DisplayUtil.dip2px(getContext(), 20)
, getScrollBarX() + ITEM_WIDTH, rect.top - DisplayUtil.dip2px(getContext(), 0));
Paint.FontMetricsInt fontMetrics = textPaint.getFontMetricsInt();
int baseline = (targetRect.bottom + targetRect.top - fontMetrics.bottom - fontMetrics.top) / 2;
textPaint.setTextAlign(Paint.Align.CENTER);
canvas.drawText("风力" + item.windy + "级", targetRect.centerX(), baseline, textPaint);
} else {
windyBoxPaint.setAlpha(windyBoxAlpha);
canvas.drawRoundRect(boxRect, 4, 4, windyBoxPaint);
}
}
onDrawTemp代码片段:
主要负责画出随着滑动而移动的温度提示的滚动条
这里和上面的绘制类似,但是多了运动轨迹的计算(因为温度的滚动条的移动多了竖直方向的,而风力文字提示的移动只有水平的)。
private void onDrawTemp(Canvas canvas, int i) {
HourItem item = listItems.get(i);
Point point = item.tempPoint;
canvas.drawCircle(point.x, point.y, 10, pointPaint);
if(currentItemIndex == i) {
//计算提示文字的运动轨迹
int Y = getTempBarY();
//画出背景图片
Drawable drawable = ContextCompat.getDrawable(getContext(), R.mipmap.hour_24_float);
drawable.setBounds(getScrollBarX(),
Y - DisplayUtil.dip2px(getContext(), 24),
getScrollBarX() + ITEM_WIDTH,
Y - DisplayUtil.dip2px(getContext(), 4));
drawable.draw(canvas);
//画天气
int res = findCurrentRes(i);
if(res != -1) {
Drawable drawTemp = ContextCompat.getDrawable(getContext(), res);
drawTemp.setBounds(getScrollBarX()+ITEM_WIDTH/2 + (ITEM_WIDTH/2 - DisplayUtil.dip2px(getContext(), 18))/2,
Y - DisplayUtil.dip2px(getContext(), 23),
getScrollBarX()+ITEM_WIDTH - (ITEM_WIDTH/2 - DisplayUtil.dip2px(getContext(), 18))/2,
Y - DisplayUtil.dip2px(getContext(), 5));
drawTemp.draw(canvas);
}
//画出温度提示
int offset = ITEM_WIDTH/2;
if(res == -1)
offset = ITEM_WIDTH;
Rect targetRect = new Rect(getScrollBarX(), Y - DisplayUtil.dip2px(getContext(), 24)
, getScrollBarX() + offset, Y - DisplayUtil.dip2px(getContext(), 4));
Paint.FontMetricsInt fontMetrics = textPaint.getFontMetricsInt();
int baseline = (targetRect.bottom + targetRect.top - fontMetrics.bottom - fontMetrics.top) / 2;
textPaint.setTextAlign(Paint.Align.CENTER);
canvas.drawText(item.temperature + "°", targetRect.centerX(), baseline, textPaint);
}
}
折线如果是直线那么显得很生硬,为了平滑一些,做了贝塞尔曲线,根据奇偶性做方向不同的贝塞尔曲线。
//温度的折线,为了折线比较平滑,做了贝塞尔曲线
private void onDrawLine(Canvas canvas, int i) {
linePaint.setColor(new Color().YELLOW);
linePaint.setStrokeWidth(3);
Point point = listItems.get(i).tempPoint;
if(i != 0){
Point pointPre = listItems.get(i-1).tempPoint;
Path path = new Path();
path.moveTo(pointPre.x, pointPre.y);
if(i % 2 == 0)
path.cubicTo(pointPre.x, pointPre.y, (pointPre.x+point.x)/2, (pointPre.y+point.y)/2+14, point.x, point.y);
else
path.cubicTo(pointPre.x, pointPre.y, (pointPre.x+point.x)/2, (pointPre.y+point.y)/2-14, point.x, point.y);
canvas.drawPath(path, linePaint);
}
}
onDrawText代码片段:
//绘制底部时间
private void onDrawText(Canvas canvas, int i) {
//此处的计算是为了文字能够居中
Rect rect = listItems.get(i).windyBoxRect;
Rect targetRect = new Rect(rect.left, rect.bottom, rect.right, rect.bottom + bottomTextHeight);
Paint.FontMetricsInt fontMetrics = textPaint.getFontMetricsInt();
int baseline = (targetRect.bottom + targetRect.top - fontMetrics.bottom - fontMetrics.top) / 2;
textPaint.setTextAlign(Paint.Align.CENTER);
String text = listItems.get(i).time;
canvas.drawText(text, targetRect.centerX(), baseline, textPaint);
}
计算部分的代码:
该方法由外部的HorizontalScrollView调用。两个参数分别是
int offset = computeHorizontalScrollOffset();
int maxOffset = computeHorizontalScrollRange() - DisplayUtil.getScreenWidth(getContext());
int maxOffset = computeHorizontalScrollRange() - DisplayUtil.getScreenWidth(getContext());
这里有一问:为什么需要减去屏幕的宽度?
答: 比如HorizontalScrollView的滚动条移动范围在0-----1000像素之间的话,computeHorizontalScrollRange()计算出的值就会是1000+屏幕宽度
//设置scrollerView的滚动条的位置,通过位置计算当前的时段
public void setScrollOffset(int offset, int maxScrollOffset){
this.maxScrollOffset = maxScrollOffset;
scrollOffset = offset;
int index = calculateItemIndex(offset);
currentItemIndex = index;
invalidate();
}
然后需要计算滑动到某位置时,当前的时刻是几。
先说说getScrollBarX()方法|:(结合下面的图片看)
已知条件是HorizontalScrollView的滚动条位置和滚动条最大滚动距离,我们需要计算的是温度提示滚动条(矩形)的left的横坐标。
所以得到温度滚动条的最大移动距离,就能计算出当前温度滚动条的位置left。
最后x = 当前的left+左侧的margin。
计算当前的时刻采取不断累加ITEM_WIDTH,一旦sum大于x,则i就是当前的item的下标
//通过滚动条偏移量计算当前选择的时刻
private int calculateItemIndex(int offset){
// Log.d(TAG, "maxScrollOffset = " + maxScrollOffset + " scrollOffset = " + scrollOffset);
int x = getScrollBarX();
int sum = MARGIN_LEFT_ITEM - ITEM_WIDTH/2;
for(int i=0; i<ITEM_SIZE; i++){
sum += ITEM_WIDTH;
if(x < sum)
return i;
}
return ITEM_SIZE - 1;
}
private int getScrollBarX(){
int x = (ITEM_SIZE - 1) * ITEM_WIDTH * scrollOffset / maxScrollOffset;
x = x + MARGIN_LEFT_ITEM;
return x;
}
计算运动轨迹代码(实质是计算Y轴的变化):
通过x的变化得到Y的变化。
先要计算当前的x处于哪两个时刻之间,因为y的变化范围必须在这两个时刻的温度的点的Y之间。
得到这两个点之后通过等比关系获得Y
看下图 ,红色字是已知的。
//计算温度提示文字的运动轨迹
private int getTempBarY(){
int x = getScrollBarX();
int sum = MARGIN_LEFT_ITEM ;
Point startPoint = null, endPoint;
int i;
for(i=0; i<ITEM_SIZE; i++){
sum += ITEM_WIDTH;
if(x < sum) {
startPoint = listItems.get(i).tempPoint;
break;
}
}
if(i+1 >= ITEM_SIZE || startPoint == null)
return listItems.get(ITEM_SIZE-1).tempPoint.y;
endPoint = listItems.get(i+1).tempPoint;
Rect rect = listItems.get(i).windyBoxRect;
int y = (int)(startPoint.y + (x - rect.left)*1.0/ITEM_WIDTH * (endPoint.y - startPoint.y));
return y;
}
项目源码地址:https://github.com/zx391324751/MoJiDemo
补充说明:代码里面设置颜色的部分代码需要改改(new Color().WHITE),改为Color.WHITE,没有必要创建一个对象,希望没有误导大家。。。