一、先看效果图
第二张图是测试显示结果,第一张图是网上找的,效果大概就和第一张最后的那个效果一样。
二、自定义View的套路
1. 自定义属性
2. 测量控件的宽高
3. 摆放控件的位置
4. 绘制控件
5. 用户交互(事件处理)
三、分析卫星菜单特有的属性
1. 控件是弧形摆放,所以要有一个弧形的半径。
2. 控件弧形摆放,从第一个摆放到最后一个要有一个角度。
3. 控件展开的背景色。
四、定义卫星菜单特有的属性
<declare-styleable name="ArcLayout">
<!--摆放的角度:0-180度 -->
<attr name="arcAngle" format="float" />
<!--摆放的半径-->
<attr name="arcRadius" format="dimension" />
<!--子菜单展开的背景色-->
<attr name="arcSpreadColor" format="color" />
</declare-styleable>
五、创建自定义卫星菜单ArcLayout,并获取特有属性
public class ArcLayout extends ViewGroup implements View.OnClickListener {
// 摆放的总角度
private float mArcAngle = 100f;
// 摆放的半径
private float mArcRadius = 120f;
// 展开的背景色
private int mArcSpreadColor = Color.TRANSPARENT;
// 开始的角度
private float mStartAngle;
public ArcLayout(Context context) {
this(context, null);
}
public ArcLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ArcLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.ArcLayout);
mArcAngle = array.getFloat(R.styleable.ArcLayout_arcAngle, mArcAngle);
mArcRadius = array.getDimension(R.styleable.ArcLayout_arcRadius, dip2px(mArcRadius));
mArcSpreadColor = array.getColor(R.styleable.ArcLayout_arcSpreadColor, mArcSpreadColor);
// 计算开始的角度
// rad 是弧度, deg 是角度
// rad(弧度) = deg(角度) * PI / 180
mStartAngle = (float) ((180 - mArcAngle) / 2 * (Math.PI / 180));
array.recycle();
// 设置背景色
setBackgroundColor(mArcSpreadColor);
mBackground = getBackground();
if (mBackground != null) {
mBackground.setAlpha(0);
}
}
private float dip2px(float value) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value, getResources().getDisplayMetrics());
}
}
六、测量子卫星菜单View的大小 和计算每个子菜单偏移平分的角度
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
// 测量子控件的宽高
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 计算平分的角度
mSquareAngle = (float) (mArcAngle / (childCount - 2) * (Math.PI / 180));
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
// 获取控件的宽高
mWidth = w;
mHeight = h;
}
七、卫星菜单控件的摆放
先看摆放示意图
其中p1x 、p1y是子控件的左上角的坐标。
子控件的摆放分两步走:
- 摆放底部中间的子控件
- 摆放弧形位置的子菜单控件
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (changed) {
layoutCenter();
layoutMenu();
}
}
/**
* 摆放第一个控件
*/
private void layoutCenter() {
mCenterChild = getChildAt(0);
// 第一个子控件的宽高
int width = mCenterChild.getMeasuredWidth();
int height = mCenterChild.getMeasuredHeight();
int l = mWidth / 2 - width / 2;
int t = mHeight - height-getPaddingBottom();
mCenterChild.layout(l, t, l + width, t + height);
// 第一个子控件的点击事件
mCenterChild.setOnClickListener(this);
}
/**
* 摆放子菜单
*/
private void layoutMenu() {
int childCount = getChildCount();
if (childCount < 2) {
return;
}
for (int i = 1; i < childCount; i++) {
View child = getChildAt(i);
// 子控件的宽高
int width = child.getMeasuredWidth();
int height = child.getMeasuredHeight();
float Q = mStartAngle + mSquareAngle * (i - 1);
// left = 宽度的一半 - x - 子控件宽度的一半
// top = 高度 - y - 子控件高度的一半
int cl = (int) (mWidth / 2 - Math.cos(Q) * mArcRadius - width / 2);
int ct = (int) (mHeight - Math.sin(Q) * mArcRadius - height / 2-getPaddingBottom());
// 摆放子菜单
child.layout(cl, ct, cl + width, ct + height);
// 默认隐藏子菜单
child.setVisibility(GONE);
final int position = i;
// 子菜单的点击事件
child.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
menuItemClick(v, position);
}
});
}
}
其实开始的时候,控件都全部摆放好,默认将子控件都隐藏。
八、处理底部中间的子控件的点击事件
1. 点击后,记录点击状态,如果菜单隐藏,就展开,如果是展开就隐藏。
2. 创建隐藏和打开子菜单的动画
3. 点击后改变菜单的状态
4. 改变背景色
5. 如果动画执行就屏蔽点击事件等一些细节处理
// 默认关闭
private Status mStatus = Status.CLOSE;
// 菜单的状态
public enum Status {
CLOSE, OPEN
}
/**
* 关闭或者打开菜单
*/
public void toggle() {
// 中间主要的View设置旋转动画
Animation animation = ArcAnimUtil.rotateCenterAnim(mDuration);
mCenterChild.startAnimation(animation);
// 背景动画
bgAnim();
// 中间子View设置平移和旋转动画
int childCount = getChildCount();
for (int i = 1; i < childCount; i++) {
final View child = getChildAt(i);
child.setVisibility(VISIBLE);
// 子控件的高
int height = child.getMeasuredHeight();
float Q = mStartAngle + mSquareAngle * (i - 1);
// 离当前View X坐标上的距离
int fromXDelta = (int) (Math.cos(Q) * mArcRadius);
// 离当前View Y坐标上的距离
int fromYDelta = (int) (Math.sin(Q) * mArcRadius - height / 2-getPaddingBottom());
// 动画
Animation anim = null;
if (mStatus == Status.CLOSE) {
// 展开的动画
anim = ArcAnimUtil
.menuAnim(fromXDelta, 0, fromYDelta, 0, i, mDuration);
child.setEnabled(true);
} else {
// 关闭的动画
anim = ArcAnimUtil
.menuAnim(0, fromXDelta, 0, fromYDelta, i, mDuration);
child.setEnabled(false);
}
anim.setAnimationListener(new AnimationAdapter() {
@Override
public void onAnimationStart(Animation animation) {
mIsStart = true;
}
@Override
public void onAnimationEnd(Animation animation) {
mIsStart = false;
if (mStatus == Status.CLOSE) {
child.clearAnimation();
child.setVisibility(GONE);
// 关闭的背景设置为透明
setClickable(false);
} else {
// 设置背景
setClickable(true);
}
}
});
child.startAnimation(anim);
}
// 改变菜单的状态
changeStatus();
}
/**
* 背景动画
*/
private void bgAnim() {
// 背景动画
ValueAnimator valueAnimator = null;
if (mStatus == Status.CLOSE) {
// 展开
valueAnimator = ObjectAnimator.ofFloat(0, 1);
} else {
// 关闭
valueAnimator = ObjectAnimator.ofFloat(1, 0);
}
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float value = (float) animation.getAnimatedValue();
int alpha = (int) (value * 255);
if (mBackground != null) {
mBackground.setAlpha(alpha);
}
}
});
valueAnimator.setDuration(mDuration);
valueAnimator.start();
}
/**
* 改变菜单的状态
*/
private void changeStatus() {
mStatus = mStatus == Status.OPEN ? Status.CLOSE : Status.OPEN;
}
九、处理子菜单的点击事件
1. 执行用户回调监听
2. 执行子菜单的点击动画
3. 执行背景动画
4. 改变菜单的状态
/**
* 子菜单的点击事件
*/
private void menuItemClick(View view, int position) {
if (mIsStart) {
return;
}
if (mOnMenuItemClickListener != null) {
mOnMenuItemClickListener.onMenuItemClick(view, position);
}
// 设置控件不能点击
setClickable(false);
int childCount = getChildCount();
for (int i = 1; i < childCount; i++) {
View child = getChildAt(i);
Animation animation = null;
if (i == position) {
animation = ArcAnimUtil
.menuItemAnim(1f, 1.2f, 1f, 1.2f, mDuration);
} else {
animation = ArcAnimUtil
.menuItemAnim(1f, 0f, 1f, 0f, mDuration);
}
animation.setAnimationListener(new AnimationAdapter() {
@Override
public void onAnimationStart(Animation animation) {
mIsStart = true;
}
@Override
public void onAnimationEnd(Animation animation) {
mIsStart = false;
}
});
child.startAnimation(animation);
child.setEnabled(false);
}
// 背景动画
bgAnim();
// 改变当前状态
changeStatus();
}
十、动画的帮助类
/**
* 卫星布局--动画的工具类
*/
public class ArcAnimUtil {
/**
* 旋转360度
*/
public static Animation rotateCenterAnim(int duration) {
RotateAnimation ta = new RotateAnimation(0, 360,
Animation.RELATIVE_TO_SELF, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5f);
ta.setDuration(duration);
ta.setFillAfter(true);
return ta;
}
public static Animation menuAnim(float fromXDelta, float toXDelta, float fromYDelta, float toYDelta, int position, int duration) {
AnimationSet set = new AnimationSet(true);
// romXDelta:动画开始前,离当前View X坐标上的距离。
// toXDelta:动画结束后,离当前View X坐标上的距离。
// fromYDelta:动画开始前,离当前View Y坐标上的距离。
// toYDelta:动画结束后,离当前View Y坐标上的距离。
Animation transAnim = new TranslateAnimation(fromXDelta, toXDelta, fromYDelta, toYDelta);
;
transAnim.setStartOffset((position * 50));
RotateAnimation ta = new RotateAnimation(0, 360,
Animation.RELATIVE_TO_SELF, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5f);
set.setDuration(duration);
set.setFillAfter(true);
set.addAnimation(ta);
set.addAnimation(transAnim);
return set;
}
public static Animation menuItemAnim(float fromX, float toX, float fromY, float toY, int duration) {
AnimationSet set = new AnimationSet(true);
ScaleAnimation sa = new ScaleAnimation(fromX, toX, fromY, toY,
Animation.RELATIVE_TO_SELF, Animation.RELATIVE_TO_SELF);
AlphaAnimation aa = new AlphaAnimation(1.0f, 0);
set.setDuration(duration);
set.setFillAfter(true);
set.addAnimation(sa);
set.addAnimation(aa);
return set;
}
}
动画的代码没什么复杂的,自已去看看就好。
十一、其它的一些处理
1. 菜单展开后,点击控件关闭菜单。
2. 菜单展开后,屏蔽掉控件的点击事件和子菜单的点击事件
3. 其它一点小细节的处理
4. 最后测试,效果图在上面开始的时候
参考了鸿洋大神的代码,感谢鸿洋大神的
Android 自定义ViewGroup手把手教你实现ArcMenu