效果圖
本文想試着從頭開始講解,中間貼的代碼只是部分的,若是須要所有代碼請翻到最後,有造好的輪子和源碼.
需求:
如效果圖所示的效果你們應該見過不少了,可是不少都是把每一個菜單的按鈕的樣式基本上固定了,雖然能夠用可是對於不一樣的項目來講風格真的能搭配上嗎?能不能作到每一個菜單樣式都能本身定義並且不用太過於麻煩?
實現思路:
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);
}
set MeasuredDimension(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
的值便可獲取到位置的四個參數了
如何獲取子View
的left
,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
的位置移動到當前的位置,位移的距離及是當前View
的left
,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
不會爲空,可是onLayou
t在被調用不少次,因此加了個標誌位,以下:
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 set Duration(int duration) {
mDuration = duration;
return this;
}
/**
* 設置插補器
*
* @param interpolator
*/
public ExpandableMenu set Interpolator(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);
}
set MeasuredDimension(widthMeasureSpec, heightMeasureSpec);
}
}
複製代碼