本篇通过自定义View模拟一个物理现象——竖直平面内小球在最低点以一定初速度在重力作用下绕圆环做变速圆周运动的效果(从最低点减速到0时上升到最高点再加速到初始速度时回到最低点)。可以用于加载等待等场景,下面按照自定义View的步骤具体说明一下。
1、定义属性
为了能在布局文件中使用我们的自定义控件,定制其属性,我们需要自定义一些控件的属性,以供更灵活的使用此控件。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="ringColor" format="color"></attr>
<attr name="ringWidth" format="dimension"></attr>
<attr name="globuleColor" format="color"></attr>
<attr name="globuleRadius" format="dimension"></attr>
<attr name="cycleTime" format="float"></attr>
<declare-styleable name="AccelerateCircularView">
<attr name="ringColor" />
<attr name="ringWidth" />
<attr name="globuleColor" />
<attr name="globuleRadius" />
<attr name="cycleTime" />
</declare-styleable>
</resources>
这里定义了圆环颜色、圆环宽度、小球颜色、小球半径、小球旋转周期(用来设定小球旋转一周所用的时间)这些属性。并定义了属性的的取值类型(format)。
2、获取属性
自定义属性完成后,我们需要在自定义View的构造方法中逐一获取这些属性。
public AccelerateCircularView(Context context) {
this(context, null);
}
public AccelerateCircularView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public AccelerateCircularView(Context context, AttributeSet attrs,
int defStyle) {
super(context, attrs, defStyle);
TypedArray attrsArray = context.getTheme().obtainStyledAttributes(
attrs, R.styleable.AccelerateCircularView, defStyle, 0);
mRingColor = attrsArray.getColor(
R.styleable.AccelerateCircularView_ringColor, Color.GRAY);
mGlobuleColor = attrsArray.getColor(
R.styleable.AccelerateCircularView_globuleColor, Color.BLUE);
mRingWidth = attrsArray.getDimension(
R.styleable.AccelerateCircularView_ringWidth, TypedValue
.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1,
getResources().getDisplayMetrics()));
mGlobuleRadius = attrsArray.getDimension(
R.styleable.AccelerateCircularView_globuleRadius, TypedValue
.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 6,
getResources().getDisplayMetrics()));
mCycleTime = attrsArray.getFloat(
R.styleable.AccelerateCircularView_cycleTime, 3000);
attrsArray.recycle();
mPaint = new Paint();
}
这里要注意通过attrsArray.recycle()及时回收TypedArray ,避免浪费内存资源 。
2、重写onMeasure
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int mWidth , mHeight ;
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthMode == MeasureSpec.EXACTLY) {
mWidth = widthSize;
} else {
mWidth = 169;
if (widthMode == MeasureSpec.AT_MOST) {
mWidth = Math.min(mWidth, widthSize);
}
}
if (heightMode == MeasureSpec.EXACTLY) {
mHeight = heightSize;
} else {
mHeight = 169;
if (heightMode == MeasureSpec.AT_MOST) {
mHeight = Math.min(mWidth, heightSize);
}
}
setMeasuredDimension(mWidth, mHeight);
}
这里主要是处理当VIew设置为“wrap_content”时需要自己给出测量结果,否则系统默认给我们测量的结果将是"match_parent"的大小。
3、重写onDraw
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int central = Math.min(getWidth(), getHeight()) / 2;
mRingRadius = central - mGlobuleRadius;
if (mGlobuleRadius < mRingWidth / 2) {// 小球嵌在环里
mRingRadius = central - mRingWidth / 2;
}
mPaint.setStrokeWidth(mRingWidth);
mPaint.setStyle(Style.STROKE);
mPaint.setAntiAlias(true);
mPaint.setColor(mRingColor);
canvas.drawCircle(central, central, mRingRadius, mPaint);// 绘制圆环
mPaint.setStyle(Style.FILL);
mPaint.setAntiAlias(true);
mPaint.setColor(mGlobuleColor);
if (currentAngle == -1) {
startCirMotion();
}
drawGlobule(canvas, central);// 绘制小球
}
/**
* 绘制小球,起始位置为圆环最低点
*
* @param canvas
* @param central
*/
private void drawGlobule(Canvas canvas, float central) {
float cx = central + (float) (mRingRadius * Math.cos(currentAngle));
float cy = (float) (central + mRingRadius * Math.sin(currentAngle));
canvas.drawCircle(cx, cy, mGlobuleRadius, mPaint);
}
4、定义动画
/**
* 旋转小球
*/
private void startCirMotion() {
ValueAnimator animator = ValueAnimator.ofFloat(90f, 450f);//起始位置在最低点
animator.setDuration((long) mCycleTime).setRepeatCount(
ValueAnimator.INFINITE);
animator.addUpdateListener(new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
Float angle = (Float) animation.getAnimatedValue();
currentAngle = angle * Math.PI / 180;
invalidate();
}
});
// animator.setInterpolator(new LinearInterpolator());// 匀速旋转
// 自定义开始减速到0后加速到初始值的Interpolator
animator.setInterpolator(new TimeInterpolator() {
@Override
public float getInterpolation(float input) {
float output;
if (input < 0.5) {
output = (float) Math.sin(input * Math.PI) / 2;// 先加速
} else {
output = 1 - (float) Math.sin(input * Math.PI) / 2;// 后减速,最高点(中间)速度为0
}
return output;
}
});
animator.start();
}
这里通过自定义Interpolator来实现对动画进度变化快慢的控制,动画设置的值为小球的当前角度。初值90°保证小球从最低点开始运动。关于属性动画Interpolator的自定义实现可以参考一下 Android 属性动画探究(一)——Interpolator解析与自定义。以上便完成了View的定义,效果如下: