先看墨迹天气效果图

5506445253_63249103744_1656936370197_694x449.gif

因为需求原因,改了一些样式

5506445253_63339371021_1657021325668_687x425.gif

平滑的线

话不多说,直接开始,首先是画出这条曲线,找到每个小时温度对应点位,连成一条线,左边显示最高温度和最低温度,最高温度对应曲线中的最高点,最低温度对应曲线中的最低点,直接上代码

1
2
3
4
5
6
7
private Point calculateTempPoint(int left, int right, int temp) {
double minHeight = tempBaseTop;
double maxHeight = tempBaseBottom;
double tempY = maxHeight - (temp - minTemp) * 1.0 / (maxTemp - minTemp) * (maxHeight - minHeight);
Point point = new Point((left + right) / 2, (int) tempY);
return point;
}

传入点的左边距,右边距和这个时间段对应的温度,返回一个Point类,Point类中有x,y两个属性,构造方法如下

1
2
3
4
public Point(int x, int y) {
this.x = x;
this.y = y;
}

tempBaseToptempBaseBottom是在初始化中按高度比例来分

1
2
tempBaseTop = (mHeight - bottomTextHeight) / 3;
tempBaseBottom = (mHeight - bottomTextHeight) * 3 / 4;

bottomTextHeight是下方数字文本高度
获取每个温度对应的坐标轴之前,先拿到24小时天气数据中的最高温和最低温,这样就能确定每个点位的距离y轴的坐标。拿到24个坐标点后,直接来第一步,画出曲线,直接上代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Path path = new Path();
Point point0 = listItems.get(0).getTempPoint();
path.moveTo(point0.x, point0.y);

for (int i = 0; i < listItems.size(); i++) {
Point point = listItems.get(i).getTempPoint();
if (i != 0) {
Point pointPre = listItems.get(i - 1).getTempPoint();
if (i == 1) {
path.lineTo(point.x, point.y);
} else {
path.rLineTo(point.x - pointPre.x, point.y - pointPre.y);
}
}
}
canvas.drawPath(path, linePaint);

直接new一个Path,使用moveTo方法移动到第一个点位,然后直接for循环,第一个点不画,从1开始,直接lineTo到指定的x,y坐标,这里第一条线使用了lineTo,后面都是用的rLineTo,也就是从上一条线的最终点开始画,就不需要频繁的moveTo了。本来打算使用三阶贝塞尔曲线画的,画出来之后发现每条线的连接处有很明显的折角,不平滑,可能是我用的方法不对,有明白的可以跟我说说,三阶贝塞尔画出来的就像下面这样

5506445253_63262601417_IMG_20220705_085830.jpg
再看下使用画直线的方式画出来的效果

5506445253_63264419320_IMG_20220705_091358.jpg
看的出来,这种短而坡度小的线,效果不是很大,就是更直了一点,然后我们再给Paint设置PathEffect试试效果,代码如下

1
2
PathEffect pathEffect = new CornerPathEffect(180);
linePaint.setPathEffect(pathEffect);

CornerPathEffect的作用是通过将线段之间的任何锐角替换为指定半径的圆角来转换绘制的几何图形(STROKEFILL 样式)。参数:半径 - 线段之间的圆角

5506445253_63268350797_IMG_20220705_094126.jpg
是不是平滑了很多。

天气Icon背景框

