日韩黑丝制服一区视频播放|日韩欧美人妻丝袜视频在线观看|九九影院一级蜜桃|亚洲中文在线导航|青草草视频在线观看|婷婷五月色伊人网站|日本一区二区在线|国产AV一二三四区毛片|正在播放久草视频|亚洲色图精品一区

分享

Android 上一個類似 PathMenu 效果的自定義 View 源碼分析

 codingSmart 2021-10-22

效果圖

本文原創(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)

具體實現(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)點擊事件了。

結(jié)尾

以上就是 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é)的,在這里說一下,激勵下自己。

與之相關(guān)

種一棵樹最好的時間是十年前,其次是現(xiàn)在

2017 | 我在 5 個月時間里分享了 98 篇文章

微信號:code-xiaosheng

公眾號

「code小生」

    轉(zhuǎn)藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多