BMI控件主要由标题、三段线、当前数值和指示图标、数值刻度和中间的文字组成。这个控件我们可以通过自定义控件实现。
在styles.xml文件中为自定义控件设置自定义属性:
<declare-styleable name="BMIView">
<attr name="title" format="string" />
<attr name="titleSize" format="dimension" />
<attr name="circleRadius" format="dimension" />
<attr name="circleStokeWidth" format="dimension" />
<attr name="upDrawable" format="reference" />
<attr name="downDrawable" format="reference" />
<attr name="normalText" format="string" />
<attr name="minNormal" format="integer" />
<attr name="maxNormal" format="integer" />
<attr name="minVal" format="integer" />
<attr name="maxVal" format="integer" />
<attr name="leftColor" format="color" />
<attr name="midColor" format="color" />
<attr name="rightColor" format="color" />
<attr name="lineWidth" format="dimension" />
<attr name="currentVal" format="float" />
<attr name="normalTextColor" format="color" />
<attr name="normalTextSize" format="dimension" />
declare-styleable>
自定义view继承自view,初始化时读取自定义属性的值,代码如下:
public class BMIView extends View {
/** 可配置的控件属性*/
private String title, normalText; // 标题和中间文字
private int minVal, minNormal, maxNormal, maxVal; //三段线的从左到右每个点的数值,即最小值、正常值的最小正常值的最大值、最大值
private float currVal; // 当前数值
private int leftColor, midColor, rightColor, normalTextColor; // 左边线段颜色、中间线段颜色、右边线段颜色和中间线段的文字颜色
private float titleSize, normalTextSize; // 标题文字大小、中间线段文字大小
private Paint textPaint, linePaint; // 画文字和线段的paint
private Bitmap bitmapUp, bitmapDown; // 指标箭头bitmap
private int width, height, lineWidth, smallCircleRadius, circleStokeWidth; // 控件的宽度、高度、线段的宽度、圆环的半径、圆环的环的宽度
public BMIView(Context context) {
super(context);
init(context, null);
}
public BMIView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public BMIView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
// 读取自定义属性,没有设置的话,就设置为默认值。
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.BMIView);
String titleStr = array.getString(R.styleable.BMIView_title);
if (titleStr != null) {
title = titleStr;
} else {
title = "";
}
titleSize = array.getDimension(R.styleable.BMIView_normalTextSize, 50);
smallCircleRadius = (int) array.getDimension(R.styleable.BMIView_circleRadius, 20);
circleStokeWidth = (int) array.getDimension(R.styleable.BMIView_circleStokeWidth, 10);
Drawable up = array.getDrawable(R.styleable.BMIView_upDrawable);
Drawable down = array.getDrawable(R.styleable.BMIView_downDrawable);
if (up != null) {
bitmapUp = Bitmap.createBitmap(up.getIntrinsicWidth(),
up.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmapUp);
up.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
up.draw(canvas);
} else {
bitmapUp = getBitmap(context, R.drawable.ic_up);
}
if (down != null) {
bitmapDown = Bitmap.createBitmap(down.getIntrinsicWidth(),
down.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmapDown);
down.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
down.draw(canvas);
} else {
bitmapDown = getBitmap(context, R.drawable.ic_down);
}
titleStr = array.getString(R.styleable.BMIView_normalText);
if (titleStr != null) {
normalText = titleStr;
} else {
normalText = "正常";
}
minNormal = array.getInteger(R.styleable.BMIView_minNormal, 316);
maxNormal = array.getInteger(R.styleable.BMIView_maxNormal, 354);
minVal = array.getInteger(R.styleable.BMIView_minVal, 0);
maxVal = array.getInteger(R.styleable.BMIView_maxVal, 400);
leftColor = array.getColor(R.styleable.BMIView_leftColor, Color.argb(255, 246, 174, 3));
midColor = array.getColor(R.styleable.BMIView_midColor, Color.argb(255, 31, 156, 13));
rightColor = array.getColor(R.styleable.BMIView_rightColor, Color.argb(255, 255, 63, 66));
normalTextColor = array.getColor(R.styleable.BMIView_normalTextColor, Color.GRAY);
lineWidth = (int) array.getDimension(R.styleable.BMIView_lineWidth, 14);
normalTextSize = array.getDimension(R.styleable.BMIView_normalTextSize, 44);
currVal = array.getFloat(R.styleable.BMIView_currentVal, 200);
array.recycle();
// 初始化画笔
textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
textPaint.setColor(Color.GRAY);
textPaint.setStyle(Paint.Style.FILL);
textPaint.setTextSize(titleSize);
linePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
linePaint.setColor(leftColor);
linePaint.setStyle(Paint.Style.STROKE);
linePaint.setStrokeWidth(lineWidth);
}
/**
* 获取drawable的id转为bitmap
*/
private Bitmap getBitmap(Context context,int vectorDrawableId) {
Bitmap bitmap=null;
Drawable vectorDrawable = context.getDrawable(vectorDrawableId);
if (vectorDrawable != null) {
bitmap = Bitmap.createBitmap(vectorDrawable.getIntrinsicWidth(),
vectorDrawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
vectorDrawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
vectorDrawable.draw(canvas);
}
return bitmap;
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
width = w;
height = h;
}
/** 以下是为所有可配置的属性提供get和set方法*/
public String getTitle() {
return title;
}
public String getNormalText() {
return normalText;
}
public void setNormalText(String normalText) {
this.normalText = normalText;
invalidate();
}
public int getMinVal() {
return minVal;
}
public void setMinVal(int minVal) {
this.minVal = minVal;
}
public int getMinNormal() {
return minNormal;
}
public void setMinNormal(int minNormal) {
this.minNormal = minNormal;
invalidate();
}
public int getMaxNormal() {
return maxNormal;
}
public void setMaxNormal(int maxNormal) {
this.maxNormal = maxNormal;
invalidate();
}
public int getMaxVal() {
return maxVal;
}
public void setMaxVal(int maxVal) {
this.maxVal = maxVal;
invalidate();
}
public float getCurrVal() {
return currVal;
}
public int getLeftColor() {
return leftColor;
}
public int getMidColor() {
return midColor;
}
public void setMidColor(int midColor) {
this.midColor = midColor;
invalidate();
}
public int getRightColor() {
return rightColor;
}
public float getTitleSize() {
return titleSize;
}
public void setTitleSize(float titleSize) {
this.titleSize = titleSize;
invalidate();
}
public Bitmap getBitmapUp() {
return bitmapUp;
}
public void setBitmapUp(Bitmap bitmapUp) {
this.bitmapUp = bitmapUp;
}
public Bitmap getBitmapDown() {
return bitmapDown;
}
public void setBitmapDown(Bitmap bitmapDown) {
this.bitmapDown = bitmapDown;
}
public int getLineWidth() {
return lineWidth;
}
public void setLineWidth(int lineWidth) {
this.lineWidth = lineWidth;
invalidate();
}
public int getSmallCircleRadius() {
return smallCircleRadius;
}
public int getNormalTextColor() {
return normalTextColor;
}
public void setNormalTextColor(int normalTextColor) {
this.normalTextColor = normalTextColor;
invalidate();
}
public float getNormalTextSize() {
return normalTextSize;
}
public void setNormalTextSize(float normalTextSize) {
this.normalTextSize = normalTextSize;
invalidate();
}
public void setSmallCircleRadius(int smallCircleRadius) {
this.smallCircleRadius = smallCircleRadius;
invalidate();
}
public int getCircleStokeWidth() {
return circleStokeWidth;
}
public void setCircleStokeWidth(int circleStokeWidth) {
this.circleStokeWidth = circleStokeWidth;
invalidate();
}
public void setCurrVal(float currVal) {
this.currVal = currVal;
invalidate();
}
public void setTitle(String title) {
this.title = title;
invalidate();
}
public void setRightColor(int rightColor) {
this.rightColor = rightColor;
invalidate();
}
public void setLeftColor(int leftColor) {
this.leftColor = leftColor;
invalidate();
}
/**
* 为bitmap设置显示的颜色
*/
private Bitmap tintBitmap(Bitmap inBitmap , int tintColor) {
if (inBitmap == null) {
return null;
}
Bitmap outBitmap = Bitmap.createBitmap (inBitmap.getWidth(), inBitmap.getHeight() , inBitmap.getConfig());
Canvas canvas = new Canvas(outBitmap);
Paint paint = new Paint();
paint.setColorFilter( new PorterDuffColorFilter(tintColor, PorterDuff.Mode.SRC_IN)) ;
canvas.drawBitmap(inBitmap , 0, 0, paint) ;
return outBitmap ;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 绘制标题
canvas.drawText(title, 20, 100, textPaint);
int offsetX = 0,offsetY = height / 3, window = width / 3;
// 绘制线条
linePaint.setColor(leftColor);
canvas.drawLine(offsetX, offsetY + height / 3, offsetX += window, offsetY + height / 3, linePaint);
linePaint.setColor(midColor);
canvas.drawLine(offsetX, offsetY + height / 3, offsetX += window, offsetY + height / 3, linePaint);
linePaint.setColor(rightColor);
canvas.drawLine(offsetX, offsetY + height / 3, offsetX + window, offsetY + height / 3, linePaint);
// 绘制当前值
if (currVal < minNormal) {
// 在左边部分
int x = (int) ((currVal / (double)minNormal) * width / 3);
int y = offsetY + height / 3 - lineWidth / 2;
if (x < smallCircleRadius + circleStokeWidth) {
x = smallCircleRadius + circleStokeWidth;
}
linePaint.setColor(Color.WHITE);
linePaint.setStyle(Paint.Style.FILL);
canvas.drawCircle(x, y, smallCircleRadius, linePaint);
linePaint.setStyle(Paint.Style.STROKE);
linePaint.setStrokeWidth(circleStokeWidth);
linePaint.setColor(leftColor);
canvas.drawCircle(x, y + lineWidth / 2, smallCircleRadius, linePaint);
String currStr = String.format(Locale.getDefault(),"%.2f", currVal);
textPaint.setTextSize(normalTextSize);
textPaint.setColor(leftColor);
bitmapDown = tintBitmap(bitmapDown, leftColor);
float strWidth = textPaint.measureText(currStr) + bitmapDown.getWidth();
x -= (int)strWidth / 2;
if (x < 0) {
x = 0;
}
canvas.drawBitmap(bitmapDown, x, y - bitmapDown.getHeight() - normalTextSize / 2, null);
canvas.drawText(currStr, x + bitmapDown.getWidth(), y - normalTextSize, textPaint);
} else if (currVal > maxNormal) {
// 当前值在右边部分
int x = width / 3 * 2 + (int) (((currVal - maxNormal) / (double)(maxVal - maxNormal)) * width / 3);
int y = offsetY + height / 3 - lineWidth / 2;
if (x + smallCircleRadius + circleStokeWidth > width) {
x = width - smallCircleRadius - circleStokeWidth;
}
linePaint.setColor(Color.WHITE);
linePaint.setStyle(Paint.Style.FILL);
canvas.drawCircle(x, y, smallCircleRadius, linePaint);
linePaint.setStyle(Paint.Style.STROKE);
linePaint.setStrokeWidth(circleStokeWidth);
linePaint.setColor(rightColor);
canvas.drawCircle(x, y + lineWidth / 2, smallCircleRadius, linePaint);
String currStr = String.format(Locale.getDefault(),"%.2f", currVal);
textPaint.setTextSize(normalTextSize);
textPaint.setColor(rightColor);
bitmapUp = tintBitmap(bitmapUp, rightColor);
float strWidth = textPaint.measureText(currStr) + bitmapUp.getWidth();
x -= (int)strWidth / 2;
if (x + (int)strWidth / 2 > width) {
x = width - (int)strWidth / 2;
}
canvas.drawBitmap(bitmapUp, x, y - bitmapUp.getHeight() - normalTextSize / 2, null);
canvas.drawText(currStr, x + bitmapUp.getWidth(), y - normalTextSize, textPaint);
} else {
// 当前值在中间部分
int x = width / 3 + (int) ((currVal - minNormal) / (double)(maxNormal - minNormal) * (width / 3)) - smallCircleRadius;
int y = offsetY + height / 3 - lineWidth / 2;
linePaint.setColor(Color.WHITE);
linePaint.setStyle(Paint.Style.FILL);
canvas.drawCircle(x, y, smallCircleRadius, linePaint);
linePaint.setStyle(Paint.Style.STROKE);
linePaint.setStrokeWidth(circleStokeWidth);
linePaint.setColor(midColor);
canvas.drawCircle(x, y + lineWidth / 2, smallCircleRadius, linePaint);
String currStr = String.format(Locale.getDefault(),"%.2f", currVal);
textPaint.setTextSize(normalTextSize);
textPaint.setColor(midColor);
float strWidth = textPaint.measureText(currStr);
x -= (int)strWidth / 2;
canvas.drawText(currStr, x, y - normalTextSize, textPaint);
}
// 绘制坐标的正常、最小值和最大值
textPaint.setTextSize(normalTextSize);
textPaint.setColor(Color.GRAY);
float textWidth = textPaint.measureText(normalText);
canvas.drawText(normalText, (width - textWidth) / 2, offsetY + height / 3 + lineWidth * 4 + normalTextSize / 2, textPaint);
textWidth = textPaint.measureText(String.valueOf(minNormal));
canvas.drawText(String.valueOf(minNormal), width / 3 - textWidth / 2, offsetY + height / 3 + lineWidth * 4 + normalTextSize / 2, textPaint);
textWidth = textPaint.measureText(String.valueOf(maxNormal));
canvas.drawText(String.valueOf(maxNormal), width / 3 * 2 - textWidth / 2, offsetY + height / 3 + lineWidth * 4 + normalTextSize / 2, textPaint);
}
}
在activity的布局文件中引入自定义控件:
<cn.edu.xjtu.bmidemo.BMIView
android:id="@+id/BMIView1"
android:layout_width="match_parent"
android:layout_height="150dp"
android:layout_marginBottom="8dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
app:normalText="normal"
app:title="test BMI 1"
app:currentVal="100"
app:minVal="0"
app:minNormal="200"
app:maxNormal="400"
app:maxVal="600"/>
在代码中设置控件的自定义属性:
BMIView view1 = findViewById(R.id.BMIView1);
// 代码设置各种值
view1.setTitle("代码设置的标题");
自定义控件部分的代码完成了,为了完美实现薄荷App的BMI页面效果,我们还需要完成添加背景和虚线分割线、支持一段文字描述的功能。
没有文字描述的控件背景是白色圆角矩形,我们可以通过xml定义,新建bmi_bg.xml:
<shape android:shape="rectangle"
xmlns:android="http://schemas.android.com/apk/res/android">
<padding android:bottom="10dp" android:left="10dp" android:right="10dp" android:top="10dp"/>
<corners android:radius="5dp" />
<solid android:color="#ffffff" />
shape>
带文字描述的控件其实是由三部分组成,控件本身、分割线和文字。控件的背景此时是顶部圆角、底部直角的白色矩形,文字的背景是顶部直角、底部圆角的白色矩形,具体实现与bmi_bg.xml类似。
新建bmi_bg_top.xml,定义顶部圆角、底部直角的白色矩形:
<shape android:shape="rectangle"
xmlns:android="http://schemas.android.com/apk/res/android">
<padding android:bottom="10dp" android:left="10dp" android:right="10dp" android:top="10dp"/>
<corners android:topLeftRadius="5dp" android:topRightRadius="5dp" />
<solid android:color="#ffffff" />
shape>
新建bmi_bg_bottom.xml,定义顶部直角、底部圆角的白色矩形:
<shape android:shape="rectangle"
xmlns:android="http://schemas.android.com/apk/res/android">
<padding android:bottom="10dp" android:left="10dp" android:right="10dp" android:top="10dp"/>
<corners android:bottomLeftRadius="5dp" android:bottomRightRadius="5dp" />
<solid android:color="#ffffff" />
shape>
同样我们可以利用xml画虚线和左右两端的半圆。新建dash_line.xml:
<layer-list
xmlns:android="http://schemas.android.com/apk/res/android"
>
<item android:height="@dimen/dash_line_circle_diameter" android:width="500dp" android:top="0dp" android:bottom="0dp" android:left="0dp"
android:right="0dp">
<shape android:shape="rectangle" >
<solid android:color="#ffffff" />
shape>
item>
<item android:gravity="start" android:height="@dimen/dash_line_circle_diameter"
android:width="@dimen/dash_line_circle_diameter" android:start="@dimen/dash_line_circle_margin_negative" >
<shape android:shape="oval">
<solid android:color="@color/bg" />
shape>
item>
<item android:gravity="end" android:height="@dimen/dash_line_circle_diameter"
android:width="@dimen/dash_line_circle_diameter" android:end="@dimen/dash_line_circle_margin_negative" >
<shape android:shape="oval">
<solid android:color="@color/bg" />
shape>
item>
<item android:gravity="center" android:height="16dp" android:left="@dimen/dash_line_circle_margin_positive"
android:right="@dimen/dash_line_circle_margin_positive">
<shape android:shape="line">
<stroke android:dashGap="@dimen/dash_line_dash_gap"
android:dashWidth="@dimen/dash_line_dash_width"
android:color="@color/gray_light"
android:width="@dimen/dash_line_width"
/>
shape>
item>
layer-list>
在activity的xml中将他们组合起来:
<cn.edu.xjtu.bmidemo.BMIView
android:id="@+id/BMIView3"
android:layout_width="match_parent"
android:layout_height="150dp"
android:layout_marginEnd="@dimen/activity_margin"
android:layout_marginStart="@dimen/activity_margin"
android:layout_marginTop="8dp"
android:background="@drawable/bmi_bg_top"
app:rightColor="@color/gray"
app:title="test BMI 3"
app:currentVal="370"
app:titleSize="20sp"
app:normalTextColor="@color/green"
app:normalText="normal"
app:normalTextSize="16sp"/>
<View
android:layout_width="match_parent"
android:layout_height="@dimen/dash_line_circle_diameter"
android:layout_marginEnd="@dimen/activity_margin"
android:layout_marginStart="@dimen/activity_margin"
android:background="@drawable/dash_line" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginEnd="@dimen/activity_margin"
android:layout_marginStart="@dimen/activity_margin"
android:textSize="16sp"
android:text="测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试"
android:background="@drawable/bmi_bg_bottom"
android:padding="16dp"
/>
dimens.xml文件:
<resources>
<dimen name="activity_margin">16dpdimen>
<dimen name="dash_line_circle_radius">14dpdimen>
<dimen name="dash_line_circle_diameter">28dpdimen>
<dimen name="dash_line_circle_margin_negative">-14dpdimen>
<dimen name="dash_line_circle_margin_positive">14dpdimen>
<dimen name="dash_line_width">2dpdimen>
<dimen name="dash_line_dash_width">6dpdimen>
<dimen name="dash_line_dash_gap">3dpdimen>
resources>
colors.xml文件:
<resources>
<color name="colorPrimary">#3F51B5color>
<color name="colorPrimaryDark">#303F9Fcolor>
<color name="colorAccent">#FF4081color>
<color name="yellow">#fff6ae03color>
<color name="green">#1f9c0dcolor>
<color name="gray">#acacaccolor>
<color name="red">#fdff3f42color>
<color name="white">#ffffffcolor>
<color name="bg">#f9f9f9f9color>
<color name="gray_light">#e2e2e2color>
resources>
如果你的手机上虚线没有效果,你需要在xml或者代码中关闭控件所在activity的硬件加速:
<activity android:name=".MainActivity"
android:hardwareAccelerated="false">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
intent-filter>
activity>
BMIView view3 = findViewById(R.id.BMIView3);
view3.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
以上内容就是实现效果图的全部代码。整个demo项目也放在我的github上:https://github.com/wwwlxmgithub/BMIDemo