曲线结束之后先画出每个天气icon所在的矩形框,每个矩形框之间都有一定的缝隙,直接上代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
Point point0 = listItems.get(0).tempPoint;
Path pathBG = new Path();
pathBG.moveTo(point0.x, point0.y);
for (int i = 0; i < listItems.size(); i++) {
Point point = listItems.get(i).getTempPoint();
if (i != 0) {
Point pointPre = listItems.get(i - 1).getTempPoint();
if (i == 1) {
pathBG.lineTo(point.x, point.y);
} else {
if (listItems.get(i).getIcon() != -1)
pathBG.rLineTo(point.x - pointPre.x - DisplayUtil.dip2px(getContext(), 1), point.y - pointPre.y);
else
pathBG.rLineTo(point.x - pointPre.x, point.y - pointPre.y);
}

Point pointBackup = listItems.get(0).getTempPoint();
if (listItems.get(i).getIcon() != -1 || (getGoneBehind(i) && i == listItems.size() - 1)) {
for (int j = 0; j < i; j++) {
if (listItems.get(j).getIcon() != -1) {
pointBackup = listItems.get(j).getTempPoint();
}
}
if (listItems.get(i).getTempPoint() != pointBackup) {
int height = mHeight - bottomTextHeight - DisplayUtil.dip2px(getContext(), 4) - point.y;
pathBG.rLineTo(0, height);
pathBG.rLineTo(pointBackup.x - point.x + DisplayUtil.dip2px(getContext(), 1), 0);
canvas.drawPath(pathBG, rectPaint);
pathBG.reset();
//移到新的点开始画
pathBG.moveTo(point.x, point.y);
}
}
}
}
1
2
3
4
5
6
7
private boolean getGoneBehind(int index) {
List<Boolean> data = new ArrayList<>();
for (int k = index; k < listItems.size(); k++) {
data.add(listItems.get(k).res == -1);
}
return !data.contains(false);
}

这里前面和画曲线一样,原理就是先画出一个天气icon所在的矩形框上方的曲线,然后从曲线的末端向下画一条直线,在向左画至矩形框的左边界,然后封闭起来。可以看到这里面有很多getIcon() != -1的判断,这些是在塞数据的时候判断当前时间段的icon是否和上一个一样,一样的话就把icon替换成-1,代码如下

1
2
3
4
5
6
7
8
int icon = list.get(0).getIcon();
for (int i = 0; i < list.size(); i++) {
if (i != 0 && icon == list.get(i).getIcon()) {
list.get(i).setIcon(-1);
} else {
icon = list.get(i).getIcon();
}
}

由于每个矩形中间需要间隙,就在顶部和底部的线时减去了1dp,代码中可以看到除了画曲线外只画了两条线,但是却实现了填充矩形的效果,这个是因为我给rectPaint设置了Style时传入的属性是Paint.Style.FILL,设置FILL属性后,三角形只需要画出两条线就可自动封闭,矩形画出三条线,如果不方便使用FILL属性的话,可以使用Path提供的方法,path.close();,效果等同于FILL,封闭矩形后使用path.reset();清除路径中的所有线条和曲线,然后移动到下一个开始画曲线的点,循环下去,看一下效果

5506445253_63317592872_IMG_20220705_162537.jpg

天气Icon

矩形结束之后就是天气icon了。在看一下墨迹天气的效果图
5506445253_63249103744_1656936370197_694x449.gif

从图中可以看到,天气的icon是在每个矩形的正中间,随着手指的滑动,最左边的icon所在的矩形如果被view的左边界盖住,矩形中的icon保持在view的左边界和矩形的右边界中间,最右边同理。代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
for (int i = 0; i < listItems.size(); i++) {
Point point = listItems.get(i).getTempPoint();
if (i != 0) {
Point pointBackup = listItems.get(0).getTempPoint();
if (listItems.get(i).getIcon() != -1 || (getGoneBehind(i) && i == listItems.size() - 1)) {
int icon = -1;
int indexBackUp = 0;
for (int j = 0; j < i; j++) {
if (listItems.get(j).getIcon() != -1) {
icon = listItems.get(j).getIcon();
indexBackUp = j;
pointBackup = listItems.get(j).getTempPoint();
}
}
if (listItems.get(i).getTempPoint() != pointBackup) {
int left = (point.x - pointBackup.x) / 2 + pointBackup.x - DisplayUtil.dip2px(getContext(), 10);
int right = (point.x - pointBackup.x) / 2 + pointBackup.x + DisplayUtil.dip2px(getContext(), 10);
int newLeft = (point.x - (pointBackup.x - getItemLeftMargin(indexBackUp))) / 2 + (pointBackup.x - getItemLeftMargin(indexBackUp));
int newRight = ((point.x + getItemRightMargin(i)) - pointBackup.x) / 2 + pointBackup.x;
if (getItemLeftMargin(indexBackUp) < 0 && newLeft + DisplayUtil.dip2px(getContext(), 20) < point.x && i - indexBackUp > 1) {
left = newLeft - DisplayUtil.dip2px(getContext(), 10);
right = left + DisplayUtil.dip2px(getContext(), 20);
} else if (getItemLeftMargin(indexBackUp) < 0 && newLeft + DisplayUtil.dip2px(getContext(), 40) >= point.x && i - indexBackUp > 1) {
left = point.x - DisplayUtil.dip2px(getContext(), 30);
right = left + DisplayUtil.dip2px(getContext(), 20);
}
if (getItemRightMargin(i) < 0 && newRight > pointBackup.x + DisplayUtil.dip2px(getContext(), 10) && i - indexBackUp > 1) {
right = newRight + DisplayUtil.dip2px(getContext(), 10);
left = right - DisplayUtil.dip2px(getContext(), 20);
}
if (getItemLeftMargin(indexBackUp) < 0 && getItemRightMargin(i) < 0) {
left = pointBackup.x - getItemLeftMargin(indexBackUp) + scrollWidth / 2 - DisplayUtil.dip2px(getContext(), 10);
right = left + DisplayUtil.dip2px(getContext(), 20);
}
Drawable drawable = ContextCompat.getDrawable(getContext(), icon);
drawable.setBounds(left,
tempBaseBottom + DisplayUtil.dip2px(getContext(), 5),
right,
tempBaseBottom + DisplayUtil.dip2px(getContext(), 25));
drawable.draw(canvas);
}
}
}
}

