| 本文原創(chuàng),轉(zhuǎn)載請注明本出處!本項目 GitHub 地址:https://github.com/totond/YMenuView
 歡迎 Star or Fork!
 網(wǎng)上這種類似 PathMenu 的菜單很多,但是基本都不符合我項目的需求,想看他們的源碼實現(xiàn)然后做出修改,進行二次開發(fā)來適應(yīng)我的項目需求,但是發(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 桌面的那些應(yīng)用窗口一樣),要適應(yīng)分屏功能,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);
 //設(shè)置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);
 //設(shè)置打開關(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 的信息做準備,然后就是設(shè)置點擊事件來控制菜單開關(guān)(開關(guān)的操作實現(xiàn)后面講),最后就是設(shè)置背景和addView() 加入父 ViewGroup 視圖。 OptionButton說是 Button,但是實際上 OptionButton 是繼承 ImageView,因為我發(fā)現(xiàn) ImageView 除了可以通過 setImageDrawable() 方法設(shè)置圖片資源之外,還可以通過setBackground() 方法設(shè)置背景,這樣的話可以很容易實現(xiàn)給圖片加框的效果(demo 中的 OptionButton 效果就是通過圓形 shape 和圖片資源合成的),下面的 OptionButton 創(chuàng)建過程中,位置計算是比較復(fù)雜的: private void initBan() {//對Ban數(shù)組進行從小到大排序
 Arrays.sort(banArray);
 }
 
 //設(shè)置選項按鈕
 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ù)、重復(fù)數(shù)字或者超出范圍");
 } else if (i == banArray[n]) {
 if (n < banArray.length - 1) {
 n++;
 }else {
 isBan = false;
 }
 continue;
 }
 }
 
 OptionButton button = new OptionButton(mContext);
 //設(shè)置動畫的模式和時長
 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)方法設(shè)置banArray數(shù)組,里面填入位置序號,然后這個位置就不放OptionButton了,如下圖,就是設(shè)置了banArray = {0,2,6}和optionPositionCount = 8: 
 前面只是生成了 OptionButton,后面還要為它們設(shè)置圖片和背景: //設(shè)置選項按鈕的backgroundpublic 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);
 }
 }
 }
 
 //設(shè)置選項按鈕的圖片資源,順便設(shè)置點擊事件
 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)動畫: //設(shè)置MenuButton彈出菜單選項時候MenuButton自身的動畫,默認為順時針旋轉(zhuǎn)180度,為空則是關(guān)閉動畫public void setMenuOpenAnimation(Animation menuOpenAnimation) {
 menuOpenAnimation.setAnimationListener(animationListener);
 this.menuOpenAnimation = menuOpenAnimation;
 
 }
 
 //設(shè)置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 里面,因為動畫的設(shè)置需要用到自身的位置信息,所以需要注冊 OnGlobalLayoutListener 來監(jiān)聽,等自身Layout 完畢之后再設(shè)置,不然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();
 //設(shè)置完后立刻注銷,不然會不斷回調(diào),浪費很多資源
 getViewTreeObserver().removeOnGlobalLayoutListener(this);
 }
 }
 });
 
 }
因為進入和退出的動畫只是基本只是相反,篇幅原因這里就只展示退出動畫的實現(xiàn)(看的時候要注意動畫的初始坐標零點默認都是基于View的左上角頂點): private void setShowAndDisappear() {setShowAnimation(mDuration);
 setDisappearAnimation(mDuration);
 //在這里才設(shè)置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ū)分OptionButtonpublic 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);
 }
 }
 }
 
 //設(shè)置選項按鈕的圖片資源,順便設(shè)置點擊事件
 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é)的,在這里說一下,激勵下自己。
 |