仿墨迹天气的折线图控件,效果杠杠滴

2016-10-28 ComputerBlue 鸿洋 鸿洋
鸿洋

hongyangAndroid

你好,欢迎关注鸿洋的公众号,每天为您推送高质量文章,让你每天都能涨知识。点击历史消息,查看所有已推送的文章,喜欢可以置顶本公众号。此外,本公众号支持投稿,如果你有原创的文章,希望通过本公众号发布,欢迎投稿。


本文由 ComputerBlue投稿。

ComputerBlue的博客地址:

http://blog.csdn.net/acmnickzhang




1 概述   

这个控件难点在于绘图时候的一些坐标计算,大小计算。

自定义一个View来绘制折线图,外面套一层自定义的HorizontalScrollView来实现横向的滚动...


效果图:




2 代码讲解   

初始化部分代码,初始化一些参数,画笔对象,因为只是个demo所以把高度之类的参数都写死了,你们可以自己改改。


  1. public Today24HourView(Context context) {  
  2.         this(context, null);  
  3.     }  
  4.   
  5.     public Today24HourView(Context context, AttributeSet attrs) {  
  6.         this(context, attrs, 0);  
  7.     }  
  8.   
  9.     public Today24HourView(Context context, AttributeSet attrs, int defStyleAttr) {  
  10.         super(context, attrs, defStyleAttr);  
  11.         init();  
  12.     }  
  13.   
  14.     private void init() {  
  15.         mWidth = MARGIN_LEFT_ITEM + MARGIN_RIGHT_ITEM + ITEM_SIZE * ITEM_WIDTH;  
  16.         mHeight = 500//暂时先写死  
  17.         tempBaseTop = (500 - bottomTextHeight)/4;  
  18.         tempBaseBottom = (500 - bottomTextHeight)*2/3;  
  19.   
  20.         initHourItems();  
  21.         initPaint();  
  22.     }  
  23.   
  24.     private void initPaint() {  
  25.         pointPaint = new Paint();  
  26.         pointPaint.setColor(new Color().WHITE);  
  27.         pointPaint.setAntiAlias(true);  
  28.         pointPaint.setTextSize(8);  
  29.   
  30.         linePaint = new Paint();  
  31.         linePaint.setColor(new Color().WHITE);  
  32.         linePaint.setAntiAlias(true);  
  33.         linePaint.setStyle(Paint.Style.STROKE);  
  34.         linePaint.setStrokeWidth(5);  
  35.   
  36.         dashLinePaint = new Paint();  
  37.         dashLinePaint.setColor(new Color().WHITE);  
  38.         PathEffect effect = new DashPathEffect(new float[]{5555}, 1);  
  39.         dashLinePaint.setPathEffect(effect);  
  40.         dashLinePaint.setStrokeWidth(3);  
  41.         dashLinePaint.setAntiAlias(true);  
  42.         dashLinePaint.setStyle(Paint.Style.STROKE);  
  43.   
  44.         windyBoxPaint = new Paint();  
  45.         windyBoxPaint.setTextSize(1);  
  46.         windyBoxPaint.setColor(new Color().WHITE);  
  47.         windyBoxPaint.setAlpha(windyBoxAlpha);  
  48.         windyBoxPaint.setAntiAlias(true);  
  49.   
  50.         textPaint = new TextPaint();  
  51.         textPaint.setTextSize(DisplayUtil.sp2px(getContext(), 12));  
  52.         textPaint.setColor(new Color().WHITE);  
  53.         textPaint.setAntiAlias(true);  
  54.   
  55.         bitmapPaint = new Paint();  
  56.         bitmapPaint.setAntiAlias(true);  
  57.     }  
  58.   
  59.     //简单初始化下,后续改为由外部传入  
  60.     private void initHourItems(){  
  61.         listItems = new ArrayList<>();  
  62.         for(int i=0; i<ITEM_SIZE; i++){  
  63.             String time;  
  64.             if(i<10){  
  65.                 time = "0" + i + ":00";  
  66.             } else {  
  67.                 time = i + ":00";  
  68.             }  
  69.             int left =MARGIN_LEFT_ITEM  +  i * ITEM_WIDTH;  
  70.             int right = left + ITEM_WIDTH - 1;  
  71.             int top = (int)(mHeight -bottomTextHeight +  
  72.                     (maxWindy - WINDY[i])*1.0/(maxWindy - minWindy)*windyBoxSubHight  
  73.                     - windyBoxMaxHeight);  
  74.             int bottom =  mHeight - bottomTextHeight;  
  75.             Rect rect = new Rect(left, top, right, bottom);  
  76.             Point point = calculateTempPoint(left, right, TEMP[i]);  
  77.   
  78.             HourItem hourItem = new HourItem();  
  79.             hourItem.windyBoxRect = rect;  
  80.             hourItem.time = time;  
  81.             hourItem.windy = WINDY[i];  
  82.             hourItem.temperature = TEMP[i];  
  83.             hourItem.tempPoint = point;  
  84.             hourItem.res = WEATHER_RES[i];  
  85.             listItems.add(hourItem);  
  86.         }  
  87.     }  


绘制部分的代码:


里面的循环是为了画出24个时刻的温度,风力和天气的图片。


  1. @Override  
  2.     protected void onDraw(Canvas canvas) {  
  3.         super.onDraw(canvas);  
  4.         for(int i=0; i<listItems.size(); i++){  
  5.             Rect rect = listItems.get(i).windyBoxRect;  
  6.             Point point = listItems.get(i).tempPoint;  
  7.             //画风力的box和提示文字  
  8.             onDrawBox(canvas, rect, i);  
  9.             //画温度的点  
  10.             onDrawTemp(canvas, i);  
  11.             //画表示天气图片  
  12.             if(listItems.get(i).res != -1 && i != currentItemIndex){  
  13.                 Drawable drawable = ContextCompat.getDrawable(getContext(), listItems.get(i).res);  
  14.                 drawable.setBounds(point.x - DisplayUtil.dip2px(getContext(), 10),  
  15.                         point.y - DisplayUtil.dip2px(getContext(), 25),  
  16.                         point.x + DisplayUtil.dip2px(getContext(), 10),  
  17.                         point.y - DisplayUtil.dip2px(getContext(), 5));  
  18.                 drawable.draw(canvas);  
  19.             }  
  20.             onDrawLine(canvas, i);  
  21.             onDrawText(canvas, i);  
  22.         }  
  23.         //底部水平的白线  
  24.         linePaint.setColor(new Color().WHITE);  
  25.         canvas.drawLine(0, mHeight - bottomTextHeight, mWidth, mHeight - bottomTextHeight, linePaint);  
  26.    
  27.     }  


onDrawBox代码片段:


1.通过drawRoundRect画下面的矩形,如果是选中的那个时刻,那么将透明度设置成255

2.画文字为了让文字在box上面并居中对齐,需要将画笔改为居中模式,然后算出一块矩形,表示在该矩形水平居中。其次baseLine是为了高度的居中。

3.里面的getScrollBarX()方法是计算偏移量,因为文字会随着滑动而移动,移动的水平位置就是由它决定。


  1. //画底部风力的BOX  
  2.     private void onDrawBox(Canvas canvas, Rect rect, int i) {  
  3.         // 新建一个矩形  
  4.         RectF boxRect = new RectF(rect);  
  5.         HourItem item = listItems.get(i);  
  6.         if(i == currentItemIndex) {  
  7.             windyBoxPaint.setAlpha(255);  
  8.             canvas.drawRoundRect(boxRect, 44, windyBoxPaint);  
  9.             //画出box上面的风力提示文字  
  10.             Rect targetRect = new Rect(getScrollBarX(), rect.top - DisplayUtil.dip2px(getContext(), 20)  
  11.                     , getScrollBarX() + ITEM_WIDTH, rect.top - DisplayUtil.dip2px(getContext(), 0));  
  12.             Paint.FontMetricsInt fontMetrics = textPaint.getFontMetricsInt();  
  13.             int baseline = (targetRect.bottom + targetRect.top - fontMetrics.bottom - fontMetrics.top) / 2;  
  14.             textPaint.setTextAlign(Paint.Align.CENTER);  
  15.             canvas.drawText("风力" + item.windy + "级", targetRect.centerX(), baseline, textPaint);  
  16.         } else {  
  17.             windyBoxPaint.setAlpha(windyBoxAlpha);  
  18.             canvas.drawRoundRect(boxRect, 44, windyBoxPaint);  
  19.         }  
  20.     }  