首先就是拿到icon所在矩形上方曲线的起始点,记住起始点的下标,xyiconicon所在矩形上方曲线结束点的x轴坐标减去icon所在矩形上方曲线起始点的x轴坐标就可以拿到这个矩形的宽,宽除2在加上icon所在矩形上方曲线结束点的x轴坐标就是矩形中间点距离左边的边距,默认icon的left和right是在中间点的基础上减去10dp和加上10dp。
然后就是判断当前icon所在的矩形左边界或者右边界是否超出view的边界,因为newLeft是随着手指滑动不断减小或者增加的,所以需要满足当前icon所在矩形的左边界超出view的边界,且左边界不能超过右边界,右边同理,需要注意的是,当前icon的矩形如果只占了一格,就不需要改变边界,一直保持在矩形的中间就好。除了这两种情况外还有一种情况,就是矩形两边都超出view的边界,这时就是让天气icon保持在view的正中间就好了。
那么如何拿到当前icon所在矩形上方曲线起始点距离view左边界的距离呢,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 点距离左边的位置
*
* @param i
* @return
*/
private int getItemLeftMargin(int i) {
int left = MARGIN_LEFT_ITEM + i * ITEM_WIDTH + ITEM_WIDTH / 2;
return left - scrollOffset;
}

/**
* 点距离右边的位置
*
* @param i
* @return
*/
private int getItemRightMargin(int i) {
int left = MARGIN_LEFT_ITEM + i * ITEM_WIDTH + ITEM_WIDTH / 2;
return scrollWidth - (left - scrollOffset);
}

MARGIN_LEFT_ITEM:左边预留宽度
ITEM_WIDTH:每个Item的宽度
scrollOffset:滚动偏移量
scrollWidthHorizontalScrollView的宽度
左边界距离屏幕左边的编辑就是当前icon所在矩形上方曲线起始点的x轴减去滑动的偏移量,右边界距离右边就是HorizontalScrollView的宽度减去距离屏幕左边界的值
24小时天气自定义view是被另一个自定义View中包含的,它继承于HorizontalScrollView,需要修改的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int offset = computeHorizontalScrollOffset();
int maxOffset = computeHorizontalScrollRange() - DisplayUtil.getScreenWidth(getContext());
if (today24HourView != null) {
today24HourView.setScrollOffset(offset, maxOffset, getWidth());
}
}

public void setToday24HourView(Today24HourView today24HourView) {
this.today24HourView = today24HourView;
invalidate();
}

computeHorizontalScrollOffset()计算水平滚动条拇指在水平范围内的水平偏移量
computeHorizontalScrollRange()滚动视图的滚动范围是其所有子视图的总宽度

