仿遥控器菜单圆盘
程序员文章站
2022-07-04 13:34:32
...
仿遥控器菜单圆盘
1.效果
2.github地址
github地址
https://github.com/caixingcun/CopyMenu
3.前言
之前有一家面试,有这么一个需求,问我需要如何实现这么一个效果
刚开始感觉相对布局什么也能实现差不多的,但面试官提问到如何处理重叠部分
之后回答说自定义控件,因为自定义控件写的少,也就只有个大概思路
一带而过了,回来花了点时间,搞出来,跟大家分享下,也是来扩充博客
4.原理
这样的需要主要麻烦的地方是在点击后,响应特定区块
如果只是单纯画图 和 点击对应区域回调,完全可以画同心圆 和 线来实现
这里是将每一个区块的线上的点计算出来 连成path
创建特定标志位记录点击区域,重绘区块实现区块变色
5.代码
step1.创建一个类继承View,创建构造函数,在构造中初始化自定义属性 和 画笔
public class MenuView extends View
public MenuView(Context context) {
this(context, null);
}
public MenuView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MenuView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//获取自定义属性。
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.menuview);
//获取字体大小,默认大小是16dp
mLineColor = ta.getColor(R.styleable.menuview_lineColor, Color.BLACK);
mArrowColor = ta.getColor(R.styleable.menuview_arrowColor, Color.BLACK);
mChoseColor = ta.getColor(R.styleable.menuview_choseColor, Color.BLACK);
mArrowChoseColor = ta.getColor(R.styleable.menuview_arrowChoseColor, Color.WHITE);
mText = (String) ta.getText(R.styleable.menuview_mtext);
mTextSize = ta.getDimension(R.styleable.menuview_mtextSize, 30);
Log.d("tag", "mTextSize" + mTextSize);
ta.recycle();
mPaint = new Paint();
mPaint.setColor(mLineColor);
mPaint.setAntiAlias(true);
mPaint.setStrokeWidth(2);//线宽
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setTextSize(mTextSize);
mPaint.setTypeface(Typeface.DEFAULT_BOLD);
}
step2.自定义属性 ,声明命名空间
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="menuview">
<!--线条颜色-->
<attr name="lineColor" format="color"/>
<!--箭头颜色-->
<attr name="arrowColor" format="color"/>
<!--选择反色-->
<attr name="choseColor" format="color"/>
<!--选择箭头反色-->
<attr name="arrowChoseColor" format="color"/>
<!--中间OK或者菜单字体大小-->
<attr name="mtextSize" format="dimension"/>
<!--中间文字内容-->
<attr name="mtext" format="string"/>
</declare-styleable>
</resources>
step3.重写onMeasure方法回去控件宽高
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Log.d("tag", "onMeasure");
//1920x1080分辨率
mWidth = getMeasuredWidth(); //获取控件宽度和高度
mHeight = getMeasuredHeight();//600,600
step4.重写onDraw方法进行绘制界面
private PointF mCenterPoint = new PointF(200f, 200f);
private float mR = mWidth * 0.5f;
private float mr = mR * 75f / 200f;
private float gen2 = 1.41421356f;
private PointF mPointA = new PointF(mCenterPoint.x - mR / gen2, mCenterPoint.y - mR / gen2);
private PointF mPointB = new PointF(mCenterPoint.x + mR / gen2, mCenterPoint.y - mR / gen2);
private PointF mPointC = new PointF(mCenterPoint.x + mr / gen2, mCenterPoint.y - mr / gen2);
private PointF mPointD = new PointF(mCenterPoint.x - mr / gen2, mCenterPoint.y - mr / gen2);
private PointF mPointE = new PointF(mCenterPoint.x - mR / gen2, mCenterPoint.y + mR / gen2);
private PointF mPointF = new PointF(mCenterPoint.x + mR / gen2, mCenterPoint.y + mR / gen2);
private PointF mPointG = new PointF(mCenterPoint.x + mr / gen2, mCenterPoint.y + mr / gen2);
private PointF mPointH = new PointF(mCenterPoint.x - mr / gen2, mCenterPoint.y + mr / gen2);
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Log.d("tag", "onDraw");
/** 圆心*/
mCenterPoint = new PointF(mWidth * 0.5f,
mHeight * 0.5f);
Log.d("tag", mCenterPoint.x + "mCenterPoint.x" + mCenterPoint.y + "mCenterPoint.y");
/**大小半径*/
mR = mWidth * 0.5f;
mr = mR * 75f / 200f;
/** 四个圆弧区域的路径相关点*/
mPointA = new PointF(mCenterPoint.x - mR / gen2, mCenterPoint.y - mR / gen2);
mPointB = new PointF(mCenterPoint.x + mR / gen2, mCenterPoint.y - mR / gen2);
mPointC = new PointF(mCenterPoint.x + mr / gen2, mCenterPoint.y - mr / gen2);
mPointD = new PointF(mCenterPoint.x - mr / gen2, mCenterPoint.y - mr / gen2);
mPointE = new PointF(mCenterPoint.x - mR / gen2, mCenterPoint.y + mR / gen2);
mPointF = new PointF(mCenterPoint.x + mR / gen2, mCenterPoint.y + mR / gen2);
mPointG = new PointF(mCenterPoint.x + mr / gen2, mCenterPoint.y + mr / gen2);
mPointH = new PointF(mCenterPoint.x - mr / gen2, mCenterPoint.y + mr / gen2);
Log.d("tag", mPointA.x + "mPointA.X" + mPointA.y + "mPointA.y");
Log.d("tag", mPointB.x + "mPointB.X" + mPointB.y + "mPointB.y");
/**画圆弧块*/
//左侧
drawSrcBlock(mChoseBlock == Block.TOP, mPointA, -135, mPointC, -45, canvas);
//顶部
drawSrcBlock(mChoseBlock == Block.RIGHT, mPointB, -45, mPointG, 45, canvas);
//右侧
drawSrcBlock(mChoseBlock == BOTTOM, mPointF, 45, mPointH, 135, canvas);
//下册
drawSrcBlock(mChoseBlock == LEFT, mPointE, 135, mPointD, 225, canvas);
/**画中心圆,方便上色*/
drawCircle(mChoseBlock == Block.CENTER, mCenterPoint, mr, canvas);
//三角形高
mTriHeight = 1.0f / 3.0f * (mR - mr);
//三角形顶端距离圆心距离
disForCenter = mr + 2f * mTriHeight;
/**画四个箭头*/
PointF top1 = new PointF(mCenterPoint.x, mCenterPoint.y - disForCenter);
PointF top2 = new PointF(mCenterPoint.x + mTriHeight,
mCenterPoint.y - disForCenter + mTriHeight);
PointF top3 = new PointF(mCenterPoint.x - mTriHeight,
mCenterPoint.y - disForCenter + mTriHeight);
drawTriangle(mChoseBlock == Block.TOP, top1, top2, top3, canvas);
PointF left1 = new PointF(mCenterPoint.x - disForCenter, mCenterPoint.y);
PointF left2 = new PointF(mCenterPoint.x - disForCenter + mTriHeight,
mCenterPoint.y + mTriHeight);
PointF left3 = new PointF(mCenterPoint.x - disForCenter + mTriHeight,
mCenterPoint.y - mTriHeight);
drawTriangle(mChoseBlock == LEFT, left1, left2, left3, canvas);
PointF bottom1 = new PointF(mCenterPoint.x, mCenterPoint.y + disForCenter);
PointF bottom2 = new PointF(mCenterPoint.x + mTriHeight, mCenterPoint.y+disForCenter - mTriHeight);
PointF bottom3 = new PointF(mCenterPoint.x - mTriHeight,
mCenterPoint.y + disForCenter - mTriHeight);
drawTriangle(mChoseBlock == BOTTOM, bottom1, bottom2, bottom3, canvas);
PointF right1 = new PointF(mCenterPoint.x + disForCenter,
mCenterPoint.y);
PointF right2 = new PointF(mCenterPoint.x + disForCenter - mTriHeight,
mCenterPoint.y+mTriHeight);
PointF right3 = new PointF(mCenterPoint.x + disForCenter - mTriHeight,
mCenterPoint.y -mTriHeight);
drawTriangle(mChoseBlock == Block.RIGHT, right1, right2, right3, canvas);
/* canvas.drawCircle(mCenterPoint.x, mCenterPoint.y, 200, mPaint);
canvas.drawCircle(mCenterPoint.x, mCenterPoint.y, 75, mPaint);
canvas.drawLine(mPointA.x, mPointA.y, mPointD.x, mPointD.y, mPaint);
canvas.drawLine(mPointB.x, mPointB.y, mPointC.x, mPointC.y, mPaint);
canvas.drawLine(mPointE.x, mPointE.y, mPointH.x, mPointH.y, mPaint);
canvas.drawLine(mPointF.x, mPointF.y, mPointG.x, mPointG.y, mPaint);*/
}
/**画箭头*/
private void drawTriangle(boolean isSlide, PointF p1, PointF p2, PointF p3,
Canvas canvas) {
mPaint.setStyle(Paint.Style.FILL); //填充中间
if (isSlide) {
mPaint.setColor(mArrowChoseColor);
} else {
mPaint.setColor(mArrowColor);
}
Path path = new Path();
path.moveTo(p1.x, p1.y);
path.lineTo(p2.x, p2.y);
path.lineTo(p3.x, p3.y);
path.close();
canvas.drawPath(path, mPaint);
}
/** 画圆 中心小圆*/
private void drawCircle(boolean isSlide, PointF centerPoint, float mr, Canvas canvas) {
mPaint.setColor(mLineColor);
if (isSlide) {
mPaint.setStyle(Paint.Style.FILL); //填充
} else {
mPaint.setStyle(Paint.Style.STROKE);
}
canvas.drawCircle(centerPoint.x, centerPoint.y, mr, mPaint);
Rect bounds = new Rect();
mPaint.getTextBounds(mText, 0, mText.length(), bounds);
int textwidth = bounds.width();
int textHeight = bounds.height();
if (isSlide) {
mPaint.setColor(mArrowChoseColor);
} else {
mPaint.setColor(mArrowColor);
}
canvas.drawText(mText, mCenterPoint.x - textwidth * 0.5f, mCenterPoint.y + textHeight * 0.5f,
mPaint);
mPaint.setColor(mLineColor);
}
/** 画扇形边框*/
private void drawSrcBlock(boolean isSolid, PointF pointA, int startAngle, PointF pointC,
int startAngle1,
Canvas canvas) {
if (isSolid) {
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(mChoseColor);
} else {
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(mLineColor);
}
Path path = new Path();
path.moveTo(pointA.x, pointA.y);
path.arcTo(new RectF(0, 0, mWidth, mWidth), startAngle, 90);
path.lineTo(pointC.x, pointC.y);
path.arcTo(new RectF(mWidth * 0.5f * 125f / 200f, mHeight * 0.5f * 125f / 200f,
mWidth * 0.5f * 275f / 200f, mWidth * 0.5f * 275f / 200f), startAngle1, -90);
path.close();
canvas.drawPath(path, mPaint);
}
step5.重写onTouchEvent对触摸事件进行处理,更改对应标志位,并将触摸状态回调出去
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
float x = event.getX();
float y = event.getY();
evaluateTouchBlock(x, y);
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
mChoseBlock = NULL;
invalidate();
break;
}
return true;
}
//计算点的触摸位置块
private void evaluateTouchBlock(float x, float y) {
float dis = GeometryUtil.getDistanceBetween2Points(mCenterPoint,
new PointF(x, y));
if (dis >= mr && dis <= mR) { //大小圆之间
float datY = y - mCenterPoint.y;
float datX = x - mCenterPoint.x;
if (datX==0) {//排除斜率为0状况
if (datY > 0) {
mChoseBlock = BOTTOM;
} else {
mChoseBlock = Block.TOP;
}
} else {
float k = datY / datX;
if ((k >= 1 || k <= -1)) {
if (datY <= 0) {
mChoseBlock = Block.TOP; //LEFT
} else {
mChoseBlock = BOTTOM; //RIGHT
}
} else {
if (datX <= 0) {
mChoseBlock = LEFT; //BOTTOM
} else {
mChoseBlock = Block.RIGHT;//TOP
}
}
}
invalidate();
} else if (dis < mr) {//小圆
mChoseBlock = Block.CENTER;
invalidate();
} else {
}
if (mOnTouchBlockListener != null) {
switch (mChoseBlock) {
case LEFT:
mOnTouchBlockListener.onLeft();
break;
case TOP:
mOnTouchBlockListener.onTop();
break;
case BOTTOM:
mOnTouchBlockListener.onBottom();
break;
case RIGHT:
mOnTouchBlockListener.onRight();
break;
case CENTER:
mOnTouchBlockListener.onCenter();
break;
}
}
}
step6.使用到的回调接口,和枚举状态类
//点击事件回调
private OnTouchBlockListener mOnTouchBlockListener;
public void setOnTouchBlockListener(
OnTouchBlockListener onTouchBlockListener) {
mOnTouchBlockListener = onTouchBlockListener;
}
public interface OnTouchBlockListener {
void onTop();
void onBottom();
void onLeft();
void onRight();
void onCenter();
}
enum Block {
LEFT, TOP, RIGHT, BOTTOM, CENTER, NULL
}
step7.工具类
package com.sy.copymenu;
import android.graphics.PointF;
/**
* 几何图形工具
*/
public class GeometryUtil {
/**
* As meaning of method name.
* 获得两点之间的距离
* @param p0
* @param p1
* @return
*/
public static float getDistanceBetween2Points(PointF p0, PointF p1) {
float distance = (float) Math.sqrt(Math.pow(p0.y - p1.y, 2) + Math.pow(p0.x - p1.x, 2));
return distance;
}
/**
* Get middle point between p1 and p2.
* 获得两点连线的中点
* @param p1
* @param p2
* @return
*/
public static PointF getMiddlePoint(PointF p1, PointF p2) {
return new PointF((p1.x + p2.x) / 2.0f, (p1.y + p2.y) / 2.0f);
}
/**
* Get point between p1 and p2 by percent.
* 根据百分比获取两点之间的某个点坐标
* @param p1
* @param p2
* @param percent
* @return
*/
public static PointF getPointByPercent(PointF p1, PointF p2, float percent) {
return new PointF(evaluateValue(percent, p1.x , p2.x), evaluateValue(percent, p1.y , p2.y));
}
/**
* 根据分度值,计算从start到end中,fraction位置的值。fraction范围为0 -> 1
* @param fraction
* @param start
* @param end
* @return
*/
public static float evaluateValue(float fraction, Number start, Number end){
return start.floatValue() + (end.floatValue() - start.floatValue()) * fraction;
}
/**
* Get the point of intersection between circle and line.
* 获取 通过指定圆心,斜率为lineK的直线与圆的交点。
*
* @param pMiddle The circle center point.
* @param radius The circle radius.
* @param lineK The slope of line which cross the pMiddle.
* @return
*/
public static PointF[] getIntersectionPoints(PointF pMiddle, float radius, Double lineK) {
PointF[] points = new PointF[2];
float radian, xOffset = 0, yOffset = 0;
if(lineK != null){
radian= (float) Math.atan(lineK);
xOffset = (float) (Math.sin(radian) * radius);
yOffset = (float) (Math.cos(radian) * radius);
}else {
xOffset = radius;
yOffset = 0;
}
points[0] = new PointF(pMiddle.x + xOffset, pMiddle.y - yOffset);
points[1] = new PointF(pMiddle.x - xOffset, pMiddle.y + yOffset);
return points;
}
}
step8. xml
<com.sy.copymenu.MenuView
android:id="@+id/menu_view"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_centerInParent="true"
android:background="@android:color/transparent"
menuview:arrowChoseColor="@android:color/white"
menuview:arrowColor="@android:color/holo_red_dark"
menuview:choseColor="@android:color/black"
menuview:lineColor="@android:color/black"
menuview:mtext="OK"
menuview:mtextSize="20sp"
/>
step9.状态回调监听
mMenuView = (MenuView) findViewById(R.id.menu_view);
mMenuView.setOnTouchBlockListener(new MenuView.OnTouchBlockListener() {
@Override
public void onTop() {
ToastUtils.showToast(mContext, "top");
}
@Override
public void onBottom() {
ToastUtils.showToast(mContext, "onBottom");
}
@Override
public void onLeft() {
ToastUtils.showToast(mContext, "onLeft");
}
@Override
public void onRight() {
ToastUtils.showToast(mContext, "onRight");
}
@Override
public void onCenter() {
ToastUtils.showToast(mContext, "onCenter");
}
});