Android ArcMenu的简单实现

本文详细介绍了如何使用自定义属性和动画效果实现弧形菜单的动态展示,包括按钮位置确定、子菜单展开和点击事件处理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、实现的效果图:

     

161602_Gecx_2700716.png161602_Koh2_2700716.png

  二、实现思路

        通过效果图,我们可以考虑到以下几个问题:

                1、如何确定各个按钮的位置

                        通过重写ViewGroup的onLayout方法实现每一个子View的位置的确定。

                2、五个子Menu是怎么显示出来的

                        因为在android中,button的位置如果发生移动,它是不能提供给用户点击的。因此,这五个按钮原本就存在于该位置,只是在刚开始的时候,我们设置它隐藏起来,当点击了一次主Menu,则通过一系列的动画效果,实现其从无到有的弹出过程。

  三、实现步骤:

        3.1、自定义属性,用于设置ArcMenu的位置:左上、右上、左下、右下;用来设置子Button和主Button的半径

<?xml version="1.0" encoding="utf-8"?>
			<resources>
				<attr name="position">
					<enum name="left_top" value="0" />
					<enum name="left_bottom" value="1" />
					<enum name="right_top" value="2" />
					<enum name="right_bottom" value="3" />
				</attr>

				<attr name="radius" format="dimension" />

				<declare-styleable name="ArcMenu">
					<attr name="position" />
					<attr name="radius" />
				</declare-styleable>
			</resources>

        3.2、布局文件设置

<com.meinkonone.custom.ArcMenu
				android:id="@+id/id_menu"
				android:layout_width="match_parent"
				android:layout_height="match_parent"
				mein:position="right_bottom"
				mein:radius="140dp" >

				<Button
					android:id="@+id/id_button"
					android:layout_width="48dp"
					android:layout_height="48dp"
					android:background="@drawable/oval_btn_bg"
					android:text="+" />

				<Button
					android:layout_width="36dp"
					android:layout_height="36dp"
					android:background="@drawable/oval_btn_bg"
					android:tag="button1"
					android:text="1" />

				<Button
					android:layout_width="36dp"
					android:layout_height="36dp"
					android:background="@drawable/oval_btn_bg"
					android:tag="button2"
					android:text="2" />

				<Button
					android:layout_width="36dp"
					android:layout_height="36dp"
					android:background="@drawable/oval_btn_bg"
					android:tag="button3"
					android:text="3" />

				<Button
					android:layout_width="36dp"
					android:layout_height="36dp"
					android:background="@drawable/oval_btn_bg"
					android:tag="button4"
					android:text="4" />

				<Button
					android:layout_width="36dp"
					android:layout_height="36dp"
					android:background="@drawable/oval_btn_bg"
					android:tag="button5"
					android:text="5" />
			</com.meinkonone.custom.ArcMenu>

        3.3、重写ViewGroup,在拥有三个参数的构造方法中,获取自定义的属性值

public ArcMenu(Context context, AttributeSet attrs, int defStyle) {
				super(context, attrs, defStyle);
				mRadius = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
					100, getResources().getDisplayMetrics());
				// 获取自定义的属性值
				TypedArray array = 	context.getTheme().obtainStyledAttributes(attrs,
				R.styleable.ArcMenu, defStyle, 0);	
				int pos = array.getInt(R.styleable.ArcMenu_position, 3);
				switch (pos) {
					case POS_LEFT_TOP:
					mPosition = Position.LEFT_TOP;
					break;
				case POS_LEFT_BOTTOM:
					mPosition = Position.LEFT_BOTTOM;
					break;
				case POS_RIGHT_TOP:
					mPosition = Position.RIGHT_TOP;
					break;
				case POS_RIGHT_BOTTOM:
					mPosition = Position.RIGHT_BOTTOM;
					break;
				}
				mRadius = (int) array.getDimension(R.styleable.ArcMenu_radius,
						TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 100,
								getResources().getDisplayMetrics()));
				Log.d("xys", "positon = " + mPosition + " , radius = " + mRadius);
				// 回收资源
				array.recycle();
			}

        3.4、重写onMeasure()方法 直接通过调用measureChild()方法进行确定大小

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
				int count = getChildCount();
				for (int i = 0; i < count; i++) {
					// 测量child
					measureChild(getChildAt(i), widthMeasureSpec, heightMeasureSpec);
				}
				super.onMeasure(widthMeasureSpec, heightMeasureSpec);
			}

        3.5、重写onLayout()方法,通过调用childView.layout()方法确定位置

                        layoutCButton();//设置主Button的位置

layoutChildButton();//设置ItemButton的位置

                        211803_pTjW_2700716.png

        3.6、设置动画

                mCButton的360°旋转

childButton的旋转+平移

childButton的点击动画(变大+透明变化)

        3.7、设置接口回调,监听childButton的点击事件

四、具体代码实现