1
2
3
4
5
6
7
8
//设置scrollerView的滚动条的位置,通过位置计算当前的时段
public void setScrollOffset(int offset, int maxScrollOffset, int scrollWidth) {
this.maxScrollOffset = maxScrollOffset;
this.scrollWidth = scrollWidth;
scrollOffset = offset;
currentItemIndex = calculateItemIndex(offset);
invalidate();
}

maxScrollOffset:最大滚动距离
scrollWidthHorizontalScrollView的宽度
scrollOffset:滚动偏移量
xml中代码如下

1
2
3
4
5
6
7
8
9
<com.weather.gorgeous.custom_view.IndexHorizontalScrollView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:fadeScrollbars="false"
android:scrollbars="none">
<com.weather.gorgeous.custom_view.Today24HourView
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.weather.gorgeous.custom_view.IndexHorizontalScrollView>

5506445253_63324907904_1657012359644_687x218.gif

天气指针

接下来就是当前天气指针了,就是根据手指滑动,向左或向右偏移,并且滑动到不同的item,改变指针中的内容,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
WeatherHoursModel item = listItems.get(i);
if (currentItemIndex == i) {
int Y = getTempBarY();
Rect targetRect = new Rect(
getScrollBarX(),
Y - DisplayUtil.dip2px(getContext(), 40),
getScrollBarX() + DisplayUtil.dip2px(getContext(), 92),
Y - DisplayUtil.dip2px(getContext(), 14));
Drawable drawable = ContextCompat.getDrawable(getContext(), R.drawable.bg_indicator_text);
drawable.setBounds(targetRect);
drawable.draw(canvas);
//画出温度提示
Paint.FontMetricsInt fontMetrics = textPaint.getFontMetricsInt();
int baseline = (targetRect.bottom + targetRect.top - fontMetrics.bottom - fontMetrics.top) / 2;
textPaint.setTextAlign(Paint.Align.CENTER);
textPaint.setTextSize(DisplayUtil.sp2px(getContext(), 10));
canvas.drawText(TimeUtils.getDateHHmm(item.getTempStamp()) + " " + item.getWeather() + " " + item.getTemperature() + "°", targetRect.centerX(), baseline, textPaint);
int height = mHeight - bottomTextHeight - DisplayUtil.dip2px(getContext(), 4);
canvas.drawLine(targetRect.centerX(), targetRect.bottom + DisplayUtil.dip2px(getContext(), 4), targetRect.centerX(), height, indicatorLinePaint);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//计算温度提示文字的运动轨迹
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).getTempPoint();
break;
}
}
if (i + 1 >= ITEM_SIZE || startPoint == null)
return listItems.get(ITEM_SIZE - 1).getTempPoint().y;
endPoint = listItems.get(i + 1).getTempPoint();
int left = MARGIN_LEFT_ITEM + i * ITEM_WIDTH;
int y = (int) (startPoint.y + (x - left) * 1.0 / ITEM_WIDTH * (endPoint.y - startPoint.y));
return y;
}
1
2
3
4
5
private int getScrollBarX() {
int x = (ITEM_SIZE - 5) * ITEM_WIDTH * scrollOffset / maxScrollOffset;
x = x + MARGIN_LEFT_ITEM;
return x;
}

5506445253_63340840294_1657023135269_687x228.gif

然后就是实现当前选中矩形改变颜色,这个很简单,代码如下

1
2
3
4
5
if (pointBackup.x < getScrollBarX() + DisplayUtil.dip2px(getContext(), 46) && getScrollBarX() + DisplayUtil.dip2px(getContext(), 46) < point.x) {
rectPaint.setColor(Color.parseColor("#33FFFFFF"));
} else {
rectPaint.setColor(Color.parseColor("#1AFFFFFF"));
}

至于下面的时间字段,和左面的最高温和最低温就不写了,很简单,计算一下位置就行,有兴趣的复制下面完整代码

完整代码