onDrawTemp代码片段:


主要负责画出随着滑动而移动的温度提示的滚动条

这里和上面的绘制类似,但是多了运动轨迹的计算(因为温度的滚动条的移动多了竖直方向的,而风力文字提示的移动只有水平的)。


  1. private void onDrawTemp(Canvas canvas, int i) {  
  2.         HourItem item = listItems.get(i);  
  3.         Point point = item.tempPoint;  
  4.         canvas.drawCircle(point.x, point.y, 10, pointPaint);  
  5.   
  6.         if(currentItemIndex == i) {  
  7.             //计算提示文字的运动轨迹  
  8.             int Y = getTempBarY();  
  9.             //画出背景图片  
  10.             Drawable drawable = ContextCompat.getDrawable(getContext(), R.mipmap.hour_24_float);  
  11.             drawable.setBounds(getScrollBarX(),  
  12.                     Y - DisplayUtil.dip2px(getContext(), 24),  
  13.                     getScrollBarX() + ITEM_WIDTH,  
  14.                     Y - DisplayUtil.dip2px(getContext(), 4));  
  15.             drawable.draw(canvas);  
  16.             //画天气  
  17.             int res = findCurrentRes(i);  
  18.             if(res != -1) {  
  19.                 Drawable drawTemp = ContextCompat.getDrawable(getContext(), res);  
  20.                 drawTemp.setBounds(getScrollBarX()+ITEM_WIDTH/2 + (ITEM_WIDTH/2 - DisplayUtil.dip2px(getContext(), 18))/2,  
  21.                         Y - DisplayUtil.dip2px(getContext(), 23),  
  22.                         getScrollBarX()+ITEM_WIDTH - (ITEM_WIDTH/2 - DisplayUtil.dip2px(getContext(), 18))/2,  
  23.                         Y - DisplayUtil.dip2px(getContext(), 5));  
  24.                 drawTemp.draw(canvas);  
  25.   
  26.             }  
  27.             //画出温度提示  
  28.             int offset = ITEM_WIDTH/2;  
  29.             if(res == -1)  
  30.                 offset = ITEM_WIDTH;  
  31.             Rect targetRect = new Rect(getScrollBarX(), Y - DisplayUtil.dip2px(getContext(), 24)  
  32.                     , getScrollBarX() + offset, Y - DisplayUtil.dip2px(getContext(), 4));  
  33.             Paint.FontMetricsInt fontMetrics = textPaint.getFontMetricsInt();  
  34.             int baseline = (targetRect.bottom + targetRect.top - fontMetrics.bottom - fontMetrics.top) / 2;  
  35.             textPaint.setTextAlign(Paint.Align.CENTER);  
  36.             canvas.drawText(item.temperature + "°", targetRect.centerX(), baseline, textPaint);  
  37.         }  
  38.     }  


onDrawLine代码片段:


折线如果是直线那么显得很生硬,为了平滑一些,做了贝塞尔曲线,根据奇偶性做方向不同的贝塞尔曲线。


  1. //温度的折线,为了折线比较平滑,做了贝塞尔曲线  
  2.     private void onDrawLine(Canvas canvas, int i) {  
  3.         linePaint.setColor(new Color().YELLOW);  
  4.         linePaint.setStrokeWidth(3);  
  5.         Point point = listItems.get(i).tempPoint;  
  6.         if(i != 0){  
  7.             Point pointPre = listItems.get(i-1).tempPoint;  
  8.             Path path = new Path();  
  9.             path.moveTo(pointPre.x, pointPre.y);  
  10.             if(i % 2 == 0)  
  11.                 path.cubicTo(pointPre.x, pointPre.y, (pointPre.x+point.x)/2, (pointPre.y+point.y)/2+14, point.x, point.y);  
  12.             else  
  13.                 path.cubicTo(pointPre.x, pointPre.y, (pointPre.x+point.x)/2, (pointPre.y+point.y)/2-14, point.x, point.y);  
  14.             canvas.drawPath(path, linePaint);  
  15.         }  
  16.     }  


