自定义View练习 - 圆形菜单
来源
关键代码都在这篇博客中有说明.
效果图
主要实现类
CircleMenuLayout.java
自定义ViewGroup的重点是测量和布局
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int measureWidth;
int measureHeight;
//获得测量大小和模式
int size = MeasureSpec.getSize(widthMeasureSpec);
int mode = MeasureSpec.getMode(widthMeasureSpec);
if (mode == MeasureSpec.EXACTLY) { //精确模式
measureWidth = measureHeight = Math.min(size, getDefaultWidth());
} else { //wrap_content模式
//获得指定的背景大小,背景有多大,控件就有多大
int suggestedMinimumWidth = getSuggestedMinimumWidth();
//如果suggestedMinimumWidth为0, 则没有背景
if (suggestedMinimumWidth == 0) { //如果没有背景,则设置为默认宽度
measureWidth = measureHeight = getDefaultWidth();
} else { //如果有背景,则设置为背景宽和默认宽度的最小值
measureWidth = measureHeight = Math.min(suggestedMinimumWidth, getDefaultWidth());
}
}
setMeasuredDimension(measureWidth, measureHeight);
//直径为测量的宽度
diameter = measureWidth;
//测量子View
for (int i = 0; i < getChildCount(); i++) {
View childView = getChildAt(i);
//默认系统是通过onMeasure给予MeasureSpec参数的,而对于inflate进来的子视图是没有MeasureSpec参数的
//因此,我们需要自己设计MeasureSpec,传递给子视图
int makeMeasureSpec = MeasureSpec.makeMeasureSpec(diameter / 3, MeasureSpec.EXACTLY);
childView.measure(makeMeasureSpec, makeMeasureSpec);
}
}
/**
* 获得屏幕宽高中的较小值
*/
public int getDefaultWidth() {
DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
int widthPixels = displayMetrics.widthPixels;
int heightPixels = displayMetrics.heightPixels;
return Math.min(widthPixels, heightPixels);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for (int i = 0; i < getChildCount(); i++) {
View childView = getChildAt(i);
int childWidth = childView.getMeasuredWidth();
int left = (int) (radius + distance * Math.cos(Math.toRadians(startAngle)) - childWidth / 2);
int top = (int) (radius + distance * Math.sin(Math.toRadians(startAngle)) - childWidth / 2);
int right = left + childWidth;
int bottom = top + childWidth;
childView.layout(left, top, right, bottom);
startAngle += 360 / getChildCount();
}
}
触摸控件,让子View伴随着滚动. 这里需要重写onTouchEvent.
private float lastX;
private float lastY;
@Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = x;
lastY = y;
break;
case MotionEvent.ACTION_MOVE:
float start = CircleUtil.getAngle(lastX, lastY, diameter); //开始角度
float end = CircleUtil.getAngle(x, y, diameter); //结束角度
float angle; //旋转的角度
//todo 这里有很大的疑问? 为什么这个角度是负数?
//判断点击的点所处的象限,如果是1,4象限,角度值是正数,否则是负数
if (CircleUtil.getQuadrant(x, y, diameter) == 1 || CircleUtil.getQuadrant(x, y, diameter) == 4) {
angle = end - start;
} else {
angle = start - end;
}
startAngle += angle;
requestLayout();//重新布局
lastX = x;
lastY = y;
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}
其中使用了工具类CircleUtils
public class CircleUtil {
/**
* 根据触摸的位置,计算角度
*
* @param xTouch
* @param yTouch
* @param d 直径
* @return
*/
public static float getAngle(float xTouch, float yTouch,int d) {
double x = xTouch - (d / 2f);
double y = yTouch - (d / 2f);
//hypot:通过直角边,求斜边
return (float) (Math.asin(y / Math.hypot(x, y)) * 180 / Math.PI);
}
/**
* 根据当前位置计算象限
*
* @param x
* @param y
* @param d 直径
* @return
*/
public static int getQuadrant(float x, float y,int d) {
int tmpX = (int) (x - d / 2);
int tmpY = (int) (y - d / 2);
if (tmpX >= 0) {
return tmpY >= 0 ? 4 : 1;
} else {
return tmpY >= 0 ? 3 : 2;
}
}
}
学到的知识
- 在自定义ViewGroup中不仅要测量自己的宽高, 还需要测量子View的大小.
- 重写onLayout主要是对子View坐标的计算. 通过规律获得坐标值.
- onTouchEvent返回true, 表示当前控件消费了touch事件.
遗留的问题
- 在onTouchEvent中为什么在第一和第四象限计算角度会是正数,而其他象限是负数? 我通过打印日志看到, end角度在 -90度 ~0和0~ 90度 之间变化. 这个规律不是很清楚.
github地址: https://github.com/cizkey/CustomPractice/tree/master/CircleMenu