Today24HourView.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
public class Today24HourView extends View {
private static final String TAG = "Today24HourView";
private static final int ITEM_SIZE = 24; //24小时
private int ITEM_WIDTH; //每个Item的宽度
private int MARGIN_LEFT_ITEM; //左边预留宽度
private int MARGIN_RIGHT_ITEM; //右边预留宽度
private int bottomTextHeight;
private int scrollWidth;

private int mHeight, mWidth;
private int tempBaseTop; //温度折线的上边Y坐标
private int tempBaseBottom; //温度折线的下边Y坐标
private Paint bitmapPaint, linePaint, rectPaint, indicatorLinePaint;
private TextPaint textPaint;

private List<WeatherHoursModel> listItems;
private int maxScrollOffset = 0;//滚动条最长滚动距离
private int scrollOffset = 0; //滚动条偏移量
private int currentItemIndex = 0; //当前滚动的位置所对应的item下标

private int maxTemp;
private int minTemp;

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() {
MARGIN_LEFT_ITEM = DisplayUtil.dip2px(getContext(), 2);
MARGIN_RIGHT_ITEM = DisplayUtil.dip2px(getContext(), 20);
ITEM_WIDTH = DisplayUtil.dip2px(getContext(), 30);
bottomTextHeight = DisplayUtil.dip2px(getContext(), 16);
mWidth = MARGIN_LEFT_ITEM + MARGIN_RIGHT_ITEM + ITEM_SIZE * ITEM_WIDTH;
mHeight = DisplayUtil.dip2px(getContext(), 140);
tempBaseTop = (mHeight - bottomTextHeight) / 3;
tempBaseBottom = (mHeight - bottomTextHeight) * 3 / 4;
listItems = new ArrayList<>();
initPaint();
}

private void initPaint() {

rectPaint = new Paint();
rectPaint.setColor(Color.parseColor("#1AFFFFFF"));
rectPaint.setAntiAlias(true);
rectPaint.setStyle(Paint.Style.FILL);
rectPaint.setStrokeCap(Paint.Cap.ROUND);
rectPaint.setStrokeJoin(Paint.Join.ROUND);
rectPaint.setStrokeWidth(1);

PathEffect pathEffect = new CornerPathEffect(180);
linePaint = new Paint();
linePaint.setColor(Color.WHITE);
linePaint.setPathEffect(pathEffect);
linePaint.setAntiAlias(true);
linePaint.setStrokeCap(Paint.Cap.ROUND);
linePaint.setStrokeJoin(Paint.Join.ROUND);
linePaint.setStyle(Paint.Style.STROKE);
linePaint.setStrokeWidth(10);

textPaint = new TextPaint();
textPaint.setColor(Color.WHITE);
textPaint.setAntiAlias(true);

bitmapPaint = new Paint();
bitmapPaint.setAntiAlias(true);

indicatorLinePaint = new Paint();
indicatorLinePaint = new Paint();
indicatorLinePaint.setColor(Color.WHITE);
indicatorLinePaint.setAntiAlias(true);
indicatorLinePaint.setStrokeCap(Paint.Cap.ROUND);
indicatorLinePaint.setStyle(Paint.Style.STROKE);
indicatorLinePaint.setStrokeWidth(2);
}

public void setHourItems(List<WeatherHoursModel> listItems) {
this.listItems.clear();
List<WeatherHoursModel> list = new ArrayList<>(listItems);
maxTemp = list.get(0).getTemperature();
minTemp = list.get(0).getTemperature();
for (WeatherHoursModel listItem : list) {
if (listItem.getTemperature() > maxTemp)
maxTemp = listItem.getTemperature();
if (listItem.getTemperature() < minTemp)
minTemp = listItem.getTemperature();
}
int icon = list.get(0).getIcon();
for (int i = 0; i < list.size(); i++) {
int left = MARGIN_LEFT_ITEM + i * ITEM_WIDTH;
int right = left + ITEM_WIDTH;
Point point = calculateTempPoint(left, right, list.get(i).getTemperature());
if (i != 0 && icon == list.get(i).getIcon()) {
list.get(i).setIcon(-1);
} else {
icon = list.get(i).getIcon();
}
list.get(i).setTempPoint(point);
}
this.listItems.addAll(list);
invalidate();
}

private Point calculateTempPoint(int left, int right, int temp) {
double minHeight = tempBaseTop;
double maxHeight = tempBaseBottom;
double tempY = maxHeight - (temp - minTemp) * 1.0 / (maxTemp - minTemp) * (maxHeight - minHeight);
Point point = new Point((left + right) / 2, (int) tempY);
return point;
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(mWidth, mHeight);
}

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
onDrawLine(canvas);
// drawLeftTempText(canvas);
for (int i = 0; i < listItems.size(); i++) {
onDrawTemp(canvas, i);
onDrawText(canvas, i);
}
}

