本文原創(chuàng),轉(zhuǎn)載請注明本出處! 本項目 GitHub 地址:https://github.com/totond/YMenuView 歡迎 Star or Fork! 網(wǎng)上這種類似 PathMenu 的菜單很多,但是基本都不符合我項目的需求,想看他們的源碼實現(xiàn)然后做出修改,進行二次開發(fā)來適應我的項目需求,但是發(fā)現(xiàn)——以我現(xiàn)在的能力,如果不是以前做過類似的功能,看別人的代碼,很難很快地找出主要實現(xiàn)思路,而且不同的作者的代碼有不同的風格(特別是命名),于是就自己按照自己的思路來實現(xiàn),然后把實現(xiàn)思路都寫出來分享一下,讓大家了解我這個自定義 View 控件是怎么實現(xiàn)的,到時候大家根據(jù)需求修改源碼,進行二次開發(fā)的時候也可以參考,也希望和大家一起探討怎樣實現(xiàn)更好。 做這個控件的目的是為了實現(xiàn)一個平板上的全屏視頻播放器的菜單欄,點擊之后會彈出一堆按鈕來讓用戶選擇 ,這樣的話網(wǎng)上很多開源控件都能實現(xiàn),問題就是這個播放器是要支持 Android7.0 的分屏功能,(平板比較坑爹,還打開了 Freeform 模式的入口,這個 Freeform 模式可以讓用戶自由調(diào)節(jié) APP 的界面寬高,就像在 Windows 桌面的那些應用窗口一樣),要適應分屏功能,APP 的寬高可能會改變,這些按鈕的位置分布情況也要根據(jù)寬高來改變,想想就蛋疼。而網(wǎng)上很多的這類型 PathMenu是固定分布方式的,所以就做出了這個可以調(diào)整選項位置的自定義菜單控件——YMenuView(取名技術(shù)不好不知道怎么取,就用這個挫名字啦(≧▽≦)/)。 具體實現(xiàn)思路思路大概如下圖: 
其中主要難點是第二個和第三個。簡單來說,本質(zhì)上 YMenuView 是一個ViewGroup,然后在里面動態(tài)生成一些控件,點擊MenuButton的時候就會把一堆OptionButton顯示/消失,這個過程加上一些動畫,就形成最后的效果。 創(chuàng)建ViewGroup這部分其實沒什么好說的,就是創(chuàng)建一個名為 YMenuView 的ViewGroup,然后獲取一些自定義的屬性(自定義屬性的介紹可以自行搜索或者看看我的筆記,這里不多說了),為下一步的創(chuàng)建 MenuButton 和 OptionButton 做準備,獲取的屬性在項目的 Github 地址上有詳細的說明了。篇幅原因,這里就放出部分重要的屬性圖示:

