Adapter模式實戰之重構鴻洋集團的Android圓形菜單建行_Android

來源:互聯網
上載者:User

對於很多開發人員來說,炫酷的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, 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[] {"資訊安全中心 ", "特色服務", "投資理財","轉賬匯款", "我的賬戶", "信用卡"};// 菜單表徵圖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圓形菜單建行的相關知識就給大家介紹到這裡,希望對大家有所協助!

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.