項目中需要定義一個圓盤形選擇菜單,效果如下圖:
圓盤形菜單
1、自定義View
思路是
1、定義一個類存儲每一個Item的信息,:
class Item {
//圖片
Bitmap bitmap;
//每個Item旋轉的角度
float angle;
//圖片中心點,x坐標
float x;
//圖片中心點,y坐標
float y;
String id;
...
}
2、通過代碼傳入基本的信息,初始化Items,包括角度、X、Y坐標、bitmap和其他的信息
求Item中心點坐標
通過三角函數值,得到x、y坐標:
x=pointX + (float) (radius * Math.cos(item.angle * Math.PI / 180))
代碼如下:
private void setUpItems() {
...
//第一個Item默認是0°
int angle = 0;
//每個Item間距相同的度數= 360 / itemCount
degreeDelta = 360 / itemCount;
//初始化每個Item
for (int index = 0; index < itemCount; index++) {
Item item = new Item();
item.angle = angle;
item.x = pointX + (float) (radius * Math.cos(item.angle * Math.PI / 180));
item.y = pointY + (float) (radius * Math.sin(item.angle * Math.PI / 180));
item.bitmap = resizeImage(ImageLoader.getInstance().loadImageSync(selectItems.get(index).getQue_img(), options));
...
items.add(item);
angle += degreeDelta;
}
}
x、y都在以radius 為半徑的圓A上,其中圓A的中心點是pointX 、pointY (位于屏幕中心)
3、draw
移動畫布,即坐標系到屏幕的中心,
@Override
public void onDraw(Canvas canvas) {
...
canvas.translate(getMeasuredWidth()/2,getMeasuredHeight()/2);
for (int index = 0; index < itemCount; index++) {
...
canvas.drawBitmap(items.get(index).bitmap, items.get(index).x - bitmap.getWidth() / 2,
items.get(index).y - bitmap.getHeight() / 2, null);
}
}
4、為View添加動畫
繼承Animation,在動畫執行的過程中,動態改變角度、x、y值
class MyAnimation extends Animation {
...
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
super.applyTransformation(interpolatedTime, t);
//每次執行動畫,運行0.5°
float angle = items.get(0).angle += 0.5;
for (int index = 0; index < itemCount; index++) {
Item item = items.get(index);
item.angle = angle;
item.x = pointX + (float) (radius * Math.cos(item.angle * Math.PI / 180));
item.y = pointY + (float) (radius * Math.sin(item.angle * Math.PI / 180));
angle += degreeDelta;
angle = angle % 360;
}
postInvalidate();
}
}
5、在觸摸事件ACTION_UP發生時,判斷該點是否在Item內,如果在Item內,則產生點擊事件
怎么判斷一個點是否在一個圓內呢?
可以通過坐標差的平方根與半徑進行對比,小于半徑在圓內
判斷 是否在Item內
/**
* 確定觸摸事件(ACTION_UP)發生的位置是否在,item(小圓圈)內
* @param x
* @param y
*/
private void confirmPointPosition(float x, float y) {
...
for (int index = 0; index < itemCount; index++) {
float imgCenterX = items.get(index).x;
float imgCenterY = items.get(index).y;
if (items.get(index).bitmap == null) {
break;
}
float imgCircle = items.get(index).bitmap.getWidth();
double r = Math.sqrt(Math.pow(x - imgCenterX, 2) + Math.pow(y - imgCenterY, 2));
if (r <= imgCircle / 2) {
...
listener.onClick(typeId, typeName, ageId);
}
}
}
2、自定義ViewGroup
自定義ViewGroup
- 將菜單項添加到ViewGroup:初始化item view,綁定數據
- 測量:先測量自身大小,再測量item view大小
- 布局:計算每個item的left、top的位置
attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="defaultMenuLayout" format="reference"/>
<declare-styleable name="CircleMenuLayout">
<attr name="item_layout" format="reference"/>
</declare-styleable>
</resources>
style.xml
<resources>
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
...
<item name="defaultMenuLayout">@style/defaultCircleMenuLayoutStyle</item>
</style>
<style name="defaultCircleMenuLayoutStyle">
<item name="item_layout">@layout/default_layout_circle_item</item>
</style>
</resources>
public class CircleMenuLayout extends ViewGroup {
private int menuItemLayoutId= R.layout.default_layout_circle_item;
private int[] items=new int[]{R.mipmap.circle_item_1,R.mipmap.circle_item_2,R.mipmap.circle_item_3,R.mipmap.circle_item_4
,R.mipmap.circle_item_5,R.mipmap.circle_item_6};
private int itemCount;
private double startAngle;
private double swapAngle;
private int radius;
private float itemRadio=0.25f;
private float paddingRadio=0.05f;
public CircleMenuLayout(Context context) {
this(context,null);
}
public CircleMenuLayout(Context context, AttributeSet attrs) {
this(context, attrs,R.attr.defaultMenuLayout);
}
public CircleMenuLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray typedArray=context.obtainStyledAttributes(attrs,R.styleable.CircleMenuLayout,defStyleAttr,0);
menuItemLayoutId=typedArray.getResourceId(R.styleable.CircleMenuLayout_item_layout,R.layout.default_layout_circle_item);
typedArray.recycle();
setPadding(0,0,0,0);
init();
}
private void init() {
itemCount=items.length;
startAngle=0;
swapAngle=360/itemCount;
buildMenuItems();
}
//將菜單項添加到ViewGroup
private void buildMenuItems() {
for (int i=0;i<itemCount;i++){
View itemView=inflaterMenuView(i);
initMenuView(itemView,i);
addView(itemView);
}
}
private void initMenuView(View itemView, final int position) {
ImageView img= (ImageView) itemView.findViewById(R.id.img);
img.setImageResource(items[position]);
}
private View inflaterMenuView(final int position) {
LayoutInflater inflater=LayoutInflater.from(getContext());
View view=inflater.inflate(menuItemLayoutId,this,false);
view.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(getContext(),"item : "+position,Toast.LENGTH_SHORT).show();
}
});
return view;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
measureSelf(widthMeasureSpec,heightMeasureSpec);
measureMenuItems();
}
/**
* 調用measure方法測量每個子view
*/
private void measureMenuItems() {
radius=Math.max(getMeasuredWidth(),getMeasuredHeight());
int childCount=getChildCount();
int specMode=MeasureSpec.EXACTLY;
int specSize= (int) (radius*itemRadio);
for (int i=0;i<childCount;i++){
View child=getChildAt(i);
if (child.getVisibility()==GONE){
continue;
}
int measureSpec=-1;
measureSpec=MeasureSpec.makeMeasureSpec(specSize,specMode);
child.measure(measureSpec,measureSpec);
}
}
/**
* 如果是精確模式,大小是寬高的最小值
* 如果是最大模式,大小是背景或屏幕的寬
*/
private void measureSelf(int widthMeasureSpec, int heightMeasureSpec) {
int reqWidth=0;
int reqHeight=0;
int widthSize=MeasureSpec.getSize(widthMeasureSpec);
int widthMode=MeasureSpec.getMode(widthMeasureSpec);
int heightSize=MeasureSpec.getSize(heightMeasureSpec);
int heightMode=MeasureSpec.getMode(heightMeasureSpec);
if (widthMode!=MeasureSpec.EXACTLY || heightMode!=MeasureSpec.EXACTLY){
reqWidth=getSuggestedMinimumWidth();
reqWidth=reqWidth==0?getDefaultWidth():reqWidth;
reqHeight=getSuggestedMinimumHeight();
reqHeight=reqHeight==0?getDefaultWidth():reqHeight;
}else {
reqWidth=reqHeight=Math.min(widthSize,heightSize);
}
setMeasuredDimension(reqWidth,reqHeight);
}
private int getDefaultWidth() {
return getResources().getDisplayMetrics().widthPixels;
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount=getChildCount();
int padding= (int) (radius*paddingRadio);
int left=0;
int top=0;
for (int i=0;i<childCount;i++){
View child=getChildAt(i);
if (child.getVisibility()==GONE){
continue;
}
int itemSize=Math.max(child.getMeasuredWidth(),child.getMeasuredHeight());
startAngle%=360;
left= (int) (radius/2+(radius/2-padding-itemSize/2)*Math.cos(Math.toRadians(startAngle)))-itemSize/2;
top= (int) (radius/2+(radius/2-padding-itemSize/2)*Math.sin(Math.toRadians(startAngle)))-itemSize/2;
child.layout(left,top,left+itemSize,top+itemSize);
startAngle+=swapAngle;
}
}
}
使用適配器,將變化隔離出去
因為每個菜單項都是一個view,可以將加載菜單項的布局、始化菜單項、綁定數據的工作通過adapter分離出去;在CircleMenuLayout 中,僅僅實現測量和布局就行。
public class CircleMenuLayout extends ViewGroup {
private ListAdapter mAdapter;
AdapterDataSetObserver mDataSetObserver;
...
public void setAdapter(ListAdapter adapter){
if (mAdapter != null && mDataSetObserver != null) {
mAdapter.unregisterDataSetObserver(mDataSetObserver);
}
if (adapter!= null){
this.mAdapter=adapter;
mDataSetObserver = new AdapterDataSetObserver();
mAdapter.registerDataSetObserver(mDataSetObserver);
buildMenuItems();
requestLayout();
}
}
@Override
protected void onAttachedToWindow() {
if (mAdapter!=null){
buildMenuItems();
}
super.onAttachedToWindow();
}
private void buildMenuItems() {
int itemCount=mAdapter.getCount();
startAngle=0;
if (itemCount>0){
swapAngle=360/itemCount;
}
for (int i=0;i<itemCount;i++){
View itemView=mAdapter.getView(i,null,null);
addView(itemView);
}
}
...
private class AdapterDataSetObserver extends DataSetObserver {
@Override
public void onChanged() {
buildMenuItems();
requestLayout();
}
@Override
public void onInvalidated() {
buildMenuItems();
requestLayout();
}
}
}
參考:android 源碼設計模式