24小時天氣(可滑動)

簡介

最新看到某星的系統自帶的天氣系統展示的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);
    }
}

最后的最后,本人語言組織能力實在是太匱乏。。。 以后一定好好學習多多積累。感冒實在是難受!謝謝支持!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,373評論 25 708
  • 發現 關注 消息 iOS 第三方庫、插件、知名博客總結 作者大灰狼的小綿羊哥哥關注 2017.06.26 09:4...
    肇東周閱讀 12,245評論 4 61
  • import csv import jsoncsvfile = open('file.csv', 'r')json...
    餓了爸閱讀 1,175評論 0 0
  • 導 語 金庸小說中,《天龍八部》最有“佛”的味道。而且,對人性的剖析也更是淋漓盡致。 正 文 《有女同車》中,張愛...
    飄雨桐V閱讀 415評論 0 1
  • 二點,困倦到睜不開眼精。卻不能睡。 一周夜班開始。 夜晚感覺涼意。微微下了些細雨,地皮未曾濕潤便停了。喜歡雨夜,雨...
    清和qinghe閱讀 273評論 0 0