public class ArcMenu extends ViewGroup implements OnClickListener {

private static final int POS_LEFT_TOP = 0;

private static final int POS_LEFT_BOTTOM = 1;

private static final int POS_RIGHT_TOP = 2;

private static final int POS_RIGHT_BOTTOM = 3;


private Position mPosition = Position.RIGHT_BOTTOM;

private int mRadius;

private Status mStatus = Status.CLOSE;


private OnMenuItemClickListener menuItemClickListener;


/**

* 菜单的主按钮

*/

private View mCButton;


/**

* 菜单的状态枚举类

*/

public enum Status {

OPEN, CLOSE

}


/**

* 菜单的位置枚举类

*/

public enum Position {

LEFT_TOP, LEFT_BOTTOM, RIGHT_TOP, RIGHT_BOTTOM

}


/**

* 自定义Menu的对外接口,用于监听按钮的点击事件

*/

public interface OnMenuItemClickListener {

void onClick(View view, int pos);

}


public void setOnMenuItemClickListener(

OnMenuItemClickListener onMenuItemClickListener) {

menuItemClickListener = onMenuItemClickListener;

}


// 避免用户进行直接的构造对象

public ArcMenu(Context context) {

this(context, null);

}


public ArcMenu(Context context, AttributeSet attrs) {

this(context, attrs, 0);

}


public ArcMenu(Context context, AttributeSet attrs, int defStyle) {

super(context, attrs, defStyle);


mRadius = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,

100, getResources().getDisplayMetrics());


// 获取自定义的属性值

TypedArray array = context.getTheme().obtainStyledAttributes(attrs,

R.styleable.ArcMenu, defStyle, 0);


int pos = array.getInt(R.styleable.ArcMenu_position, 3);


switch (pos) {

case POS_LEFT_TOP:

mPosition = Position.LEFT_TOP;

break;

case POS_LEFT_BOTTOM:

mPosition = Position.LEFT_BOTTOM;

break;

case POS_RIGHT_TOP:

mPosition = Position.RIGHT_TOP;

break;

case POS_RIGHT_BOTTOM:

mPosition = Position.RIGHT_BOTTOM;

break;

}


mRadius = (int) array.getDimension(R.styleable.ArcMenu_radius,

TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 100,

getResources().getDisplayMetrics()));


Log.d("xys", "positon = " + mPosition + " , radius = " + mRadius);


// 回收资源

array.recycle();

}


@Override

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

int count = getChildCount();


for (int i = 0; i < count; i++) {

// 测量child

measureChild(getChildAt(i), widthMeasureSpec, heightMeasureSpec);

}

super.onMeasure(widthMeasureSpec, heightMeasureSpec);

}


@Override

protected void onLayout(boolean changed, int arg1, int arg2, int arg3,

int arg4) {

if (changed) {

layoutCButton();


// 定位menuItem

int count = getChildCount();

for (int i = 0; i < count - 1; i++) {

View childView = getChildAt(i + 1);


childView.setVisibility(GONE);


int cl = 0;

int ct = 0;


int cWidth = childView.getMeasuredWidth();

int cHeight = childView.getMeasuredHeight();


// mPosition是左上角时的坐标

cl = (int) (mRadius * Math.sin(Math.PI / 2 / (count - 2) * i));

ct = (int) (mRadius * Math.cos(Math.PI / 2 / (count - 2) * i));


// 如果位于是左下角或者右下角时

if (mPosition == Position.LEFT_BOTTOM

|| mPosition == Position.RIGHT_BOTTOM) {

ct = getMeasuredHeight() - ct - cHeight;

}


// 如果位于右上角或者是右下角时

if (mPosition == Position.RIGHT_BOTTOM

|| mPosition == Position.RIGHT_TOP) {

cl = getMeasuredWidth() - cl - cWidth;

}


Log.d("xys", cl + ", " + ct + ", " + cl + cWidth + ", " + ct

+ cHeight);


// 绘画childView

childView.layout(cl, ct, cl + cWidth, ct + cHeight);

}


}


}


/**

* 定位主菜单按钮

*/

private void layoutCButton() {

mCButton = getChildAt(0);

mCButton.setOnClickListener(this);


int l = 0;

int t = 0;


int width = mCButton.getMeasuredWidth();

int height = mCButton.getMeasuredHeight();


switch (mPosition) {

case LEFT_TOP:

l = 0;

t = 0;

break;

case LEFT_BOTTOM:

l = 0;

t = getMeasuredHeight() - height;

break;

case RIGHT_TOP:

l = getMeasuredWidth() - width;

t = 0;

break;

case RIGHT_BOTTOM:

l = getMeasuredWidth() - width;

t = getMeasuredHeight() - height;

break;

}

mCButton.layout(l, t, l + width, t + width);

}


@Override

public void onClick(View view) {

rotateCButton(view, 0f, 360f, 300);


toggleMenu(300);

}


/**

* 切换菜单

*/