private void onDrawTemp(Canvas canvas, int i) {
WeatherHoursModel item = listItems.get(i);
if (currentItemIndex == i) {
int Y = getTempBarY();
Rect targetRect = new Rect(
getScrollBarX(),
Y - DisplayUtil.dip2px(getContext(), 40),
getScrollBarX() + DisplayUtil.dip2px(getContext(), 92),
Y - DisplayUtil.dip2px(getContext(), 14));
Drawable drawable = ContextCompat.getDrawable(getContext(), R.drawable.bg_indicator_text);
drawable.setBounds(targetRect);
drawable.draw(canvas);
//画出温度提示
Paint.FontMetricsInt fontMetrics = textPaint.getFontMetricsInt();
int baseline = (targetRect.bottom + targetRect.top - fontMetrics.bottom - fontMetrics.top) / 2;
textPaint.setTextAlign(Paint.Align.CENTER);
textPaint.setTextSize(DisplayUtil.sp2px(getContext(), 10));
canvas.drawText(TimeUtils.getDateHHmm(item.getTempStamp()) + " " + item.getWeather() + " " + item.getTemperature() + "°", targetRect.centerX(), baseline, textPaint);
int height = mHeight - bottomTextHeight - DisplayUtil.dip2px(getContext(), 4);
canvas.drawLine(targetRect.centerX(), targetRect.bottom + DisplayUtil.dip2px(getContext(), 4), targetRect.centerX(), height, indicatorLinePaint);
}
}

