Android使用RecyclerView實現溫度折線圖

開局先道歉

首先很抱歉對引用到的自定義view作者感到抱歉,雜亂無章幾個月之後已經找不到原文的出處,因爲本次折線圖是在此基礎上進行修改,萬分感謝其指導,現貼出部分代碼僅供初學者參考。

緣由

在安卓小白的道路上繼續前進,也閱讀完郭霖大神的《第一行代碼》。其中針對最後一個項目《酷歐天氣》進行了改進,把天氣溫度列表改成折線圖的形式,更加直觀。同時數據也通過和風天氣api換成了實時天氣,之後會討論這個問題。

樣式圖

如果該樣式符合需要請繼續往下看,因爲是可滑動的,然而沒有gif工具的製作,只能看到平面圖了。中間折線圖部分就是今天介紹的亮點。
在這裏插入圖片描述

核心view代碼

由於工作問題,無法上傳代碼,並且公司上的代碼不可轉移。。。也就造成只能這樣分享給大家並沒有源碼,但是針對代碼中的部分我會盡我所能詳細介紹。

折線view

public class WeatherLineView extends View {

    /**
     * 默認最小寬度
     */
    private static final int defaultMinWidth = 100;

    /**
     * 默認最小高度
     */
    private static final int defaultMinHeight = 80;

    /**
     * 字體最小默認16dp
     */
    private int mTemperTextSize = 16;

    /**
     * 文字顏色
     */
    private int mWeaTextColor = Color.BLACK;

    /**
     * 線的寬度
     */
    private int mWeaLineWidth = 1;

    /**
     * 圓點的寬度
     */
    private int mWeaDotRadius = 4;

    /**
     * 文字和點的間距
     */
    private int mTextDotDistance = 4;

    /**
     * 畫文字的畫筆
     */
    private TextPaint mTextPaint;

    /**
     * 文字的FontMetrics
     */
    private Paint.FontMetrics mTextFontMetrics;

    /**
     * 畫點最高溫度的畫筆
     */
    private Paint mDotHighPaint;

    /**
     * 畫點最低溫度的畫筆
     */
    private Paint mDotColdPaint;

    /**
     * 畫線最高溫度畫筆
     */
    private Paint mLineHighPaint;

    /**
     * 畫線最低溫度畫筆
     */
    private Paint mLineColdPaint;
    /**
     * 7天最低溫度的數據
     */
    private int mLowestTemperData;

    /**
     * 7天最高溫度的數據
     */
    private int mHighestTemperData;

    /**
     * 分別代表最左邊的,中間的,右邊的三個當天最低溫度值
     */
    private int mLowTemperData[];

    private int mHighTemperData[];

    public WeatherLineView(Context context) {
        this(context, null);
    }

