Android:一步步開發一個高度可定製化的擴展菜單

效果圖

這裏寫圖片描述
這裏寫圖片描述

本文想試着從頭開始講解,中間貼的代碼只是部分的,若是須要所有代碼請翻到最後,有造好的輪子和源碼.

需求:

如效果圖所示的效果你們應該見過不少了,可是不少都是把每一個菜單的按鈕的樣式基本上固定了,雖然能夠用可是對於不一樣的項目來講風格真的能搭配上嗎?能不能作到每一個菜單樣式都能本身定義並且不用太過於麻煩?

實現思路:

1.自定義ViewGroup,用戶只須要往這個組件裏面添加按鈕便可,組件負責處理菜單按鈕的功能,顯示,動畫等等

2.添加菜單項要是能從xml文件中添加就更好了,方便預覽菜單按鈕的效果

詳細思路:

以前瞭解過其餘相似的項目的內部實現方式,有的是默認把全部按鈕疊加在一塊兒 ,讓展開按鈕覆蓋後面的菜單按鈕,點擊展開按鈕的時候用ObjectAnimation將其餘組件移動到位置,我的以爲這樣實現起來是否太過於複雜,爲什麼不能先把菜單按鈕放置到展開以後的位置,而後經過動畫來作位移的效果,配合按鈕的顯示與隱藏也能達到一樣的效果,並且菜單項自己是沒有發生位置變化的.

代碼實現:

1. 第一步,自定義一個ViewGroup,可以讓添加到其中的View按照效果圖擺放

設計思路:第一個ChildView和最後一個ChildView分別看成點擊展開和關閉的按鈕,都放置到右下角,其餘的菜單按照自動換行的效果擺放,以下圖

這裏寫圖片描述

代碼實現:

建立自定義ViewGroup,繼承ViewGroup,重寫構造方法:

public class ExpandableMenu extends ViewGroup {
    

    public ExpandableMenu(@NonNull Context context) {
        this(context, null);
    }

    public ExpandableMenu(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        mContext = context;
    }

複製代碼

這時候須要重寫onMeasure方法來測量子組件而且規定父組件的大小

循環測量子組件,由於通常菜單的使用場景就是覆蓋到頂部,因此父組件的大小就乾脆都設置爲match_parent

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        for (int i = 0; i < getChildCount(); i++) {
            View childView = getChildAt(i);
            measureChild(childView, widthMeasureSpec, heightMeasureSpec);
        }
        setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
    }
複製代碼

初次以外還須要重寫onLayout方法來設置子View的位置

傳入的參數依次爲isChanged,left,top,right,bottom,其中的int值爲父組件的四個方向的位置,就是咱們用於放置子組件的依據

@Override
protected void onLayout(boolean b, int i, int i1, int i2, int i3) {

}
複製代碼

規定子View的位置的方法爲:

childView.layout(left, top, right, bottom);
複製代碼
能獲取到childView的寬高,那麼只須要肯定其left,top的值便可獲取到位置的四個參數了

如何獲取子Viewleft,top?

這裏寫圖片描述

簡要的畫了個圖說明一下,本來是應該考慮每一個組件的magin的,後來發現能夠按照簡單的方法來,不考慮magin,這樣onLayout的實現會簡單不少,並且能夠用padding來達到和magin一樣的效果

爲了達到自動換行的效果,須要設置一個X方向和Y方向的標誌位,每放置一個子View須要對X,Y的值進行變化,X的值是每次減小一個子View的寬度,Y不變,直到X<=0,即須要換行,此時X恢復,Y須要變化,具體的實現代碼以下:

@Override
    protected void onLayout(boolean b, int i, int i1, int i2, int i3) {
        int left = i2;
        int top = i3;
        int x = 0;
        int y = 0;

        for (int j = 0; j < getChildCount(); j++) {
            View childView = getChildAt(j);
            int width = childView.getMeasuredWidth();
            int height = childView.getMeasuredHeight();
            left = left - width;
            x++;
            if (top == i3) {
                top = i3 -  height;
            }
            if (left < 0) {
                y++;
                x = 1;
                left = i2 -  width;
                top = top -  height;
            }
            if (j == getChildCount() - 1) {
                // 最後一個,放置在第一個的位置
                childView.layout(i2 - width, i3 - height, i2, i3);
            } else {
                childView.layout(left, top, left + width, top + height);
            }
        }
    }
複製代碼

以後須要編寫展開菜單和隱藏菜單的動畫,這部分很簡單,因此直接放代碼,封裝了兩個方法,處理動畫和菜單項的顯示隱藏

