前面主要說(shuō)了自定義View的一些知識(shí),這篇文章主要是利用自定義View做一個(gè)仿小米運(yùn)動(dòng)計(jì)步功能的控件,如下圖所示:

分析一下思路:
1.畫(huà)背景
2.畫(huà)一個(gè)最外部的圓
3.畫(huà)圓上的小圓點(diǎn)
4.畫(huà)豎線,環(huán)繞一周
5.畫(huà)圓環(huán)
6.畫(huà)文字
7.添加動(dòng)畫(huà)
為了可以自定義各個(gè)控件的顯示效果,自定義View的屬性還是必要的。
自定義屬性
自定義屬性主要是自定義了各個(gè)部件的顏色,format是該屬性的取值類型。
這里要注意的是,自定義屬性的name定義成了XiaoMiStep,那么自定義View的名字也要是XiaoMiStep,保持一致。
<declare-styleable name="XiaoMiStep">
<!--背景-->
<attr name="backGroundColor" format="color"></attr>
<!--最外層圓-->
<attr name="outerCircleColor" format="color"></attr>
<!--外層圓上的小圓點(diǎn)顏色-->
<attr name="outerDotColor" format="color"></attr>
<!--豎線的顏色-->
<attr name="lineColor" format="color"></attr>
<!--圓環(huán)的顏色-->
<attr name="ringColor" format="color"></attr>
<!--步數(shù)顏色-->
<attr name="stepNumColor" format="color"></attr>
<!--其他字的顏色-->
<attr name="othetTextColor" format="color"></attr>
</declare-styleable>
然后就是在布局文件中申明我們的自定義view。
這樣,自定義View XiaoMiStep在xml布局文件里引用的時(shí)候,代碼如下:
<com.example.ahuang.viewandgroup.view.XiaoMiStep
android:id="@+id/xiaoMiStep"
android:layout_width="match_parent"
android:layout_height="wrap_content"
custom:backGroundColor="#0FA9C1"
custom:lineColor="#8ED8E5"
custom:othetTextColor="#8ED8E5"
custom:outerCircleColor="#8ED8E5"
custom:outerDotColor="#ffffff"
custom:ringColor="#8ED8E5"
custom:stepNumColor="#ffffff"/>
記得最后要引入我們的命名空間
xmlns:custom="http://schemas.android.com/apk/res-auto" 引入我們自定義的屬性
獲得atts.xml定義的屬性值
自定義View一般需要實(shí)現(xiàn)一下三個(gè)構(gòu)造方法,這三個(gè)構(gòu)造方法是一層調(diào)用一層的,屬于遞進(jìn)關(guān)系。因此,我們只需要在最后一個(gè)構(gòu)造方法中來(lái)獲得View的屬性了。
- 通過(guò)theme.obtainStyledAttributes()方法獲得自定義控件的主題樣式數(shù)組
- 就是遍歷每個(gè)屬性來(lái)獲得對(duì)應(yīng)屬性的值,也就是我們?cè)趚ml布局文件中寫(xiě)的屬性值
- 就是在循環(huán)結(jié)束之后記得調(diào)用array.recycle()來(lái)回收資源
public XiaoMiStep(Context context) {
this(context, null);
}
public XiaoMiStep(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public XiaoMiStep(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//獲得atts.xml定義的屬性值,存儲(chǔ)在TypedArray中
TypedArray ta = context.getTheme().obtainStyledAttributes(attrs, R.styleable.XiaoMiStep, defStyleAttr, 0);
int n = ta.getIndexCount();
for (int i = 0; i < n; i++) {
int attr = ta.getIndex(i);
switch (attr) {
case R.styleable.XiaoMiStep_backGroundColor: //背景顏色
background_color = ta.getColor(attr, Color.WHITE); //默認(rèn)為白色
break;
case R.styleable.XiaoMiStep_outerCircleColor: //最外側(cè)圓
outer_circle_color = ta.getColor(attr, Color.WHITE);
break;
case R.styleable.XiaoMiStep_outerDotColor: //最外側(cè)圓上的小圓點(diǎn)
outer_dot_color = ta.getColor(attr, Color.WHITE);
break;
case R.styleable.XiaoMiStep_lineColor: //最外側(cè)線的顏色
line_color = ta.getColor(attr, Color.WHITE);
break;
case R.styleable.XiaoMiStep_ringColor: //圓環(huán)的顏色
ring_color = ta.getColor(attr, Color.WHITE);
break;
case R.styleable.XiaoMiStep_stepNumColor: //步數(shù)的顏色
step_num_color = ta.getColor(attr, Color.WHITE);
break;
case R.styleable.XiaoMiStep_othetTextColor: //其他文字顏色
othet_text_color = ta.getColor(attr, Color.WHITE);
break;
}
}
ta.recycle();
init();
}
初始化畫(huà)筆
private void init() {
mPaint = new Paint(); //畫(huà)筆
mPaint.setAntiAlias(true);
arcPaint = new Paint(); //圓環(huán)畫(huà)筆
arcPaint.setAntiAlias(true);
textPaint = new Paint(); //文字畫(huà)筆
textPaint.setAntiAlias(true);
pointPaint = new Paint(); //點(diǎn)
pointPaint.setAntiAlias(true);
animSet = new AnimatorSet(); //動(dòng)畫(huà)組合
}
重寫(xiě)onMesure方法確定view大小
當(dāng)你沒(méi)有重寫(xiě)onMeasure方法時(shí)候,系統(tǒng)調(diào)用默認(rèn)的onMeasure方法:
@OverrideprotectedvoidonMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
這個(gè)方法的作用是:測(cè)量控件的大小。其實(shí)Android系統(tǒng)在加載布局的時(shí)候是由系統(tǒng)測(cè)量各子View的大小來(lái)告訴父View我需要占多大空間,然后父View會(huì)根據(jù)自己的大小來(lái)決定分配多大空間給子View。MeasureSpec的specMode模式一共有三種:
MeasureSpec.EXACTLY:父視圖希望子視圖的大小是specSize中指定的大??;一般是設(shè)置了明確的值或者是MATCH_PARENT
MeasureSpec.AT_MOST:子視圖的大小最多是specSize中的大?。槐硎咀硬季窒拗圃谝粋€(gè)最大值內(nèi),一般為WARP_CONTENT
MeasureSpec.UNSPECIFIED:父視圖不對(duì)子視圖施加任何限制,子視圖可以得到任意想要的大小;表示子布局想要多大就多大,很少使用。
想要設(shè)置WARP_CONTENT,只要重寫(xiě)onMeasure方法
另外,在onMeasure()方法里實(shí)現(xiàn)了動(dòng)畫(huà)效果。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width;
int height;
int widthMode = MeasureSpec.getMode(widthMeasureSpec); //寬度的測(cè)量模式
int widthSize = MeasureSpec.getSize(widthMeasureSpec); //寬度的測(cè)量值
int heightMode = MeasureSpec.getMode(heightMeasureSpec); //高度的測(cè)量模式
int heightSize = MeasureSpec.getSize(heightMeasureSpec); //高度的測(cè)量值
//如果布局里面設(shè)置的是固定值,這里取布局里面的固定值;如果設(shè)置的是match_parent,則取父布局的大小
if (widthMode == MeasureSpec.EXACTLY) {
width = widthSize;
} else {
//如果布局里面沒(méi)有設(shè)置固定值,這里取布局的寬度的1/2
width = widthSize * 1 / 2;
}
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else {
//如果布局里面沒(méi)有設(shè)置固定值,這里取布局的高度的3/4
height = heightSize * 3 / 4;
}
widthBg = width;
heightBg = height;
ra_out_circle = heightBg * 3 / 9;
ra_inner_circle = heightBg * 3 / 10;
line_length = 30;
setMeasuredDimension(width, height);
startAnim();
}
重寫(xiě)onDraw方法進(jìn)行繪畫(huà)
代碼已經(jīng)很詳細(xì)了。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//繪制底層背景
mPaint.setColor(background_color);
mPaint.setStyle(Paint.Style.FILL);
RectF rectF_back = new RectF(0, 0, widthBg, heightBg);
canvas.drawRect(rectF_back, mPaint);
//繪制最外層的圓
mPaint.setColor(outer_circle_color);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(3);
canvas.drawCircle(widthBg / 2, heightBg / 2, ra_out_circle, mPaint);
//繪制圓上的小圓點(diǎn)
pointPaint.setColor(outer_dot_color);
pointPaint.setStrokeWidth(10);
canvas.drawCircle((float) (widthBg / 2 + ra_out_circle * Math.cos(angle * 3.14 / 180)), (float) (heightBg / 2 + ra_out_circle * Math.sin(angle * 3.14 / 180)), 10, pointPaint);
//畫(huà)line
drawLines(canvas);
//畫(huà)圓弧
arcPaint.setStyle(Paint.Style.STROKE);
arcPaint.setStrokeWidth(30);
arcPaint.setColor(ring_color);
RectF arcRect = new RectF((widthBg / 2 - ra_inner_circle + line_length / 2), (heightBg / 2 - ra_inner_circle + line_length / 2), (widthBg / 2 + ra_inner_circle - line_length / 2), (heightBg / 2 + ra_inner_circle - line_length / 2));
canvas.drawArc(arcRect, -90, currentFootNumPre, false, arcPaint);
//繪制步數(shù)
textPaint.setColor(step_num_color);
textPaint.setStrokeWidth(25);
textPaint.setTextSize(widthBg / 6);
canvas.drawText(String.valueOf(currentFootNum), (widthBg / 3 - 50), heightBg / 2 + 50, textPaint);
textPaint.setStrokeWidth(10);
textPaint.setColor(othet_text_color);
textPaint.setTextSize(widthBg / 20);
canvas.drawText("步", (widthBg / 2 + 200), heightBg / 2 + 50, textPaint);
//繪制公里
currentDistance = (float) (myFootNum * 6.4 / 8000);
//小數(shù)點(diǎn)后一位
java.text.DecimalFormat df = new java.text.DecimalFormat("#.0");
String currentDis = df.format(currentDistance);
canvas.drawText(currentDis + "公里", (widthBg / 3 - 30), heightBg / 2 + 150, textPaint);
//中間豎線
mPaint.setStrokeWidth(8);
canvas.drawLine(widthBg / 2 + 10, heightBg / 2 + 110, widthBg / 2 + 10, heightBg / 2 + 155, mPaint);
//繪制卡路里
currentCal = myFootNum * 230 / 8000;
canvas.drawText(String.valueOf(currentCal) + "千卡", (widthBg / 2 + 40), heightBg / 2 + 150, textPaint);
}
private void drawLines(Canvas canvas) {
mPaint.setColor(line_color);
mPaint.setStrokeWidth(4);
for (int i = 0; i < 360; i++) {
canvas.drawLine(widthBg / 2, (heightBg / 2 - ra_inner_circle), widthBg / 2, (heightBg / 2 - ra_inner_circle + line_length), mPaint);
canvas.rotate(1, widthBg / 2, heightBg / 2);
}
}
默認(rèn)一圈代表8000步,6.4公里,230千卡,初始步數(shù)根據(jù)以下代碼設(shè)置。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_xiao_mi_setp);
ButterKnife.bind(this);
mXiaoMiStep.setMyFootNum(4500);
}
public void setMyFootNum(int myFootNum) {
this.myFootNum = myFootNum;
}
實(shí)現(xiàn)動(dòng)畫(huà)
主要是
外圓上的小圓點(diǎn)動(dòng)畫(huà),是根據(jù)角度確定。
步數(shù)動(dòng)畫(huà)在 0-myFootNum之間
畫(huà)圓弧的動(dòng)畫(huà)在 0-myFootNum * 360 / 8000
private void startAnim() {
//小圓點(diǎn)動(dòng)畫(huà)
final ValueAnimator dotAnimator =ValueAnimator.ofInt(-90, (myFootNum*360/8000-90));
dotAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
angle = (int) dotAnimator.getAnimatedValue();
postInvalidate();
}
});
dotAnimator.setInterpolator(new LinearInterpolator());
//步數(shù)動(dòng)畫(huà)實(shí)現(xiàn)
final ValueAnimator walkAnimator = ValueAnimator.ofInt(0, myFootNum);
walkAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
currentFootNum = (int) walkAnimator.getAnimatedValue();
postInvalidate();
}
});
//畫(huà)弧動(dòng)畫(huà)的實(shí)現(xiàn)
final ValueAnimator arcAnimator = ValueAnimator.ofInt(0, (myFootNum * 360 / 8000));
arcAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
currentFootNumPre = (int) arcAnimator.getAnimatedValue();
postInvalidate();
}
});
animSet.setDuration(3000);
animSet.playTogether(walkAnimator, arcAnimator, dotAnimator);
animSet.start();
}
效果圖如下所示,當(dāng)然,你也可以通過(guò)改變xml布局的custom選項(xiàng),來(lái)改變各個(gè)部分的顏色。
