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