    public WeatherLineView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public WeatherLineView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs, defStyleAttr);
        initPaint();
    }

    /**
     * 設置當天的三個低溫度數據,中間的數據就是當天的最低溫度數據,
     * 第一個數據是當天和前天的數據加起來的平均數,
     * 第二個數據是當天和明天的數據加起來的平均數
     *
     * @param low  最低溫度
     * @param high 最高溫度
     */
    public void setLowHighData(int low[], int high[]) {
        mLowTemperData = low;
        mHighTemperData = high;
        invalidate();
    }

    /**
     * 設置15天裏面的最低和最高的溫度數據
     *
     * @param low  最低溫度
     * @param high 最高溫度
     */
    public void setLowHighestData(int low, int high) {
        mLowestTemperData = low;
        mHighestTemperData = high;
        invalidate();
    }

    /**
     * 設置畫筆信息
     */
    private void initPaint() {
        mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
        mTextPaint.setTextSize(mTemperTextSize);
        mTextPaint.setColor(mWeaTextColor);
        mTextFontMetrics = mTextPaint.getFontMetrics();

        mDotHighPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mDotHighPaint.setStyle(Paint.Style.FILL);
        mDotHighPaint.setColor(getResources().getColor(R.color.red));

        mDotColdPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mDotColdPaint.setStyle(Paint.Style.FILL);
        mDotColdPaint.setColor(getResources().getColor(R.color.green));

        mLineHighPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mLineHighPaint.setStyle(Paint.Style.STROKE);
        mLineHighPaint.setStrokeWidth(mWeaLineWidth);
        mLineHighPaint.setColor(getResources().getColor(R.color.red));
        mLineHighPaint.setStrokeJoin(Paint.Join.ROUND);

        mLineColdPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mLineColdPaint.setStyle(Paint.Style.STROKE);
        mLineColdPaint.setStrokeWidth(mWeaLineWidth);
        mLineColdPaint.setColor(getResources().getColor(R.color.green));
        mLineColdPaint.setStrokeJoin(Paint.Join.ROUND);
    }

    /**
     * 獲取自定義屬性並賦初始值
     */
    private void init(Context context, AttributeSet attrs, int defStyleAttr) {
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.WeatherLineView,
                defStyleAttr, 0);
        mTemperTextSize = (int) a.getDimension(R.styleable.WeatherLineView_temperTextSize,
                dp2px(context, mTemperTextSize));
        mWeaTextColor = a.getColor(R.styleable.WeatherLineView_weatextColor, Color.parseColor("#b07b5c"));
        mWeaLineWidth = (int) a.getDimension(R.styleable.WeatherLineView_weaLineWidth,
                dp2px(context, mWeaLineWidth));
        mWeaDotRadius = (int) a.getDimension(R.styleable.WeatherLineView_weadotRadius,
                dp2px(context, mWeaDotRadius));
        mTextDotDistance = (int) a.getDimension(R.styleable.WeatherLineView_textDotDistance,
                dp2px(context, mTextDotDistance));
        a.recycle();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int width = getSize(widthMode, widthSize, 0);
        int height = getSize(heightMode, heightSize, 1);
        setMeasuredDimension(width, height);
    }

    /**
     * @param mode Mode
     * @param size Size
     * @param type 0表示寬度,1表示高度
     * @return 寬度或者高度
     */
    private int getSize(int mode, int size, int type) {
        // 默認
        int result;
        if (mode == MeasureSpec.EXACTLY) {
            result = size;
        } else {
            if (type == 0) {
                // 最小不能低於最小的寬度
                result = dp2px(getContext(), defaultMinWidth) + getPaddingLeft() + getPaddingRight();
            } else {
                // 最小不能小於最小的寬度加上一些數據
                int textHeight = (int) (mTextFontMetrics.bottom - mTextFontMetrics.top);
                // 加上2個文字的高度
                result = dp2px(getContext(), defaultMinHeight) + 2 * textHeight +
                        // 需要加上兩個文字和圓點的間距
                        getPaddingTop() + getPaddingBottom() + 2 * mTextDotDistance;
            }
            if (mode == MeasureSpec.AT_MOST) {
                result = Math.min(result, size);
            }
        }
        return result;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (mLowTemperData == null || mHighTemperData == null
                || mLowestTemperData == 0 || mHighestTemperData == 0) {
            return;
        }
        //設置背景顏色,爲了統一已經放在xml中和其他佈局一起設置
        //canvas.drawColor(getResources().getColor(R.color.transparent));
        // 文本的高度
        int textHeight = (int) (mTextFontMetrics.bottom - mTextFontMetrics.top);
        // 一個基本的高度,由於最下面的時候,有文字和圓點和文字的寬度需要留空間
        int baseHeight = getHeight() - textHeight - mTextDotDistance;
        // 最低溫度相關
        // 最低溫度中間
        int calowMiddle = baseHeight - cacHeight(mLowTemperData[1]);
        canvas.drawCircle(getWidth() / 2.0f, calowMiddle, mWeaDotRadius, mDotColdPaint);
        // 畫溫度文字
        String text = mLowTemperData[1] + "°";
        int baseX = (int) (canvas.getWidth() / 2.0f - mTextPaint.measureText(text) / 2.0f);
        // mTextFontMetrics.top爲負的
        // 需要加上文字高度和文字與圓點之間的空隙
        int baseY = (int) (calowMiddle - mTextFontMetrics.top) + mTextDotDistance;
        canvas.drawText(text, baseX, baseY, mTextPaint);
        if (mLowTemperData[0] != 100) {
            // 最低溫度左邊
            int calowLeft = baseHeight - cacHeight(mLowTemperData[0]);
            canvas.drawLine(0, calowLeft, getWidth() / 2.0f, calowMiddle, mLineColdPaint);
        }
        if (mLowTemperData[2] != 100) {
            // 最低溫度右邊
            int calowRight = baseHeight - cacHeight(mLowTemperData[2]);
            canvas.drawLine(getWidth() / 2.0f, calowMiddle, getWidth(), calowRight, mLineColdPaint);
        }
        // 最高溫度相關
        // 最高溫度中間
        int calHighMiddle = baseHeight - cacHeight(mHighTemperData[1]);
        canvas.drawCircle(getWidth() / 2, calHighMiddle, mWeaDotRadius, mDotHighPaint);
        // 畫溫度文字
        String text2 = String.valueOf(mHighTemperData[1]) + "°";
        int baseX2 = (int) (canvas.getWidth() / 2.0f - mTextPaint.measureText(text2) / 2.0f);
        int baseY2 = (int) (calHighMiddle - mTextFontMetrics.bottom) - mTextDotDistance;
        canvas.drawText(text2, baseX2, baseY2, mTextPaint);
        if (mHighTemperData[0] != 100) {
            // 最高溫度左邊
            int calHighLeft = baseHeight - cacHeight(mHighTemperData[0]);
            canvas.drawLine(0, calHighLeft, getWidth() / 2.0f, calHighMiddle, mLineHighPaint);
        }
        if (mHighTemperData[2] != 100) {
            // 最高溫度右邊
            int calHighRight = baseHeight - cacHeight(mHighTemperData[2]);
            canvas.drawLine(getWidth() / 2.0f, calHighMiddle, getWidth(), calHighRight, mLineHighPaint);
        }
    }

    private int cacHeight(int tem) {
        // 最低,最高溫度之差
        int temDistance = mHighestTemperData - mLowestTemperData;
        int textHeight = (int) (mTextFontMetrics.bottom - mTextFontMetrics.top);
        // view的最高和最低之差,需要減去文字高度和文字與圓點之間的空隙
        int viewDistance = getHeight() - 2 * textHeight - 2 * mTextDotDistance;
        // 今天的溫度和最低溫度之間的差別
        int currTemDistance = tem - mLowestTemperData;
        return currTemDistance * viewDistance / temDistance;
    }

    public static int dp2px(Context context, float dpVal) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
                dpVal, context.getResources().getDisplayMetrics());
    }
}

