天气预报的曲线图是之前参加考核时的一个扩展要求,对于新手来说看起来很难,涉及到自定义 View 的知识,但是只要有一定的思路,其实实现起来也就不到一天的功夫。话不多说,先看看小米的天气预报的曲线和我实现的效果吧。

一.思路
1.首先我们需要实现 RecyclerView 的横向滑动,这个最简单。
2.接着我们就要在每个 item 里面进行划线,这是实现曲线图的关键思路。
3.画出先后我们还有处理的问题就是坐标的变换,由于 item 的坐标和我们实现的曲线的坐标不同,需要进行计算转换。
二.画线的思路
1.首先我们要知道在整一个 RecyclerView 中 每一 item 都有自己的布局,即每一个 item 都有自己的坐标系,但是这些坐标是紧靠在一起的。具体的如下图:
item坐标
2.接着画线的时候就可以根据这个坐标来画,这里就可以分为三种情况:
######## (1)画第一个 item :我们只需要从中间点开始画向右边,中间的坐标很容易确定,就是温度数据经过简单的换算。而右边的坐标的确定就需要下一个点的坐标,显然右边的坐标就是两个点的坐标的中点,这样我们就画出了第一个item 的曲线。
######## (2)接着就是中间的 item , 确定点的思路其实就和上面的一样了,只不过 这里需要的是从左边到中间,再从中间到右边。这里需要前面一个点和后面一个点的坐标
######## (3)最后一个 item 的画线也是同样的思路,这里就不再解释。
三.坐标的变换
通过观察我们可以看到小米天气的曲线的坐标和布局的坐标如下图所示

