Adapter模式实战之重构鸿洋集团的Android圆形菜单建行

Adapter模式实战之重构鸿洋集团的Android圆形菜单建行,第1张

概述对于很多开发人员来说,炫酷的UI效果是最吸引他们注意力的,很多人也因为这些炫酷的效果而去学习一些比较知名的UI库。而做出炫酷效果的前提是你必须对自定义View有所理解,作为90的小民自然也不例外。特别对于刚处在

对于很多开发人员来说,炫酷的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;// 菜单布局资源IDprivate int mMenuItemLayoutID = R.layout.circle_menu_item;// MenuItem的点击事件接口private OnItemClickListener mOnMenuItemClickListener;public CircleMenulayout(Context context,AttributeSet attrs) {super(context,attrs);// 无视paddingsetpadding(0,0);}// 设置菜单条目的图标和文本public voID setMenuItemIconsAndTexts(int[] images,String[] texts) {if (images == null && texts == null) {throw new IllegalArgumentException("菜单项文本和图片至少设置其一");}mItemimgs = images;mItemTexts = texts;// 初始化mMenuCountmMenuItemCount = images == null ? texts.length : images.length;if (images != null && texts != null) {mMenuItemCount = Math.min(images.length,texts.length);}// 构建菜单项buildMenuItems();}// 构建菜单项private voID buildMenuItems() {// 根据用户设置的参数,初始化menu itemfor (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() {@OverrIDepublic 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宽高@OverrIDeprotected 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的位置@OverrIDeprotected 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_DIMENSION);// 根据menu item的个数,计算item的布局占用的角度float angleDelay = 360 / childCount;// 遍历所有菜单项设置它们的位置for (int i = 0; i < childCount; i++) {final VIEw child = getChildAt(i);if (child.getVisibility() == GONE) {continue;}// 菜单项的起始角度mStartAngle %= 360;// 计算,中心点到menu item中心的距离float distanceFromCenter = mRadius / 2f - itemWIDth / 2 - mpadding;// distanceFromCenter cosa 即menu item中心点的left坐标left = mRadius / 2 + (int)Math.round(distanceFromCenter* Math.cos(Math.toradians(mStartAngle)) * - 1 / 2f * itemWIDth);// distanceFromCenter sina 即menu item的纵坐标top = mRadius / 2 + (int) Math.round(distanceFromCenter* Math.sin( Math.toradians(mStartAngle) ) * - 1 / 2f * itemWIDth);// 布局child vIEwchild.layout(left,top,left + itemWIDth,top + itemWIDth);// 叠加尺寸mStartAngle += angleDelay;}}

onLayout函数看起来稍显复杂,但它的含义就是将所有菜单项按照圆弧的形式布局。整个圆为360度,如果每个菜单项占用的角度为60度,那么第一个菜单项的角度为0~60,那么第二个菜单项的角度就是60~120,以此类推将所有菜单项按照圆形布局。首先要去计算每个菜单项的left 和 top位置 ,计算公式的图形化表示如图所示。

上图右下角那个小圆就是我们的菜单项,那么他的left坐标就是mRadius / 2 + tmp * coas,top坐标则是mRadius / 2 + tmp * sina 。这里的tmp就是我们代码中的distanceFromCenter变量。到了这一步之后小民的第一版圆形菜单算是完成了。
下面我们就来集成一下这个圆形菜单。

创建一个工程之后,首先在布局xml中添加圆形菜单控件,代码如下 :

<linearLayout xmlns:androID="http://schemas.androID.com/apk/res/androID"xmlns:tools="http://schemas.androID.com/tools"androID:layout_wIDth="match_parent"androID:layout_height="match_parent"androID:background="@drawable/bg"androID:gravity="center"androID:orIEntation="horizontal" ><com.dp.Widgets.CircleMenulayoutxmlns:androID="http://schemas.androID.com/apk/res/androID"androID:ID="@+ID/ID_menulayout"androID:layout_wIDth="wrap_content"androID:layout_height="wrap_content"androID:background="@drawable/circle_bg" /></linearLayout>

为了更好的显示效果,在布局xml中我们为圆形菜单的上一层以及圆形菜单本书都添加了一个背景图。然后在MainActivity中设置菜单项数据以及点击事件等。代码如下所示 :

