原版效果分析

分析总共有如下几部分:
- 背景的深色大圆形
- 前景的浅色的小圆形
- 垂直的竖线
- 底部的椭圆形的底座
- loading 时转圈的弧形
还有一个就是 当停止loading的时候 有一个上下移动的抖动效果
实现效果:

code:
1: 定义xml属性:
<declare-styleable name="LoadingMarker">
<attr name="darkClor" format="color" />
<attr name="lightColor" format="color" />
<attr name="isLoading" format="boolean" />
</declare-styleable>
说明如下:
- darkColor: 深色大圆颜色,及竖线颜色
- lightColor: 浅色的小圆的颜色,及底部椭圆的颜色
- isLoading: 是否转圈圈
2: View的定义:
- 2.1: 构造方法,初始化需要的变量:
private void init(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
//拿到 color
TypedArray ta = context.getTheme().obtainStyledAttributes(attrs, R.styleable.LoadingMarker, defStyleAttr, 0);
darkColor = ta.getColor(R.styleable.LoadingMarker_darkClor, Color.BLACK);
lightColor = ta.getColor(R.styleable.LoadingMarker_lightColor, Color.BLUE);
isLoading = ta.getBoolean(R.styleable.LoadingMarker_isLoading, false);
ta.recycle();
log(String.format("darkColor -> %d lightColor -> %d black -> %d darkColor -> %d", darkColor, lightColor, Color.BLACK, Color.BLUE));
darkPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
darkPaint.setColor(darkColor);
darkPaint.setStrokeWidth(4);
lightPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
lightPaint.setColor(lightColor);
lightPaint.setStrokeWidth(4);
ovalPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
ovalPaint.setColor(lightColor);
ovalPaint.setStrokeWidth(6);
loadingAnim = ValueAnimator.ofFloat();
loadingAnim.setFloatValues(0f, 360f);
loadingAnim.setDuration(1000 * 1);
loadingAnim.setRepeatMode(ValueAnimator.RESTART);
loadingAnim.setRepeatCount(ValueAnimator.INFINITE);
loadingAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
startAngle = (Float) animation.getAnimatedValue();
log(String.format("startAngle -> %s", startAngle));
postInvalidate();
}
});
setLoading(isLoading);
}
可以看到,我们先拿到了xml里的自定义属性:darkColor lightColor isLoading
,
同时我们自定义了3个画笔,分别是
darkPaint 用来画背景的大圆形,
lightPaint 用来画前面的小圆形 ,
ovalPaint 用来画loading的弧形
接下来 定义了一个ValueAniator 用来生成 loading的圆弧的起始角度
- 2.2: 测量,View大小的确定:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int defWidth = (int) (DisplayUtil.getScreenWidth(getContext()) * 0.9f / 13.0f);
int defHeith = (int) (defWidth * 1.5f);
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 = defWidth;
}
if (heightMode == MeasureSpec.EXACTLY) {
mHeight = heightSize;
} else {
mHeight = defHeith;
}
log(String.format("mWidth -> %d mHeight -> %s", mWidth, mHeight));
setMeasuredDimension(mWidth, mHeight);
}
可以看到,当测量模式是不是EXACTLY(而是:AT_MOST 或者 UNSPECIFICED)时,我们规定了一个默认尺寸:
宽度是屏幕宽度的0.9/13,
高度是宽度的1.5倍,
这个可以在自己的使用过程中,按照UI的设计进行修改
- 2.3: 绘制View的内容:
@Override
protected void onDraw(final Canvas canvas) {
// super.onDraw(canvas);
//父类onDraw 空实现 注不注掉 都可
int measureWidth = getMeasuredWidth();
int measureHeight = getMeasuredHeight();
final int centerX = measureWidth / 2;
final int centerY = centerX;
//背景 大圆 半径
int radius = measureWidth / 2;
//前景 小圆 半径
int smallRadius = radius / 3;
//loading 弧形 半径
final int ovalRadius = radius * 2 / 3;
//底座 椭圆 半径
int ovalBotXRadius = smallRadius * 2 / 3;
int ovalBotYRadius = smallRadius / 3;
// log(String.format(
// "measureWidth -> %d measureHeight -> %d\n" +
// "centerX -> %d centerY -> %d\n" +
// "radius -> %d smallRadius -> %d", measureWidth, measureHeight, centerX, centerY, radius, smallRadius));
//底层实心 圆
darkPaint.setStyle(Paint.Style.FILL);
canvas.drawCircle(centerX, centerY, radius, darkPaint);
//上层 小圆
lightPaint.setStyle(Paint.Style.FILL);
canvas.drawCircle(centerX, centerY, smallRadius, lightPaint);
//画 底座
RectF ovalBot = new RectF(centerX - ovalBotXRadius, measureHeight - ovalBotYRadius * 2, centerX + ovalBotXRadius, measureHeight);
canvas.drawArc(ovalBot,0,360,true,lightPaint);
//画 竖线
canvas.drawLine(centerX, centerY * 2, centerX, measureHeight - ovalBotYRadius, darkPaint);
ovalPaint.setStyle(Paint.Style.STROKE);
// RectF oval = new RectF(centerX - ovalRadius, centerY - ovalRadius, centerX + ovalRadius, centerY + ovalRadius);
// canvas.drawArc(oval, 90, 180, false, ovalPaint);
if (isLoading) {
RectF oval = new RectF(centerX - ovalRadius, centerY - ovalRadius, centerX + ovalRadius, centerY + ovalRadius);
canvas.drawArc(oval, startAngle, 220, false, ovalPaint);
}
}
首先我们拿到 Viwe 的宽高,由于此时view已经经过了测量,因此,getMeasureWidth 和 getMeasureHeight 是可以取得值的.
然后我们计算出如下的值:
-
centerX: 大圆形的圆心x,为view宽度的一半
-
centerY: 大圆形的圆心y,同圆心的x坐标
-
radius: 大圆形的半径,为view宽度的一半
-
smallRadius: 前面小圆形的半径 为大圆形半径的1/3
-
ovalRadius: loading的圆弧的半径,为大圆半径的2/3
-
ovalBotXRadius: 底部小椭圆的长轴半径,为小圆形的半径的2/3
-
ovalBotYRadius: 底部小椭圆的短轴半径,为小圆形半径的1/3,即其长轴半径的一半
下面就是绘制的过程了:依次绘制了:
-
背景的实心大圆形
-
上层的小圆形
-
底座的小椭圆形
-
竖直的线
-
loading的圆弧
这里要注意,需要先画椭圆形底座,再画竖线,营造一个遮挡的关系 -
2.3: 如何实现的转圈圈:
通过标志位isLoading,并提供相应的set get 方法来控制loading的操作.
public boolean isLoading() {
return isLoading;
}
public void setLoading(boolean loading) {
isLoading = loading;
if (loading) {
if (!loadingAnim.isStarted()) {
loadingAnim.start();
}
} else {
loadingAnim.cancel();
ViewCompat.offsetTopAndBottom(this,-30);
postDelayed(new Runnable() {
@Override
public void run() {
ViewCompat.offsetTopAndBottom(LoadingMarker.this, 30);
}
}, 200);
}
postInvalidate();
}
如果想要进行loading动画,则loading = true,
半段了ValueAnimator 是否在执行,如果没有,则开始动画,
在动画进度的回调里,把动画当前进度,作为loading圆弧的起始角度,声明为全局变量
loadingAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
startAngle = (Float) animation.getAnimatedValue();
log(String.format("startAngle -> %s", startAngle));
postInvalidate();
}
});
然后通过postInvalidate(); 让View重新绘制
在onDraw()发放里
if (isLoading) {
RectF oval = new RectF(centerX - ovalRadius, centerY - ovalRadius, centerX + ovalRadius, centerY + ovalRadius);
canvas.drawArc(oval, startAngle, 220, false, ovalPaint);
}
通过这两行代码,完成了 圆弧的绘制.
停止loading时
public boolean isLoading() {
return isLoading;
}
public void setLoading(boolean loading) {
isLoading = loading;
if (loading) {
if (!loadingAnim.isStarted()) {
loadingAnim.start();
}
} else {
loadingAnim.cancel();
ViewCompat.offsetTopAndBottom(this,-30);
postDelayed(new Runnable() {
@Override
public void run() {
ViewCompat.offsetTopAndBottom(LoadingMarker.this, 30);
}
}, 200);
}
postInvalidate();
}
先停止了ValueAnimator,然后通过ViewCompatOffsetTopAndBottom(-30),让View整体上移30像素,随后又通过延迟post一个Runnable() 让view 下移30像素,这两个操作模拟一个刷新完毕抖动的想过.
当然也可以通过插值动画来进行一个更和谐美观的抖动.这个要留到以后慢慢完善了.
整体的实现大致如上,如有错误,请随时指出哈(手动滑稽...)
最后附上工程github地址
网友评论