记得这个东西原来有个同事问过我,当时正在自学自定义View和属性动画这一块,对这个功能也没有写过,所以就google了一下,发了几个类似效果的github项目给朋友,今天礼拜天难得有心情写写代码,所以想想实现一下这个自定义View的效果。
首先,我们从这个gif的效果图中就可以得知这个自定义View我们需要哪些自定义属性,内部圆环的颜色、外部圆环的颜色、圆环的宽度、字体的大小、颜色,话不多说,直接撸码。
<!-- 运动圆环自定义属性 -->
<declare-styleable name="MotionCrcle">
<attr name="outerCrcleColor" format="color"></attr>
<attr name="innerCrcleColor" format="color"></attr>
<attr name="crcleTextColor" format="color"></attr>
<attr name="crcleTextSize" format="integer"></attr>
<attr name="crcleWidth" format="integer"></attr>
</declare-styleable>
/**
* Created by Administrator on 2018/5/13.
* 运动圆环自定义View
*/
public class MotionCrcle extends View {
/**
* 外部圆环颜色
*/
private int mOuterColor = Color.BLUE;
/**
* 里面圆环颜色
*/
private int mInnerColor = Color.RED;
/**
* 跑步数的文字大小
*/
private int mTextSize = 30;
/**
* 跑步文字的颜色
*/
private int mTextColor = Color.BLACK;
/**
* 圆环的宽度
*/
private int mCrcleWidth = 45;
private Paint mOuterPaint, mInnerPaint, mTextPaint;
private float mMaxSteps = 0;
private int mCurrentSteps = 0;
public MotionCrcle(Context context) {
this(context, null);
}
public MotionCrcle(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public MotionCrcle(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.MotionCrcle);
mOuterColor = array.getColor(R.styleable.MotionCrcle_outerCrcleColor, mOuterColor);
mInnerColor = array.getColor(R.styleable.MotionCrcle_innerCrcleColor, mInnerColor);
mCrcleWidth = array.getInt(R.styleable.MotionCrcle_crcleWidth, mCrcleWidth);
mTextColor = array.getColor(R.styleable.MotionCrcle_crcleTextColor, mTextColor);
mTextSize = array.getInt(R.styleable.MotionCrcle_crcleTextSize, mTextSize);
initPaint();
array.recycle();
}
public synchronized void setmMaxSteps(float mMaxSteps) {
this.mMaxSteps = mMaxSteps;
}
public synchronized void setmCurrentSteps(int mCurrentSteps) {
this.mCurrentSteps = mCurrentSteps;
invalidate();
}
private void initPaint() {
mOuterPaint = new Paint();
mOuterPaint.setAntiAlias(true);
mOuterPaint.setStrokeWidth(mCrcleWidth);
mOuterPaint.setStrokeCap(Paint.Cap.ROUND);
mOuterPaint.setStyle(Paint.Style.STROKE);
mOuterPaint.setColor(mOuterColor);
mInnerPaint = new Paint();
mInnerPaint.setAntiAlias(true);
mInnerPaint.setStrokeWidth(mCrcleWidth);
mInnerPaint.setStrokeCap(Paint.Cap.ROUND);
mInnerPaint.setStyle(Paint.Style.STROKE);
mInnerPaint.setColor(mInnerColor);
mTextPaint = new Paint();
mTextPaint.setAntiAlias(true);
mTextPaint.setTextSize(mTextSize);
mTextPaint.setColor(mTextColor);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
if(width != height){
/**
* 因为运动圆环是一个正方形,所以我们要设置一个最小值作为View的长度
*/
int min = Math.min(width, height);
widthMeasureSpec = MeasureSpec.makeMeasureSpec(min, MeasureSpec.EXACTLY);
heightMeasureSpec = MeasureSpec.makeMeasureSpec(min, MeasureSpec.EXACTLY);
setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
}
}
@Override
protected void onDraw(Canvas canvas) {
//因为圆环本身是具有宽度的,所以我们这里左边的顶点要往右mCrcleWidth个距离,右边底部的点要往左,所以要减去mCrcleWidth
RectF recf = new RectF(mCrcleWidth, mCrcleWidth,
getWidth() - mCrcleWidth, getHeight() - mCrcleWidth);
//135是外部圆环的初始角度,270是所画的总角度数
canvas.drawArc(recf, 135, 270, false, mOuterPaint);
if(mCurrentSteps == 0){
return;
}
//算出当前步数是最大步数的比值
float angle = mCurrentSteps / mMaxSteps;
canvas.drawArc(recf, 135, 270 * angle, false, mInnerPaint);
String stepText = mCurrentSteps+"";
Rect bounds = new Rect();
//算出步数文字的baseLine,也就是基准线
mTextPaint.getTextBounds(stepText, 0, stepText.length(), bounds);
int x = getWidth() / 2 - bounds.width() / 2;
Paint.FontMetricsInt metrices = mTextPaint.getFontMetricsInt();
int diffY = (metrices.bottom - metrices.top) / 2 - metrices.bottom;
int baseLine = getHeight() / 2 + diffY;
canvas.drawText(stepText, x, baseLine, mTextPaint);
}
}
文字的绘制和画圆画弧不一样,其实仔细想想也明白,如果绘制按照左上角开始的话是不现实的,因为文字不可能是简单的顶部或底部对其,应该是重心对齐,简单说就是基准线,所以代码中基准线的算法是bottom-top再除以2减去bottom,以基准线开始绘制,top就是负数,bottom是正数。
public class MainActivity extends AppCompatActivity {
private Button mBtn;
private baohuo.rzj.com.myapplication.Views.MotionCrcle mCrcle;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mBtn = (Button) findViewById(R.id.btn);
mCrcle = (MotionCrcle) findViewById(R.id.motion_crcle);
mCrcle.setmMaxSteps(10000);
final ValueAnimator animator = ObjectAnimator.ofFloat(0,4500);
animator.setDuration(2000);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
float value = (float) valueAnimator.getAnimatedValue();
mCrcle.setmCurrentSteps((int) value);
}
});
mBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
animator.start();
}
});
}
}
以上是Activity的代码,非常简单,不做解释,这个值得一提的是,我在写的时候内部圆环一直没有绘制,文字在不断变化,通过debug发现,我把步数最大值和当前步数设置为int,一个小的int除以大的int的到的只有0,所以我把最大步数改为float,这样得出的值就有小数位,最后附上xml代码。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="baohuo.rzj.com.myapplication.MainActivity">
<Button
android:id="@+id/btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="30dp"
android:text="运动圆环开启"
android:gravity="center"/>
<baohuo.rzj.com.myapplication.Views.MotionCrcle
android:id="@+id/motion_crcle"
android:layout_below="@+id/btn"
android:layout_width="200dp"
android:layout_height="200dp"
app:outerCrcleColor="@color/colorPrimary"
app:innerCrcleColor="@color/colorAccent"
app:crcleTextColor="@color/colorPrimaryDark"
app:crcleTextSize="40"
app:crcleWidth="20"/>
</RelativeLayout>