其中位移動畫是從第一個View的位置移動到當前的位置,位移的距離及是當前Viewleft,top值與第一個子View的對應參數的差.
/**
     * 展開菜單
     */
    private void expand() {
        // 隱藏第一個按鈕
        getChildAt(0).setVisibility(GONE);
        // 顯示最後一個按鈕
        getChildAt(getChildCount() - 1).setVisibility(VISIBLE);
        isExpend = true;
        for (int i = 1; i < getChildCount() - 1; i++) {
            View childView = getChildAt(i);
            TranslateAnimation animation = new TranslateAnimation(
                    getChildAt(0).getLeft() - childView.getLeft(), 0.0f, getChildAt(0).getTop() - childView.getTop(), 0.0f
            );
            animation.setInterpolator(mInterpolator);
            childView.setVisibility(VISIBLE);
            animation.setDuration(mDuration);
            childView.startAnimation(animation);
        }
    }
複製代碼

對應的隱藏菜單的方法

執行與展開相反的動畫,而且在動畫結束的時候把菜單項隱藏
/**
     * 關閉菜單
     */
    private void close() {
        // 顯示第一個按鈕
        getChildAt(0).setVisibility(VISIBLE);
        // 隱藏最後一個按鈕
        getChildAt(getChildCount() - 1).setVisibility(GONE);
        // 收回菜單
        isExpend = false;
        for (int i = 1; i < getChildCount() - 1; i++) {
            final View childView = getChildAt(i);
            TranslateAnimation animation = new TranslateAnimation(
                    0.0f, getChildAt(0).getLeft() - childView.getLeft(), 0.0f, getChildAt(0).getTop() - childView.getTop()
            );
            animation.setInterpolator(mInterpolator);
            animation.setAnimationListener(new Animation.AnimationListener() {
                @Override
                public void onAnimationStart(Animation animation) {

                }

                @Override
                public void onAnimationEnd(Animation animation) {
                    childView.setVisibility(GONE);
                }

                @Override
                public void onAnimationRepeat(Animation animation) {

                }
            });
            childView.setVisibility(VISIBLE);
            animation.setDuration(mDuration);
            childView.startAnimation(animation);
        }
    }
複製代碼

以後須要給按鈕設置展開和關閉的點擊事件,同時默認隱藏其餘按鈕,代碼以下:

private void init() {
        for (int i = 0; i < getChildCount(); i++) {
            View childView = getChildAt(i);
            if (i != 0) {
                childView.setVisibility(GONE);
            }
        }
        // 設置點擊事件
        getChildAt(0).setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                expand();
            }
        });
        getChildAt(getChildCount() - 1).setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                close();
            }
        });
    }
複製代碼

init()方法須要在子View已經添加進來以後再調用,因此我將init方法放置在了onLayout以後,保證獲取到的子View不會爲空,可是onLayout在被調用不少次,因此加了個標誌位,以下:

if (!isInited) {
     init();
     isInited = true;
}
複製代碼

至此組件的編寫就完了,沒有多餘的方法,要添加菜單能夠直接在xml內添加,也能夠用代碼添加,只要注意菜單項的大小應當相同,而且第一個和最後一個view是用於展開和關閉的,至於點擊事件,在添加到View以前設置便可,不用給第一個和最後一個view設置點擊事件,由於設置了也會被覆蓋掉.

xml使用方式以下:

<com.brioal.view.ExpandableMenu
        android:id="@+id/expandableMenu"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom">
        <!--展開按鈕-->
        <ImageButton
            android:layout_width="105dp"
            android:layout_height="105dp"
            android:background="@android:color/transparent"
            android:padding="5dp"
            android:scaleType="centerCrop"
            android:src="@drawable/ic_add_black"/>
        <!--菜單按鈕-->
        <ImageButton
            android:layout_width="105dp"
            android:layout_height="105dp"
            android:background="@android:color/transparent"
            android:padding="5dp"
            android:scaleType="centerCrop"
            android:src="@mipmap/ic_launcher_round"/>
        <!--菜單按鈕-->
        <ImageButton
            android:layout_width="105dp"
            android:layout_height="105dp"
            android:background="@android:color/transparent"
            android:padding="5dp"
            android:scaleType="centerCrop"
            android:src="@mipmap/ic_launcher_round"/>
        <!--菜單按鈕-->
        <ImageButton
            android:layout_width="105dp"
            android:layout_height="105dp"
            android:background="@android:color/transparent"
            android:padding="5dp"
            android:scaleType="centerCrop"
            android:src="@mipmap/ic_launcher_round"/>
        <!--菜單按鈕-->
        <ImageButton
            android:layout_width="105dp"
            android:layout_height="105dp"
            android:background="@android:color/transparent"
            android:padding="5dp"
            android:scaleType="centerCrop"
            android:src="@mipmap/ic_launcher_round"/>
        <!--菜單按鈕-->
        <ImageButton
            android:layout_width="105dp"
            android:layout_height="105dp"
            android:background="@android:color/transparent"
            android:padding="5dp"
            android:scaleType="centerCrop"
            android:src="@mipmap/ic_launcher_round"/>
        <!--結束按鈕-->
        <ImageButton
            android:layout_width="105dp"
            android:layout_height="105dp"
            android:background="@android:color/transparent"
            android:padding="5dp"
            android:scaleType="centerCrop"
            android:src="@drawable/ic_close_black"/>
    </com.brioal.view.ExpandableMenu>