onDrawText代码片段:


  1. //绘制底部时间  
  2.     private void onDrawText(Canvas canvas, int i) {  
  3.         //此处的计算是为了文字能够居中  
  4.         Rect rect = listItems.get(i).windyBoxRect;  
  5.         Rect targetRect = new Rect(rect.left, rect.bottom, rect.right, rect.bottom + bottomTextHeight);  
  6.         Paint.FontMetricsInt fontMetrics = textPaint.getFontMetricsInt();  
  7.         int baseline = (targetRect.bottom + targetRect.top - fontMetrics.bottom - fontMetrics.top) / 2;  
  8.         textPaint.setTextAlign(Paint.Align.CENTER);  
  9.   
  10.         String text = listItems.get(i).time;  
  11.         canvas.drawText(text, targetRect.centerX(), baseline, textPaint);  
  12.     }  


计算部分的代码:


该方法由外部的HorizontalScrollView调用。两个参数分别是

int offset = computeHorizontalScrollOffset();
int maxOffset = computeHorizontalScrollRange() - DisplayUtil.getScreenWidth(getContext());


这里有一问:为什么需要减去屏幕的宽度?


答:    比如HorizontalScrollView的滚动条移动范围在0-----1000像素之间的话,computeHorizontalScrollRange()计算出的值就会是1000+屏幕宽度


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


然后需要计算滑动到某位置时,当前的时刻是几。

先说说getScrollBarX()方法|:(结合下面的图片看)

已知条件是HorizontalScrollView的滚动条位置和滚动条最大滚动距离,我们需要计算的是温度提示滚动条(矩形)的left的横坐标。

所以得到温度滚动条的最大移动距离,就能计算出当前温度滚动条的位置left。

最后x = 当前的left+左侧的margin。

计算当前的时刻采取不断累加ITEM_WIDTH,一旦sum大于x,则i就是当前的item的下标。


  1. //通过滚动条偏移量计算当前选择的时刻  
  2.     private int calculateItemIndex(int offset){  
  3. //        Log.d(TAG, "maxScrollOffset = " + maxScrollOffset + "  scrollOffset = " + scrollOffset);  
  4.         int x = getScrollBarX();  
  5.         int sum = MARGIN_LEFT_ITEM  - ITEM_WIDTH/2;  
  6.         for(int i=0; i<ITEM_SIZE; i++){  
  7.             sum += ITEM_WIDTH;  
  8.             if(x < sum)  
  9.                 return i;  
  10.         }  
  11.         return ITEM_SIZE - 1;  
  12.     }  


  1. private int getScrollBarX(){  
  2.         int x = (ITEM_SIZE - 1) * ITEM_WIDTH * scrollOffset / maxScrollOffset;  
  3.         x = x + MARGIN_LEFT_ITEM;  
  4.         return x;  
  5.     }  




计算运动轨迹代码(实质是计算Y轴的变化):


通过x的变化得到Y的变化。

先要计算当前的x处于哪两个时刻之间,因为y的变化范围必须在这两个时刻的温度的点的Y之间。

得到这两个点之后通过等比关系获得Y

看下图 ,红色字是已知的。


  1. //计算温度提示文字的运动轨迹  
  2.     private int getTempBarY(){  
  3.         int x = getScrollBarX();  
  4.         int sum = MARGIN_LEFT_ITEM ;  
  5.         Point startPoint = null, endPoint;  
  6.         int i;  
  7.         for(i=0; i<ITEM_SIZE; i++){  
  8.             sum += ITEM_WIDTH;  
  9.             if(x < sum) {  
  10.                 startPoint = listItems.get(i).tempPoint;  
  11.                 break;  
  12.             }  
  13.         }  
  14.         if(i+1 >= ITEM_SIZE || startPoint == null)  
  15.             return listItems.get(ITEM_SIZE-1).tempPoint.y;  
  16.         endPoint = listItems.get(i+1).tempPoint;  
  17.   
  18.         Rect rect = listItems.get(i).windyBoxRect;  
  19.         int y = (int)(startPoint.y + (x - rect.left)*1.0/ITEM_WIDTH * (endPoint.y - startPoint.y));  
  20.         return y;  
  21.     }  




项目源码地址:

  • https://github.com/zx391324751/MoJiDemo 



  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值