adapter

貼了adapter的全部代碼,其實是沒有必要的,但是從頭看到尾對於一部分人可能更放心一點,不會雲裏霧裏。這部分沒有什麼註釋 ,都是基礎的RecyclerView裏的adapter的操作,看一下ViewHolder對應的頁面就很好理解了。

public class WeatherDataAdapter extends RecyclerView.Adapter<WeatherDataAdapter.WeatherDataViewHolder> {

    private Context mContext;
    private LayoutInflater mInflater;
    private List<ForecastBean.DailyForecastBean> mDatas;
    private int mLowestTem;
    private int mHighestTem;

    public WeatherDataAdapter(Context context, List<ForecastBean.DailyForecastBean> datats, int lowtem, int hightem) {
        mContext = context;
        mInflater = LayoutInflater.from(context);
        mDatas = datats;
        mLowestTem = lowtem;
        mHighestTem = hightem;
    }

    @Override
    public WeatherDataViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = mInflater.inflate(R.layout.item_weather_item, parent, false);
        WeatherDataViewHolder viewHolder = new WeatherDataViewHolder(view);
        viewHolder.dayText = view.findViewById(R.id.id_day_text_tv);
        viewHolder.dayIcon =  view.findViewById(R.id.id_day_icon_iv);
        viewHolder.weatherLineView =  view.findViewById(R.id.wea_line);
        viewHolder.nighticon =  view.findViewById(R.id.id_night_icon_iv);
        viewHolder.nightText =  view.findViewById(R.id.id_night_text_tv);
        viewHolder.dateText = view.findViewById(R.id.date_text);
        viewHolder.weekText = view.findViewById(R.id.week_text);
        viewHolder.windText = view.findViewById(R.id.wind_text);
        viewHolder.windLevelText = view.findViewById(R.id.windLevel_text);
        return viewHolder;
    }

    @Override
    @TargetApi(26)
    public void onBindViewHolder(WeatherDataViewHolder holder, int position) {
        ForecastBean.DailyForecastBean weatherModel = mDatas.get(position);
        holder.dayText.setText(weatherModel.getCond_txt_d());
        holder.weatherLineView.setLowHighestData(mLowestTem, mHighestTem);
        holder.nightText.setText(weatherModel.getCond_txt_n());
        String dateText = weatherModel.getDate();
        holder.dateText.setText(getDateString(dateText));
        holder.weekText.setText(getWeekString(dateText));
        holder.windText.setText(weatherModel.getWind_dir());
        holder.windLevelText.setText(weatherModel.getWind_sc() + "級");
        String weatherDay = weatherModel.getCond_txt_d();
        if (weatherDay.contains("多雲")) {
            holder.dayIcon.setImageResource(R.drawable.ic_cloud);
        } else if (weatherDay.contains("晴")) {
            holder.dayIcon.setImageResource(R.drawable.ic_sun);
        } else if (weatherDay.contains("陰")) {
            holder.dayIcon.setImageResource(R.drawable.ic_overcast);
        } else if (weatherDay.contains("雨")) {
            holder.dayIcon.setImageResource(R.drawable.ic_rain);
        } else if (weatherDay.contains("雪")) {
            holder.dayIcon.setImageResource(R.drawable.ic_snow);
        } else if (weatherDay.contains("霧")) {
            holder.dayIcon.setImageResource(R.drawable.ic_fog);
        }
        String weatherNight = weatherModel.getCond_txt_n();
        if (weatherNight.contains("多雲")) {
            holder.nighticon.setImageResource(R.drawable.ic_cloud);
        } else if (weatherNight.contains("晴")) {
            holder.nighticon.setImageResource(R.drawable.ic_sun);
        } else if (weatherNight.contains("陰")) {
            holder.nighticon.setImageResource(R.drawable.ic_overcast);
        } else if (weatherNight.contains("雨")) {
            holder.nighticon.setImageResource(R.drawable.ic_rain);
        } else if (weatherNight.contains("雪")) {
            holder.nighticon.setImageResource(R.drawable.ic_snow);
        } else if (weatherNight.contains("霧")) {
            holder.nighticon.setImageResource(R.drawable.ic_fog);
        }
        int low[] = new int[3];
        int high[] = new int[3];
        low[1] = Integer.valueOf(weatherModel.getTmp_min());
        high[1] = Integer.valueOf(weatherModel.getTmp_max());
        if (position <= 0) {
            low[0] = 100;
            high[0] = 100;
        } else {
            ForecastBean.DailyForecastBean weatherModelLeft = mDatas.get(position - 1);
            low[0] = (Integer.valueOf(weatherModelLeft.getTmp_min()) + Integer.valueOf(weatherModel.getTmp_min())) / 2;
            high[0] = (Integer.valueOf(weatherModelLeft.getTmp_max()) + Integer.valueOf(weatherModel.getTmp_max())) / 2;
        }
        if (position >= mDatas.size() - 1) {
            low[2] = 100;
            high[2] = 100;
        } else {
            ForecastBean.DailyForecastBean weatherModelRight = mDatas.get(position + 1);
            low[2] = (Integer.valueOf(weatherModel.getTmp_min()) + Integer.valueOf(weatherModelRight.getTmp_min())) / 2;
            high[2] = (Integer.valueOf(weatherModel.getTmp_max()) + Integer.valueOf(weatherModelRight.getTmp_max())) / 2;
        }
        holder.weatherLineView.setLowHighData(low, high);
    }

    @Override
    public int getItemCount() {
        return mDatas.size();
    }

    public class WeatherDataViewHolder extends RecyclerView.ViewHolder {

        TextView dateText;
        TextView weekText;
        TextView dayText;
        ImageView dayIcon;
        WeatherLineView weatherLineView;
        ImageView nighticon;
        TextView nightText;
        TextView windText;
        TextView windLevelText;
        public WeatherDataViewHolder(View itemView) {
            super(itemView);
        }
    }

    /**
     * 獲取周幾
     * @param weekText
     * @return
     */
    @TargetApi(26)
    private String getWeekString(String weekText) {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
        ParsePosition pos = new ParsePosition(0);
        Date date = simpleDateFormat.parse(weekText, pos);

        String[] weekOfDays = {"週日", "週一", "週二", "週三", "週四", "週五", "週六"};
        Calendar calendar = Calendar.getInstance();
        if (date == null) {
            return null;
        }
        calendar.setTime(date);
        int w = calendar.get(Calendar.DAY_OF_WEEK) - 1;
        if (w < 0) {
            w = 0;
        }
        return weekOfDays[w];
    }

    /**
     * 修改日期格式
     * @param dateString
     * @return
     */
    private String getDateString(String dateString) {
        String[] strings = dateString.split("-");
        return strings[1] + "月" + strings[2] + "日";
    }
}