/**
* 温度的折线,为了折线比较平滑,做了贝塞尔曲线
* 画了贝塞尔曲线后一点都不平滑,放弃
* 直接使用直线连接起来,用CornerPathEffect改变连接处的弧度,顺滑无比
*
* @param canvas
*/
private void onDrawLine(Canvas canvas) {
if (listItems.size() > 0) {
Path path = new Path();
Point point0 = listItems.get(0).getTempPoint();
path.moveTo(point0.x, point0.y);
Path pathBG = new Path();
pathBG.moveTo(point0.x, point0.y);

for (int i = 0; i < listItems.size(); i++) {
Point point = listItems.get(i).getTempPoint();
if (i != 0) {
Point pointPre = listItems.get(i - 1).getTempPoint();
if (i == 1) {
path.lineTo(point.x, point.y);
pathBG.lineTo(point.x, point.y);
} else {
path.rLineTo(point.x - pointPre.x, point.y - pointPre.y);
if (listItems.get(i).getIcon() != -1)
pathBG.rLineTo(point.x - pointPre.x - DisplayUtil.dip2px(getContext(), 1), point.y - pointPre.y);
else
pathBG.rLineTo(point.x - pointPre.x, point.y - pointPre.y);
}

Point pointBackup = listItems.get(0).getTempPoint();
if (listItems.get(i).getIcon() != -1 || (getGoneBehind(i) && i == listItems.size() - 1)) {
int icon = -1;
int indexBackUp = 0;
for (int j = 0; j < i; j++) {
if (listItems.get(j).getIcon() != -1) {
icon = listItems.get(j).getIcon();
indexBackUp = j;
pointBackup = listItems.get(j).getTempPoint();
}
}
if (pointBackup.x < getScrollBarX() + DisplayUtil.dip2px(getContext(), 46) && getScrollBarX() + DisplayUtil.dip2px(getContext(), 46) < point.x) {
rectPaint.setColor(Color.parseColor("#33FFFFFF"));
} else {
rectPaint.setColor(Color.parseColor("#1AFFFFFF"));
}
if (listItems.get(i).getTempPoint() != pointBackup) {
int height = mHeight - bottomTextHeight - DisplayUtil.dip2px(getContext(), 4) - point.y;
pathBG.rLineTo(0, height);
pathBG.rLineTo(pointBackup.x - point.x + DisplayUtil.dip2px(getContext(), 1), 0);
canvas.drawPath(pathBG, rectPaint);
pathBG.reset();
//移到新的点开始画
pathBG.moveTo(point.x, point.y);

int left = (point.x - pointBackup.x) / 2 + pointBackup.x - DisplayUtil.dip2px(getContext(), 10);
int right = (point.x - pointBackup.x) / 2 + pointBackup.x + DisplayUtil.dip2px(getContext(), 10);
int newLeft = (point.x - (pointBackup.x - getItemLeftMargin(indexBackUp))) / 2 + (pointBackup.x - getItemLeftMargin(indexBackUp));
int newRight = ((point.x + getItemRightMargin(i)) - pointBackup.x) / 2 + pointBackup.x;
if (getItemLeftMargin(indexBackUp) < 0 && newLeft + DisplayUtil.dip2px(getContext(), 20) < point.x && i - indexBackUp > 1) {
left = newLeft - DisplayUtil.dip2px(getContext(), 10);
right = left + DisplayUtil.dip2px(getContext(), 20);
} else if (getItemLeftMargin(indexBackUp) < 0 && newLeft + DisplayUtil.dip2px(getContext(), 40) >= point.x && i - indexBackUp > 1) {
left = point.x - DisplayUtil.dip2px(getContext(), 30);
right = left + DisplayUtil.dip2px(getContext(), 20);
}
if (getItemRightMargin(i) < 0 && newRight > pointBackup.x + DisplayUtil.dip2px(getContext(), 10) && i - indexBackUp > 1) {
right = newRight + DisplayUtil.dip2px(getContext(), 10);
left = right - DisplayUtil.dip2px(getContext(), 20);
}
if (getItemLeftMargin(indexBackUp) < 0 && getItemRightMargin(i) < 0) {
left = pointBackup.x - getItemLeftMargin(indexBackUp) + scrollWidth / 2 - DisplayUtil.dip2px(getContext(), 10);
right = left + DisplayUtil.dip2px(getContext(), 20);
}
Drawable drawable = ContextCompat.getDrawable(getContext(), icon);
drawable.setBounds(left,
tempBaseBottom + DisplayUtil.dip2px(getContext(), 5),
right,
tempBaseBottom + DisplayUtil.dip2px(getContext(), 25));
drawable.draw(canvas);
}
}
}
}
canvas.drawPath(path, linePaint);
}
}

private boolean getGoneBehind(int index) {
List<Boolean> data = new ArrayList<>();
for (int k = index; k < listItems.size(); k++) {
data.add(listItems.get(k).getIcon() == -1);
}
return !data.contains(false);
}

//绘制底部时间
private void onDrawText(Canvas canvas, int i) {
textPaint.setTextAlign(Paint.Align.CENTER);
String text = TimeUtils.getDateHHmm(listItems.get(i).getTempStamp());
int left = MARGIN_LEFT_ITEM + i * ITEM_WIDTH;
int right = left + ITEM_WIDTH - 1;
int bottom = mHeight - bottomTextHeight;
Rect targetRect = new Rect(left, bottom, right, bottom + bottomTextHeight);
Paint.FontMetricsInt fontMetrics = textPaint.getFontMetricsInt();
int baseline = (targetRect.bottom + targetRect.top - fontMetrics.bottom - fontMetrics.top) / 2;
textPaint.setTextSize(DisplayUtil.sp2px(getContext(), 12));
if (i % 2 == 0)
canvas.drawText(text, left + (ITEM_WIDTH - 1) / 2, baseline, textPaint);
}

public void drawLeftTempText(Canvas canvas) {
if (listItems.size() > 0) {
//画最左侧的文字
textPaint.setTextAlign(Paint.Align.LEFT);
textPaint.setTextSize(DisplayUtil.sp2px(getContext(), 13));
canvas.drawText(maxTemp + "°", DisplayUtil.sp2px(getContext(), 15), tempBaseTop, textPaint);
canvas.drawText(minTemp + "°", DisplayUtil.sp2px(getContext(), 15), tempBaseBottom, textPaint);
}
}

