1. 程式人生 > >仿墨跡天氣的折線圖控制元件,效果槓槓滴.

仿墨跡天氣的折線圖控制元件,效果槓槓滴.

概述:

這個控制元件難點在於繪圖時候的一些座標計算,大小計算。 自定義一個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,沒有必要建立一個物件,希望沒有誤導大家。。。