Adapter模式實(shí)戰(zhàn)之重構(gòu)鴻洋集團(tuán)的Android圓形菜單建行
對于很多開發(fā)人員來說,炫酷的UI效果是最吸引他們注意力的,很多人也因?yàn)檫@些炫酷的效果而去學(xué)習(xí)一些比較知名的UI庫。而做出炫酷效果的前提是你必須對自定義View有所理解,作為90的小民自然也不例外。特別對于剛處在開發(fā)初期的小民,對于自定義View這件事覺得又神秘又帥氣,于是小民決定深入研究自定義View以及相關(guān)的知識點(diǎn)。
在此之前我們先來看看洋神的原版效果圖:
記得那是2014年的第一場雪,比以往時(shí)候來得稍晚一些。小民的同事洋叔是一位資深的研發(fā)人員,擅長寫UI特效,在開發(fā)領(lǐng)域知名度頗高。最近洋叔剛發(fā)布了一個(gè)效果不錯的圓形菜單,這個(gè)菜單的每個(gè)Item環(huán)形排布,并且可以轉(zhuǎn)動。小民決定仿照洋叔的效果實(shí)現(xiàn)一遍,但是對于小民這個(gè)階段來說只要實(shí)現(xiàn)環(huán)形布局就不錯了,轉(zhuǎn)動部分作為下個(gè)版本功能,就當(dāng)作自定義View的練習(xí)了。
在google了自定義View相關(guān)的知識點(diǎn)之后,小民就寫好了這個(gè)圓形菜單布局視圖,我們一步一步來講解,代碼如下:
// 圓形菜單 public class CircleMenuLayout extends ViewGroup { // 圓形直徑 private int mRadius; // 該容器內(nèi)child item的默認(rèn)尺寸 private static final float RADIO_DEFAULT_CHILD_DIMENSION = 1 / 4f; // 該容器的內(nèi)邊距,無視padding屬性,如需邊距請用該變量 private static final float RADIO_PADDING_LAYOUT = 1 / 12f; // 該容器的內(nèi)邊距,無視padding屬性,如需邊距請用該變量 private float mPadding; // 布局時(shí)的開始角度 private double mStartAngle = 0; // 菜單項(xiàng)的文本 private String[] mItemTexts; // 菜單項(xiàng)的圖標(biāo) private int[] mItemImgs; // 菜單的個(gè)數(shù) private int mMenuItemCount; // 菜單布局資源id private int mMenuItemLayoutId = R.layout.circle_menu_item; // MenuItem的點(diǎn)擊事件接口 private OnItemClickListener mOnMenuItemClickListener; public CircleMenuLayout(Context context, AttributeSet attrs) { super(context, attrs); // 無視padding setPadding(0, 0, 0, 0); } // 設(shè)置菜單條目的圖標(biāo)和文本 public void setMenuItemIconsAndTexts(int[] images, String[] texts) { if (images == null && texts == null) { throw new IllegalArgumentException("菜單項(xiàng)文本和圖片至少設(shè)置其一"); } mItemImgs = images; mItemTexts = texts; // 初始化mMenuCount mMenuItemCount = images == null ? texts.length : images.length; if (images != null && texts != null) { mMenuItemCount = Math.min(images.length, texts.length); } // 構(gòu)建菜單項(xiàng) buildMenuItems(); } // 構(gòu)建菜單項(xiàng) private void buildMenuItems() { // 根據(jù)用戶設(shè)置的參數(shù),初始化menu item for (int i = 0; i < mMenuItemCount; i++) { View itemView = inflateMenuView(i); // 初始化菜單項(xiàng) 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]); } // 設(shè)置MenuItem的布局文件,必須在setMenuItemIconsAndTexts之前調(diào)用 public void setMenuItemLayoutId(int mMenuItemLayoutId) { this.mMenuItemLayoutId = mMenuItemLayoutId; } // 設(shè)置MenuItem的點(diǎn)擊事件接口 public void setOnItemClickListener(OnItemClickListener listener) { this.mOnMenuItemClickListener = listener; } // 代碼省略 }
小民的思路大致是這樣的,首先讓用戶通過setMenuItemIconsAndTexts函數(shù)將菜單項(xiàng)的圖標(biāo)和文本傳遞進(jìn)來,根據(jù)這些圖標(biāo)和文本構(gòu)建菜單項(xiàng),菜單項(xiàng)的布局視圖由mMenuItemLayoutId存儲起來,這個(gè)mMenuItemLayoutId默認(rèn)為circle_menu_item.xml,這個(gè)xml布局為一個(gè)ImageView顯示在一個(gè)文本控件的上面。為了菜單項(xiàng)的可定制型,小民還添加了一個(gè)setMenuItemLayoutId函數(shù)讓用戶可以設(shè)置菜單項(xiàng)的布局,希望用戶可以定制各種各樣的菜單樣式。在用戶設(shè)置了菜單項(xiàng)的相關(guān)數(shù)據(jù)之后,小民會根據(jù)用戶設(shè)置進(jìn)來的圖標(biāo)和文本數(shù)量來構(gòu)建、初始化相等數(shù)量的菜單項(xiàng),并且將這些菜單項(xiàng)添加到圓形菜單CircleMenuLayout中。然后添加了一個(gè)可以設(shè)置用戶點(diǎn)擊菜單項(xiàng)的處理接口的setOnItemClickListener函數(shù),使得菜單的點(diǎn)擊事件可以被用戶自定義處理。
在將菜單項(xiàng)添加到CircleMenuLayout之后就是要對這些菜單項(xiàng)進(jìn)行尺寸丈量和布局了,我們先來看丈量尺寸的代碼,如下 :
//設(shè)置布局的寬高,并策略menu item寬高 @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 丈量自身尺寸 measureMyself(widthMeasureSpec, heightMeasureSpec); // 丈量菜單項(xiàng)尺寸 measureChildViews(); } private void measureMyself(int widthMeasureSpec, int heightMeasureSpec) { int resWidth = 0; int resHeight = 0; // 根據(jù)傳入的參數(shù),分別獲取測量模式和測量值 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) { // 主要設(shè)置為背景圖的高度 resWidth = getSuggestedMinimumWidth(); // 如果未設(shè)置背景圖片,則設(shè)置為屏幕寬高的默認(rèn)值 resWidth = resWidth == 0 ? getDefaultWidth() : resWidth; resHeight = getSuggestedMinimumHeight(); // 如果未設(shè)置背景圖片,則設(shè)置為屏幕寬高的默認(rèn)值 resHeight = resHeight == 0 ? getDefaultWidth() : resHeight; } else { // 如果都設(shè)置為精確值,則直接取小值; resWidth = resHeight = Math.min(width, height); } setMeasuredDimension(resWidth, resHeight); } private void measureChildViews() { // 獲得半徑 mRadius = Math.max(getMeasuredWidth(), getMeasuredHeight()); // menu item數(shù)量 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; } // 計(jì)算menu item的尺寸;以及和設(shè)置好的模式,去對item進(jìn)行測量 int makeMeasureSpec = -1; makeMeasureSpec = MeasureSpec.makeMeasureSpec(childSize, childMode); child.measure(makeMeasureSpec, makeMeasureSpec); } mPadding = RADIO_PADDING_LAYOUT * mRadius; }
代碼比較簡單,就是先測量CircleMenuLayout的尺寸,然后測量每個(gè)菜單項(xiàng)的尺寸。尺寸獲取了之后就到了布局這一步,這也是整個(gè)圓形菜單的核心所在。代碼如下 :
// 布局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_DIMENSION); // 根據(jù)menu item的個(gè)數(shù),計(jì)算item的布局占用的角度 float angleDelay = 360 / childCount; // 遍歷所有菜單項(xiàng)設(shè)置它們的位置 for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); if (child.getVisibility() == GONE) { continue; } // 菜單項(xiàng)的起始角度 mStartAngle %= 360; // 計(jì)算,中心點(diǎn)到menu item中心的距離 float distanceFromCenter = mRadius / 2f - itemWidth / 2 - mPadding; // distanceFromCenter cosa 即menu item中心點(diǎn)的left坐標(biāo) left = mRadius / 2 + (int)Math.round(distanceFromCenter * Math.cos(Math.toRadians(mStartAngle)) * - 1 / 2f * itemWidth); // distanceFromCenter sina 即menu item的縱坐標(biāo) top = mRadius / 2 + (int) Math.round(distanceFromCenter * Math.sin( Math.toRadians(mStartAngle) ) * - 1 / 2f * itemWidth); // 布局child view child.layout(left, top, left + itemWidth, top + itemWidth); // 疊加尺寸 mStartAngle += angleDelay; } }
onLayout函數(shù)看起來稍顯復(fù)雜,但它的含義就是將所有菜單項(xiàng)按照圓弧的形式布局。整個(gè)圓為360度,如果每個(gè)菜單項(xiàng)占用的角度為60度,那么第一個(gè)菜單項(xiàng)的角度為0~60,那么第二個(gè)菜單項(xiàng)的角度就是60~120,以此類推將所有菜單項(xiàng)按照圓形布局。首先要去計(jì)算每個(gè)菜單項(xiàng)的left 和 top位置 ,計(jì)算公式的圖形化表示如圖所示。
上圖右下角那個(gè)小圓就是我們的菜單項(xiàng),那么他的left坐標(biāo)就是mRadius / 2 + tmp * coas , top坐標(biāo)則是mRadius / 2 + tmp * sina 。這里的tmp就是我們代碼中的distanceFromCenter變量。到了這一步之后小民的第一版圓形菜單算是完成了。
下面我們就來集成一下這個(gè)圓形菜單。
創(chuàng)建一個(gè)工程之后,首先在布局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.CircleMenuLayout xmlns: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中我們?yōu)閳A形菜單的上一層以及圓形菜單本書都添加了一個(gè)背景圖。然后在MainActivity中設(shè)置菜單項(xiàng)數(shù)據(jù)以及點(diǎn)擊事件等。代碼如下所示 :
public class MainActivity extends Activity { private CircleMenuLayout mCircleMenuLayout; // 菜單標(biāo)題 private String[] mItemTexts = new String[] { "安全中心 ", "特色服務(wù)", "投資理財(cái)", "轉(zhuǎn)賬匯款", "我的賬戶", "信用卡" }; // 菜單圖標(biāo) 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 }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 初始化圓形菜單 mCircleMenuLayout = (CircleMenuLayout) findViewById(R.id.id_menulayout); // 設(shè)置菜單數(shù)據(jù)項(xiàng) mCircleMenuLayout.setMenuItemIconsAndTexts(mItemImgs, mItemTexts); // 設(shè)置菜單項(xiàng)點(diǎn)擊事件 mCircleMenuLayout.setOnItemClickListener(new OnItemClickListener() { @Override public void onClick(View view, int pos) { Toast.makeText(MainActivity.this, mItemTexts[pos], Toast.LENGTH_SHORT).show(); } }); } }
運(yùn)行效果如前文的動圖所示。
小民得意洋洋的蹦出了一個(gè)字:真酷!同時(shí)也為自己的學(xué)習(xí)能力感到驕傲,臉上寫滿了滿足與自豪,感覺自己又朝高級工程師邁近了一步。
“這不是洋叔寫的圓形菜單嘛,小民也下載了?”整準(zhǔn)備下班的主管看到這個(gè)UI效果問道。小民只好把其中的緣由、實(shí)現(xiàn)方式一一說給主管聽,小民還特地強(qiáng)調(diào)了CircleMenuLayout的可定制型,通過setMenuItemLayoutId函數(shù)設(shè)置菜單項(xiàng)的布局id,這樣菜單項(xiàng)的UI效果就可以被用戶定制化了。主管掃視了小民的代碼,似乎察覺出了什么。于是轉(zhuǎn)身找來還在埋頭研究代碼的洋叔,并且把小民的實(shí)現(xiàn)簡單介紹了一遍,洋叔老師在掃視了一遍代碼之后就發(fā)現(xiàn)了其中的問題所在。
“小民吶,你剛才說用戶通過setMenuItemLayoutId函數(shù)可以設(shè)定菜單項(xiàng)的UI效果。那么問題來了,在你的CircleMenuLayout中默認(rèn)實(shí)現(xiàn)的是circle_menu_item.xml的邏輯,比如加載菜單項(xiàng)布局之后會通過findViewById找到布局中的各個(gè)子視圖,并且進(jìn)行數(shù)據(jù)綁定。例如設(shè)置圖標(biāo)和文字,但這是針對circle_menu_item.xml這個(gè)布局的具體實(shí)現(xiàn)。如果用戶設(shè)置菜單項(xiàng)布局為other_menu_item.xml,并且每個(gè)菜單項(xiàng)修改為就是一個(gè)Button,那么此時(shí)他必須修改CircleMenuLayout中初始化菜單項(xiàng)的代碼。因?yàn)椴季肿兞?,菜單?xiàng)里面的子View類型也變化了,菜單需要的數(shù)據(jù)也發(fā)生了變化。例如菜單項(xiàng)不再需要圖標(biāo),只需要文字。這樣一來,用戶每換一種菜單樣式就需要修改一次CircleMenuLayout類一次,并且設(shè)置菜單數(shù)據(jù)的接口也需要改變。這樣就沒有定制型可言了嘛,而且明顯違反了開閉原則。反復(fù)對CircleMenuLayout進(jìn)行修改不免會引入各種各樣的問題……”洋叔老師果然一針見血,深刻?。⌒∶襁@才發(fā)現(xiàn)了問題所在,于是請教洋叔老師應(yīng)該如何處理比較合適。
“這種情況你應(yīng)該使用Adapter,就像ListView中的Adapter一樣,讓用戶來自定義菜單項(xiàng)的布局、解析、數(shù)據(jù)綁定等工作,你需要知道的僅僅是每個(gè)菜單項(xiàng)都是一個(gè)View。這樣一來就將變化通過Adapter層隔離出去,你依賴的只是Adapter這個(gè)抽象。每個(gè)用戶可以有不同的實(shí)現(xiàn),你只需要實(shí)現(xiàn)圓形菜單的丈量、布局工作即可。這樣就可以擁抱變化,可定制性就得到了保證。當(dāng)然,你可以提供一個(gè)默認(rèn)的Adapter,也就是使用你的 circle_menu_item.xml布局實(shí)現(xiàn)的菜單,這樣沒有定制需求的用戶就可以使用這個(gè)默認(rèn)的實(shí)現(xiàn)了?!毙∶耦l頻點(diǎn)頭,屢屢稱是?!斑@確實(shí)是我之前沒有考慮好,也是經(jīng)驗(yàn)確實(shí)不足,我再好好重構(gòu)一下。”小民發(fā)現(xiàn)問題之后也承認(rèn)了自己的不足,兩位前輩看小民這么好學(xué)就陪著小民一塊重構(gòu)代碼。
在兩位前輩的指點(diǎn)下,經(jīng)過不到五分鐘重構(gòu),小民的CircleMenuLayout成了下面這樣。
// 圓形菜單 public class CircleMenuLayout extends ViewGroup { // 字段省略 // 設(shè)置Adapter public void setAdapter(ListAdapter mAdapter) { this.mAdapter = mAdapter; } // 構(gòu)建菜單項(xiàng) private void buildMenuItems() { // 根據(jù)用戶設(shè)置的參數(shù),初始化menu item for (int i = 0; i < mAdapter.getCount(); i++) { final View itemView = mAdapter.getView(i, null, this); final int position = i; itemView.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (mOnMenuItemClickListener != null) { mOnMenuItemClickListener.onClick(itemView, position); } } }); // 添加view到容器中 addView(itemView); } } @Override protected void onAttachedToWindow() { if (mAdapter != null) { buildMenuItems(); } super.onAttachedToWindow(); } // 丈量、布局代碼省略 }
現(xiàn)在的CircleMenuLayout把解析xml、初始化菜單項(xiàng)的具體工作移除,添加了一個(gè)Adapter,在用戶設(shè)置了Adapter之后,在onAttachedToWindow函數(shù)中調(diào)用Adapter的getCount函數(shù)獲取菜單項(xiàng)的數(shù)量,然后通過getView函數(shù)獲取每個(gè)View,最后將這些菜單項(xiàng)的View添加到圓形菜單中,圓形菜單布局再將他們布局到特定的位置即可。
我們看現(xiàn)在使用CircleMenuLayout是怎樣的形式。首先定義了一個(gè)實(shí)體類MenuItem來存儲菜單項(xiàng)圖標(biāo)和文本的信息,代碼如下 :
static class MenuItem { public int imageId; public String title; public MenuItem(String title, int resId) { this.title = title; imageId = resId; } }
然后再實(shí)現(xiàn)一個(gè)Adapter,這個(gè)Adapter的類型就是ListAdapter。我們需要在getView中加載菜單項(xiàng)xml、綁定數(shù)據(jù)等,相關(guān)代碼如下 :
static class CircleMenuAdapter extends BaseAdapter { List<MenuItem> mMenuItems; public CircleMenuAdapter(List<MenuItem> menuItems) { mMenuItems = menuItems; } // 加載菜單項(xiàng)布局,并且初始化每個(gè)菜單 @Override public 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; } // 初始化菜單項(xiàng) private void initMenuItem(View itemView, int position) { // 獲取數(shù)據(jù)項(xiàng) 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); // 數(shù)據(jù)綁定 iv.setImageResource(item.imageId); tv.setText(item.title); } // 省略獲取item count等代碼 }
這與我們在ListView中使用Adapter是一致的,實(shí)現(xiàn)getView、getCount等函數(shù),在getView中加載每一項(xiàng)的布局文件,并且綁定數(shù)據(jù)等。最終將菜單View返回,然后這個(gè)View就會被添加到CircleMenuLayout中。這一步的操作原來是放在CircleMenuLayout中的,現(xiàn)在被獨(dú)立出來,并且通過Adapter進(jìn)行了隔離。這樣就將易變的部分通過Adapter抽象隔離開來,即使用戶有成千上萬中菜單項(xiàng)UI效果,那么通過Adapter就可以很容易的進(jìn)行擴(kuò)展、實(shí)現(xiàn),而不需要每次都修改CircleMenuLayout中的代碼。CircleMenuLayout布局類相當(dāng)于提供了一個(gè)圓形布局抽象,至于每一個(gè)子View是啥樣的它并不需要關(guān)心。通過Adapter隔離變化,擁抱變化,就是這么簡單。
“原來ListView、RecyclerView通過一個(gè)Adapter是這個(gè)原因,通過Adapter將易變的部分獨(dú)立出去交給用戶處理。又通過觀察者模式將數(shù)據(jù)和UI解耦合,使得View與數(shù)據(jù)沒有依賴,一份數(shù)據(jù)可以作用于多個(gè)UI,應(yīng)對UI的易變性。原來如此!”小民最后總結(jié)道。
例如,當(dāng)我們的產(chǎn)品發(fā)生變化,需要將圓形菜單修改為普通的ListView樣式,那么我們要做的事很簡單,就是將xml布局中的CircleMenuLayout修改為ListView,然后將Adapter設(shè)置給ListView即可。代碼如下 :
public class MainActivity extends Activity { private ListView mListView; List<MenuItem> mMenuItems = new ArrayList<MenuItem>(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 模擬數(shù)據(jù) mockMenuItems(); mListView = (ListView) findViewById(R.id.id_menulayout); // 設(shè)置適配器 mListView.setAdapter(new CircleMenuAdapter(mMenuItems)); // 設(shè)置點(diǎn)擊事件 mListView.setOnItemClickListener(new OnItemClickListener(){ @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { Toast.makeText(MainActivity.this, mMenuItems.get(position).title, Toast.LENGTH_SHORT).show(); } }); }
這樣我們就完成了UI替換,成本很低,也基本不會引發(fā)其他錯誤。這也就是為什么我們在CircleMenuLayout中要使用ListAdapter的原因,就是為了與現(xiàn)有的ListView、GridView等組件進(jìn)行兼容,當(dāng)然我們也沒有啥必要重新再定義一個(gè)Adapter類型,從此我們就可以任意修改我們的菜單Item樣式了,保證了這個(gè)組件的靈活性!! 替換為ListView的效果如下所示:
“走,我請兩位前輩吃烤魚去!”小民在重構(gòu)完CircleMenuLayout之后深感收獲頗多,為了報(bào)答主管和洋叔的指點(diǎn)嚷嚷著要請吃飯?!澳蔷妥甙?!”主管倒是爽快的答應(yīng)了,洋叔老師也是立馬應(yīng)允,三人收拾好電腦后就朝著樓下的巫山烤魚店走去。
20.9總結(jié)
Adapter模式的經(jīng)典實(shí)現(xiàn)在于將原本不兼容的接口融合在一起,使之能夠很好的進(jìn)行合作。但是在實(shí)際開發(fā)中,Adapter模式也有一些靈活的實(shí)現(xiàn)。例如ListView中的隔離變化,使得整個(gè)UI架構(gòu)變得更靈活,能夠擁抱變化。Adapter模式在開發(fā)中運(yùn)用非常廣泛,因此掌握Adapter模式是非常必要的。
關(guān)于Adapter模式實(shí)戰(zhàn)之重構(gòu)鴻洋集團(tuán)的Android圓形菜單建行的相關(guān)知識就給大家介紹到這里,希望對大家有所幫助!
- Android 自定義組件衛(wèi)星菜單的實(shí)現(xiàn)
- Android衛(wèi)星菜單效果的實(shí)現(xiàn)方法
- Android自定義VIew實(shí)現(xiàn)衛(wèi)星菜單效果淺析
- Android實(shí)現(xiàn)自定義的衛(wèi)星式菜單(弧形菜單)詳解
- Android編程實(shí)現(xiàn)仿優(yōu)酷圓盤旋轉(zhuǎn)菜單效果的方法詳解【附demo源碼下載】
- Android學(xué)習(xí)教程之圓形Menu菜單制作方法(1)
- Android自定義view實(shí)現(xiàn)圓形與半圓形菜單
- Android圓形旋轉(zhuǎn)菜單開發(fā)實(shí)例
- Android自定義ViewGroup實(shí)現(xiàn)帶箭頭的圓角矩形菜單
- Android仿優(yōu)酷圓形菜單學(xué)習(xí)筆記分享
- Android實(shí)現(xiàn)衛(wèi)星菜單效果
相關(guān)文章
Json數(shù)據(jù)解析模擬美團(tuán)界面顯示
這篇文章主要介紹了Json數(shù)據(jù)解析模擬美團(tuán)界面顯示,涉及到j(luò)son數(shù)據(jù)解析相關(guān)知識,本文寫的非常不錯,具有參考價(jià)值,特此分享供大家學(xué)習(xí)2016-01-01TextView使用SpannableString設(shè)置復(fù)合文本 SpannableString實(shí)現(xiàn)TextView的鏈接
這篇文章主要為大家詳細(xì)介紹了如何利用SpannableString實(shí)現(xiàn)TextView的鏈接效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-08-08Android 動態(tài)添加Fragment的實(shí)例代碼
這篇文章主要介紹了Android 動態(tài)添加Fragment的實(shí)例代碼的相關(guān)資料,非常不錯,具有參考借鑒價(jià)值,需要的朋友可以參考下2016-08-08Android Camera2實(shí)現(xiàn)最簡單的預(yù)覽框顯示
這篇文章主要為大家詳細(xì)介紹了Android Camera2實(shí)現(xiàn)最簡單的預(yù)覽框顯示,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-05-05