//设置scrollerView的滚动条的位置,通过位置计算当前的时段
public void setScrollOffset(int offset, int maxScrollOffset, int scrollWidth) {
this.maxScrollOffset = maxScrollOffset;
this.scrollWidth = scrollWidth;
scrollOffset = offset;
currentItemIndex = calculateItemIndex();
invalidate();
}

/**
* 点距离左边的位置
*
* @param i
* @return
*/
private int getItemLeftMargin(int i) {
int left = MARGIN_LEFT_ITEM + i * ITEM_WIDTH + (ITEM_WIDTH - 1) / 2;
return left - scrollOffset;
}

/**
* 点距离右边的位置
*
* @param i
* @return
*/
private int getItemRightMargin(int i) {
int left = MARGIN_LEFT_ITEM + i * ITEM_WIDTH + (ITEM_WIDTH - 1) / 2;
return scrollWidth - (left - scrollOffset);
}

//通过滚动条偏移量计算当前选择的时刻
private int calculateItemIndex() {
int x = getScrollBarX();
int sum = MARGIN_LEFT_ITEM - ITEM_WIDTH;
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 - 5) * ITEM_WIDTH * scrollOffset / maxScrollOffset;
x = x + MARGIN_LEFT_ITEM;
return x;
}

//计算温度提示文字的运动轨迹
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).getTempPoint();
break;
}
}
if (i + 1 >= ITEM_SIZE || startPoint == null)
return listItems.get(ITEM_SIZE - 1).getTempPoint().y;
endPoint = listItems.get(i + 1).getTempPoint();
int left = MARGIN_LEFT_ITEM + i * ITEM_WIDTH;
int y = (int) (startPoint.y + (x - left) * 1.0 / ITEM_WIDTH * (endPoint.y - startPoint.y));
return y;
}
}

IndexHorizontalScrollView.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class IndexHorizontalScrollView extends HorizontalScrollView {

private Today24HourView today24HourView;

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

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

public IndexHorizontalScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int offset = computeHorizontalScrollOffset();
int maxOffset = computeHorizontalScrollRange() - DisplayUtil.getScreenWidth(getContext());
if (today24HourView != null) {
today24HourView.setScrollOffset(offset, maxOffset, getWidth());
}
}

public void setToday24HourView(Today24HourView today24HourView) {
this.today24HourView = today24HourView;
invalidate();
}
}

xml中使用

1
2
3
4
5
6
7
8
9
<com.weather.gorgeous.custom_view.IndexHorizontalScrollView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:fadeScrollbars="false"
android:scrollbars="none">
<com.weather.gorgeous.custom_view.Today24HourView
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.weather.gorgeous.custom_view.IndexHorizontalScrollView>

Activity中使用

1
2
binding.indexHorizontalScrollView.setToday24HourView(binding.today24Hour);
binding.today24Hour.setHourItems(data)

实体类

WeatherHoursModel.java

1
2
3
4
5
6
7
8
9
10
11
12
public class WeatherHoursModel {
// 天气图标
private int icon;
// 温度
private int temperature;
//天气
private String weather;
// 时间戳
private long tempStamp;
//x,y轴
private Point tempPoint;
}

工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static String getDateHHmm(long time) {
String formatTime = new SimpleDateFormat("HH:mm").format(new Date(time));
return formatTime;
}

public static int dip2px(Context context, float dipValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dipValue * scale + 0.5f);
}

public static int sp2px(Context context, float spValue) {
final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
return (int) (spValue * fontScale + 0.5f);
}

public static int getScreenWidth(Context context){
DisplayMetrics dm = context.getResources().getDisplayMetrics();
return dm.widthPixels;
}

到这里就结束了,有写的不好的地方还请指出。

Demo地址

Github

参考链接

Android 仿墨迹天气24小时预报
24小时天气(可滑动)
MoJiDemo-作者zx391324751