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中写了这么一个构造方法。
其他我也不知道该说些什么了,第一次分享成果,虽然处理的不太好,但是也可以用嘛。有不懂的地方可以问我,源码是上传不了了,之后会分享全部的天气制作过程。

发布了5 篇原创文章 · 获赞 10 · 访问量 406

猜你喜欢

转载自blog.csdn.net/qq_39168470/article/details/103401172