創(chuàng)建 MenuButton 和 OptionButton如上圖所示,MenuButton 就是那個用于按下彈出菜單的按鈕,OptionButton就是可彈出收回的選項按鈕。 MenuButton下面先來看看如何創(chuàng)建 MenuButton: private void setMenuButton() { mYMenuButton = new Button(mContext); //設置MenuButton的大小位置 LayoutParams layoutParams = new LayoutParams(mYMenuButtonWidth, mYMenuButtonHeight); layoutParams.setMarginEnd(mYMenuButtonRightMargin); layoutParams.bottomMargin = mYMenuButtonBottomMargin; layoutParams.addRule(ALIGN_PARENT_RIGHT); layoutParams.addRule(ALIGN_PARENT_BOTTOM); //生成ID mYMenuButton.setId(generateViewId());
mYMenuButton.setLayoutParams(layoutParams); //設置打開關(guān)閉事件 mYMenuButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (!isShowMenu) { showMenu(); } else { closeMenu(); } } }); mYMenuButton.setBackgroundResource(mMenuButtonBackGroundId); addView(mYMenuButton); } 主要是動態(tài)生成一個 Button,利用 LayoutParams 來控制它的位置,生成ID是為后面要使用 mYMenuButton 的信息做準備,然后就是設置點擊事件來控制菜單開關(guān)(開關(guān)的操作實現(xiàn)后面講),最后就是設置背景和addView() 加入父 ViewGroup 視圖。 OptionButton說是 Button,但是實際上 OptionButton 是繼承 ImageView,因為我發(fā)現(xiàn) ImageView 除了可以通過 setImageDrawable() 方法設置圖片資源之外,還可以通過 setBackground() 方法設置背景,這樣的話可以很容易實現(xiàn)給圖片加框的效果(demo 中的 OptionButton 效果就是通過圓形 shape 和圖片資源合成的),下面的 OptionButton 創(chuàng)建過程中,位置計算是比較復雜的: private void initBan() { //對Ban數(shù)組進行從小到大排序 Arrays.sort(banArray); }
//設置選項按鈕 private void setOptionButtons() throws Exception { optionButtonList = new ArrayList<>(optionPositionCount); initBan(); boolean isBan = true; for (int i = 0,n = 0; i < optionPositionCount; i++) { if (isBan && banArray.length > 0) { //Ban判斷 if (i > banArray[n] || banArray[n] > optionPositionCount - 1) { throw new Exception("Ban數(shù)組設置不合理,含有負數(shù)、重復數(shù)字或者超出范圍"); } else if (i == banArray[n]) { if (n < banArray.length - 1) { n++; }else { isBan = false; } continue; } }
OptionButton button = new OptionButton(mContext); //設置動畫的模式和時長 button.setSD_Animation(mOptionSD_AnimationMode); button.setDuration(mOptionSD_AnimationDuration); int btnId = generateViewId(); button.setId(btnId);
RelativeLayout.LayoutParams layoutParams = new LayoutParams(mYOptionButtonWidth, mYOptionButtonHeight);
//計算OptionButton的位置 int position = i % optionColumns;
layoutParams.rightMargin = mYOptionToMenuRightMargin + mYOptionHorizontalMargin * position + mYOptionButtonWidth * position;
layoutParams.bottomMargin = mYOptionToMenuBottomMargin + (mYOptionButtonHeight + mYOptionVerticalMargin) * (i / optionColumns); layoutParams.addRule(ALIGN_PARENT_BOTTOM); layoutParams.addRule(ALIGN_PARENT_RIGHT);
button.setLayoutParams(layoutParams); addView(button); optionButtonList.add(button); } } 先不看 Ban 判斷,看下面的位置計算,OptionButton 的布局是矩形的,每一排中的每個 ImageView 從左到右的,而 optionColumns 是列數(shù)(也就是每排的個數(shù)),通過這個屬性和總個數(shù)就可以確定所有OptionButton 的布局。如下面就是 optionPositionCount = 8,optionColumns = 3 的效果: 
然后再看Ban判斷,這段代碼的目的就是讓序號為Ban數(shù)組里面的位置跳過這一輪循環(huán),不放置OptionButton。所以Ban這個功能可以通過setBanArray(int... banArray) 方法設置banArray數(shù)組,里面填入位置序號,然后這個位置就不放OptionButton了,如下圖,就是設置了banArray = {0,2,6} 和optionPositionCount = 8 : 
前面只是生成了 OptionButton,后面還要為它們設置圖片和背景: //設置選項按鈕的background public void setOptionBackGrounds(@DrawableRes Integer drawableId){ for (int i = 0; i < optionButtonList.size(); i++) { if (drawableId == null){ optionButtonList.get(i).setBackground(null); }else { optionButtonList.get(i).setBackgroundResource(drawableId); } } }
//設置選項按鈕的圖片資源,順便設置點擊事件 private void setOptionsImages(int... drawableIds) throws Exception { this.drawableIds = drawableIds; if (optionPositionCount > drawableIds.length + banArray.length) { throw new Exception("Drawable資源數(shù)量不足"); }
for (int i = 0; i < optionButtonList.size(); i++) { optionButtonList.get(i).setOnClickListener(new MyOnClickListener(i)); if (drawableIds == null){ optionButtonList.get(i).setImageDrawable(null); }else { optionButtonList.get(i).setImageResource(drawableIds[i]); }
} } 實現(xiàn)動畫MenuButton 的動畫沒什么好說的,開關(guān)動畫就是兩個旋轉(zhuǎn)(一個逆時針,一個順時針),動畫已經(jīng)在 xml 寫好了,太簡單了就不展示出來,想看的話直接看源碼好了: //初始化MenuButton的點擊動畫 private void initMenuAnim() { menuOpenAnimation = AnimationUtils.loadAnimation(mContext, R.anim.rotate_open); menuCloseAnimation = AnimationUtils.loadAnimation(mContext, R.anim.rotate_close); animationListener = new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { mYMenuButton.setClickable(false);
}
@Override public void onAnimationEnd(Animation animation) { mYMenuButton.setClickable(true); }
@Override public void onAnimationRepeat(Animation animation) {
} }; menuOpenAnimation.setDuration(mOptionSD_AnimationDuration); menuCloseAnimation.setDuration(mOptionSD_AnimationDuration); menuOpenAnimation.setAnimationListener(animationListener); menuCloseAnimation.setAnimationListener(animationListener); } 還開放了方法,可以在外部改變這個開關(guān)動畫: //設置MenuButton彈出菜單選項時候MenuButton自身的動畫,默認為順時針旋轉(zhuǎn)180度,為空則是關(guān)閉動畫 public void setMenuOpenAnimation(Animation menuOpenAnimation) { menuOpenAnimation.setAnimationListener(animationListener); this.menuOpenAnimation = menuOpenAnimation;
}
//設置MenuButton收回菜單選項時候MenuButton自身的動畫,默認為逆時針旋轉(zhuǎn)180度,為空則是關(guān)閉動畫 public void setMenuCloseAnimation(Animation menuCloseAnimation) { menuCloseAnimation.setAnimationListener(animationListener); this.menuCloseAnimation = menuCloseAnimation; } 然后重點就是OptionButton的動畫了,它的動畫有四種: sd_animMode | 描述 |
---|
FROM_BUTTON_LEFT | 選項從菜單鍵左邊緣飛入 | FROM_BUTTON_TOP | 選項從菜單鍵上邊緣飛入 | FROM_RIGHT | 選項從View左邊緣飛入 | FROM_BOTTOM | 選項從View左邊緣飛入 |
這些動畫封裝在 OptionButton 里面,因為動畫的設置需要用到自身的位置信息,所以需要注冊 OnGlobalLayoutListener 來監(jiān)聽,等自身Layout 完畢之后再設置,不然getLeft() 等方法返回的都是0: private void init(){ setClickable(true);
//在獲取到寬高參數(shù)之后再進行初始化 ViewTreeObserver viewTreeObserver = getViewTreeObserver(); viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { if (getX() != 0 && getY() != 0 && getWidth() != 0 && getHeight() != 0) { setShowAndDisappear(); //設置完后立刻注銷,不然會不斷回調(diào),浪費很多資源 getViewTreeObserver().removeOnGlobalLayoutListener(this); } } });
} 因為進入和退出的動畫只是基本只是相反,篇幅原因這里就只展示退出動畫的實現(xiàn)(看的時候要注意動畫的初始坐標零點默認都是基于View的左上角頂點): private void setShowAndDisappear() { setShowAnimation(mDuration); setDisappearAnimation(mDuration); //在這里才設置Gone很重要,讓View可以一開始就觸發(fā)onGlobalLayout()進行初始化 setVisibility(GONE); }
public void setDisappearAnimation(int duration) { //獲取父ViewGroup的對象,用于獲取寬高參數(shù) YMenuView parent = (YMenuView) getParent(); AlphaAnimation alphaAnimation = new AlphaAnimation(1,0); alphaAnimation.setDuration(duration); TranslateAnimation translateAnimation = new TranslateAnimation(0,0,0,0); switch (mSD_Animation) { case FROM_BUTTON_LEFT: //從MenuButton的左邊移入 translateAnimation= new TranslateAnimation(0,parent.getYMenuButton().getX() - getRight() ,0,0); translateAnimation.setDuration(duration); break; case FROM_RIGHT: //從右邊緣移出 translateAnimation = new TranslateAnimation(0, (parent.getWidth()- getX()), 0, 0); translateAnimation.setDuration(duration); break; case FROM_BUTTON_TOP: //從MenuButton的上邊移入 translateAnimation = new TranslateAnimation(0, 0, 0, parent.getYMenuButton().getY() - getBottom()); translateAnimation.setDuration(duration); break; case FROM_BOTTOM: //從下邊緣移出 translateAnimation = new TranslateAnimation(0,0,0,parent.getHeight() - getY()); translateAnimation.setDuration(duration); } disappearAnimation = new AnimationSet(true); disappearAnimation.addAnimation(translateAnimation); disappearAnimation.addAnimation(alphaAnimation); disappearAnimation.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) {
@Override public void onAnimationEnd(Animation animation) { setVisibility(GONE); }
@Override public void onAnimationRepeat(Animation animation) {
} }); } 實現(xiàn)點擊事件由于 OptionButton 都是在代碼動態(tài)生成的,所以它們的ID也是動態(tài)生成的,不能作為switch語句的case條件,所以這里自己寫了一個接口OnOptionsClickListener,來讓OptionButton的每次點擊都調(diào)用OnOptionsClickListener的帶索引參數(shù)的方法,這樣就實現(xiàn)讓點擊事件可以在外部實現(xiàn)并加以區(qū)分OptionButton了: //用于讓用戶在外部實現(xiàn)點擊事件的接口,index可以區(qū)分OptionButton public interface OnOptionsClickListener { public void onOptionsClick(int index); }
private class MyOnClickListener implements OnClickListener { private int index;
public MyOnClickListener(int index) { this.index = index; }
@Override public void onClick(View v) { if (mOnOptionsClickListener != null) { mOnOptionsClickListener.onOptionsClick(index); } } }
//設置選項按鈕的圖片資源,順便設置點擊事件 private void setOptionsImages(int... drawableIds) throws Exception { this.drawableIds = drawableIds; if (optionPositionCount > drawableIds.length + banArray.length) { throw new Exception("Drawable資源數(shù)量不足"); }
for (int i = 0; i < optionButtonList.size(); i++) { optionButtonList.get(i).setOnClickListener(new MyOnClickListener(i)); if (drawableIds == null){ optionButtonList.get(i).setImageDrawable(null); }else { optionButtonList.get(i).setImageResource(drawableIds[i]); }
} } 這樣做完之后,外部就可以通過實現(xiàn) OnOptionsClickListener 接口來實現(xiàn)點擊事件了。 以上就是 YMenuView 的主要思路了,至于一些細節(jié)大家有興趣的話可以去代碼的GitHub https://github.com/totond/YMenuView 上Fork下來或者直接下載下來看看,有什么意見或者建議的話也可以在issue上提出。 雖然 YMenuView 的實現(xiàn)挺簡單的,功能也不多,但是足夠?qū)崿F(xiàn)我的需求了,我寫這篇文章的目的就是把思路記錄下來,還有讓有類似需求的朋友們參考一下,看了之后二次開發(fā)也方便一些。
最近剛正式入職,事情比較多,很多天晚上忙完都是懶得開電腦,所以沒怎么寫博客。雖然寫博客耗時比較長,但是我覺得這是一件很有意義的事情,不但總結(jié)鞏固了自己的知識,還能幫助他人,我要堅持下去?,F(xiàn)在快穩(wěn)定下來了,后面再忙都會抽多點時間來總結(jié)的,在這里說一下,激勵下自己。
|