一、需求分析
产品角度:工作中往往存在很多特殊需求,转盘轨道菜单就是其中一种,比如汽车内置显示屏呼出菜单,比如电视机菜单。
技术角度:通过数学三角函数+Canvas Api实现,数学知识非常重要,以往的自定义View,很多都是通过数学三角函数、向量来实现的,本例也不例外。
效果预览
代码实现
public class OribitView extends View {
private final String TAG = "OribitView";
private DisplayMetrics displayMetrics;
private float mOutlineRaduis;
private float mInlineRadius;
private TextPaint mPaint;
private int lineWidth = 5;
private int textSize = 18;
private int itemCount = 5;
private int mTouchSlop = 0;
private float rotateDegreeRadian = 0;
private OnItemClickListener onItemClickListener;
private float eStartX = 0f;
private float eStartY = 0f;
private boolean isMoveTouch = false;
private float startDegreeRadian = 0l; //记录用于落点角度,用于参考
private long startDownTime = 0l;
private final List<OribitItemPoint> mOribitItemPoints = new ArrayList<>();
public OribitView(Context context) {
this(context,null);
}
public OribitView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
public OribitView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
displayMetrics = context.getResources().getDisplayMetrics();
initPaint();
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
}
private void initPaint() {
// 实例化画笔并打开抗锯齿
mPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG );
mPaint.setAntiAlias(true);
mPaint.setTextSize(dpTopx(textSize));
}
private float dpTopx(int dp){
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,dp,getResources().getDisplayMetrics());
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
if(widthMode!=MeasureSpec.EXACTLY){
widthSize = displayMetrics.widthPixels/2;
}
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if(heightMode!=MeasureSpec.EXACTLY){
heightSize = displayMetrics.widthPixels/2;
}
widthSize = heightSize = Math.min(widthSize,heightSize);
setMeasuredDimension(widthSize,heightSize);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mOutlineRaduis = w/2.0f - dpTopx(lineWidth);
mInlineRadius = mOutlineRaduis*3/5.0f-dpTopx(lineWidth);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getWidth();
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(dpTopx(lineWidth/2));
mPaint.setColor(Color.GRAY);
int id = canvas.save();
float centerRadius = (mOutlineRaduis+mInlineRadius)/2;
float itemRadius = (mOutlineRaduis - mInlineRadius)/2;
canvas.translate(width/2,height/2);
canvas.drawCircle(0,0,mOutlineRaduis,mPaint);
canvas.drawCircle(0,0,mInlineRadius,mPaint);
float strokeWidth = mPaint.getStrokeWidth();
mPaint.setStrokeWidth(itemRadius*2 - dpTopx(lineWidth/2));
mPaint.setColor(Color.LTGRAY);
canvas.drawCircle(0,0,centerRadius,mPaint);
mPaint.setStrokeWidth(strokeWidth);
float degree = (float) (2*Math.asin(itemRadius/centerRadius));
//计算出从原点过item的切线夹角,求出每个圆所占夹角大小
float spaceDegree = (float) ((Math.PI*2 - degree*itemCount)/itemCount);
mPaint.setColor(Color.RED);
for (int i=0;i<mOribitItemPoints.size();i++){
OribitItemPoint itemPoint = mOribitItemPoints.get(i);
float x = (float) (centerRadius*Math.cos(rotateDegreeRadian + i*(spaceDegree+degree)));
float y = (float) (centerRadius*Math.sin(rotateDegreeRadian + i*(spaceDegree+degree)));
itemPoint.x = x;
itemPoint.y = y;
OribitItem oribitItem = itemPoint.getOribitItem();
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(Color.DKGRAY);
canvas.drawCircle(x,y,itemRadius-dpTopx(lineWidth/2),mPaint);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(oribitItem.backgroundColor);
canvas.drawCircle(x,y,itemRadius-dpTopx(lineWidth/2),mPaint);
mPaint.setColor(oribitItem.textColor);
String text = String.valueOf(oribitItem.text);
Rect bounds = new Rect();
mPaint.getTextBounds(text, 0, text.length(), bounds);
float textBaseline = getTextPaintBaseline(mPaint) - y- bounds.height();
canvas.drawText(text,x-bounds.width()/2,-textBaseline,mPaint);
}
canvas.restoreToCount(id);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
eStartX = event.getX()-getWidth()/2;
//这里转为原点为画布中心的点,便于计算角度
eStartY = event.getY()-getHeight()/2;
//求出落点与坐标系x轴方向的夹角(
float locationRadian = (float) Math.asin(eStartY/ (float) Math.sqrt(Math.pow(eStartX,2) + Math.pow(eStartY,2)));
//根据正弦值计算起点在那个象限
if(eStartY>0) {
//一二象限
if (eStartX < 0) {
startDegreeRadian = (float) (Math.PI - locationRadian);
} else {
startDegreeRadian = locationRadian;
}
}else{
//三四象限
if (eStartX > 0) {
startDegreeRadian = (float) (Math.PI*2 - Math.abs(locationRadian));;
} else {
startDegreeRadian = (float) (Math.PI + Math.abs(locationRadian));
}
}
startDownTime = System.currentTimeMillis();
getParent().requestDisallowInterceptTouchEvent(true);
super.onTouchEvent(event);
return true;
case MotionEvent.ACTION_MOVE:
float cx = event.getX() - getWidth()/2;
float cy = event.getY() - getHeight()/2;
float dx = cx - eStartX;
float dy = cy - eStartY;
float slideSlop = (float) Math.sqrt(Math.pow(dx,2) + Math.pow(dy,2));
if(slideSlop>mTouchSlop){
isMoveTouch = true;
}else{
isMoveTouch = false;
}
if(isMoveTouch){
float lineWidth = (float) Math.sqrt(Math.pow(cx,2) + Math.pow(cy,2));
float degreeRadian = (float) Math.asin(cy/lineWidth);
float dr = 0;
if(cy>0) {
if (cx > 0) {
dr = degreeRadian;
} else {
dr = (float) ((Math.PI - degreeRadian));
}
}else{
if (cx > 0) {
dr = (float) (Math.PI*2 - Math.abs(degreeRadian));
} else {
dr = (float) ((Math.PI + Math.abs(degreeRadian)));
}
}
rotateDegreeRadian += (dr-startDegreeRadian);
startDegreeRadian = dr;
eStartX = cx;
eStartY = cy;
postInvalidate();
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_OUTSIDE:
getParent().requestDisallowInterceptTouchEvent(false);
if(isMoveTouch){
isMoveTouch = false;
break;
}
if(System.currentTimeMillis()-startDownTime>500){
break;
}
float upX = event.getX() - getWidth()/2;
float upY = event.getY() - getHeight()/2;
handleClickTap(upX,upY);
break;
}
return super.onTouchEvent(event);
}
private void handleClickTap(float upX, float upY) {
if(itemCount==0 || mOribitItemPoints==null) return;
OribitItemPoint clickItemPoint =null;
float itemRadius = (mOutlineRaduis - mInlineRadius)/2;
for(OribitItemPoint itemPoint : mOribitItemPoints){
if(Float.isNaN(itemPoint.x) || Float.isNaN(itemPoint.y)){
continue;
}
float dx = (itemPoint.x - upX);
float dy = (itemPoint.y - upY);
float clickSlop = (float) Math.sqrt(Math.pow(dx,2) + Math.pow(dy,2));
if(clickSlop>=itemRadius){
continue;
}
clickItemPoint = itemPoint;
break;
}
if(clickItemPoint==null) return;
if(this.mOribitItemPoints==null) return;
this.onItemClickListener.onItemClick(this,clickItemPoint.oribitItem);
}
public int getItemCount() {
return itemCount;
}
public static float getTextPaintBaseline(Paint p) {
Paint.FontMetrics fontMetrics = p.getFontMetrics();
return (fontMetrics.descent - fontMetrics.ascent) / 2 -fontMetrics.descent;
}
public void showItems(List<OribitItem> oribitItems) {
mOribitItemPoints.clear();
if(oribitItems!=null){
for (OribitItem item : oribitItems){
OribitItemPoint point = new OribitItemPoint();
point.x = Float.NaN;
point.y = Float.NaN;
point.oribitItem = item;
mOribitItemPoints.add(point);
}
}
this.itemCount = mOribitItemPoints.size();
postInvalidate();
}
public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
this.onItemClickListener = onItemClickListener;
}
public static class OribitItem {
public String text;
public int textColor;
public int backgroundColor;
}
static class OribitItemPoint<T extends OribitItem> extends PointF{
private T oribitItem;
public void setOribitItem(T oribitItem) {
this.oribitItem = oribitItem;
}
public T getOribitItem() {
return oribitItem;
}
}
public interface OnItemClickListener{
public void onItemClick(View contentView,OribitItem item);
}
}
使用方法
OribitView oribitView = findViewById(R.id.oribitView);
oribitView.setOnItemClickListener(new OribitView.OnItemClickListener() {
@Override
public void onItemClick(View contentView, OribitView.OribitItem item) {
Toast.makeText(contentView.getContext(),item.text,Toast.LENGTH_SHORT).show();
}
});
List<OribitView.OribitItem> oribitItems = new ArrayList<>();
String[] chs = new String[]{"鲜花","牛奶","橘子","生活","新闻","热点"};
int[] colors = new int[]{0xffc4e1ff,0xffff7575,0xff6fb7b7,0xffff9922,0xffb766dd,0xff28ff28};
for (int i=0;i<chs.length;i++){
OribitView.OribitItem item = new OribitView.OribitItem();
item.text = chs[i];
item.textColor = Color.WHITE;
item.backgroundColor = colors[i];
oribitItems.add(item);
}
oribitView.showItems(oribitItems);