public class MainActivity extends Activity {private CircleMenulayout mCircleMenulayout;// 菜单标题private String[] mItemTexts = new String[] {"安全中心 ","特色服务","投资理财","转账汇款","我的账户","xyk"};// 菜单图标Private int[] mItemimgs = new int[] {R.drawable.home_mbank_1_normal,R.drawable.home_mbank_2_normal,R.drawable.home_mbank_3_normal,R.drawable.home_mbank_4_normal,R.drawable.home_mbank_5_normal,R.drawable.home_mbank_6_normal};@OverrIDeprotected voID onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentVIEw(R.layout.activity_main);// 初始化圆形菜单mCircleMenulayout = (CircleMenulayout) findVIEwByID(R.ID.ID_menulayout);// 设置菜单数据项mCircleMenulayout.setMenuItemIconsAndTexts(mItemimgs,mItemTexts);// 设置菜单项点击事件mCircleMenulayout.setonItemClickListener(new OnItemClickListener() {@OverrIDepublic voID onClick(VIEw vIEw,int pos) {Toast.makeText(MainActivity.this,mItemTexts[pos],Toast.LENGTH_SHORT).show();}});}}

运行效果如前文的动图所示。

小民得意洋洋的蹦出了一个字:真酷!同时也为自己的学习能力感到骄傲,脸上写满了满足与自豪,感觉自己又朝高级工程师迈近了一步。

“这不是洋叔写的圆形菜单嘛,小民也下载了?”整准备下班的主管看到这个UI效果问道。小民只好把其中的缘由、实现方式一一说给主管听,小民还特地强调了CircleMenulayout的可定制型,通过setMenuItemLayoutID函数设置菜单项的布局ID,这样菜单项的UI效果就可以被用户定制化了。主管扫视了小民的代码,似乎察觉出了什么。于是转身找来还在埋头研究代码的洋叔,并且把小民的实现简单介绍了一遍,洋叔老师在扫视了一遍代码之后就发现了其中的问题所在。

“小民呐,你刚才说用户通过setMenuItemLayoutID函数可以设定菜单项的UI效果。那么问题来了,在你的CircleMenulayout中默认实现的是circle_menu_item.xml的逻辑,比如加载菜单项布局之后会通过findVIEwByID找到布局中的各个子视图,并且进行数据绑定。例如设置图标和文字,但这是针对circle_menu_item.xml这个布局的具体实现。如果用户设置菜单项布局为other_menu_item.xml,并且每个菜单项修改为就是一个button,那么此时他必须修改CircleMenulayout中初始化菜单项的代码。因为布局变了,菜单项里面的子VIEw类型也变化了,菜单需要的数据也发生了变化。例如菜单项不再需要图标,只需要文字。这样一来,用户每换一种菜单样式就需要修改一次CircleMenulayout类一次,并且设置菜单数据的接口也需要改变。这样就没有定制型可言了嘛,而且明显违反了开闭原则。反复对CircleMenulayout进行修改不免会引入各种各样的问题……”洋叔老师果然一针见血,深刻啊!小民这才发现了问题所在,于是请教洋叔老师应该如何处理比较合适。

“这种情况你应该使用Adapter,就像ListVIEw中的Adapter一样,让用户来自定义菜单项的布局、解析、数据绑定等工作,你需要知道的仅仅是每个菜单项都是一个VIEw。这样一来就将变化通过Adapter层隔离出去,你依赖的只是Adapter这个抽象。每个用户可以有不同的实现,你只需要实现圆形菜单的丈量、布局工作即可。这样就可以拥抱变化,可定制性就得到了保证。当然,你可以提供一个默认的Adapter,也就是使用你的 circle_menu_item.xml布局实现的菜单,这样没有定制需求的用户就可以使用这个默认的实现了。”小民频频点头,屡屡称是。“这确实是我之前没有考虑好,也是经验确实不足,我再好好重构一下。”小民发现问题之后也承认了自己的不足,两位前辈看小民这么好学就陪着小民一块重构代码。

在两位前辈的指点下,经过不到五分钟重构,小民的CircleMenulayout成了下面这样。

// 圆形菜单public class CircleMenulayout extends VIEwGroup {// 字段省略// 设置Adapterpublic voID setAdapter(listadapter mAdapter) {this.mAdapter = mAdapter;}// 构建菜单项private voID buildMenuItems() {// 根据用户设置的参数,初始化menu itemfor (int i = 0; i < mAdapter.getCount(); i++) {final VIEw itemVIEw = mAdapter.getVIEw(i,null,this);final int position = i;itemVIEw.setonClickListener(new OnClickListener() {@OverrIDepublic voID onClick(VIEw v) {if (mOnMenuItemClickListener != null) {mOnMenuItemClickListener.onClick(itemVIEw,position);}}});// 添加vIEw到容器中addVIEw(itemVIEw);}}@OverrIDeprotected voID onAttachedToWindow() {if (mAdapter != null) {buildMenuItems();}super.onAttachedToWindow();}// 丈量、布局代码省略}

现在的CircleMenulayout把解析xml、初始化菜单项的具体工作移除,添加了一个Adapter,在用户设置了Adapter之后,在onAttachedToWindow函数中调用Adapter的getCount函数获取菜单项的数量,然后通过getVIEw函数获取每个VIEw,最后将这些菜单项的VIEw添加到圆形菜单中,圆形菜单布局再将他们布局到特定的位置即可。

我们看现在使用CircleMenulayout是怎样的形式。首先定义了一个实体类MenuItem来存储菜单项图标和文本的信息,代码如下 :

static class MenuItem {public int imageID;public String Title;public MenuItem(String Title,int resID) {this.Title = Title;imageID = resID;}}

然后再实现一个Adapter,这个Adapter的类型就是listadapter。我们需要在getVIEw中加载菜单项xml、绑定数据等,相关代码如下 :

static class CircleMenuAdapter extends BaseAdapter {List<MenuItem> mMenuItems;public CircleMenuAdapter(List<MenuItem> menuItems) {mMenuItems = menuItems;}// 加载菜单项布局,并且初始化每个菜单@OverrIDepublic VIEw getVIEw(final int position,VIEw convertVIEw,VIEwGroup parent) {LayoutInflater mInflater = LayoutInflater.from(parent.getContext());VIEw itemVIEw = mInflater.inflate(R.layout.circle_menu_item,parent,false);initMenuItem(itemVIEw,position);return itemVIEw;}// 初始化菜单项private voID initMenuItem(VIEw itemVIEw,int position) {// 获取数据项final MenuItem item = getItem(position); 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.setimageResource(item.imageID);tv.setText(item.Title);}// 省略获取item count等代码}

这与我们在ListVIEw中使用Adapter是一致的,实现getVIEw、getCount等函数,在getVIEw中加载每一项的布局文件,并且绑定数据等。最终将菜单VIEw返回,然后这个VIEw就会被添加到CircleMenulayout中。这一步的 *** 作原来是放在CircleMenulayout中的,现在被独立出来,并且通过Adapter进行了隔离。这样就将易变的部分通过Adapter抽象隔离开来,即使用户有成千上万中菜单项UI效果,那么通过Adapter就可以很容易的进行扩展、实现,而不需要每次都修改CircleMenulayout中的代码。CircleMenulayout布局类相当于提供了一个圆形布局抽象,至于每一个子VIEw是啥样的它并不需要关心。通过Adapter隔离变化,拥抱变化,就是这么简单。

“原来ListVIEw、RecyclerVIEw通过一个Adapter是这个原因,通过Adapter将易变的部分独立出去交给用户处理。又通过观察者模式将数据和UI解耦合,使得VIEw与数据没有依赖,一份数据可以作用于多个UI,应对UI的易变性。原来如此!”小民最后总结道。

例如,当我们的产品发生变化,需要将圆形菜单修改为普通的ListVIEw样式,那么我们要做的事很简单,就是将xml布局中的CircleMenulayout修改为ListVIEw,然后将Adapter设置给ListVIEw即可。代码如下 :

public class MainActivity extends Activity {private ListVIEw mListVIEw;List<MenuItem> mMenuItems = new ArrayList<MenuItem>();@OverrIDeprotected voID onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentVIEw(R.layout.activity_main);// 模拟数据mockMenuItems();mListVIEw = (ListVIEw) findVIEwByID(R.ID.ID_menulayout);// 设置适配器mListVIEw.setAdapter(new CircleMenuAdapter(mMenuItems));// 设置点击事件mListVIEw.setonItemClickListener(new OnItemClickListener(){@OverrIDepublic voID onItemClick(AdapterVIEw<?> parent,VIEw vIEw,int position,long ID) {Toast.makeText(MainActivity.this,mMenuItems.get(position).Title,Toast.LENGTH_SHORT).show();}});}

这样我们就完成了UI替换,成本很低,也基本不会引发其他错误。这也就是为什么我们在CircleMenulayout中要使用listadapter的原因,就是为了与现有的ListVIEw、GrIDVIEw等组件进行兼容,当然我们也没有啥必要重新再定义一个Adapter类型,从此我们就可以任意修改我们的菜单Item样式了,保证了这个组件的灵活性!! 替换为ListVIEw的效果如下所示:


“走,我请两位前辈吃烤鱼去!”小民在重构完CircleMenulayout之后深感收获颇多,为了报答主管和洋叔的指点嚷嚷着要请吃饭。“那就走吧!”主管倒是爽快的答应了,洋叔老师也是立马应允,三人收拾好电脑后就朝着楼下的巫山烤鱼店走去。

20.9总结

Adapter模式的经典实现在于将原本不兼容的接口融合在一起,使之能够很好的进行合作。但是在实际开发中,Adapter模式也有一些灵活的实现。例如ListVIEw中的隔离变化,使得整个UI架构变得更灵活,能够拥抱变化。Adapter模式在开发中运用非常广泛,因此掌握Adapter模式是非常必要的。

关于Adapter模式实战之重构鸿洋集团的AndroID圆形菜单建行的相关知识就给大家介绍到这里,希望对大家有所帮助!

总结

以上是内存溢出为你收集整理的Adapter模式实战之重构鸿洋集团的Android圆形菜单建行全部内容,希望文章能够帮你解决Adapter模式实战之重构鸿洋集团的Android圆形菜单建行所遇到的程序开发问题。

如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/web/1149578.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-31
下一篇 2022-05-31

发表评论

登录后才能评论

评论列表(0条)

保存