開局先道歉
首先很抱歉對引用到的自定義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中寫了這麼一個構造方法。
其他我也不知道該說些什麼了,第一次分享成果,雖然處理的不太好,但是也可以用嘛。有不懂的地方可以問我,源碼是上傳不了了,之後會分享全部的天氣製作過程。