炫酷,拉风的UI效果,对于我们每位开发人员来说都是相当具有吸引力的。
上图是雏形,可以扩展成为表盘,转盘,圆形菜单,下图就是扩展的圆形菜单。由于录制工具很不清晰,UI特效效果真心不错。如有感兴趣的,请往后面看。当然灵感来源于上图。
标题是自定义的圆形菜单,我主要讲解圆形菜单的开发流程,如有对上图感兴趣的,请留言。让我给大家一一道来,代码如下:
public class CircleMenuLayout extends ViewGroup {
//圆形半径
private int mRadius;
//开始角度
private double mStartAngle = ;
//padding属性 默认值为0
private float mPadding = ;
//滑动时 item偏移量
private int offsetRotation = ;
//最后一次触摸
private long lastTouchTime;
//判断触摸点 是否在圆类 默认false
boolean isRange = false;
//手指触摸的x,y值
float x = , y = ;
// 手指滑动的方向 默认向右
boolean isLeft = false;
//适配
private ListAdapter mAdapter;
//转动速度 默认速度为0
private float speed = ;
// 每个item 的默认尺寸
private static final float ITEM_DIMENSION = / f;
//转动快慢
private static final int ROTATION_DEGREE = ;
//distanceFromCenter Item到中心的距离
private static final float DISTANCE_FROM_CENTER = / f;
//speed attenuation 速度衰减
private static final int SPEED_ATTENUATION = ;
//每次转动的角度
private static final int ANGLE = ;
//消息
private static final int EMPTY_MESSAGE = ;
// MenuItem的点击事件接口
private OnItemClickListener mOnMenuItemClickListener;
//线程 处理item的转动
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (msg.what == EMPTY_MESSAGE) {
if (speed > ) {
if (isLeft) {
//向左转动
offsetRotation -= ANGLE;
} else {
offsetRotation += ANGLE;
}
//速度衰减
speed -= SPEED_ATTENUATION;
postInvalidate();
handler.sendEmptyMessageDelayed(EMPTY_MESSAGE, );
}
}
}
};
/**
* @param context
* @param attrs
*/
public CircleMenuLayout(Context context, AttributeSet attrs) {
super(context, attrs);
setPadding(, , , );
}
/**
* @param context
*/
public CircleMenuLayout(Context context) {
super(context);
setPadding(, , , );
}
public void setAdapter(ListAdapter mAdapter) {
this.mAdapter = mAdapter;
}
// 构建菜单项
private void buildMenuItems() {
// 根据用户设置的参数,初始化menu item
for (int i = ; i < mAdapter.getCount(); i++) {
final View itemView = mAdapter.getView(i, null, this);
final int position = i;
if (itemView != null) {
itemView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (mOnMenuItemClickListener != null) {
mOnMenuItemClickListener.onItemClickListener(itemView, position);
}
}
});
}
// 添加view到容器中
addView(itemView);
}
}
//窗口关联
@Override
protected void onAttachedToWindow() {
if (mAdapter != null) {
buildMenuItems();
}
super.onAttachedToWindow();
}
//设置布局的宽高,并策略menu item宽高
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 丈量自身尺寸
measureMyself(widthMeasureSpec, heightMeasureSpec);
// 丈量菜单项尺寸
measureChildViews();
}
private void measureMyself(int widthMeasureSpec, int heightMeasureSpec) {
int resWidth = ;
int resHeight = ;
// 根据传入的参数,分别获取测量模式和测量值
int width = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
// 如果宽或者高的测量模式非精确值
if (widthMode != MeasureSpec.EXACTLY
|| heightMode != MeasureSpec.EXACTLY) {
// 主要设置为背景图的高度
resWidth = getSuggestedMinimumWidth();
// 如果未设置背景图片,则设置为屏幕宽高的默认值
resWidth = resWidth == ? getDefaultWidth() : resWidth;
resHeight = getSuggestedMinimumHeight();
// 如果未设置背景图片,则设置为屏幕宽高的默认值
resHeight = resHeight == ? getDefaultWidth() : resHeight;
} else {
// 如果都设置为精确值,则直接取小值;
resWidth = resHeight = Math.min(width, height);
}
setMeasuredDimension(resWidth, resHeight);
}
private void measureChildViews() {
// 获得半径
mRadius = Math.min(getMeasuredWidth(), getMeasuredHeight()) / ;
// menu item数量
final int count = getChildCount();
// menu item尺寸
int childSize = (int) (mRadius * ITEM_DIMENSION);
// menu item测量模式
int childMode = MeasureSpec.EXACTLY;
// 迭代测量
for (int i = ; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() == GONE) {
continue;
}
// 计算menu item的尺寸;以及和设置好的模式,去对item进行测量
int makeMeasureSpec = -;
makeMeasureSpec = MeasureSpec.makeMeasureSpec(childSize,
childMode);
child.measure(makeMeasureSpec, makeMeasureSpec);
}
}
// 布局menu item的位置
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
refresh();
}
//刷新 偏移角度移动item
public void refresh() {
final int childCount = getChildCount();
// 根据menu item的个数,计算item的布局占用的角度
float angleDelay = / childCount;
// 遍历所有菜单项设置它们的位置
for (int i = ; i < childCount; i++) {
final View child = getChildAt(i);
if (child.getVisibility() == GONE) {
continue;
}
int x = (int) Math.round(Math.sin(Math.toRadians(angleDelay * (i + ) - offsetRotation)) * (mRadius * DISTANCE_FROM_CENTER));
int y = (int) Math.round(Math.cos(Math.toRadians(angleDelay * (i + ) - offsetRotation)) * (mRadius * DISTANCE_FROM_CENTER));
//计算item 距离左边距 上边距 的距离
if (x <= && y >= ) {
x = mRadius - Math.abs(x);
y = mRadius - y;
} else if (x <= && y <= ) {
y = mRadius + Math.abs(y);
x = mRadius - Math.abs(x);
} else if (x >= && y <= ) {
y = mRadius + Math.abs(y);
x = mRadius + x;
} else if (x >= && y >= ) {
x = mRadius + x;
y = mRadius - Math.abs(y);
}
//计算item中心点 距离左边距 上边距 的距离
x = x - (int) (mRadius * ITEM_DIMENSION) / ;
y = y - (int) (mRadius * ITEM_DIMENSION) / ;
// 布局child view
child.layout(x, y,
x + (int) (mRadius * ITEM_DIMENSION), y + (int) (mRadius * ITEM_DIMENSION));
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//碰撞 手指与大圆的碰撞 计算距离
x = event.getX();
y = event.getY();
//手指与大圆触摸的误差
int error = ;
if ((x - mRadius) * (x - mRadius) + (y - mRadius) * (y - mRadius) < (mRadius + error) * (mRadius + error)) {
isRange = true;
lastTouchTime = System.currentTimeMillis();
} else {
isRange = false;
}
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
float x1 = event.getX();
float y1 = event.getY();
if (isRange) {
long timeStamp = System.currentTimeMillis() - lastTouchTime;
float distance = (float) Math.sqrt((x1 - x) * (x1 - x) + (y1 - y) * (y1 - y));
float speed = distance / timeStamp;
if (x1 - x > ) {
isLeft = false;
} else {
isLeft = true;
}
//计算速度
speed(speed);
}
break;
}
return true;
}
public void speed(float speed) {
this.speed = speed * ROTATION_DEGREE;
handler.sendEmptyMessage(EMPTY_MESSAGE);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setColor(Color.parseColor("#dddddd"));
canvas.drawCircle(mRadius, mRadius, mRadius, paint);
refresh();
}
//定义点击接口
public interface OnItemClickListener {
public void onItemClickListener(View v, int position);
}
// 设置MenuItem的点击事件接口
public void setOnItemClickListener(OnItemClickListener listener) {
this.mOnMenuItemClickListener = listener;
}
/**
* 获得默认该layout的尺寸
*
* @return
*/
private int getDefaultWidth() {
WindowManager wm = (WindowManager) getContext().getSystemService(
Context.WINDOW_SERVICE);
DisplayMetrics outMetrics = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(outMetrics);
return Math.min(outMetrics.widthPixels, outMetrics.heightPixels);
}
}
看到这里,我相信很多人都晕了。好吧,那我们一起来理一理。
设计思路
1、通用模式
上图是图片加文字,如果我想换成按钮呢,或者我只需要图片。这里就需要定制。怎么办呢,我采用了适配模式,大家都还记得 ListView的用法,我这里也借鉴了一下:
public void setAdapter(ListAdapter mAdapter) {
this.mAdapter = mAdapter;
}
这样就可以实现Menu的高度定制。
2、构建菜单项
代码参考buildMenuItems(),对mAdapter遍历获取子View,添加点击事件,调用addView()添加到ViewGroup,这个时候系统就会调用onMeasure()对子View计算大小。
3、计算item大小
代码参考measureMyself()和measureChildViews(),确定每个item的尺寸大小。
4、item布局
首先计算item(x,y)距离圆心的长度,我画了一个草图:
int x = (int) Math.round(Math.sin(Math.toRadians(a)) * temp);
int y = (int) Math.round(Math.cos(Math.toRadians(a)) * temp);
temp我赋值为半径的三分之二,当然你可以更改成你满意的长度。
然后要计算(x,y)的坐标,通过坐标项象来计算:
if (x <= && y >= ) { //第二项象
x = mRadius - Math.abs(x);
y = mRadius - y;
} else if (x <= && y <= ) {//第三项象
y = mRadius + Math.abs(y);
x = mRadius - Math.abs(x);
} else if (x >= && y <= ) {//第四项象
y = mRadius + Math.abs(y);
x = mRadius + x;
} else if (x >= && y >= ) {//第一项象
x = mRadius + x;
y = mRadius - Math.abs(y);
}
计算到这来,你可能已经发现了问题,如果用(x,y)坐标来表示菜单项的left 和 top位置,那么你会发现整个item相对于父控件是向右下偏移了。为了解决偏移问题,我采用了item控件的中心点来表示菜单项的left 和 top位置。
x = x - item的宽度/ ;
y = y - item的高度 / ;
最后调用layout()方法,确定item的位置。
5、手势旋转
上面已经完成了静态的Menu,那么怎么才能通过滑动阴影部分使Menu旋转起来呢?
需要重写onTouchEvent()方法,并把返回值改为true。处理手势按下(ACTION_DOWN),抬起(ACTION_UP)的状态。
首先我们要判断手指按下是否在阴影局域内。注意手指按下是指尖局域与屏幕接触,并不是一点,所以有误差。
x = event.getX();
y = event.getY();
if ((x - 圆心x) * (x - 圆心x) + (y - 圆心y) * (y - 圆心y) < (圆心x+ 误差) * (圆心y+ 误差)) {
isRange = true;
}
然后我们要计算运动的速度,我刚开始的想法是用重力加速度,非常感谢我同事小贾,他给了我更好的意见:速度=距离/时间。
ACTION_DOWN:
ACTION_UP:
long timeStamp = System.currentTimeMillis() - lastTouchTime;
float distance = (float) Math.sqrt((x1 - x) * (x1 - x) + (y1 - y) * (y1 - y));
float speed = distance / timeStamp;
然后我们通过对比手指按下的x的坐标,和抬起x的坐标,来判断用户是向左滑,还是右滑。
if (x1 - x > ) {
isLeft = false;
} else {
isLeft = true;
}
最后通过handler来改变每次运动的角度,使Menu很自然的旋转了起来:
if (isLeft) {
//向左转动
offsetRotation -= ANGLE;
} else {
//向右转动
offsetRotation += ANGLE;
}
//速度衰减
speed -= SPEED_ATTENUATION;
invalidate();//重绘
handler.sendEmptyMessageDelayed(EMPTY_MESSAGE, );
使用
1、xml布局
<com.github.ws.viewdemo.widget.CircleMenuLayout
android:id="@+id/cm"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#f0f0f0">
</com.github.ws.viewdemo.widget.CircleMenuLayout>
2、class文件
circleMenuLayout.setAdapter(new MyAdapter());
circleMenuLayout.setOnItemClickListener(new CircleMenuLayout.OnItemClickListener() {
@Override
public void onItemClickListener(View v, int position) {
Toast.makeText(MainActivity.this, mList.get(position).text + "", Toast.LENGTH_SHORT).show();
}
});
源码我已上传到github,地址https://github.com/HpWens/ViewDemo,再一次感谢大家的关注。