Adapter模式实战-重构鸿洋的Android建行圆形菜单
对于很多开发人员来说,炫酷的UI效果是最吸引他们注意力的,很多人也因为这些炫酷的效果而去学习一些比较知名的UI库。而做出炫酷效果的前提是你必须对自定义View有所理解,作为90的小民自然也不例外。特别对于刚处在开发初期的小民,对于自定义View这件事觉得又神秘又帅气,于是小民决定深入研究自定义View以及相关的知识点。
在此之前我们先来看看洋神的原版效果图:
记得那是2014年的第一场雪,比以往时候来得稍晚一些。小民的同事洋叔是一位资深的研发人员,擅长写UI特效,在开发领域知名度颇高。最近洋叔刚发布了一个效果不错的圆形菜单,这个菜单的每个Item环形排布,并且可以转动。小民决定仿照洋叔的效果实现一遍,但是对于小民这个阶段来说只要实现环形布局就不错了,转动部分作为下个版本功能,就当作自定义View的练习了。
在google了自定义View相关的知识点之后,小民就写好了这个圆形菜单布局视图,我们一步一步来讲解,代码如下:
// 圆形菜单
public class CircleMenuLayout extends ViewGroup {
// 圆形直径
private int mRadius;
// 该容器内child item的默认尺寸
private static final float RADIO_DEFAULT_CHILD_DIMENSION = 1 / 4f;
// 该容器的内边距,无视padding属性,如需边距请用该变量
private static final float RADIO_PADDING_LAYOUT = 1 / 12f;
// 该容器的内边距,无视padding属性,如需边距请用该变量
private float mPadding;
// 布局时的开始角度
private double mStartAngle = 0;
// 菜单项的文本
private String[] mItemTexts;
// 菜单项的图标
private int[] mItemImgs;
// 菜单的个数
private int mMenuItemCount;
// 菜单布局资源id
private int mMenuItemLayoutId = R.layout.circle_menu_item;
// MenuItem的点击事件接口
private OnItemClickListener mOnMenuItemClickListener;
public CircleMenuLayout(Context context, AttributeSet attrs) {
super(context, attrs);
// 无视padding
setPadding(0, 0, 0, 0);
}
// 设置菜单条目的图标和文本
public void setMenuItemIconsAndTexts(int[] images, String[] texts) {
if (images == null && texts == null) {
throw new IllegalArgumentException("菜单项文本和图片至少设置其一");
}
mItemImgs = images;
mItemTexts = texts;
// 初始化mMenuCount
mMenuItemCount = images == null ? texts.length : images.length;
if (images != null && texts != null) {
mMenuItemCount = Math.min(images.length, texts.length);
}
// 构建菜单项
buildMenuItems();
}
// 构建菜单项
private void buildMenuItems() {
// 根据用户设置的参数,初始化menu item
for (int i = 0; i < mMenuItemCount; i++) {
View itemView = inflateMenuView(i);
// 初始化菜单项
initMenuItem(itemView, i);
// 添加view到容器中
addView(itemView);
}
}
private View inflateMenuView(final int childIndex) {
LayoutInflater mInflater = LayoutInflater.from(getContext());
View itemView = mInflater.inflate(mMenuItemLayoutId, this, false);
itemView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (mOnMenuItemClickListener != null) {
mOnMenuItemClickListener.onClick(v, childIndex);
}
}
});
return itemView;
}
private void initMenuItem(View itemView, int childIndex) {
ImageView iv = (ImageView) itemView
.findViewById(R.id.id_circle_menu_item_image);
TextView tv = (TextView) itemView
.findViewById(R.id.id_circle_menu_item_text);
iv.setVisibility(View.VISIBLE);
iv.setImageResource(mItemImgs[childIndex]);
tv.setVisibility(View.VISIBLE);
tv.setText(mItemTexts[childIndex]);
}
// 设置MenuItem的布局文件,必须在setMenuItemIconsAndTexts之前调用
public void setMenuItemLayoutId(int mMenuItemLayoutId) {
this.mMenuItemLayoutId = mMenuItemLayoutId;
}
// 设置MenuItem的点击事件接口
public void setOnItemClickListener(OnItemClickListener listener) {
this.mOnMenuItemClickListener = listener;
}
// 代码省略
}
小民的思路大致是这样的,首先让用户通过setMenuItemIconsAndTexts函数将菜单项的图标和文本传递进来,根据这些图标和文本构建菜单项,菜单项的布局视图由mMenuItemLayoutId存储起来,这个mMenuItemLayoutId默认为circle_menu_item.xml
,这个xml布局为一个ImageView显示在一个文本控件的上面。为了菜单项的可定制型,小民还添加了一个setMenuItemLayoutId函数让用户可以设置菜单项的布局,希望用户可以定制各种各样的菜单样式。在用户设置了菜单项的相关数据之后,小民会根据用户设置进来的图标和文本数量来构建、初始化相等数量的菜单项,并且将这些菜单项添加到圆形菜单CircleMenuLayout中。然后添加了一个可以设置用户点击菜单项的处理接口的setOnItemClickListener函数,使得菜单的点击事件可以被用户自定义处理。
在将菜单项添加到CircleMenuLayout之后就是要对这些菜单项进行尺寸丈量和布局了,我们先来看丈量尺寸的代码,如下 :
//设置布局的宽高,并策略menu item宽高
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 丈量自身尺寸
measureMyself(widthMeasureSpec, heightMeasureSpec);
// 丈量菜单项尺寸
measureChildViews();
}
private void measureMyself(int widthMeasureSpec, int heightMeasureSpec) {
int resWidth = 0;
int resHeight = 0;
// 根据传入的参数,分别获取测量模式和测量值
int width = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
// 如果宽或者高的测量模式非精确值
if (widthMode != MeasureSpec.EXACTLY
|| heightMode != MeasureSpec.EXACTLY) {
// 主要设置为背景图的高度
resWidth = getSuggestedMinimumWidth();
// 如果未设置背景图片,则设置为屏幕宽高的默认值
resWidth = resWidth == 0 ? getDefaultWidth() : resWidth;
resHeight = getSuggestedMinimumHeight();
// 如果未设置背景图片,则设置为屏幕宽高的默认值
resHeight = resHeight == 0 ? getDefaultWidth() : resHeight;
} else {
// 如果都设置为精确值,则直接取小值;
resWidth = resHeight = Math.min(width, height);
}
setMeasuredDimension(resWidth, resHeight);
}
private void measureChildViews() {
// 获得半径
mRadius = Math.max(getMeasuredWidth(), getMeasuredHeight());
// menu item数量
final int count = getChildCount();
// menu item尺寸
int childSize = (int) (mRadius * RADIO_DEFAULT_CHILD_DIMENSION);
// menu item测量模式
int childMode = MeasureSpec.EXACTLY;
// 迭代测量
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() == GONE) {
continue;
}
// 计算menu item的尺寸;以及和设置好的模式,去对item进行测量
int makeMeasureSpec = -1;
makeMeasureSpec = MeasureSpec.makeMeasureSpec(childSize,
childMode);
child.measure(makeMeasureSpec, makeMeasureSpec);
}
mPadding = RADIO_PADDING_LAYOUT * mRadius;
}
代码比较简单,就是先测量CircleMenuLayout的尺寸,然后测量每个菜单项的尺寸。尺寸获取了之后就到了布局这一步,这也是整个圆形菜单的核心所在。代码如下 :
// 布局menu item的位置
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int childCount = getChildCount();
int left, top;
// menu item 的尺寸
int itemWidth = (int) (mRadius * RADIO_DEFAULT_CHILD_