乡下人产国偷v产偷v自拍,国产午夜片在线观看,婷婷成人亚洲综合国产麻豆,久久综合给合久久狠狠狠9

  • <output id="e9wm2"></output>
    <s id="e9wm2"><nobr id="e9wm2"><ins id="e9wm2"></ins></nobr></s>

    • 分享

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

       codingSmart 2021-10-22

      效果圖

      本文原創(chuàng),轉(zhuǎn)載請(qǐng)注明本出處!
      本項(xiàng)目 GitHub 地址:https://github.com/totond/YMenuView
      歡迎 Star or Fork!

      前言

      網(wǎng)上這種類似 PathMenu 的菜單很多,但是基本都不符合我項(xiàng)目的需求,想看他們的源碼實(shí)現(xiàn)然后做出修改,進(jìn)行二次開(kāi)發(fā)來(lái)適應(yīng)我的項(xiàng)目需求,但是發(fā)現(xiàn)——以我現(xiàn)在的能力,如果不是以前做過(guò)類似的功能,看別人的代碼,很難很快地找出主要實(shí)現(xiàn)思路,而且不同的作者的代碼有不同的風(fēng)格(特別是命名),于是就自己按照自己的思路來(lái)實(shí)現(xiàn),然后把實(shí)現(xiàn)思路都寫出來(lái)分享一下,讓大家了解我這個(gè)自定義 View 控件是怎么實(shí)現(xiàn)的,到時(shí)候大家根據(jù)需求修改源碼,進(jìn)行二次開(kāi)發(fā)的時(shí)候也可以參考,也希望和大家一起探討怎樣實(shí)現(xiàn)更好。

      需求

      做這個(gè)控件的目的是為了實(shí)現(xiàn)一個(gè)平板上的全屏視頻播放器的菜單欄,點(diǎn)擊之后會(huì)彈出一堆按鈕來(lái)讓用戶選擇 ,這樣的話網(wǎng)上很多開(kāi)源控件都能實(shí)現(xiàn),問(wèn)題就是這個(gè)播放器是要支持 Android7.0 的分屏功能,(平板比較坑爹,還打開(kāi)了 Freeform 模式的入口,這個(gè) Freeform 模式可以讓用戶自由調(diào)節(jié) APP 的界面寬高,就像在 Windows 桌面的那些應(yīng)用窗口一樣),要適應(yīng)分屏功能,APP 的寬高可能會(huì)改變,這些按鈕的位置分布情況也要根據(jù)寬高來(lái)改變,想想就蛋疼。而網(wǎng)上很多的這類型 PathMenu是固定分布方式的,所以就做出了這個(gè)可以調(diào)整選項(xiàng)位置的自定義菜單控件——YMenuView(取名技術(shù)不好不知道怎么取,就用這個(gè)挫名字啦(≧▽≦)/)。

      具體實(shí)現(xiàn)

      具體實(shí)現(xiàn)思路

      思路大概如下圖:

      其中主要難點(diǎn)是第二個(gè)和第三個(gè)。簡(jiǎn)單來(lái)說(shuō),本質(zhì)上 YMenuView 是一個(gè)ViewGroup,然后在里面動(dòng)態(tài)生成一些控件,點(diǎn)擊MenuButton的時(shí)候就會(huì)把一堆OptionButton顯示/消失,這個(gè)過(guò)程加上一些動(dòng)畫,就形成最后的效果。

      創(chuàng)建ViewGroup

      這部分其實(shí)沒(méi)什么好說(shuō)的,就是創(chuàng)建一個(gè)名為 YMenuView 的ViewGroup,然后獲取一些自定義的屬性(自定義屬性的介紹可以自行搜索或者看看我的筆記,這里不多說(shuō)了),為下一步的創(chuàng)建 MenuButton 和 OptionButton 做準(zhǔn)備,獲取的屬性在項(xiàng)目的 Github 地址上有詳細(xì)的說(shuō)明了。篇幅原因,這里就放出部分重要的屬性圖示:

      創(chuàng)建 MenuButton 和 OptionButton

      如上圖所示,MenuButton 就是那個(gè)用于按下彈出菜單的按鈕,OptionButton就是可彈出收回的選項(xiàng)按鈕。

      MenuButton

      下面先來(lái)看看如何創(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è)置打開(kāi)關(guān)閉事件
         mYMenuButton.setOnClickListener(new OnClickListener() {
             @Override
             public void onClick(View v) {
                 if (!isShowMenu) {
                     showMenu();
                 } else {
                     closeMenu();
                 }
             }
         });
         mYMenuButton.setBackgroundResource(mMenuButtonBackGroundId);
         addView(mYMenuButton);
      }

      主要是動(dòng)態(tài)生成一個(gè) Button,利用 LayoutParams 來(lái)控制它的位置,生成ID是為后面要使用 mYMenuButton 的信息做準(zhǔn)備,然后就是設(shè)置點(diǎn)擊事件來(lái)控制菜單開(kāi)關(guān)(開(kāi)關(guān)的操作實(shí)現(xiàn)后面講),最后就是設(shè)置背景和addView() 加入父 ViewGroup 視圖。

      OptionButton

      說(shuō)是 Button,但是實(shí)際上 OptionButton 是繼承 ImageView,因?yàn)槲野l(fā)現(xiàn) ImageView 除了可以通過(guò) setImageDrawable() 方法設(shè)置圖片資源之外,還可以通過(guò) setBackground() 方法設(shè)置背景,這樣的話可以很容易實(shí)現(xiàn)給圖片加框的效果(demo 中的 OptionButton 效果就是通過(guò)圓形 shape 和圖片資源合成的),下面的 OptionButton 創(chuàng)建過(guò)程中,位置計(jì)算是比較復(fù)雜的:

      private void initBan() {
         //對(duì)Ban數(shù)組進(jìn)行從小到大排序
         Arrays.sort(banArray);
      }

      //設(shè)置選項(xiàng)按鈕
      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è)置不合理,含有負(fù)數(shù)、重復(fù)數(shù)字或者超出范圍");
                 } else if (i == banArray[n]) {
                     if (n < banArray.length - 1) {
                         n++;
                     }else {
                         isBan = false;
                     }
                     continue;
                 }
             }

             OptionButton button = new OptionButton(mContext);
             //設(shè)置動(dòng)畫的模式和時(shí)長(zhǎng)
             button.setSD_Animation(mOptionSD_AnimationMode);
             button.setDuration(mOptionSD_AnimationDuration);
             int btnId = generateViewId();
             button.setId(btnId);

             RelativeLayout.LayoutParams layoutParams = new LayoutParams(mYOptionButtonWidth, mYOptionButtonHeight);

             //計(jì)算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 判斷,看下面的位置計(jì)算,OptionButton 的布局是矩形的,每一排中的每個(gè) ImageView 從左到右的,而 optionColumns 是列數(shù)(也就是每排的個(gè)數(shù)),通過(guò)這個(gè)屬性和總個(gè)數(shù)就可以確定所有OptionButton 的布局。如下面就是 optionPositionCount = 8,optionColumns = 3 的效果:

      然后再看Ban判斷,這段代碼的目的就是讓序號(hào)為Ban數(shù)組里面的位置跳過(guò)這一輪循環(huán),不放置OptionButton。所以Ban這個(gè)功能可以通過(guò)setBanArray(int... banArray)方法設(shè)置banArray數(shù)組,里面填入位置序號(hào),然后這個(gè)位置就不放OptionButton了,如下圖,就是設(shè)置了banArray = {0,2,6}optionPositionCount = 8

      前面只是生成了 OptionButton,后面還要為它們?cè)O(shè)置圖片和背景:

      //設(shè)置選項(xiàng)按鈕的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);
             }
         }
      }

      //設(shè)置選項(xiàng)按鈕的圖片資源,順便設(shè)置點(diǎn)擊事件
      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]);
             }

         }
      }

      實(shí)現(xiàn)動(dòng)畫

      MenuButton 的動(dòng)畫沒(méi)什么好說(shuō)的,開(kāi)關(guān)動(dòng)畫就是兩個(gè)旋轉(zhuǎn)(一個(gè)逆時(shí)針,一個(gè)順時(shí)針),動(dòng)畫已經(jīng)在 xml 寫好了,太簡(jiǎn)單了就不展示出來(lái),想看的話直接看源碼好了:

      //初始化MenuButton的點(diǎn)擊動(dòng)畫
      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);
      }

      還開(kāi)放了方法,可以在外部改變這個(gè)開(kāi)關(guān)動(dòng)畫:

      //設(shè)置MenuButton彈出菜單選項(xiàng)時(shí)候MenuButton自身的動(dòng)畫,默認(rèn)為順時(shí)針旋轉(zhuǎn)180度,為空則是關(guān)閉動(dòng)畫
      public void setMenuOpenAnimation(Animation menuOpenAnimation) {
         menuOpenAnimation.setAnimationListener(animationListener);
         this.menuOpenAnimation = menuOpenAnimation;

      }

      //設(shè)置MenuButton收回菜單選項(xiàng)時(shí)候MenuButton自身的動(dòng)畫,默認(rèn)為逆時(shí)針旋轉(zhuǎn)180度,為空則是關(guān)閉動(dòng)畫
      public void setMenuCloseAnimation(Animation menuCloseAnimation) {
         menuCloseAnimation.setAnimationListener(animationListener);
         this.menuCloseAnimation = menuCloseAnimation;
      }

      然后重點(diǎn)就是OptionButton的動(dòng)畫了,它的動(dòng)畫有四種:

      sd_animMode描述
      FROM_BUTTON_LEFT選項(xiàng)從菜單鍵左邊緣飛入
      FROM_BUTTON_TOP選項(xiàng)從菜單鍵上邊緣飛入
      FROM_RIGHT選項(xiàng)從View左邊緣飛入
      FROM_BOTTOM選項(xiàng)從View左邊緣飛入

      這些動(dòng)畫封裝在 OptionButton 里面,因?yàn)閯?dòng)畫的設(shè)置需要用到自身的位置信息,所以需要注冊(cè) OnGlobalLayoutListener 來(lái)監(jiān)聽(tīng),等自身Layout 完畢之后再設(shè)置,不然getLeft()等方法返回的都是0:

      private void init(){
         setClickable(true);

         //在獲取到寬高參數(shù)之后再進(jìn)行初始化
         ViewTreeObserver viewTreeObserver = getViewTreeObserver();
         viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
             @Override
             public void onGlobalLayout() {
                 if (getX() != 0 && getY() != 0 && getWidth() != 0 && getHeight() != 0) {
                     setShowAndDisappear();
                     //設(shè)置完后立刻注銷,不然會(huì)不斷回調(diào),浪費(fèi)很多資源
                     getViewTreeObserver().removeOnGlobalLayoutListener(this);
                 }
             }
         });

      }

      因?yàn)檫M(jìn)入和退出的動(dòng)畫只是基本只是相反,篇幅原因這里就只展示退出動(dòng)畫的實(shí)現(xiàn)(看的時(shí)候要注意動(dòng)畫的初始坐標(biāo)零點(diǎn)默認(rèn)都是基于View的左上角頂點(diǎn)):

      private void setShowAndDisappear() {
         setShowAnimation(mDuration);
         setDisappearAnimation(mDuration);
         //在這里才設(shè)置Gone很重要,讓View可以一開(kāi)始就觸發(fā)onGlobalLayout()進(jìn)行初始化
         setVisibility(GONE);
      }

      public void setDisappearAnimation(int duration) {
         //獲取父ViewGroup的對(duì)象,用于獲取寬高參數(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) {

             }
         });
      }

      實(shí)現(xiàn)點(diǎn)擊事件

      由于 OptionButton 都是在代碼動(dòng)態(tài)生成的,所以它們的ID也是動(dòng)態(tài)生成的,不能作為switch語(yǔ)句的case條件,所以這里自己寫了一個(gè)接口OnOptionsClickListener,來(lái)讓OptionButton的每次點(diǎn)擊都調(diào)用OnOptionsClickListener的帶索引參數(shù)的方法,這樣就實(shí)現(xiàn)讓點(diǎn)擊事件可以在外部實(shí)現(xiàn)并加以區(qū)分OptionButton了:

      //用于讓用戶在外部實(shí)現(xiàn)點(diǎ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);
                 }
             }
         }

         //設(shè)置選項(xiàng)按鈕的圖片資源,順便設(shè)置點(diǎn)擊事件
         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]);
                 }

             }
         }

      這樣做完之后,外部就可以通過(guò)實(shí)現(xiàn) OnOptionsClickListener 接口來(lái)實(shí)現(xiàn)點(diǎn)擊事件了。

      結(jié)尾

      以上就是 YMenuView 的主要思路了,至于一些細(xì)節(jié)大家有興趣的話可以去代碼的GitHub https://github.com/totond/YMenuView 上Fork下來(lái)或者直接下載下來(lái)看看,有什么意見(jiàn)或者建議的話也可以在issue上提出。


      雖然 YMenuView 的實(shí)現(xiàn)挺簡(jiǎn)單的,功能也不多,但是足夠?qū)崿F(xiàn)我的需求了,我寫這篇文章的目的就是把思路記錄下來(lái),還有讓有類似需求的朋友們參考一下,看了之后二次開(kāi)發(fā)也方便一些。

      后話

      最近剛正式入職,事情比較多,很多天晚上忙完都是懶得開(kāi)電腦,所以沒(méi)怎么寫博客。雖然寫博客耗時(shí)比較長(zhǎng),但是我覺(jué)得這是一件很有意義的事情,不但總結(jié)鞏固了自己的知識(shí),還能幫助他人,我要堅(jiān)持下去?,F(xiàn)在快穩(wěn)定下來(lái)了,后面再忙都會(huì)抽多點(diǎn)時(shí)間來(lái)總結(jié)的,在這里說(shuō)一下,激勵(lì)下自己。

      與之相關(guān)

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

      2017 | 我在 5 個(gè)月時(shí)間里分享了 98 篇文章

      微信號(hào):code-xiaosheng

      公眾號(hào)

      「code小生」

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

        0條評(píng)論

        發(fā)表

        請(qǐng)遵守用戶 評(píng)論公約

        類似文章 更多