仿墨跡天氣的折線圖控制元件,效果槓槓滴.
阿新 • • 發佈:2019-02-09
概述:
這個控制元件難點在於繪圖時候的一些座標計算,大小計算。 自定義一個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;
}
補充說明:程式碼裡面設定顏色的部分程式碼需要改改(new Color().WHITE),改為Color.WHITE,沒有必要建立一個物件,希望沒有誤導大家。。。