本文主要是介绍仿墨迹天气的折线图控件,效果杠杠滴,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
2016-10-28 ComputerBlue 鸿洋
本文由 ComputerBlue投稿。
ComputerBlue的博客地址:
http://blog.csdn.net/acmnickzhang
这个控件难点在于绘图时候的一些坐标计算,大小计算。
自定义一个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);
- }
- }
onDrawLine代码片段:
折线如果是直线那么显得很生硬,为了平滑一些,做了贝塞尔曲线,根据奇偶性做方向不同的贝塞尔曲线。
- //温度的折线,为了折线比较平滑,做了贝塞尔曲线
- 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());
这里有一问:为什么需要减去屏幕的宽度?
答: 比如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
这篇关于仿墨迹天气的折线图控件,效果杠杠滴的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!