首先在这里感谢三位大佬 Jason,Hello ,403,还有同事以及初中学霸的帮助(对角度的算法,提供)。
两位大佬的传送门:
Jason:http://my.csdn.net/luofen521
Hello:http://www.jianshu.com/u/cff42ea9b87a
先说说我们的效果
前言:大体效果就是这样的,对,也许你们说,不是就一个圆形菜单么。没错,就是一个圆形菜单,但是这个圆形菜单是你点击那里,就显示到哪里。当时老板出这个,有一种想死的心,在网上找遍了,没有gitHub上面也没有,而且我还没怎么写过自定义,没办法,老板提出来,出不来,就要GG,硬着头皮上(没写过自定义View的同学不要害怕,可以自己试试)
首先:新建一个View,我取名为CircleMenu
然后实现三个构造法方法
public CircleMenu(Context context) {
this(context,null);
}
public CircleMenu(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
public CircleMenu(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mResources = getResources();
}
都让它调用三个参数的构造方法,我们看到,在第三个构造方法里面初始化了Resources,因为得到图片要。
然后我们要想,如果要实现一个圆形菜单,我们首先的要实现什么,然后要实现什么。
1.画一个圆出来
2.然后画一个圆环出来
3.把文字和图片添加到圆环里面去
4.修改菜单点击的位子
5.实现菜单的点击事件
第一步:画圆(我们把圆环一起画出来)
在onMeasure方法里面初始化画笔以及圆的半径,还有画笔的宽度
//控制圆显示最大的大小,最大为200,最小为宽高最小值的三分之一
if (Math.min(getMeasuredWidth(), getMeasuredHeight()) > 200){
abroadRadius = 200;
}else {
abroadRadius = Math.min(getMeasuredWidth(), getMeasuredHeight()) / 3;
}
paintSize = abroadRadius / 3; //取画笔的宽度为半径的三分之一
abrodPaint = new Paint();
abrodPaint.setColor(abroadBgColor);
abrodPaint.setAlpha(50);
abrodPaint.setStrokeWidth(paintSize);
abrodPaint.setAntiAlias(false);
abrodPaint.setStyle(Paint.Style.STROKE);
然后再onDraw方法里面把圆画出来,这里的X,Y都是取的控件的中心位置
X = getMeasuredWidth() / 2f;
Y = getMeasuredHeight() / 2f;
canvas.drawCircle(X,Y,abroadRadius,abrodPaint);
这样,我们的圆和圆环就一起画出来的,这里要注意一个事情,就是,圆环的大小,因为圆环是利用画笔加宽画出来的,我们看到的圆环外边的半径是要根据半径加画笔一半,就是我们的圆环外边到圆中心的半径。
第三部,添加文字和图片
图片和文字,怎么放在一起呢,我用一个数组Object[]放进去的,然后再判断类型,文字为String类型,图片为int类型
写一个方法canvasText(Canvas canvas),在onDraw里面去调用
private void canvasText(Canvas canvas) {
//计算每个占位的角度
int itemSize = strlist.length;
angle = 360f / itemSize;
float centerX = X;//中点
float centerY = Y;
Log.e("CircleMenu:","X:"+X + " Y:"+Y);
textradius = abroadRadius;
//计算添加文字的区域
final RectF textf = new RectF(centerX - textradius,centerY - textradius, centerX + textradius, centerY + textradius);
for (int i = 0; i
看到里面有两个方法,一个drawText和drawBitmap,一个是添加文字,一个是添加图片,添加文字,我是用的鸿神的算法
/**
* 绘制文本
*/
private void drawText(RectF mRange,float mRadius,float startAngle, float sweepAngle,
String string,Canvas canvas,int mItemCount)
{
Path path = new Path();
path.addArc(mRange, startAngle, sweepAngle);
float textWidth = textPaint.measureText(string);
// 利用水平偏移让文字居中
float hOffset = (float) (mRadius * Math.PI / mItemCount / 2 - textWidth / 2);// 水平偏移
float vOffset = mRadius / 2 / 6;// 垂直偏移
canvas.drawTextOnPath(string, path, hOffset, vOffset, textPaint);
}
/**
* 绘制图片
*/
private void drawBitmap(int X,int Y ,float mRadius,float offsetAngle,float angle, int img, Canvas canvas) {
int imgWidth = (int) mRadius / 6;
float x = (float) (X + mRadius * Math.cos(Math.toRadians(offsetAngle+(angle / 4))));
float y = (float) (Y + mRadius * Math.sin(Math.toRadians(offsetAngle+(angle / 4))));
RectF rectf = new RectF(x - imgWidth *2/ 3, y - imgWidth*2 / 3, x + imgWidth
*2/ 3, y + imgWidth*2/3);
Bitmap bitmap =((BitmapDrawable) mResources.getDrawable(img)).getBitmap();
canvas.drawBitmap(bitmap, null, rectf, null);
}donw
好了,就这样,我们的圆形菜单初步完成了
点击的位子就好办了,我们首先,onTouchEvent方法得到MotionEvent.ACTION_DOWN获得点击的downX,downY,然后把我们的中心,把我们的X,Y设置为donwX,donwY,然后调用 invalidate();从新绘制就好了,这样,我们的菜单就初步完成了。然后就是我们的点击方法,提供给外部,所以我们写一个接口,然后实现点击方法
接口:
public interface CircleOnClickItemListener {
void onItem(View view,int pos);
}
然后我们再实Item的点击方法,我们看到在canvasText方法里面有一句代码 AngleMap.put(i,offsetAngle); 这个就是我们记录每个Item的角度,位子,以及我们点击的哪个
@Override
public boolean onTouchEvent(MotionEvent event) {
float downX;
float downY;
switch (event.getAction()){
case MotionEvent.ACTION_UP:
//防止按下直接消失,所以我们这里直接返回为true
return true;
case MotionEvent.ACTION_DOWN:
downX = event.getX();
downY = event.getY();
Log.e("event", "ACTION_DOWN:X:" + downX + " Y:" + downY);
float distanceX = Math.abs(X-downX);
float distanceY = Math.abs(Y- downY);
float distanceZ = (float) Math.sqrt(Math.pow(distanceX,2) + Math.pow(distanceY,2));
if (distanceZ size){
float radius = 0;
// 第一象限
if (downX >= getMeasuredWidth() / 2 && downY >= getMeasuredHeight() / 2) {
Log.e("event", "ACTION_DOWN:X:" + downX + " Y:" + downY);
radius = (int) (Math.atan((downY - getMeasuredHeight() / 2) * 1.0f
/ (downX - getMeasuredWidth() / 2)) * 180 / Math.PI);
}
// 第二象限
if (downX <= getMeasuredWidth() / 2 && downY >= getMeasuredHeight() / 2) {
Log.e("event", "ACTION_DOWN:X:" + downX + " Y:" + downY);
radius = (int) (Math.atan((getMeasuredWidth() / 2 - downX)
/ (downY - getMeasuredHeight() / 2))
* 180 / Math.PI + 90);
}
// 第三象限
if (downX <= getMeasuredWidth() / 2 && downY <= getMeasuredHeight() / 2) {
Log.e("event", "ACTION_DOWN:X:" + downX + " Y:" + downY);
radius = (int) (Math.atan((getMeasuredHeight() / 2 - downY)
/ (getMeasuredWidth() / 2 - downX))
* 180 / Math.PI + 180);
}
// 第四象限
if (downX >= getMeasuredWidth() / 2 && downY <= getMeasuredHeight() / 2) {
Log.e("event", "ACTION_DOWN:X:" + downX + " Y:" + downY);
radius = (int) (Math.atan((downX - getMeasuredWidth() / 2)
/ (getMeasuredHeight() / 2 - downY))
* 180 / Math.PI + 270);
}
for (int i : AngleMap.keySet()){
int x = (int)(AngleMap.get(i) - angle);
if (radius > AngleMap.get(i) - angle && radius < AngleMap.get(i)){
Log.e("event","x:"+x + " y:"+AngleMap.get(i) + " radius:"+radius);
circleOnClickItemListener.onItem(this,i);
break;
}
}
return true;
}
break;
}
return super.onTouchEvent(event);
}
里面有段代码,这个作用,就是要去判断是否点击的时候圆弧内
float distanceX = Math.abs(X-downX);
float distanceY = Math.abs(Y- downY);
float distanceZ = (float) Math.sqrt(Math.pow(distanceX,2) + Math.pow(distanceY,2));
在if (distanceZ
public class CircleMenu extends View {
private int abroadRadius ; //外部半径
private Paint abrodPaint ; //外部圆画笔
private float paintSize ; //画笔宽度
private float offsetAngle = 0; //初始角度
private Paint textPaint; //文字画笔
private Paint bitmapPaint; //图片画笔
private Resources mResources;
private float textradius;
private float radiusSize; //半径+画笔的宽度一半 = 看到圆的半径
private float size ; //内边到外边距的大小
private Map AngleMap; //记录每个Itme的角度值
/**
* 文字的大小
*/
private float mTextSize = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP, 15, getResources().getDisplayMetrics());
private static int abroadBgColor = 0xff489cf0; //圆形菜单颜色
/**
* 菜单Item
*/
private Object[] strlist = new Object[]{"item2","item3",R.drawable.set,R.drawable.set,R.drawable.set,"item4","item5","item6"};
private float X = 100; //默认位置
private float Y = 100;
private float angle; //每个Item所占的角度
public CircleMenu(Context context) {
this(context,null);
}
public CircleMenu(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
public CircleMenu(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mResources = getResources();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//控制圆显示最大的大小,最大为200,最小为宽高最小值的三分之一
if (Math.min(getMeasuredWidth(), getMeasuredHeight()) > 200){
abroadRadius = 200;
}else {
abroadRadius = Math.min(getMeasuredWidth(), getMeasuredHeight()) / 3;
}
X = getMeasuredWidth() / 2f;
Y = getMeasuredHeight() / 2f;
radiusSize = abroadRadius + (paintSize/2);
size = radiusSize - paintSize;
Log.e("event","中心点X:"+X+ " 中心点Y:"+Y + " 半径:"+abroadRadius);
AngleMap = new HashMap<>();
paintSize = abroadRadius / 3; //取画笔的宽度为半径的三分之一
abrodPaint = new Paint();
abrodPaint.setColor(abroadBgColor);
abrodPaint.setAlpha(50);
abrodPaint.setStrokeWidth(paintSize);
abrodPaint.setAntiAlias(false);
abrodPaint.setStyle(Paint.Style.STROKE);
textPaint = new Paint();
textPaint.setColor(Color.BLACK);
textPaint.setAntiAlias(false);
textPaint.setTextSize(mTextSize);
bitmapPaint = new Paint();
bitmapPaint.setFilterBitmap(true);
bitmapPaint.setDither(true);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawCircle(X,Y,abroadRadius,abrodPaint);
canvasText(canvas);
}
private void canvasText(Canvas canvas) {
//计算每个占位的角度
int itemSize = strlist.length;
angle = 360f / itemSize;
float centerX = X;//中点
float centerY = Y;
Log.e("CircleMenu:","X:"+X + " Y:"+Y);
textradius = abroadRadius;
//计算添加文字的区域
final RectF textf = new RectF(centerX - textradius,centerY - textradius, centerX + textradius, centerY + textradius);
for (int i = 0; isize){
float radius = 0;
// 第一象限
if (downX >= getMeasuredWidth() / 2 && downY >= getMeasuredHeight() / 2) {
Log.e("event", "ACTION_DOWN:X:" + downX + " Y:" + downY);
radius = (int) (Math.atan((downY - getMeasuredHeight() / 2) * 1.0f
/ (downX - getMeasuredWidth() / 2)) * 180 / Math.PI);
}
// 第二象限
if (downX <= getMeasuredWidth() / 2 && downY >= getMeasuredHeight() / 2) {
Log.e("event", "ACTION_DOWN:X:" + downX + " Y:" + downY);
radius = (int) (Math.atan((getMeasuredWidth() / 2 - downX)
/ (downY - getMeasuredHeight() / 2))
* 180 / Math.PI + 90);
}
// 第三象限
if (downX <= getMeasuredWidth() / 2 && downY <= getMeasuredHeight() / 2) {
Log.e("event", "ACTION_DOWN:X:" + downX + " Y:" + downY);
radius = (int) (Math.atan((getMeasuredHeight() / 2 - downY)
/ (getMeasuredWidth() / 2 - downX))
* 180 / Math.PI + 180);
}
// 第四象限
if (downX >= getMeasuredWidth() / 2 && downY <= getMeasuredHeight() / 2) {
Log.e("event", "ACTION_DOWN:X:" + downX + " Y:" + downY);
radius = (int) (Math.atan((downX - getMeasuredWidth() / 2)
/ (getMeasuredHeight() / 2 - downY))
* 180 / Math.PI + 270);
}
//遍历Map
for (int i : AngleMap.keySet()){
int x = (int)(AngleMap.get(i) - angle);
//判断点击的位置,是否在item的区间
if (radius > AngleMap.get(i) - angle && radius < AngleMap.get(i)){
Log.e("event","x:"+x + " y:"+AngleMap.get(i) + " radius:"+radius);
circleOnClickItemListener.onItem(this,i);
break;
}
}
return true;
}
break;
}
return super.onTouchEvent(event);
}
//判断显示隐藏
private boolean isShowView = false;
public boolean isShowView(){
return isShowView;
}
//外部提供显示隐藏的方法
public void isShow(float x, float y, float width, float height){
isShowView = true;
this.setVisibility(VISIBLE);
calculation(x,y,width,height);
AngleMap.clear();
invalidate();
AngleMap = new HashMap<>();
offsetAngle = 0;
}
private CircleOnClickItemListener circleOnClickItemListener;
public void setCircleOnClickItemListener(CircleOnClickItemListener circleOnClickItemListener){
this.circleOnClickItemListener = circleOnClickItemListener;
}
//计算,显示的位子
public void calculation(float downX ,float downY,float width,float height){
downY = downY - dip2px(25); //25位状态的高度。这个我是直接写死的
//防止点击角落的位子,或者边缘的位子,圆弧菜单有有一部分显示不出来,所以我们计算点击的位子,到边缘的距离
if (downX > width - radiusSize) {
X = downX > downX - radiusSize ? width - radiusSize : downX;
}else {
X = downX < radiusSize ? radiusSize : downX;
}
if (downY >= height - radiusSize) {
Y = height - radiusSize - dip2px(25);
}else {
Y = downY < radiusSize ? radiusSize : downY ;
}
}
//如果显示,就隐藏
public void chie(){
isShowView = false;
this.setVisibility(INVISIBLE);
}
private int dip2px(float dpValue) {
final float scale = getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
}
Activity的代码
public class MainActivity extends AppCompatActivity {
private CircleMenu circleMenu;
private int screenWidth;
private int screenHeight;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
DisplayMetrics dm = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(dm);
//宽度
screenWidth = dm.widthPixels;
//高度
screenHeight = dm.heightPixels;
circleMenu = (CircleMenu) findViewById(R.id.menu_circle);
circleMenu.setCircleOnClickItemListener(new CircleOnClickItemListener() {
@Override
public void onItem(View view, int pos) {
Toast.makeText(MainActivity.this,pos+"",Toast.LENGTH_SHORT).show();
}
});
}
float downX;
float downY;
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_UP:
if (circleMenu != null){
if (!circleMenu.isShowView()){
//传入,点击的位子,和页面的宽高
circleMenu.isShow(downX,downY,screenWidth,screenHeight);
}else {
circleMenu.chie();
}
}
break;
case MotionEvent.ACTION_DOWN:
downX = event.getX();
downY = event.getY();
break;
}
return super.onTouchEvent(event);
}
}
总结:第一次自己写自定义View ,百度了很多,也问了几位大佬。其他在复杂的控件,不要害怕,网上没有,我们就自己写,自己写的话,首先要理解这个控件都有什么功能,然后根据功能去分步实现,第一步到最后一步,从什么开始实现,怎么去做,不要怕,程序猿千万不要被bug吓到。撸起袖子就是干...程序猿没有怂的....