編輯:關於Android編程
最近公司的項目加入天氣模塊,需要實現下面的效果:
然後根據自己的構想實現了下面的效果:
下面會詳細的介紹實現的過程。
其實在拿到設計的一個效果,我們首先要做的就是去思考,怎麼實現,就算不好實現,也要實現一個折中的兩邊都可以妥協的方案。
由於當前是要展示10天以上的天氣的情況,那麼如果采用一個view繪制的形式肯定會影響到性能,那其實很快就想到了ListView,這不就是一個橫向的ListView的效果麼,考慮到ListView並沒有橫向的效果,轉而就想到了RecyclerView,RecyclerView的LinearLayoutManager可以直接設置為橫向的,這解決了使用什麼來實現的問題。
這個問題不是太難,可以這樣,拿到這15天的最低溫度和最高溫度,這個需要我們計算,從這15個最高以及最低溫度裡面找到最大以及最小的,然後使用這兩個溫度差去映射View的高度,最後可以得到需要的繪制的圓點View的Y軸的計算公式:
這個也很簡單,用
然後再經過上面的計算就可以得到需要繪制的Y軸的位置。
自定義WeatherLineView,需要下面的幾個屬性:
具體的實現:
public class WeatherLineView extends View {
/**
* 默認最小寬度50dp
*/
private static final int defaultMinWidth = 100;
/**
* 默認最小高度80dp
*/
private static final int defaultMinHeight = 80;
/**
* 字體最小默認16dp
*/
private int mTemperTextSize = 16;
/**
* 文字顏色
*/
private int mWeaTextColor = Color.BLACK;
/**
* 線的寬度
*/
private int mWeaLineWidth = 1;
/**
* 圓點的寬度
*/
private int mWeaDotRadius = 5;
/**
* 文字和點的間距
*/
private int mTextDotDistance = 5;
/**
* 畫文字的畫筆
*/
private TextPaint mTextPaint;
/**
* 文字的FontMetrics
*/
private Paint.FontMetrics mTextFontMetrics;
/**
* 畫點的畫筆
*/
private Paint mDotPaint;
/**
* 畫線的畫筆
*/
private Paint mLinePaint;
/**
* 15天最低溫度的數據
*/
private int mLowestTemperData;
/**
* 15天最高溫度的數據
*/
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();
mDotPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mDotPaint.setStyle(Paint.Style.FILL);
mDotPaint.setColor(mWeaTextColor);
mLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mLinePaint.setStyle(Paint.Style.STROKE);
mLinePaint.setStrokeWidth(mWeaLineWidth);
mLinePaint.setColor(mWeaTextColor);
}
/**
* 獲取自定義屬性並賦初始值
*/
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;
}
canvas.drawColor(Color.YELLOW);
// 文本的高度
int textHeight = (int) (mTextFontMetrics.bottom - mTextFontMetrics.top);
// 一個基本的高度,由於最下面的時候,有文字和圓點和文字的寬度需要留空間
int baseHeight = getHeight() - textHeight - mTextDotDistance;
// 最低溫度相關
// 最低溫度中間
int calowMiddle = baseHeight - cacHeight(mLowTemperData[1]);
canvas.drawCircle(getWidth() / 2, calowMiddle, mWeaDotRadius, mDotPaint);
// 畫溫度文字
String text = String.valueOf(mLowTemperData[1]) + "°";
int baseX = (int) (canvas.getWidth() / 2 - mTextPaint.measureText(text) / 2);
// mTextFontMetrics.top為負的
// 需要加上文字高度和文字與圓點之間的空隙
int baseY = (int) (calowMiddle - mTextFontMetrics.top) + mTextDotDistance;
canvas.drawText(text, baseX, baseY, mTextPaint);
if (mLowTemperData[0] != 0) {
// 最低溫度左邊
int calowLeft = baseHeight - cacHeight(mLowTemperData[0]);
canvas.drawLine(0, calowLeft, getWidth() / 2, calowMiddle, mLinePaint);
}
if (mLowTemperData[2] != 0) {
// 最低溫度右邊
int calowRight = baseHeight - cacHeight(mLowTemperData[2]);
canvas.drawLine(getWidth() / 2, calowMiddle, getWidth(), calowRight, mLinePaint);
}
// 最高溫度相關
// 最高溫度中間
int calHighMiddle = baseHeight - cacHeight(mHighTemperData[1]);
canvas.drawCircle(getWidth() / 2, calHighMiddle, mWeaDotRadius, mDotPaint);
// 畫溫度文字
String text2 = String.valueOf(mHighTemperData[1]) + "°";
int baseX2 = (int) (canvas.getWidth() / 2 - mTextPaint.measureText(text2) / 2);
int baseY2 = (int) (calHighMiddle - mTextFontMetrics.bottom) - mTextDotDistance;
canvas.drawText(text2, baseX2, baseY2, mTextPaint);
if (mHighTemperData[0] != 0) {
// 最高溫度左邊
int calHighLeft = baseHeight - cacHeight(mHighTemperData[0]);
canvas.drawLine(0, calHighLeft, getWidth() / 2, calHighMiddle, mLinePaint);
}
if (mHighTemperData[2] != 0) {
// 最高溫度右邊
int calHighRight = baseHeight - cacHeight(mHighTemperData[2]);
canvas.drawLine(getWidth() / 2, calHighMiddle, getWidth(), calHighRight, mLinePaint);
}
}
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());
}
}
上面就是折線的天氣View,代碼的注釋比較詳細了。
預覽的效果是這樣的:
當然這裡那些屬性我沒有加上去,因為代碼裡面有設置默認值,如果覺得不滿足要求的話,可以自己設置。
public class WeaDataAdapter extends RecyclerView.Adapter {
private Context mContext;
private LayoutInflater mInflater;
private List mDatas;
private int mLowestTem;
private int mHighestTem;
public WeaDataAdapter(Context context, List 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 = (TextView) view.findViewById(R.id.id_day_text_tv);
viewHolder.dayIcon = (ImageView) view.findViewById(R.id.id_day_icon_iv);
viewHolder.weatherLineView = (WeatherLineView) view.findViewById(R.id.wea_line);
viewHolder.nighticon = (ImageView) view.findViewById(R.id.id_night_icon_iv);
viewHolder.nightText = (TextView) view.findViewById(R.id.id_night_text_tv);
return viewHolder;
}
@Override
public void onBindViewHolder(WeatherDataViewHolder holder, int position) {
// 最低溫度設置為15,最高溫度設置為30
Resources resources = mContext.getResources();
WeatherDailyModel weatherModel = mDatas.get(position);
holder.dayText.setText(weatherModel.getText_day());
int iconday = resources.getIdentifier("wth_code_" + weatherModel.getCode_day(), "drawable", mContext.getPackageName());
if (iconday == 0) {
holder.dayIcon.setImageResource(R.drawable.wth_code_99);
} else {
holder.dayIcon.setImageResource(iconday);
}
holder.weatherLineView.setLowHighestData(mLowestTem, mHighestTem);
int iconight = resources.getIdentifier("wth_code_" + weatherModel.getCode_day(), "drawable", mContext.getPackageName());
if (iconight == 0) {
holder.nighticon.setImageResource(R.drawable.wth_code_99);
} else {
holder.nighticon.setImageResource(iconight);
}
holder.nightText.setText(weatherModel.getText_night());
int low[] = new int[3];
int high[] = new int[3];
low[1] = weatherModel.getLow();
high[1] = weatherModel.getHigh();
if (position <= 0) {
low[0] = 0;
high[0] = 0;
} else {
WeatherDailyModel weatherModelLeft = mDatas.get(position - 1);
low[0] = (weatherModelLeft.getLow() + weatherModel.getLow()) / 2;
high[0] = (weatherModelLeft.getHigh() + weatherModel.getHigh()) / 2;
}
if (position >= mDatas.size() - 1) {
low[2] = 0;
high[2] = 0;
} else {
WeatherDailyModel weatherModelRight = mDatas.get(position + 1);
low[2] = (weatherModel.getLow() + weatherModelRight.getLow()) / 2;
high[2] = (weatherModel.getHigh() + weatherModelRight.getHigh()) / 2;
}
holder.weatherLineView.setLowHighData(low, high);
}
@Override
public int getItemCount() {
return mDatas.size();
}
public static class WeatherDataViewHolder extends RecyclerView.ViewHolder {
TextView dayText;
ImageView dayIcon;
WeatherLineView weatherLineView;
ImageView nighticon;
TextView nightText;
public WeatherDataViewHolder(View itemView) {
super(itemView);
}
}
}
Model裡面的字段:
public static class WeatherDailyModel {
/**
* date : 2016-05-30
* text_day : 多雲
* code_day : 4
* text_night : 陰
* code_night : 9
* high : 34
* low : 22
*/
private String date;
private String text_day;
private int code_day;
private String text_night;
private int code_night;
private int high;
private int low;
}
private void initView() {
//得到控件
mRecyclerView = (RecyclerView) findViewById(R.id.id_recyclerview_horizontal);
//設置布局管理器
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
mRecyclerView.setLayoutManager(layoutManager);
}
private void fillDatatoRecyclerView(List daily) {
mWeatherModels = daily;
Collections.sort(daily, new Comparator() {
@Override
public int compare(WeatherDailyModel lhs,
WeatherDailyModel rhs) {
// 排序找到溫度最低的,按照最低溫度升序排列
return lhs.getLow() - rhs.getLow();
}
});
int low = daily.get(0).getLow();
Collections.sort(daily, new Comparator() {
@Override
public int compare(WeatherDailyModel lhs,
WeatherDailyModel rhs) {
// 排序找到溫度最高的,按照最高溫度降序排列
return rhs.getHigh() - lhs.getHigh();
}
});
int high = daily.get(0).getHigh();
mWeaDataAdapter = new WeaDataAdapter(this, mWeatherModels, low, high);
mRecyclerView.setAdapter(mWeaDataAdapter);
}
這樣其實就搞定了,WeatherLineView可能比較麻煩一點吧,但是只要是想清楚了,就很好了,從上面也看到WeatherLineView沒什麼含金量的,就是很普通的繪制。
不知不覺中,帶你一步步深入了解View系列的文章已經寫到第四篇了,回顧一下,我們一共學習了LayoutInflater的原理分析、視圖的繪制流程、視圖的狀態及重繪等知識,
之前寫過一篇文章《Android學習小Demo(13)Android中關於ContentObserver的使用》,在裡面利用ContentOberver去監測短信URI內
當一個應用在後台執行時,前台界面就不會有什麼信息,這時用戶根本不知道程序是否在執行、執行進度如何、應用程序是否遇到錯誤終止等,這時需要使用進度條來提示用戶後台程序執行的進
前言 下拉刷新組件在開發中使用率是非常高的,基本上聯網的APP都會采用這種方式。對於開發效率而言,使用獲得大家認可的開源庫必然是效率最高的,但是不重復發明輪子的