一、前言
雷达扫描动画是比较常见的动画,其绘制难点不在于旋转,而在于着色和收放处理,本篇会进行一次实践。
二、实现
利用不同的Animator去进行组合,我们这里定义了3种不同的Animator,并且通过不同的组合形式实现。另外我们这里实现了图标的圆形布局,这个和我们之前的转盘菜单类似,只不过这次使用的ViewGroup,通过onLayout布局而不是通过onDraw绘制。我们需要对Layout的测量和布局进行学习
2.1 测量
我们这里测量,目的是保证每个icon大小一致,防止有的大有的小的情况,毕竟在一些情况下,Drawable会影响到View的默认尺寸。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(0, widthMeasureSpec), getDefaultSize(0, heightMeasureSpec));
widthMeasureSpec = heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(getMeasuredWidth(),
View.MeasureSpec.EXACTLY);
//正方形布局
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = getMeasuredWidth();
int height = getMeasuredHeight();
outS = width / 2; // 188/125 = width/S => S = width/(188/125) = (125*width)/188
centerS = (int) (((125 * width) / 188f) / 2);
innerS = (int) (((62 * width) / 188f) / 2);
pointInner = (int) (((10 * width) / 188f) / 2);
centerX = width / 2;
centerY = height / 2;
int count = getChildCount();
if (count <= 1) {
return;
}
// 计算ItemView大小,保证View分布咋圆上
int childMaxWidth = (int) ((outS - innerS) * 1f / 2);
if (childMaxWidth <= 0) {
childMaxWidth = (int) (dp2px(5) * 2);
}
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(MeasureSpec.makeMeasureSpec(childMaxWidth, MeasureSpec.EXACTLY),
getPaddingLeft() + getPaddingRight() +
lp.leftMargin + lp.rightMargin,
lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(MeasureSpec.makeMeasureSpec(childMaxWidth, MeasureSpec.EXACTLY),
getPaddingTop() + getPaddingBottom() +
lp.topMargin + lp.bottomMargin,
lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
2.2 布局
这里我们主要实现的逻辑是保证每个View都能分布到圆上,我们计算出View按45度角的区域分配空间。
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
int width = getMeasuredWidth();
int height = getMeasuredHeight();
if (width == 0 || height == 0) {
return;
}
int centerX = width / 2;
int centerY = height / 2;
float outlineLength = (float) (Math.cos(Math.toRadians(45)) * width / 2);
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
ImageView child = (ImageView) getChildAt(i);
float perAngle = 360f / childCount;
float angle = (i % childCount) * perAngle;
float r = outlineLength * 9 / 10;
float dx = (float) Math.sin(Math.toRadians(angle)) * r;
float dy = (float) Math.cos(Math.toRadians(angle)) * r;
float center_x = centerX + dx;
float center_y = centerY + dy;
IconItem iconItem = new IconItem(child, angle);
mIcons.add(iconItem);
child.layout((int) (center_x - child.getMeasuredWidth() / 2), (int) (center_y - child.getMeasuredHeight() / 2), (int) (center_x + child.getMeasuredWidth() / 2), (int) (center_y + child.getMeasuredHeight() / 2));
}
}
2.3 动画
本篇我们使用了大量的属性动画,属性动画是按序列执行的,当然,这里一定要处理好动画的顺序问题。
2.4 完整逻辑
下面是完整代码,这里我们最多只允许6个icon展示,因此一定要清楚的是,子View的数量不能超过6个,当然,你也可以扩展更多的icon数量。
public class RadarScanAppViewLayout extends FrameLayout {
private int outS;
private int centerS;
private int innerS;
private int pointInner;
private int centerX;
private int centerY;
private float pointerAngle = 0;
private boolean isInit = false;
private int MAX_ICON = 6;
private Paint mCirclePaint;
private Paint mDotPaint;
private Paint mPointerPaint;
private Paint mPointBitmapPaint;
private Bitmap mBitmapPointer;
private Rect mRectPointer;
private float mCircle0Scale = 1;
private float mCircle1Scale = 1;
private float mCircle2Scale = 1;
private float mCircle3Scale = 1;
private float mPointerLenRatio = 0;
private float mPointerAngle = 45;
private float mDismissFraction;
private boolean mbPointerLooperEnd;
private boolean isFirstLoadAppIcon = true;
private ArrayList<IconItem> mIcons = new ArrayList<IconItem>();
private ArrayList<Drawable> mAllIconDrawable = new ArrayList<Drawable>();
private int fromIndex = 0;
private int rotateRoundCount = 0;
private class IconItem {
private final float cur_angle;
private boolean hide;
private Drawable icon;
private long startDismissT = 0;
private ImageView iconView;
public IconItem(ImageView iconView, float angle) {
this.iconView = iconView;
this.cur_angle = angle;
}
private boolean isInit() {
return this.icon != null;
}
public void setIcon(Drawable iconDrawable) {
this.icon = iconDrawable;
this.iconView.setImageDrawable(icon);
}
}
public RadarScanAppViewLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context);
}
public RadarScanAppViewLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public RadarScanAppViewLayout(Context context) {
super(context);
init(context);
}
private void init(Context context) {
setWillNotDraw(false);
mCirclePaint = new Paint();
mCirclePaint.setAntiAlias(true);
mCirclePaint.setColor(0xffFFEAC4);
mCirclePaint.setStyle(Paint.Style.FILL);
mDotPaint = new Paint();
mDotPaint.setAntiAlias(true);
mDotPaint.setColor(0xffFFEAC4);
mDotPaint.setStyle(Paint.Style.FILL);
mPointerPaint = new Paint();
mPointerPaint.setAntiAlias(true);
mPointerPaint.setColor(0xffFFEAC4);
mPointerPaint.setStrokeWidth(dp2px(1.5f));
mPointerPaint.setStyle(Paint.Style.FILL);
mBitmapPointer = BitmapFactory.decodeResource(context.getResources(), R.mipmap.icon_radar_pointer);
mPointBitmapPaint = new Paint();
mPointBitmapPaint.setAntiAlias(true);
for (int i = 0; i < MAX_ICON; i++) {
ImageView imageView = createImageView();
if (imageView != null) {
addView(imageView);
}
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (!isInit) {
return;
}
mRectPointer = new Rect(centerX - outS, centerY - outS, centerX + outS, centerY + outS);
drawCircle(canvas);
updateIcons();
}
private float dp2px(float dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
}
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
if (!isInit) {
return;
}
drawPointer(canvas);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
int width = getMeasuredWidth();
int height = getMeasuredHeight();
if (width == 0 || height == 0) {
return;
}
int centerX = width / 2;
int centerY = height / 2;
float outlineLength = (float) (Math.cos(Math.toRadians(45)) * width / 2);
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
ImageView child = (ImageView) getChildAt(i);
float perAngle = 360f / childCount;
float angle = (i % childCount) * perAngle;
float r = outlineLength * 9 / 10;
float dx = (float) Math.sin(Math.toRadians(angle)) * r;
float dy = (float) Math.cos(Math.toRadians(angle)) * r;
float center_x = centerX + dx;
float center_y = centerY + dy;
IconItem iconItem = new IconItem(child, angle);
mIcons.add(iconItem);
child.layout((int) (center_x - child.getMeasuredWidth() / 2), (int) (center_y - child.getMeasuredHeight() / 2), (int) (center_x + child.getMeasuredWidth() / 2), (int) (center_y + child.getMeasuredHeight() / 2));
}
}
private ImageView createImageView() {
ImageView imageView = new ImageView(this.getContext());
FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
imageView.setLayoutParams(lp);
return imageView;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(0, widthMeasureSpec), getDefaultSize(0, heightMeasureSpec));
widthMeasureSpec = heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(getMeasuredWidth(),
View.MeasureSpec.EXACTLY);
//正方形布局
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = getMeasuredWidth();
int height = getMeasuredHeight();
outS = width / 2; // 188/125 = width/S => S = width/(188/125) = (125*width)/188
centerS = (int) (((125 * width) / 188f) / 2);
innerS = (int) (((62 * width) / 188f) / 2);
pointInner = (int) (((10 * width) / 188f) / 2);
centerX = width / 2;
centerY = height / 2;
int count = getChildCount();
if (count <= 1) {
return;
}
int childMaxWidth = (int) ((outS - innerS) * 1f / 2);
if (childMaxWidth <= 0) {
childMaxWidth = (int) (dp2px(5) * 2);
}
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(MeasureSpec.makeMeasureSpec(childMaxWidth, MeasureSpec.EXACTLY),
getPaddingLeft() + getPaddingRight() +
lp.leftMargin + lp.rightMargin,
lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(MeasureSpec.makeMeasureSpec(childMaxWidth, MeasureSpec.EXACTLY),
getPaddingTop() + getPaddingBottom() +
lp.topMargin + lp.bottomMargin,
lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
private void updateIcons() {
if (mIcons == null) return;
int iconNum = mIcons.size();
for (int i = 0; i < iconNum; i++) {
IconItem item = mIcons.get(i);
if (!item.isInit()) {
continue;
}
if (mbPointerLooperEnd) {
float hide_process = mDismissFraction;
if (hide_process > 0 && hide_process <= 1f) {
float alpha = 0.7f * (1 - hide_process);
item.iconView.setAlpha((int) (alpha * 255));
}
}else{
item.iconView.setAlpha((int) ( 255));
}
}
}
public void showRadarAnimation() {
if (isInit) {
return;
}
isInit = true;
enterAnimation();
}
private void updateRardarIcons(List<Drawable> drawableList) {
if (null == drawableList || drawableList.isEmpty()) {
return;
}
Log.d("logView", "updateRardarIcons allSize=" + drawableList.size());
for (int i = 0; i < drawableList.size(); i++) {
if (MAX_ICON <= i) {
break;
}
Drawable drawable = drawableList.get(i);
if (drawable == null) {
continue;
}
IconItem iconItem = null;
if (mIcons.size() > i) {
iconItem = mIcons.get(i);
iconItem.setIcon(drawable);
}
}
}
private void switchRardarIcons() {
post(new Runnable() {
@Override
public void run() {
doSwitchIcons();
}
});
}
private void doSwitchIcons() {
int size = mAllIconDrawable.size();
Log.d("logView", "switchRardarIcons allSize=" + size + ",fromIndex=" + this.fromIndex);
if (size <= 0) {
return;
}
if (mbPointerLooperEnd) {
Log.d("logView", "switchRardarIcons 开始结束动画,不允许切换icon");
return;
}
if (this.fromIndex >= size) {
Log.d("logView", "所有icon都已经展示了");
return;
}
if ((size - this.fromIndex) < MAX_ICON) {
List<Drawable> bmplist = new ArrayList<>();
for (int i = size - 1; i >= 0; i--) {
if (bmplist.size() >= MAX_ICON) {
break;
}
bmplist.add(mAllIconDrawable.get(i));
}
this.fromIndex = size;
updateRardarIcons(bmplist);
} else {
List<Drawable> bmplist = new ArrayList<>();
for (int i = this.fromIndex; i < (this.fromIndex + MAX_ICON); i++) {
if (bmplist.size() >= MAX_ICON) {
break;
}
bmplist.add(mAllIconDrawable.get(i));
}
updateRardarIcons(bmplist);
this.fromIndex += MAX_ICON;
}
}
public void addRadarIcons(List<Drawable> drawables) {
int sumSize = mAllIconDrawable.size();
if (sumSize >= MAX_ICON * 6) {
return;
}
Log.d("logView", "addRadarIcons size=" + sumSize + ",bitmaps.size=" + drawables.size());
if (null != drawables && !drawables.isEmpty()) {
for (int i = 0; i < drawables.size(); i++) {
Drawable drawable = drawables.get(i);
if (drawable == null) {
continue;
}
mAllIconDrawable.add(drawable);
}
}
if (isFirstLoadAppIcon && mAllIconDrawable.size() > 0) {
this.fromIndex = 0;
isFirstLoadAppIcon = false;
switchRardarIcons();
}
}
public void recycle() {
if (mAllIconDrawable == null) return;
mAllIconDrawable.clear();
}
private void drawCircle(Canvas canvas) {
mCirclePaint.setAlpha((int) (0.1 * 255));
canvas.drawCircle(centerX, centerY, mCircle1Scale * outS, mCirclePaint);
canvas.drawCircle(centerX, centerY, mCircle2Scale * centerS, mCirclePaint);
canvas.drawCircle(centerX, centerY, mCircle3Scale * innerS, mCirclePaint);
canvas.drawCircle(centerX, centerY, mCircle0Scale * pointInner, mDotPaint);
}
private void drawPointer(Canvas canvas) {
if (pointerAngle > 0 || mPointerLenRatio > 0) {
canvas.save();
canvas.rotate(180 + pointerAngle, centerX, centerY);
canvas.drawLine(centerX, centerY, centerX, (centerY + outS * mPointerLenRatio), mPointerPaint);
canvas.restore();
if (mPointerLenRatio > 0.3f) {
float alpha = mPointerLenRatio;
canvas.save();
canvas.rotate(pointerAngle, centerX, centerY);
mPointBitmapPaint.setAlpha((int) (alpha * 255));
canvas.drawBitmap(mBitmapPointer, null, mRectPointer, mPointBitmapPaint);
canvas.restore();
}
if (mbPointerLooperEnd) {
mPointerAngle = pointerAngle;
if (mPointerAngle > 360) {
mPointerAngle = -360 + mPointerAngle;
}
}
}
}
private void pointerLooperControl() {
final ValueAnimator sweep = ObjectAnimator.ofFloat(0f, 360f);
sweep.setInterpolator(new LinearInterpolator());
sweep.setDuration(1000);
sweep.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
pointerAngle = (Float) valueAnimator.getAnimatedValue();
postInvalidate();
}
});
sweep.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
}
@Override
public void onAnimationEnd(Animator animator) {
if (!mbPointerLooperEnd) {
pointerLooperControl();
notifySwitchIcon();
} else {
pointerDismissControl();
}
}
@Override
public void onAnimationCancel(Animator animator) {
}
@Override
public void onAnimationRepeat(Animator animator) {
}
});
sweep.start();
}
private void notifySwitchIcon() {
if (rotateRoundCount > 0 && rotateRoundCount % 3 == 0) {
switchRardarIcons();
}
rotateRoundCount++;
if (rotateRoundCount > (Integer.MAX_VALUE - 3)) {
rotateRoundCount = 0;
}
Log.d("logView", "notifySwitchIcon rotateRoundCount=" + rotateRoundCount);
}
private void pointerShowControl() {
final AnimatorSet set = new AnimatorSet();
ValueAnimator step1 = ObjectAnimator.ofFloat(0.0f, 1f);
step1.setInterpolator(new AccelerateInterpolator());
step1.setDuration(300);
step1.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mPointerLenRatio = (Float) valueAnimator.getAnimatedValue();
postInvalidate();
}
});
set.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
}
@Override
public void onAnimationEnd(Animator animator) {
if (mbPointerLooperEnd) {
pointerDismissControl();
} else {
pointerLooperControl();
}
}
@Override
public void onAnimationCancel(Animator animator) {
}
@Override
public void onAnimationRepeat(Animator animator) {
}
});
set.playSequentially(step1);
set.start();
}
private void pointerDismissControl() {
final AnimatorSet set = new AnimatorSet();
final ValueAnimator sweep = ObjectAnimator.ofFloat(0f, 360f);
sweep.setInterpolator(new LinearInterpolator());
sweep.setDuration(1000);
sweep.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
pointerAngle = (Float) valueAnimator.getAnimatedValue();
mDismissFraction = valueAnimator.getAnimatedFraction();
invalidate();
}
});
ValueAnimator dismiss = ObjectAnimator.ofFloat(1f, 0f);
dismiss.setDuration(500);
dismiss.setInterpolator(new DecelerateInterpolator());
dismiss.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mPointerLenRatio = (Float) valueAnimator.getAnimatedValue();
invalidate();
}
});
set.playSequentially(sweep, dismiss);
set.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
}
@Override
public void onAnimationEnd(Animator animator) {
dismissCircleControl();
}
@Override
public void onAnimationCancel(Animator animator) {
}
@Override
public void onAnimationRepeat(Animator animator) {
}
});
set.start();
}
private void enterAnimation() {
showCircleControl();
}
private void showCircleControl() {
pointerShowControl();
}
private void dismissCircleControl() {
long duration = 300;
AnimatorSet set = new AnimatorSet();
ValueAnimator circle0 = ObjectAnimator.ofFloat(1f, 0f);
circle0.setInterpolator(new AccelerateInterpolator());
circle0.setDuration(duration);
circle0.setStartDelay(duration * 3 / 4);
circle0.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mCircle0Scale = (Float) valueAnimator.getAnimatedValue();
invalidate();
}
});
ValueAnimator circle1 = ObjectAnimator.ofFloat(1f, 0f);
circle1.setInterpolator(new AccelerateInterpolator());
circle1.setDuration(duration);
circle1.setStartDelay(duration / 2);
circle1.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mCircle1Scale = (Float) valueAnimator.getAnimatedValue();
invalidate();
}
});
ValueAnimator circle2 = ObjectAnimator.ofFloat(1f, 0f);
circle2.setDuration(duration);
circle2.setStartDelay(duration / 4);
circle2.setInterpolator(new AccelerateInterpolator());
circle2.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mCircle2Scale = (Float) valueAnimator.getAnimatedValue();
invalidate();
}
});
ValueAnimator circle3 = ObjectAnimator.ofFloat(1f, 0f);
circle3.setDuration(duration);
circle3.setInterpolator(new DecelerateInterpolator());
circle3.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mCircle3Scale = (Float) valueAnimator.getAnimatedValue();
invalidate();
}
});
set.playTogether(circle0, circle1, circle2, circle3);
set.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
}
@Override
public void onAnimationEnd(Animator animator) {
if (mAnimationListener != null) {
mAnimationListener.onRadarAnimationEnd();
}
}
@Override
public void onAnimationCancel(Animator animator) {
}
@Override
public void onAnimationRepeat(Animator animator) {
}
});
set.start();
}
public IAnimListener mAnimationListener;
public void setListener(IAnimListener listener) {
mAnimationListener = listener;
}
public void setSweepEnd() {
mbPointerLooperEnd = true;
}
public static interface IAnimListener {
public void onRadarAnimationEnd();
}
}
三、用法
具体用法也是很简单
-
调用addRadarIcons添加drawable
-
showRadarAnimation 开始动画
-
setSweepEnd 停止动画循环
四、总结
本篇其实主要学习的是测量、布局和动画,实际上本篇作为典型案例,指定View大小的方式,实现View大小一致,然后通过圆周角进行布局,最终通过动画组合展示。