adapter對應的item頁面

以防萬一還是把頁面也貼出來吧,寫的挺雜的。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <TextView
            android:id="@+id/date_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="#fff"
            android:textSize="14sp"
            android:layout_gravity="center_horizontal"/>
        <TextView
            android:id="@+id/week_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="#fff"
            android:textSize="14sp"
            android:layout_gravity="center_horizontal"/>

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:orientation="horizontal">

            <ImageView
                android:id="@+id/id_day_icon_iv"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:src="@drawable/ic_sun"/>

            <TextView
                android:id="@+id/id_day_text_tv"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:textColor="#fff"
                android:textSize="18sp"/>
        </LinearLayout>

        <com.kxqin.coolweather.WeatherLineView
            android:id="@+id/wea_line"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:layout_gravity="center_horizontal">

            <ImageView
                android:id="@+id/id_night_icon_iv"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:src="@drawable/ic_cloud"/>

            <TextView
                android:id="@+id/id_night_text_tv"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:textColor="#fff"
                android:textSize="18sp"/>
        </LinearLayout>

        <TextView
            android:id="@+id/wind_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:textColor="#fff"/>

        <TextView
            android:id="@+id/windLevel_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:textColor="#fff"/>
    </LinearLayout>

</LinearLayout>

這是item的preview圖,單個效果就是這樣子的,用RecyclerView傳入多個數據就連成了一幅溫度折線圖。
在這裏插入圖片描述