public void toggleMenu(int duration) {

// 为所有的menuItem添加旋转+平移动画

int count = getChildCount();


for (int i = 0; i < count - 1; i++) {

final View chileView = getChildAt(i + 1);

chileView.setVisibility(VISIBLE);

// end 0,0

// start位置

// mPosition是左上角时的坐标

int cl = (int) (mRadius * Math.sin(Math.PI / 2 / (count - 2) * i));

int ct = (int) (mRadius * Math.cos(Math.PI / 2 / (count - 2) * i));


// 标志平移过程的x,y是变大还是缩小

int xflag = 1;

int yflag = 1;


// 位于左上角或者左下角时 平移过程中,x变小

if (mPosition == Position.LEFT_TOP

|| mPosition == Position.LEFT_BOTTOM) {

xflag = -1;

}

// 位于左上角或者右上角时 平移过程中 y变小

if (mPosition == Position.LEFT_TOP

|| mPosition == Position.RIGHT_TOP) {

yflag = -1;

}


AnimationSet animSet = new AnimationSet(true);

Animation tranAnim = null;


// to open

if (mStatus == Status.CLOSE) {

tranAnim = new TranslateAnimation(xflag * cl, 0, yflag * ct, 0);

chileView.setClickable(true);

chileView.setFocusable(true);

} else {// to close

tranAnim = new TranslateAnimation(0, xflag * cl, 0, yflag * ct);

chileView.setClickable(false);

chileView.setFocusable(false);

}

tranAnim.setFillAfter(true);

tranAnim.setDuration(duration);


tranAnim.setAnimationListener(new AnimationListener() {


@Override

public void onAnimationStart(Animation arg0) {

}


@Override

public void onAnimationRepeat(Animation arg0) {

}


@Override

public void onAnimationEnd(Animation arg0) {

if (mStatus == Status.CLOSE) {

chileView.setVisibility(GONE);

}

}

});


RotateAnimation rotateAnim = new RotateAnimation(0, 720,

Animation.RELATIVE_TO_SELF, 0.5f,

Animation.RELATIVE_TO_SELF, 0.5f);

rotateAnim.setDuration(duration);

rotateAnim.setFillAfter(true);


animSet.addAnimation(rotateAnim);

animSet.addAnimation(tranAnim);

animSet.setStartOffset((i * 100) / count);


chileView.startAnimation(animSet);


final int pos = i + 1;


// 设置childView的点击事件

chileView.setOnClickListener(new OnClickListener() {


@Override

public void onClick(View arg0) {

if (menuItemClickListener != null) {// 事件的处理

menuItemClickListener.onClick(chileView, pos);

}

menuItemAnim(pos - 1);// 动画的设置

changeStatus();

}

});


}

changeStatus();

}


/**

* 判断menuItem的状态

* @return

*/

public boolean isOpen() {

if (mStatus == Status.OPEN) {

return true;

} else {

return false;

}

}


/***

* 设置menuItem的点击动画

* @param i

*/

private void menuItemAnim(int pos) {

for (int i = 0; i < getChildCount() - 1; i++) {

View childView = getChildAt(i + 1);

if (i == pos) {

childView.startAnimation(scaleBigAnim(300));

} else {

childView.startAnimation(scaleSmallAnim(300));

}


// 再次隐藏childView

childView.setFocusable(false);

childView.setClickable(false);

}

}


/**

* 为当前点击的item设置变大以及降低透明度的动画

* @param duration

* @return

*/

private Animation scaleBigAnim(int duration) {

AnimationSet animSet = new AnimationSet(true);


ScaleAnimation scaleAnim = new ScaleAnimation(1.0f, 2.0f, 1.0f, 2.0f,

Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF,

0.5f);

AlphaAnimation alphaAnim = new AlphaAnimation(1f, 0.0f);

animSet.addAnimation(scaleAnim);

animSet.addAnimation(alphaAnim);

animSet.setDuration(duration);

animSet.setFillAfter(true);

return animSet;

}


/**

* 为当前点击的item的其他按钮设置变小大以及降低透明度的动画

* @param duration

* @return

*/

private Animation scaleSmallAnim(int duration) {

AnimationSet animSet = new AnimationSet(true);

ScaleAnimation scaleAnim = new ScaleAnimation(1.0f, 0.0f, 1.0f, 0.0f,

Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF,

0.5f);

AlphaAnimation alphaAnim = new AlphaAnimation(1f, 0.0f);

animSet.addAnimation(scaleAnim);

animSet.addAnimation(alphaAnim);

animSet.setDuration(duration);

animSet.setFillAfter(true);

return animSet;

}


/**

* 改变menuItem的状态

*/

private void changeStatus() {

mStatus = (mStatus == Status.CLOSE ? Status.OPEN : Status.CLOSE);

}


/**

* 绕自身旋转

* @param view

* @param start

* @param end

* @param duration

*/

private void rotateCButton(View view, float start, float end, int duration) {

RotateAnimation anim = new RotateAnimation(start, end,

Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF,

0.5f);

anim.setDuration(duration);

anim.setFillAfter(true);

view.startAnimation(anim);// 启动anim

}

}


转载于:https://my.oschina.net/mkonone/blog/652045

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值