複製代碼

而後是整個ExpandableMenu的代碼

package com.brioal.view;

import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.Animation;
import android.view.animation.Interpolator;
import android.view.animation.TranslateAnimation;

/**
 * email:brioal@foxmail.com
 * github:https://github.com/Brioal
 * Created by Brioal on 2018/3/27.
 */

public class ExpandableMenu extends ViewGroup {
    private Context mContext;
    // 動畫的間隔
    private int mDuration = 500;
    // 插補器
    private Interpolator mInterpolator = new AccelerateDecelerateInterpolator();
    // 菜單是否展開了
    private boolean isExpend = false;
    // 是否已經初始化了
    private boolean isInited = false;

    public ExpandableMenu(@NonNull Context context) {
        this(context, null);
    }

    public ExpandableMenu(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        mContext = context;

    }

    /**
     * 設置動畫間隔
     *
     * @param duration
     */
    public ExpandableMenu setDuration(int duration) {
        mDuration = duration;
        return this;
    }

    /**
     * 設置插補器
     *
     * @param interpolator
     */
    public ExpandableMenu setInterpolator(Interpolator interpolator) {
        mInterpolator = interpolator;
        return this;
    }



    @Override
    protected void onLayout(boolean b, int i, int i1, int i2, int i3) {
        int left = i2;
        int top = i3;
        int x = 0;
        int y = 0;

        for (int j = 0; j < getChildCount(); j++) {
            View childView = getChildAt(j);
            int width = childView.getMeasuredWidth();
            int height = childView.getMeasuredHeight();
            left = left - width;
            x++;
            if (top == i3) {
                top = i3 -  height;
            }
            if (left < 0) {
                y++;
                x = 1;
                left = i2 -  width;
                top = top -  height;
            }
            if (j == getChildCount() - 1) {
                // 最後一個,放置在第一個的位置
                childView.layout(i2 - width, i3 - height, i2, i3);
            } else {
                childView.layout(left, top, left + width, top + height);
            }
        }
        if (!isInited) {
            init();
            isInited = true;
        }
    }

    private void init() {
        for (int i = 0; i < getChildCount(); i++) {
            View childView = getChildAt(i);
            if (i != 0) {
                childView.setVisibility(GONE);
            }
        }
        // 設置點擊事件
        getChildAt(0).setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                expand();
            }
        });
        getChildAt(getChildCount() - 1).setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                close();
            }
        });
    }

    /**
     * 菜單是不是展開的
     * @return
     */
    public boolean isExpend() {
        return isExpend;
    }


    /**
     * 關閉菜單
     */
    private void close() {
        // 顯示第一個按鈕
        getChildAt(0).setVisibility(VISIBLE);
        // 隱藏最後一個按鈕
        getChildAt(getChildCount() - 1).setVisibility(GONE);
        // 收回菜單
        isExpend = false;
        for (int i = 1; i < getChildCount() - 1; i++) {
            final View childView = getChildAt(i);
            TranslateAnimation animation = new TranslateAnimation(
                    0.0f, getChildAt(0).getLeft() - childView.getLeft(), 0.0f, getChildAt(0).getTop() - childView.getTop()
            );
            animation.setInterpolator(mInterpolator);
            animation.setAnimationListener(new Animation.AnimationListener() {
                @Override
                public void onAnimationStart(Animation animation) {

                }

                @Override
                public void onAnimationEnd(Animation animation) {
                    childView.setVisibility(GONE);
                }

                @Override
                public void onAnimationRepeat(Animation animation) {

                }
            });
            childView.setVisibility(VISIBLE);
            animation.setDuration(mDuration);
            childView.startAnimation(animation);
        }
    }

    /**
     * 展開菜單
     */
    private void expand() {
        // 隱藏第一個按鈕
        getChildAt(0).setVisibility(GONE);
        // 顯示最後一個按鈕
        getChildAt(getChildCount() - 1).setVisibility(VISIBLE);
        isExpend = true;
        for (int i = 1; i < getChildCount() - 1; i++) {
            View childView = getChildAt(i);
            TranslateAnimation animation = new TranslateAnimation(
                    getChildAt(0).getLeft() - childView.getLeft(), 0.0f, getChildAt(0).getTop() - childView.getTop(), 0.0f
            );
            animation.setInterpolator(mInterpolator);
            childView.setVisibility(VISIBLE);
            animation.setDuration(mDuration);
            childView.startAnimation(animation);
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        for (int i = 0; i < getChildCount(); i++) {
            View childView = getChildAt(i);
            measureChild(childView, widthMeasureSpec, heightMeasureSpec);
        }
        setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
    }

}

複製代碼

輪子地址:ExpandableMenu

相關文章
相關標籤/搜索