先看一下效果图
1.创建一个类
MiuiWeatherView
public class MiuiWeatherView extends View {
private static int DEFAULT_BULE = 0XFF00BFFF;
private static int DEFAULT_GRAY = Color.GRAY;
private int backgroundColor;
private int minViewHeight; //控件的最低高度
private int minPointHeight;//折线最低点的高度
private int lineInterval; //折线线段长度
private float pointRadius; //折线点的半径
private float textSize; //字体大小
private float pointGap; //折线单位高度差
private int defaultPadding; //折线坐标图四周留出来的偏移量
private float iconWidth; //天气图标的边长
private int viewHeight;
private int viewWidth;
private int screenWidth;
private int screenHeight;
private Paint linePaint; //线画笔
private Paint textPaint; //文字画笔
private Paint circlePaint; //圆点画笔
private List data = new ArrayList<>(); //元数据
private List> weatherDatas = new ArrayList<>(); //对元数据中分组后的集合
private List dashDatas = new ArrayList<>(); //之间虚线的x坐标集合
private List points = new ArrayList<>(); //折线拐点的集合
private int maxTemperature;//元数据中的最高和最低温度
private int minTemperature;
private VelocityTracker velocityTracker;
private Scroller scroller;
private ViewConfiguration viewConfiguration;
public MiuiWeatherView(Context context) {
this(context, null);
}
public MiuiWeatherView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MiuiWeatherView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
scroller = new Scroller(context);
viewConfiguration = ViewConfiguration.get(context);
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.MiuiWeatherView);
minPointHeight = (int) ta.getDimension(R.styleable.MiuiWeatherView_min_point_height, dp2pxF(context, 60));
lineInterval = (int) ta.getDimension(R.styleable.MiuiWeatherView_line_interval, dp2pxF(context, 60));
backgroundColor = ta.getColor(R.styleable.MiuiWeatherView_background_color, Color.WHITE);
ta.recycle();
setBackgroundColor(backgroundColor);
initSize(context);
initPaint(context);
}
/**
* 初始化默认数据
*/
private void initSize(Context c) {
screenWidth = getResources().getDisplayMetrics().widthPixels;
screenHeight = getResources().getDisplayMetrics().heightPixels;
minViewHeight = 3 * minPointHeight; //默认3倍
pointRadius = dp2pxF(c, 2.5f);
textSize = sp2pxF(c, 10);
defaultPadding = (int) (0.5 * minPointHeight); //默认0.5倍
iconWidth = (1.0f / 3.0f) * lineInterval; //默认1/3倍
}
/**
* 计算折线单位高度差
*/
private void calculatePontGap() {
int lastMaxTem = -Integer.MAX_VALUE;
int lastMinTem = Integer.MAX_VALUE;
for (WeatherBean bean : data) {
if (bean.temperature > lastMaxTem) {
maxTemperature = bean.temperature;
lastMaxTem = bean.temperature;
}
if (bean.temperature < lastMinTem) {
minTemperature = bean.temperature;
lastMinTem = bean.temperature;
}
}
float gap = (maxTemperature - minTemperature) * 1.0f;
gap = (gap == 0.0f ? 1.0f : gap); //保证分母不为0
pointGap = (viewHeight - minPointHeight - 2 * defaultPadding) / gap;
}
private void initPaint(Context c) {
linePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
linePaint.setStrokeWidth(dp2px(c, 1));
textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
textPaint.setTextSize(textSize);
textPaint.setColor(Color.BLACK);
textPaint.setTextAlign(Paint.Align.CENTER);
circlePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
circlePaint.setStrokeWidth(dp2pxF(c, 1));
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
initSize(getContext());
calculatePontGap();
}
/**
* 公开方法,用于设置元数据
*
* @param data
*/
public void setData(List data) {
if (data == null) {
return;
}
this.data = data;
notifyDataSetChanged();
}
public List getData(){
return data;
}
public void notifyDataSetChanged(){
if(data == null){
return;
}
weatherDatas.clear();
points.clear();
dashDatas.clear();
initWeatherMap(); //初始化相邻的分组
requestLayout();
invalidate();
}
/**
* 根据元数据中连续相同的做分组,
* pair中的first值为连续相同的数量,second值为对应天气
*/
private void initWeatherMap() {
weatherDatas.clear();
String lastWeather = "";
int count = 0;
for (int i = 0; i < data.size(); i++) {
WeatherBean bean = data.get(i);
if (i == 0) {
lastWeather = bean.weather;
}
if (bean.weather != lastWeather) {
Pair pair = new Pair<>(count, lastWeather);
weatherDatas.add(pair);
count = 1;
} else {
count++;
}
lastWeather = bean.weather;
if (i == data.size() - 1) {
Pair pair = new Pair<>(count, lastWeather);
weatherDatas.add(pair);
}
}
for (int i = 0; i < weatherDatas.size(); i++) {
int c = weatherDatas.get(i).first;
String w = weatherDatas.get(i).second;
Log.d("ccy", "weatherMap i =" + i + ";count = " + c + ";weather = " + w);
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (heightMode == MeasureSpec.EXACTLY) {
viewHeight = Math.max(heightSize, minViewHeight);
} else {
viewHeight = minViewHeight;
}
int totalWidth = 0;
if (data.size() > 1) {
totalWidth = 2 * defaultPadding + lineInterval * (data.size() - 1);
}
viewWidth = Math.max(screenWidth, totalWidth); //默认控件最小宽度为屏幕宽度
setMeasuredDimension(viewWidth, viewHeight);
calculatePontGap();
Log.d("ccy", "viewHeight = " + viewHeight + ";viewWidth = " + viewWidth);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (data.isEmpty()) {
return;
}
drawAxis(canvas);
drawLinesAndPoints(canvas);
drawTemperature(canvas);
drawWeatherDash(canvas);
drawWeatherIcon(canvas);
}
/**
* 画时间轴
*
* @param canvas
*/
private void drawAxis(Canvas canvas) {
canvas.save();
linePaint.setColor(DEFAULT_GRAY);
linePaint.setStrokeWidth(dp2px(getContext(), 1));
canvas.drawLine(defaultPadding,
viewHeight - defaultPadding,
viewWidth - defaultPadding,
viewHeight - defaultPadding,
linePaint);
float centerY = viewHeight - defaultPadding + dp2pxF(getContext(), 15);
float centerX;
for (int i = 0; i < data.size(); i++) {
String text = data.get(i).time;
centerX = defaultPadding + i * lineInterval;
Paint.FontMetrics m = textPaint.getFontMetrics();
canvas.drawText(text, 0, text.length(), centerX, centerY - (m.ascent + m.descent) / 2, textPaint);
}
canvas.restore();
}
/**
* 画折线和它拐点的园
*
* @param canvas
*/
private void drawLinesAndPoints(Canvas canvas) {
canvas.save();
linePaint.setColor(DEFAULT_BULE);
linePaint.setStrokeWidth(dp2pxF(getContext(), 1));
linePaint.setStyle(Paint.Style.STROKE);
Path linePath = new Path(); //用于绘制折线
points.clear();
int baseHeight = defaultPadding + minPointHeight;
float centerX;
float centerY;
for (int i = 0; i < data.size(); i++) {
int tem = data.get(i).temperature;
tem = tem - minTemperature;
centerY = (int) (viewHeight - (baseHeight + tem * pointGap));
centerX = defaultPadding + i * lineInterval;
points.add(new PointF(centerX, centerY));
if (i == 0) {
linePath.moveTo(centerX, centerY);
} else {
linePath.lineTo(centerX, centerY);
}
}
canvas.drawPath(linePath, linePaint); //画出折线
//接下来画折线拐点的园
float x, y;
for (int i = 0; i < points.size(); i++) {
x = points.get(i).x;
y = points.get(i).y;
//先画一个颜色为背景颜色的实心园覆盖掉折线拐角
circlePaint.setStyle(Paint.Style.FILL_AND_STROKE);
circlePaint.setColor(backgroundColor);
canvas.drawCircle(x, y,
pointRadius + dp2pxF(getContext(), 1),
circlePaint);
//再画出正常的空心园
circlePaint.setStyle(Paint.Style.STROKE);
circlePaint.setColor(DEFAULT_BULE);
canvas.drawCircle(x, y,
pointRadius,
circlePaint);
}
canvas.restore();
}
/**
* 画温度描述值
*
* @param canvas
*/
private void drawTemperature(Canvas canvas) {
canvas.save();
textPaint.setTextSize(1.2f * textSize); //字体放大一丢丢
float centerX;
float centerY;
String text;
for (int i = 0; i < points.size(); i++) {
text = data.get(i).temperatureStr;
centerX = points.get(i).x;
centerY = points.get(i).y - dp2pxF(getContext(), 13);
Paint.FontMetrics metrics = textPaint.getFontMetrics();
canvas.drawText(text,
centerX,
centerY - (metrics.ascent + metrics.descent)/2,
textPaint);
}
textPaint.setTextSize(textSize);
canvas.restore();
}
/**
* 画不同温度之间的虚线
*
* @param canvas
*/
private void drawWeatherDash(Canvas canvas) {
canvas.save();
linePaint.setColor(DEFAULT_GRAY);
linePaint.setStrokeWidth(dp2pxF(getContext(), 0.5f));
linePaint.setAlpha(0xcc);
//设置画笔画出虚线
float[] f = {dp2pxF(getContext(), 5), dp2pxF(getContext(), 1)}; //两个值分别为循环的实线长度、空白长度
PathEffect pathEffect = new DashPathEffect(f, 0);
linePaint.setPathEffect(pathEffect);
dashDatas.clear();
int interval = 0;
float startX, startY, endX, endY;
endY = viewHeight - defaultPadding;
//0坐标点的虚线手动画上
canvas.drawLine(defaultPadding,
points.get(0).y + pointRadius + dp2pxF(getContext(), 2),
defaultPadding,
endY,
linePaint);
dashDatas.add((float) defaultPadding);
for (int i = 0; i < weatherDatas.size(); i++) {
interval += weatherDatas.get(i).first;
if(interval > points.size()-1){
interval = points.size()-1;
}
startX = endX = defaultPadding + interval * lineInterval;
startY = points.get(interval).y + pointRadius + dp2pxF(getContext(), 2);
dashDatas.add(startX);
canvas.drawLine(startX, startY, endX, endY, linePaint);
}
//这里注意一下,当最后一组的连续天气数为1时,是不需要计入虚线集合的,否则会多画一个天气图标
//若不理解,可尝试去掉下面这块代码并观察运行效果
if(weatherDatas.get(weatherDatas.size()-1).first == 1
&& dashDatas.size() > 1){
dashDatas.remove(dashDatas.get(dashDatas.size()-1));
}
linePaint.setPathEffect(null);
linePaint.setAlpha(0xff);
canvas.restore();
}
/**
* 若相邻虚线都在屏幕内,图标的x位置即在两虚线的中间
* 若有一条虚线在屏幕外,图标的x位置即在屏幕边沿到另一条虚线的中间
* 若两条都在屏幕外,图标x位置紧贴某一条虚线或屏幕中间
* @param canvas
*/
private void drawWeatherIcon(Canvas canvas) {
canvas.save();
textPaint.setTextSize(0.9f * textSize); //字体缩小一丢丢
boolean leftUsedScreenLeft = false;
boolean rightUsedScreenRight = false;
int scrollX = getScrollX(); //范围控制在0 ~ viewWidth-screenWidth
float left, right;
float iconX, iconY;
float textY; //文字的x坐标跟图标是一样的,无需额外声明
iconY = viewHeight - (defaultPadding + minPointHeight / 2.0f);
textY = iconY + iconWidth / 2.0f + dp2pxF(getContext(), 10);
Paint.FontMetrics metrics = textPaint.getFontMetrics();
for (int i = 0; i < dashDatas.size() - 1; i++) {
left = dashDatas.get(i);
right = dashDatas.get(i + 1);
//以下校正的情况为:两条虚线都在屏幕内或只有一条在屏幕内
if (left < scrollX && //仅左虚线在屏幕外
right < scrollX + screenWidth) {
left = scrollX;
leftUsedScreenLeft = true;
}
if (right > scrollX + screenWidth && //仅右虚线在屏幕外
left > scrollX) {
right = scrollX + screenWidth;
rightUsedScreenRight = true;
}
if (right - left > iconWidth) { //经过上述校正之后左右距离还大于图标宽度
iconX = left + (right - left) / 2.0f;
} else { //经过上述校正之后左右距离小于图标宽度,则贴着在屏幕内的虚线
if (leftUsedScreenLeft) {
iconX = right - iconWidth / 2.0f;
} else {
iconX = left + iconWidth / 2.0f;
}
}
//以下校正的情况为:两条虚线都在屏幕之外
if (right < scrollX) { //两条都在屏幕左侧,图标紧贴右虚线
iconX = right - iconWidth / 2.0f;
} else if (left > scrollX + screenWidth) { //两条都在屏幕右侧,图标紧贴左虚线
iconX = left + iconWidth / 2.0f;
} else if (left < scrollX && right > scrollX + screenWidth) { //一条在屏幕左一条在屏幕右,图标居中
iconX = scrollX + (screenWidth / 2.0f);
}
leftUsedScreenLeft = rightUsedScreenRight = false; //重置标志位
}
textPaint.setTextSize(textSize);
canvas.restore();
}
private float lastX = 0;
private float x = 0;
@Override
public boolean onTouchEvent(MotionEvent event) {
if (velocityTracker == null) {
velocityTracker = VelocityTracker.obtain();
}
velocityTracker.addMovement(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (!scroller.isFinished()) { //fling还没结束
scroller.abortAnimation();
}
lastX = x = event.getX();
return true;
case MotionEvent.ACTION_MOVE:
x = event.getX();
int deltaX = (int) (lastX - x);
if (getScrollX() + deltaX < 0) { //越界恢复
scrollTo(0, 0);
return true;
} else if (getScrollX() + deltaX > viewWidth - screenWidth) {
scrollTo(viewWidth - screenWidth, 0);
return true;
}
scrollBy(deltaX, 0);
lastX = x;
break;
case MotionEvent.ACTION_UP:
x = event.getX();
velocityTracker.computeCurrentVelocity(1000); //计算1秒内滑动过多少像素
int xVelocity = (int) velocityTracker.getXVelocity();
if (Math.abs(xVelocity) > viewConfiguration.getScaledMinimumFlingVelocity()) { //滑动速度可被判定为抛动
scroller.fling(getScrollX(), 0, -xVelocity, 0, 0, viewWidth - screenWidth, 0, 0);
invalidate();
}
break;
}
return super.onTouchEvent(event);
}
@Override
public void computeScroll() {
super.computeScroll();
if (scroller.computeScrollOffset()) {
scrollTo(scroller.getCurrX(), scroller.getCurrY());
invalidate();
}
}
//工具类
public static int dp2px(Context c, float dp) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, c.getResources().getDisplayMetrics());
}
public static int sp2px(Context c, float sp) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, c.getResources().getDisplayMetrics());
}
public static float dp2pxF(Context c, float dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, c.getResources().getDisplayMetrics());
}
public static float sp2pxF(Context c, float sp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, c.getResources().getDisplayMetrics());
}
}
2.在values文件下创建attr布局
3.创建一个bean类
WeatherBean
public class WeatherBean {
public String weather; //取值
public int temperature; //温度值
public String temperatureStr; //温度的描述值
public String time; //时间值
public WeatherBean( int temperature,String time) {
this.temperature = temperature;
this.time = time;
this.temperatureStr = temperature + "℃";
}
}
4.XML布局
<包名.MiuiWeatherView
android:id="@+id/weather"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:line_interval="60dp"
app:min_point_height="60dp"
app:background_color="#ffffff">包名.MiuiWeatherView>
5.MainActivity
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
MiuiWeatherView weatherView = (MiuiWeatherView) findViewById(R.id.weather);
List data = new ArrayList<>();
WeatherBean b1 = new WeatherBean(26,"00:00");
WeatherBean b2 = new WeatherBean(28,"01:00");
WeatherBean b3 = new WeatherBean(30,"02:00");
WeatherBean b4 = new WeatherBean(34,"03:00");
WeatherBean b5 = new WeatherBean(35,"04:00");
WeatherBean b6 = new WeatherBean(36,"05:00");
WeatherBean b7 = new WeatherBean(37,"06:00");
WeatherBean b8 = new WeatherBean(38,"07:00");
WeatherBean b9 = new WeatherBean(39,"08:00");
WeatherBean b10 = new WeatherBean(36,"09:00");
WeatherBean b11= new WeatherBean(37,"10:00");
WeatherBean b12= new WeatherBean(38,"11:00");
WeatherBean b13= new WeatherBean(34,"12:00");
WeatherBean b14= new WeatherBean(36,"13:00");
WeatherBean b15= new WeatherBean(35,"14:00");
WeatherBean b16= new WeatherBean(37,"15:00");
WeatherBean b17= new WeatherBean(40,"16:00");
WeatherBean b18= new WeatherBean(36,"17:00");
WeatherBean b19= new WeatherBean(38,"18:00");
WeatherBean b20= new WeatherBean(35,"19:00");
WeatherBean b21= new WeatherBean(37,"20:00");
WeatherBean b22= new WeatherBean(34,"21:00");
WeatherBean b23= new WeatherBean(36,"22:00");
WeatherBean b24= new WeatherBean(38,"23:00");
//...b3、b4......bn
data.add(b1);
data.add(b2);
data.add(b3);
data.add(b4);
data.add(b5);
data.add(b6);
data.add(b7);
data.add(b8);
data.add(b9);
data.add(b10);
data.add(b11);
data.add(b12);
data.add(b13);
data.add(b14);
data.add(b15);
data.add(b16);
data.add(b17);
data.add(b18);
data.add(b19);
data.add(b20);
data.add(b21);
data.add(b22);
data.add(b23);
data.add(b24);
weatherView.setData(data);
}