概述:
这个控件难点在于绘图时候的一些坐标计算,大小计算。
自定义一个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
作者:acmnickzhang 发表于2016/10/21 11:16:46 原文链接
阅读:63 评论:1 查看评论