方法介紹

setLowHighestData()方法設置最高溫度和最低溫度,因爲要計算位置,所以最高值和最低值要知道。通過adapter調用即可。

這是activity裏的代碼,其中調用這個方法傳入溫度數據,我這裏傳入的直接是天氣預報類。

 private void fillDatatoRecyclerView(List<ForecastBean.DailyForecastBean> daily) {
        mWeatherModels.clear();
        mWeatherModels.addAll(daily);
        Collections.sort(daily, new Comparator<ForecastBean.DailyForecastBean>() {
            @Override
            public int compare(ForecastBean.DailyForecastBean lhs,
                               ForecastBean.DailyForecastBean rhs) {
                // 排序找到溫度最低的,按照最低溫度升序排列
                return Integer.valueOf(lhs.getTmp_min()) - Integer.valueOf(rhs.getTmp_min());
            }
        });

        int low = Integer.valueOf(daily.get(0).getTmp_min());

        Collections.sort(daily, new Comparator<ForecastBean.DailyForecastBean>() {
            @Override
            public int compare(ForecastBean.DailyForecastBean lhs,
                               ForecastBean.DailyForecastBean rhs) {
                // 排序找到溫度最高的,按照最高溫度降序排列
                return Integer.valueOf(rhs.getTmp_max()) - Integer.valueOf(lhs.getTmp_max());
            }
        });
        int high = Integer.valueOf(daily.get(0).getTmp_max());

        mWeaDataAdapter = new WeatherDataAdapter(this, mWeatherModels, low, high);
        mRecyclerView.setAdapter(mWeaDataAdapter);
    }

用排序方法找到最低溫度和最高溫度,傳入adapter中,adapter中寫了這麼一個構造方法。
其他我也不知道該說些什麼了,第一次分享成果,雖然處理的不太好,但是也可以用嘛。有不懂的地方可以問我,源碼是上傳不了了,之後會分享全部的天氣製作過程。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章