项目中需要定义一个圆盘形选择菜单,效果如下图:
1、自定义View
思路是
1、定义一个类存储每一个Item的信息,:
class Item {
//图片
Bitmap bitmap;
//每个Item旋转的角度
float angle;
//图片中心点,x坐标
float x;
//图片中心点,y坐标
float y;
String id;
...
}
2、通过代码传入基本的信息,初始化Items,包括角度、X、Y坐标、bitmap和其他的信息
通过三角函数值,得到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内,则产生点击事件
怎么判断一个点是否在一个圆内呢?
可以通过坐标差的平方根与半径进行对比,小于半径在圆内
/**
* 确定触摸事件(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:初始化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 源码设计模式