Android自定義實(shí)現(xiàn)轉(zhuǎn)盤菜單
一、前言
旋轉(zhuǎn)菜單是一種占用空間較大,實(shí)用性稍弱的UI,一方面由于展示空間的問(wèn)題,其展示的數(shù)據(jù)有限,但另一方面真由于這個(gè)原因,對(duì)用戶而言趣味性和操作性反而更有好。

二、繪制原理
繪制原理很簡(jiǎn)單,通過(guò)細(xì)微的觀察,我們發(fā)現(xiàn)文字是不需要旋轉(zhuǎn)的,也就是每個(gè)菜單是不需要自旋轉(zhuǎn),只需要旋轉(zhuǎn)其位置坐標(biāo)即可,實(shí)際上其難點(diǎn)并不是繪制,而是在于觸摸事件的處理方式。
本篇菜單特性:
- 動(dòng)態(tài)設(shè)置菜單
- 計(jì)算旋轉(zhuǎn)方向和旋轉(zhuǎn)角度
- 支持點(diǎn)擊
難點(diǎn)1:
旋轉(zhuǎn)方向判斷,旋轉(zhuǎn)時(shí)記錄起始點(diǎn),計(jì)算出旋轉(zhuǎn)方向。
首先,我們要理解,Touch事件也存在抽象的坐標(biāo)體系,和View左上角重合,因此我們需要轉(zhuǎn)換坐標(biāo)
float cx = event.getX() - getWidth() / 2F; float cy = event.getY() - getHeight() / 2F;
旋轉(zhuǎn)角度的計(jì)算
這種計(jì)算是為了計(jì)算出與原始落點(diǎn)位置的夾角,這里的方法是計(jì)算使用Math.asin反正切函數(shù),然后結(jié)合坐標(biāo)系進(jìn)行判斷
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)));
}
}
由于對(duì)Math的了解我們知道,Math.asin不能反映真實(shí)的夾角,因此需要做上面的補(bǔ)充。但是后來(lái)我們發(fā)現(xiàn),Math.atan2函數(shù)的存在,直接可以求出斜率夾角,而且不會(huì)丟失象限關(guān)系,一下子就省了好幾行代碼。
dr = (float) Math.atan2(cy, cx);
難點(diǎn)2:實(shí)時(shí)更新
為了旋轉(zhuǎn),我們可能忘記記錄最新位置,這個(gè)可能導(dǎo)致圓反向旋轉(zhuǎn),因此要實(shí)時(shí)記錄位置
eStartX = cx; eStartY = cy;
難點(diǎn)3:由于攔截了UP事件,因此需要對(duì)UP事件進(jìn)行專門處理
if (System.currentTimeMillis() - startDownTime > 500) {
break;
}
float upX = event.getX() - getWidth() / 2F;
float upY = event.getY() - getHeight() / 2F;
handleClickTap(upX, upY);
全部代碼:
public class OribitView extends View {
private final String TAG = "OribitView";
private DisplayMetrics displayMetrics;
private float mOutlineRaduis;
private float mInlineRadius;
private TextPaint mPaint;
private float lineWidth = 5f;
private float textSize = 12f;
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; //記錄用于落點(diǎn)角度,用于參考
private long startDownTime = 0l;
Rect bounds = new Rect();
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();
setLayerType(LAYER_TYPE_SOFTWARE,null);
}
private void initPaint() {
// 實(shí)例化畫(huà)筆并打開(kāi)抗鋸齒
mPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
mPaint.setAntiAlias(true);
mPaint.setTextSize(dpToPx(textSize));
}
private float dpToPx(float 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 / 4));
mPaint.setColor(Color.GRAY);
int id = canvas.save();
float centerRadius = (mOutlineRaduis + mInlineRadius) / 2;
float itemRadius = (mOutlineRaduis - mInlineRadius) / 2;
canvas.translate(width / 2F, height / 2F);
// canvas.drawCircle(0, 0, mOutlineRaduis, mPaint); //畫(huà)外框
// canvas.drawCircle(0, 0, mInlineRadius, mPaint); //畫(huà)內(nèi)框
float strokeWidth = mPaint.getStrokeWidth();
mPaint.setStrokeWidth(itemRadius * 2 - dpToPx(lineWidth / 2));
mPaint.setColor(Color.DKGRAY);
mPaint.setShadowLayer(10,0,10,Color.DKGRAY);
canvas.drawCircle(0, 0, centerRadius, mPaint);
mPaint.setStrokeWidth(strokeWidth);
float degree = (float) (2 * Math.asin(itemRadius / centerRadius));
//計(jì)算出從原點(diǎn)過(guò)item的切線夾角,求出每個(gè)圓所占夾角大小
float spaceDegree = (float) ((Math.PI * 2 - degree * itemCount) / itemCount);
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.FILL);
mPaint.setColor(oribitItem.backgroundColor);
//減去線寬
float strokeOffset = dpToPx(lineWidth / 2);
canvas.drawCircle(x, y, itemRadius - strokeOffset, mPaint);
mPaint.setColor(oribitItem.textColor);
String text = String.valueOf(oribitItem.text);
mPaint.getTextBounds(text, 0, text.length(), bounds);
float textBaseline = getTextPaintBaseline(mPaint) - y - bounds.height() + strokeOffset;
canvas.drawText(text, x - bounds.width() / 2F, -textBaseline, mPaint);
}
canvas.restoreToCount(id);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
eStartX = event.getX() - getWidth() / 2F;
//這里轉(zhuǎn)為原點(diǎn)為畫(huà)布中心的點(diǎn),便于計(jì)算角度
eStartY = event.getY() - getHeight() / 2F;
//求出落點(diǎn)與坐標(biāo)系x軸方向的夾角(
float locationRadian = (float) Math.asin(eStartY / (float) Math.sqrt(Math.pow(eStartX, 2) + Math.pow(eStartY, 2)));
// //根據(jù)正弦值計(jì)算起點(diǎn)在那個(gè)象限
// 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));
// }
// }
startDegreeRadian = locationRadian;
startDownTime = System.currentTimeMillis();
getParent().requestDisallowInterceptTouchEvent(true);
super.onTouchEvent(event);
return true;
case MotionEvent.ACTION_MOVE:
//坐標(biāo)轉(zhuǎn)換
float cx = event.getX() - getWidth() / 2F;
float cy = event.getY() - getHeight() / 2F;
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)));
// }
// }
dr = (float) Math.atan2(cy, cx);
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() / 2F;
float upY = event.getY() - getHeight() / 2F;
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) {
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[]{"鮮花", "牛奶", "橘子", "生活", "新聞", "熱點(diǎn)"};
int[] colors = new int[]{argb(random.nextFloat(), random.nextFloat(), random.nextFloat()),
argb(random.nextFloat(), random.nextFloat(), random.nextFloat()),
argb(random.nextFloat(), random.nextFloat(), random.nextFloat()),
argb(random.nextFloat(), random.nextFloat(), random.nextFloat()),
argb(random.nextFloat(), random.nextFloat(), random.nextFloat()),
argb(random.nextFloat(), random.nextFloat(), random.nextFloat())
};
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);
三、總結(jié)
本篇難點(diǎn)主要是事件處理,當(dāng)然可能有人會(huì)問(wèn),使用Layout添加豈不是更方便,答案是肯定的,但是本篇主要重點(diǎn)介紹Canvas 繪制,后續(xù)有Layout的布局,當(dāng)然這里其實(shí)區(qū)別并不大,不同點(diǎn)是一個(gè)需要onLayout的調(diào)用,另一個(gè)是onDraw的調(diào)用,做好坐標(biāo)軸轉(zhuǎn)換即可,難度并不大。
以上就是Android自定義實(shí)現(xiàn)轉(zhuǎn)盤菜單的詳細(xì)內(nèi)容,更多關(guān)于Android轉(zhuǎn)盤菜單的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android使用OKhttp3實(shí)現(xiàn)登錄注冊(cè)功能+springboot搭建后端的詳細(xì)過(guò)程
這篇教程主要實(shí)現(xiàn)Android使用OKhttp3實(shí)現(xiàn)登錄注冊(cè)的功能,后端使用SSM框架,本文通過(guò)實(shí)例圖文相結(jié)合給大家介紹的非常詳細(xì),需要的朋友參考下吧2021-07-07
Android利用LitePal操作數(shù)據(jù)庫(kù)存取圖片
這篇文章主要為大家詳細(xì)介紹了Android利用LitePal操作數(shù)據(jù)庫(kù)存取圖片的方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-08-08
AndroidStudio升級(jí)4.1后啟動(dòng)失敗Plugin問(wèn)題解決
這篇文章主要介紹了AndroidStudio升級(jí)4.1后啟動(dòng)失敗Plugin問(wèn)題,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-10-10
android將搜索引擎設(shè)置為中國(guó)雅虎無(wú)法搜索問(wèn)題解決方法
android 進(jìn)入搜索,將搜索引擎設(shè)置為中國(guó)雅虎,無(wú)法搜索到相關(guān)網(wǎng)絡(luò)結(jié)果,該問(wèn)題是由于yahoo的搜索接口改變導(dǎo)致,具體解決方法如下,感興趣的朋友可以參考下哈2013-06-06
淺談Android View繪制三大流程探索及常見(jiàn)問(wèn)題
下面小編就為大家?guī)?lái)一篇淺談Android View繪制三大流程探索及常見(jiàn)問(wèn)題。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-03-03
Android實(shí)現(xiàn)圖片左右滑動(dòng)效果
現(xiàn)在滑動(dòng)效果用的比較多,尤其是在手機(jī)端上面,本文介紹了Android實(shí)現(xiàn)圖片左右滑動(dòng)效果,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧。2016-10-10
Android開(kāi)發(fā)實(shí)現(xiàn)Switch控件修改樣式功能示例【附源碼下載】
這篇文章主要介紹了Android開(kāi)發(fā)實(shí)現(xiàn)Switch控件修改樣式功能,涉及Android Switch開(kāi)關(guān)控件樣式設(shè)置與事件響應(yīng)相關(guān)操作技巧,需要的朋友可以參考下2019-04-04
Android連接MySQL數(shù)據(jù)庫(kù)詳細(xì)教程
在Android應(yīng)用程序中連接 MySQL 數(shù)據(jù)庫(kù)可以幫助開(kāi)發(fā)人員實(shí)現(xiàn)更豐富的數(shù)據(jù)管理功能,本教程將介紹如何在Android應(yīng)用程序中使用低版本的MySQL Connector/J驅(qū)動(dòng)程序來(lái)連接MySQL數(shù)據(jù)庫(kù),需要的朋友可以參考下2023-05-05