因此最高温的坐标的可以换算成布局的高度的 1/2 减去温度数据,最低温可以换算成布局的高度直接减去温度数据。
四.画线的自定义 View
public class DiagramView extends View {
private Paint mPaint;//画笔工具
private TextPaint mTextPaint;//画文字的工具
private Path mPath;//画线
private int mNextHeightY;//下一个高点的坐标
private int mNextLowY;//下一个低点的坐标
private int mHeightY;//高点坐标
private int mLowY;//低点坐标
private int mPreHeightY;//前一个高点坐标
private int mPreLowY;//前一个低点坐标
//计算过后的高点的坐标
private int mCaculatedNextHeightY;
private int mCaculatedNextLowY;
private int mCaculatedHeightY;
private int mCaculatedLowY;
private int mCaculatedPreHeightY;
private int mCaculatedPreLowY;
//每一个item 的高度和宽度
private int mWidth ;
private int mHeight;
//数字
private String mHeightText;
private String mLowText;
//分别为第一个item 中间的item 和最后的item ,因为这三种item 需要画线的情况不同,所以设置三个状态进行判断
private static final int sFIRSTITEM = 0;
private static final int sMEDIUMITEM = 1;
private static final int sLASTITEM = 2;
private int mItemType;
private static final String TAG = "DiagramView";
public DiagramView(Context context) {
this(context, null);
}
public DiagramView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public DiagramView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//自定View 重写onDraw 方法时需要设置这个标志,表示需要重绘,否则会使用默认的onDraw 方法
setWillNotDraw(false);
init();
}
/**
* 在onDraw 方法里进行曲线的绘制和温度数据的显示
* @param canvas
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mWidth = getWidth();
mHeight = getHeight();
caculateY(mHeight);//根据屏幕的高度计算出坐标
//描点
canvas.drawCircle(mWidth/ 2, mCaculatedHeightY, 4, mPaint);
canvas.drawCircle(mWidth/ 2, mCaculatedLowY, 4, mPaint);
//画数值
canvas.drawText(mHeightText,mWidth/2-mTextPaint.getTextSize()/2,mCaculatedHeightY-5,mTextPaint);
canvas.drawText(mLowText,mWidth/2-mTextPaint.getTextSize()/2,mCaculatedLowY+mTextPaint.getTextSize(),mTextPaint);
//根据不同的item 画出不同的状态的曲线
switch (mItemType) {
//第一个item 只需要从中间到右边
case sFIRSTITEM:
mPath.moveTo(mWidth / 2, mCaculatedHeightY);
mPath.lineTo(mWidth, (mCaculatedHeightY + mCaculatedNextHeightY) / 2);
mPath.moveTo(mWidth / 2, mCaculatedLowY);
mPath.lineTo(mWidth, (mCaculatedNextLowY + mCaculatedLowY) / 2);
break;
//中间的item 需要从左边画到中间,再从中间画到右边
case sMEDIUMITEM:
mPath.moveTo(0, (mCaculatedPreHeightY+mCaculatedHeightY)/2);
mPath.lineTo(mWidth / 2, mCaculatedHeightY);
mPath.moveTo(0, (mCaculatedPreLowY+mCaculatedLowY)/2);
mPath.lineTo(mWidth / 2,mCaculatedLowY);
mPath.moveTo(mWidth / 2, mCaculatedHeightY);
mPath.lineTo(mWidth, (mCaculatedHeightY + mCaculatedNextHeightY) / 2);
mPath.moveTo(mWidth / 2, mCaculatedLowY);
mPath.lineTo(mWidth, (mCaculatedNextLowY + mCaculatedLowY) / 2);
break;
//最后一个item 只需要从坐标画到中间即可
case sLASTITEM:
mPath.moveTo(0, (mCaculatedPreHeightY+mCaculatedHeightY)/2);
mPath.lineTo(mWidth / 2, mCaculatedHeightY);
mPath.moveTo(0, (mCaculatedLowY+mCaculatedPreLowY)/2);
mPath.lineTo(mWidth / 2, mCaculatedLowY);
break;
default:
break;
}
canvas.drawPath(mPath, mPaint);
}
/**
* 最后一个item 的数据赋值,并重绘
* @param preHeightY
* @param preLowY
* @param heightY
* @param lowY
* @param itemType
* @param last
*/
public void draws(int preHeightY,int preLowY,int heightY, int lowY,int itemType,boolean last) {
this.mPreHeightY = preHeightY;
this.mPreLowY = preLowY;
this.mHeightY = heightY;
this.mLowY =lowY;
this.mItemType =itemType;
invalidate();
}
/**
* 中间的item 的数据赋值并重绘
* @param preHeightY
* @param preLowY
* @param heightY
* @param lowY
* @param nextHeighY
* @param nextLowY
* @param itemType
*/
public void draws(int preHeightY,int preLowY,int heightY, int lowY, int nextHeighY, int nextLowY,int itemType) {
this.mPreHeightY =preHeightY;
this.mPreLowY = preLowY;
this.mHeightY = heightY;
this.mLowY = lowY;
this.mNextHeightY = nextHeighY;
this.mNextLowY = nextLowY;
this.mItemType =itemType;
invalidate();
}
/**
* 第一个item 的数据赋值并重绘
* @param heightY
* @param lowY
* @param nextHeighY
* @param nextLowY
* @param itemType
*/
public void draws(int heightY, int lowY, int nextHeighY, int nextLowY,int itemType) {
this.mHeightY = heightY;
this.mLowY = lowY;
this.mNextHeightY = nextHeighY;
this.mNextLowY = nextLowY;
this.mItemType =itemType;
invalidate();
}
/**
* 根据屏幕高度和温度数据计算出坐标,为了显示在中间高点多加了/8项,低点多加了/4/8项
* @param height
*/
private void caculateY(int height){
mCaculatedNextHeightY = height/2+height/8-mNextHeightY;
mCaculatedNextLowY = height/2+height/4+height/8-mNextLowY;
mCaculatedHeightY = height/2+height/8-mHeightY;
mCaculatedLowY=height/2+height/4+height/8-mLowY;
mCaculatedPreHeightY=height/2+height/8-mPreHeightY;
mCaculatedPreLowY=height/2+height/4+height/8-mPreLowY;
}
/**
* 初始化画笔工具
*/
private void init() {
mPaint = new Paint();
mPaint.setColor(Color.parseColor("#b6b3b3"));
mPaint.setAntiAlias(true);
mPaint.setStrokeWidth(2);
mPaint.setStrokeCap(Paint.Cap.ROUND);
mPaint.setStrokeJoin(Paint.Join.BEVEL);
mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
mPath = new Path();
mItemType = 0;
mTextPaint = new TextPaint();
mTextPaint.setColor(Color.parseColor("#424242"));
mTextPaint.setAntiAlias(true);
mTextPaint.setStrokeWidth(1);
mTextPaint.setTextSize(30);
mTextPaint.setStrokeCap(Paint.Cap.ROUND);
}
/**
* 设置温度数值
* @param heightText
* @param lowText
*/
public void setText(int heightText,int lowText){
this.mHeightText = heightText+"";
this.mLowText = lowText+"";
}
}
五.数据的适配器
public class DiagramAdapter extends RecyclerView.Adapter<DiagramAdapter.ViewHolder> {
private int[] mHeight;
private int[] mLows;
private int times ;
public DiagramAdapter(int[] height, int[] low) {
mHeight = height;
mLows = low;
caculateTimes();
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_dailydata, null, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
//设置RecyclerView 的这个holder 不复用
holder.setIsRecyclable(false);
int prePosition = position - 1;
int nextPosition = position + 1;
//这里默认显示15天的数据
switch (position) {
case 0:
holder.mDiagramView.draws(times*mHeight[position], times*mLows[position], times*mHeight[nextPosition], times*mLows[nextPosition], 0);
holder.itemView.setBackgroundResource(R.drawable.drawableBackground);
break;
case 14:
holder.mDiagramView.draws(times*mHeight[prePosition], times*mLows[prePosition], times*mHeight[position], times*mLows[position], 2, true);
break;
default:
holder.mDiagramView.draws(times*mHeight[prePosition],times* mLows[prePosition],times* mHeight[position],times* mLows[position], times*mHeight[nextPosition], times*mLows[nextPosition], 1);
break;
}
holder.mDiagramView.setText(mHeight[position],mLows[position]);
}
@Override
public int getItemCount() {
return mHeight.length;
}
class ViewHolder extends RecyclerView.ViewHolder {
DiagramView mDiagramView;
public ViewHolder(View itemView) {
super(itemView);
mDiagramView = itemView.findViewById(R.id.dv);
}
}
/**
* 为了让温差大小不同的曲线都能显示并且显示的效果不会相差太大
* 这里对数据进行了不同倍数的放大,差别大的放大倍数小,差别晓得放大倍数大
*/
private void caculateTimes(){
int max =mHeight[0];
int min = mLows[0];
for (int i=1;i<mHeight.length;i++){
if (mHeight[i]>max){
max = mHeight[i];
}
if (mHeight[i]<min){
min = mHeight[i];
}
}
int difference = max-min;
if (difference<=10&&difference>5){
times = 5;
}else if (difference<=5&&difference>=3){
times = 7;
}else if (difference<3){
times = 10;
}else if (difference>10&&difference<=13){
times = 3;
}else {
times = 1;
}
}
}
六.遇到的问题
1.在画点和线的时候 onDraw 方法不执行。
这里需要在构造器内设置一个标志 。
public DiagramView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setWillNotDraw(false);
}
如果是我们自定义的View ,重写 onDraw 方法,就应该设置这个参数为 false ,表示将进行重绘。
2.使用 RecyclerView 横向布局的时候如果item 不能居中的话,这时候需要注意 item 的高度应该和 RecyclerView 的高度一致,就像垂直的 RecyclerView 的宽度和 item 的宽度一致一样。
大概的思路就是这样啦!
Demo 地址:https://github.com/yishengma/Diagram
[分享一首歌:I (金泰妍/Verbal Jint)]
网友评论