簡介
最新看到某星的系統自帶的天氣系統展示的24小時的天氣感覺不錯,打算動手實現個。主要效果:展示24小時每個小時的天氣信息,并且可滑動,加入滾動顯示動畫。效果圖如下:
效果圖
天氣.gif
實現
我們根據效果圖一一分析下如何實現,根據效果圖的展示,我們先不考慮滑動,只看界面來,首先有兩條溫度線,一條0-24的時間軸,時間軸上方是圓角矩形展示的風力,溫度線中間展示溫度折線。這樣分析出來 那么感覺很簡單了,剩下的就是滑動了,我們可以使用橫向的HorizontalScrollView重寫onDraw方法實現。步驟如下:
1、初始化展示溫度數據(這里用了一個bean類包含了天氣信息,會貼出)
2、繪制時間軸、溫度線、風力等信息(代碼會在下面直接貼出)
3、設置滑動效果
天氣bean類
public class HourItem {
public String time; //時間點
public Rect windyBoxRect; //表示風力的box
public int windy; //風力
public int temperature; //溫度
public Point tempPoint; //溫度的點坐標
public int res = -1; //圖片資源(有則繪制)
}
整個View的源碼:
/**
* 作者: Sunshine
* 時間: 2016/10/28.
* 郵箱: 44493547@qq.com
* 描述: 24小時天氣類
*/
public class Today24HourView extends View {
private static final String TAG = Today24HourView.class.getSimpleName();
private static final int ITEM_SIZE = 24; //24小時
private static final int ITEM_WIDTH = 140; //每個Item的寬度
private static final int MARGIN_LEFT_ITEM = 100; //左邊預留寬度
private static final int MARGIN_RIGHT_ITEM = 100; //右邊預留寬度
private static final int windyBoxAlpha = 80;
private static final int windyBoxMaxHeight = 80;
private static final int windyBoxMinHeight = 20;
private static final int windyBoxSubHight = windyBoxMaxHeight - windyBoxMinHeight;
private static final int bottomTextHeight = 60;
private int mHeight, mWidth;
private int tempBaseTop; //溫度折線的上邊Y坐標
private int tempBaseBottom; //溫度折線的下邊Y坐標
private Paint bitmapPaint, windyBoxPaint, linePaint, pointPaint, dashLinePaint;
private TextPaint textPaint;
private List<HourItem> listItems;
private int maxScrollOffset = 0;//滾動條最長滾動距離
private int scrollOffset = 0; //滾動條偏移量
private int currentItemIndex = 0; //當前滾動的位置所對應的item下標
private int currentWeatherRes = -1;
private int maxTemp = 26;
private int minTemp = 21;
private int maxWindy = 5;
private int minWindy = 2;
private static final int TEMP[] = {22, 23, 23, 23, 23,
22, 23, 23, 23, 22,
21, 21, 22, 22, 23,
23, 24, 24, 25, 25,
25, 26, 25, 24};
private static final int WINDY[] = {2, 2, 3, 3, 3,
4, 4, 4, 3, 3,
3, 4, 4, 4, 4,
2, 2, 2, 3, 3,
3, 5, 5, 5};
private static final int WEATHER_RES[] ={R.mipmap.w0,
R.mipmap.w1,
R.mipmap.w3,
-1,
-1,
R.mipmap.w5,
R.mipmap.w7,
R.mipmap.w9,
-1, -1
,-1,
R.mipmap.w10,
R.mipmap.w15, -1, -1
,-1, -1, -1, -1, -1
,R.mipmap.w18, -1, -1,
R.mipmap.w19};
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(Color.WHITE);
pointPaint.setAntiAlias(true);
pointPaint.setTextSize(8);
linePaint = new Paint();
linePaint.setColor(Color.WHITE);
linePaint.setAntiAlias(true);
linePaint.setStyle(Paint.Style.STROKE);
linePaint.setStrokeWidth(5);
dashLinePaint = new Paint();
dashLinePaint.setColor(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(Color.WHITE);
windyBoxPaint.setAlpha(windyBoxAlpha);
windyBoxPaint.setAntiAlias(true);
textPaint = new TextPaint();
textPaint.setTextSize(ConvertUtils.sp2px(getContext(), 12));
textPaint.setColor(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);
}
}
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);
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 - ConvertUtils.dp2px(getContext(), 10),
point.y - ConvertUtils.dp2px(getContext(), 25),
point.x + ConvertUtils.dp2px(getContext(), 10),
point.y - ConvertUtils.dp2px(getContext(), 5));
drawable.draw(canvas);
}
onDrawLine(canvas, i);
onDrawText(canvas, i);
}
//底部水平的白線
linePaint.setColor(Color.WHITE);
drawLeftTempText(canvas,10);
canvas.drawLine(0, mHeight - bottomTextHeight, mWidth, mHeight - bottomTextHeight, linePaint);
//中間溫度的虛線
Path path1 = new Path();
path1.moveTo(MARGIN_LEFT_ITEM, tempBaseTop);
path1.quadTo(mWidth - MARGIN_RIGHT_ITEM, tempBaseTop, mWidth - MARGIN_RIGHT_ITEM, tempBaseTop);
canvas.drawPath(path1, dashLinePaint);
Path path2 = new Path();
path2.moveTo(MARGIN_LEFT_ITEM, tempBaseBottom);
path2.quadTo(mWidth - MARGIN_RIGHT_ITEM, tempBaseBottom, mWidth - MARGIN_RIGHT_ITEM, tempBaseBottom);
canvas.drawPath(path2, dashLinePaint);
}
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 - ConvertUtils.dp2px(getContext(), 24),
getScrollBarX() + ITEM_WIDTH,
Y - ConvertUtils.dp2px(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 - ConvertUtils.dp2px(getContext(), 18))/2,
Y - ConvertUtils.dp2px(getContext(), 23),
getScrollBarX()+ITEM_WIDTH - (ITEM_WIDTH/2 - ConvertUtils.dp2px(getContext(), 18))/2,
Y - ConvertUtils.dp2px(getContext(), 5));
drawTemp.draw(canvas);
}
//畫出溫度提示
int offset = ITEM_WIDTH/2;
if(res == -1)
offset = ITEM_WIDTH;
Rect targetRect = new Rect(getScrollBarX(), Y - ConvertUtils.dp2px(getContext(), 24)
, getScrollBarX() + offset, Y - ConvertUtils.dp2px(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 int findCurrentRes(int i) {
if(listItems.get(i).res != -1)
return listItems.get(i).res;
for(int k=i; k>=0; k--){
if(listItems.get(k).res != -1)
return listItems.get(k).res;
}
return -1;
}
//畫底部風力的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 - ConvertUtils.dp2px(getContext(), 20)
, getScrollBarX() + ITEM_WIDTH, rect.top - ConvertUtils.dp2px(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);
}
}
//溫度的折線,為了折線比較平滑,做了貝塞爾曲線
private void onDrawLine(Canvas canvas, int i) {
linePaint.setColor(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+point.x)/2, (pointPre.y+point.y)/2-7, (pointPre.x+point.x)/2, (pointPre.y+point.y)/2+7, point.x, point.y);
else
path.cubicTo((pointPre.x+point.x)/2, (pointPre.y+point.y)/2+7, (pointPre.x+point.x)/2, (pointPre.y+point.y)/2-7, point.x, point.y);
canvas.drawPath(path, linePaint);
}
}
//繪制底部時間
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);
}
public void drawLeftTempText(Canvas canvas, int offset){
//畫最左側的文字
textPaint.setTextAlign(Paint.Align.LEFT);
canvas.drawText(maxTemp + "°", ConvertUtils.dp2px(getContext(), 6) + offset, tempBaseTop, textPaint);
canvas.drawText(minTemp + "°", ConvertUtils.dp2px(getContext(), 6) + offset, tempBaseBottom, textPaint);
}
//設置scrollerView的滾動條的位置,通過位置計算當前的時段
public void setScrollOffset(int offset, int maxScrollOffset){
this.maxScrollOffset = maxScrollOffset;
scrollOffset = offset;
int index = calculateItemIndex(offset);
currentItemIndex = index;
invalidate();
}
//通過滾動條偏移量計算當前選擇的時刻
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;
}
//計算溫度提示文字的運動軌跡
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;
}
}
HorizontalScrollView的設置
/**
* 作者: Sunshine
* 時間: 2016/10/28.
* 郵箱: 44493547@qq.com
* 描述: IndexHorizontalScrollView
*/
public class IndexHorizontalScrollView extends HorizontalScrollView {
private static final String TAG = IndexHorizontalScrollView.class.getSimpleName();
private Paint textPaint;
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);
init();
}
private void init() {
textPaint = new Paint();
textPaint.setTextSize(ConvertUtils.sp2px(getContext(), 12));
textPaint.setAntiAlias(true);
textPaint.setColor(new Color().WHITE);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int offset = computeHorizontalScrollOffset();
int maxOffset = computeHorizontalScrollRange() - ScreenUtils.getScreenWidth(getContext());
if(today24HourView != null){
// today24HourView.drawLeftTempText(canvas, offset);
today24HourView.setScrollOffset(offset, maxOffset);
}
}
public void setToday24HourView(Today24HourView today24HourView){
this.today24HourView = today24HourView;
}
}
最后的展示設置:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_today24_hour"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorPrimary"
tools:context="com.sunshine.rxjavademo.ui.Today24HourActivity">
<com.sunshine.rxjavademo.view.IndexHorizontalScrollView
android:id="@+id/indexHorizontalScrollView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorPrimary"
android:fadeScrollbars="false">
<com.sunshine.rxjavademo.view.Today24HourView
android:id="@+id/today24HourView"
android:layout_width="wrap_content"
android:layout_height="match_parent" />
</com.sunshine.rxjavademo.view.IndexHorizontalScrollView>
</RelativeLayout>
public class Today24HourActivity extends AppCompatActivity {
private IndexHorizontalScrollView indexHorizontalScrollView;
private Today24HourView today24HourView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_today24_hour);
indexHorizontalScrollView = (IndexHorizontalScrollView)findViewById(R.id.indexHorizontalScrollView);
today24HourView = (Today24HourView)findViewById(R.id.today24HourView);
indexHorizontalScrollView.setToday24HourView(today24HourView);
}
}