Android開發之自定義局部導航菜單

如今,要實現導航功能方案有很多。比如:

 

  1、用3.0+自帶的Toolbar + Fragment導航。

 

  2、用Tabhost實現導航。小弟學淺,就隻用過這兩種方案實現導航。

 

  但是這兩種方案都有一個很明顯的弊端:導航的位置太過於固定瞭。比如Toolbar的就隻能在標題欄處(ps:源碼修改大神跳過)。還有Tabhost,雖然自定義Tabhost比直接繼承TabActivity更加靈活,但是卻沒有選項切換動畫(ps:也許是我沒發現)。

 

    

 

  有時候,我們僅僅是想在一個畫面的一角處,貼上一個導航,用於切換導航啊,屬性設置之內的。這個時候不管是Toolbar還是Tabhost都有些大材小用,心有餘而力不足的感覺瞭。比如下圖所示:

 

 

 

 

 

最近剛好項目有這方面的需要,就查瞭點資料。發現原理其實挺簡單的,如下圖:

 

 

 

上面幾個tab用Button或者TextView來做就行,反正能響應點擊就行。下面的ImageView用於切換動畫,比如默認是tab1,這個時候點擊瞭tab3,那麼下面的ImageView就從tab1移動到tab3並且停留。

 

 

 

原理講明白之後,接下來就是具體的實現瞭,一般這類需要都能有兩種方式實現:

 

用xml中實現

用java代碼動態實現。

 

 

Xml界面與java代碼控制分離是Android開發的亮點,也是無數入門書籍的敲門磚,但是這種實現就有一種非常大的局限性:今天這個項目有3個tab,明天的項目有4個tab,這個時候需要去改xml不說,還要去改一些底層實現,比如對ImageView的寬度的壓縮等等。為瞭移植性和拓展性,我選擇瞭java代碼實現,直接subClass LinearLayout來實現。我隻做瞭一些基本的操作,大傢可以在我的代碼上添加自己的操作,比如給每一個tab添加selector,添加事件回調等等。先上圖,我的最精簡實現:

 

 

 

中間的移動是有動畫效果的哈,不是直接點哪兒就出現在哪兒,太生硬瞭。

 

 

 

 

 

接下來講解具體的實現過程:

 

 

 

 

 

子類化LinearLayout,當然也可以選擇子類化其他ViewGroup,看個人愛好。

 

 

public class CustomMenu extends LinearLayout implements OnClickListener

 

 

 

 

 

 

 

在attrs.xml文件中申明自定義xml屬性

 

 

<?xml version="1.0" encoding="utf-8"?>

 

<resources>

 

 

 

<declare-styleable name="CustomMenu">

 

<attr name="buttonNumber" format="integer" />

 

<attr name="indexbitmap" format="reference" />

 

<attr name="buttonHeight" format="dimension" />

 

</declare-styleable>

 

 

 

</resources>

 

 

 

其中buttonNumber:導航的tab個數

 

     indexbitmap:移動的圖片,就是下面那一橫線

 

buttonHeight:導航的高度

 

 

 

 

 

 

 

在xml佈局文件添加佈局,layout_width與layout_height可以隨意使用match_parent、wrap_content、或者限定dp

 

 

 

 

<LinearLayout xmlns:android="https://schemas.android.com/apk/res/android"

 

xmlns:custommenu="https://schemas.android.com/apk/res/com.example.fragmentdemo"

 

android:layout_width="match_parent"

 

android:layout_height="match_parent"

 

android:orientation="vertical" >

 

 

 

<TextView

 

android:layout_width="match_parent"

 

android:layout_height="30dp"

 

android:text="@string/hello_world" />

 

 

 

<com.example.fragmentdemo.fragmentmenu.CustomMenu

 

android:layout_width="match_parent"

 

android:layout_height="0dp"

 

android:layout_weight="1"

 

custommenu:buttonNumber="5"

 

custommenu:buttonHeight="40dp"

 

custommenu:indexbitmap="@drawable/a"

 

>

 

 

 

</com.example.fragmentdemo.fragmentmenu.CustomMenu>

 

 

 

<TextView

 

android:layout_width="match_parent"

 

android:layout_height="wrap_content"

 

android:text="@string/hello_world" />

 

</LinearLayout>

 

 

 

為瞭突出隨意性,故意在上下添加瞭兩個TextView,layout_width與layout_height可以設置為match_parent、wrap_content或者30dp等等。在xml屬性中,我將導航欄的數量設置為5個,導航欄的高度為40dp,導航的移動圖片為drawable

 

 

 

 

 

 

 

在java代碼中首先讀取自定義的xml屬性值

 

 

 

 

private void readXML(Context context,AttributeSet attr){

 

    TypedArray a = context.obtainStyledAttributes(attr, R.styleable.CustomMenu);

 

    //讀取按鈕數量

 

    buttonNumber = a.getInt(R.styleable.CustomMenu_buttonNumber, 4);

 

    //讀取按鈕的高度

 

    buttonHeight = (int) a.getDimension(R.styleable.CustomMenu_buttonHeight, 30);

 

    //讀取圖片

 

    int bitmapID = a.getResourceId(R.styleable.CustomMenu_indexbitmap, R.drawable.a);

 

    bitmap = BitmapFactory.decodeResource(getResources(), bitmapID);

 

    bitmap_width = bitmap.getWidth();

 

    a.recycle();

 

}

 

 

 

註釋已經寫得很清楚瞭,就是用來讀取在xml中自定義的屬性,這兒註意buttonNumber、buttonHeight、bitmap、bitmap_width都是成員屬性。

 

 

 

 

 

 

 

添加幾個tab,個數是根據buttonNumber限制瞭的,還有tab的高度也是根據buttonHeight限制瞭的。

 

 

 

 

//設置本身為豎直方向

 

    setOrientation(LinearLayout.VERTICAL);

 

    //添加一個橫向的LinearLayout,高度為設置的高度

 

    LayoutParams p = new LayoutParams(LayoutParams.MATCH_PARENT, buttonHeight);

 

    LinearLayout linearLayout = new LinearLayout(context);

 

    linearLayout.setOrientation(LinearLayout.HORIZONTAL);

 

    linearLayout.setPadding(0, 0, 0, 0);

 

    linearLayout.setGravity(Gravity.CENTER);

 

    addView(linearLayout, p);

 

    //向這個橫向的LinearLayout添加指定個Button

 

    LayoutParams btn_p = new LayoutParams(LayoutParams.MATCH_PARENT, buttonHeight, 1);

 

    for(int i = 0;i<buttonNumber;i++){

 

        Button button = new Button(context);

 

        button.setText("按鈕"+i);

 

        button.setTextSize(15);

 

        button.setBackgroundColor(getResources().getColor(R.color.defaultColor));

 

        button.setId(ID+i);

 

        button.setOnClickListener(this);

 

        //添加到容器

 

        button_container.add(button);

 

        //添加到佈局

 

        linearLayout.addView(button, btn_p);

 

    }

 

 

 

這兒首先添加一個橫向的LinearLayout用來添加tab,高度用用戶輸入的值,然後添加用戶指定數量的tab(Button),設置權重(weight)為1。在這兒我把Button的文字、背景顏色等都給瞭默認值,大傢可以在xml中拓展,或者在代碼中暴露方法讓用戶設置。這兒有一個ID,我給瞭默認值

 

private static final int ID = 0xcc33cc;

 

這是為瞭區分onClick事件,大傢可以自己選擇區分方式,不過在這裡用ID是有好處的,後面我會介紹。

 

 

 

 

 

 

 

添加ImageView,暫時不做處理,因為Bitmap要因為tab的寬度來動態調整

 

 

 

 

imageView = new ImageView(context);

 

LayoutParams iv_p = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);

 

iv_p.setMargins(0, 5, 0, 0);

 

addView(imageView,iv_p);

 

 

 

這裡隻是放置一個ImageView,具體的內容要等到後面設置,因為內容是動態的,在構造函數期間不能確定其寬高。

 

 

 

 

 

 

 

在onMearsure方法中,獲取本View的寬度與高度

 

 

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

 

    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

 

    //隻執行這個方法一次

 

    if(width==0 || height==0){            

 

        //得到自身的高度與高度

 

        width = MeasureSpec.getSize(widthMeasureSpec);

 

        height = MeasureSpec.getSize(heightMeasureSpec);

 

        //做其他的初始化

 

        initial();

 

    }

 

}

 

 

 

大傢都知道,onMeasure方法會根據傳入的參數確定控件的大小。一般在這個方法做控件的動態伸縮和子控件的伸縮。在這裡,我隻是簡單的得到瞭本控件的寬度和高度。Width和height都是成員變量。這裡用瞭if語句是因為這個方法默認會執行兩次,原因呢大概是作為ViewGroup剛開始會繪制一次,填充子控件後又會繪制一次,具體的不太清楚,大傢可以查查其他資料。這裡用if限定隻執行一次。然後在initial()方法中,做剩下的初始化部分。

 

 

 

 

 

 

 

做一些初始化操作

 

 

//如果圖片的寬度比按鈕的寬度大,則對圖片進行處理

 

if(bitmap_width>width/buttonNumber){

 

    //縮小圖片

 

    bitmap = dealBitmap(bitmap, (float) (width)/buttonNumber/bitmap_width);

 

}

 

 

 

private Bitmap dealBitmap(Bitmap bitmap ,float bili) {

 

    Matrix matrix = new Matrix();

 

    matrix.postScale(bili, bili); // 長和寬放大縮小的比例

 

    Bitmap resizeBmp = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);

 

    return resizeBmp;

 

}

 

 

 

如果圖片的寬度大於瞭每一個tab的寬度,那麼就對圖片進行縮放,默認是縮放到與tab等同寬度。大傢可以自己定義這個縮放范圍。甚至可以通過設置回調接口暴露給外部設置。

 

 

 

//設置偏移值

 

imageView_offset = (width/buttonNumber-bitmap_width)/2;

 

//設置圖片

 

imageView.setImageBitmap(bitmap);

 

//初始化圖片位置

 

initialImageViewOffset();

 

 

 

設置偏移值,這個imageView_offset也是成員變量。目的是讓ImageView放在tab的正中間。原理類似於下圖:

 

 

 

然後設置bitmap,因為這個時候bitmap的寬高已經確定瞭。然後調用initialImageViewOffset()方法將剛才確定的offset的值設置進去

 

 

 

private void initialImageViewOffset() {

 

    //偏移值大於0則進行圖片移動

 

    if(imageView_offset>0){

 

        Matrix matrix = new Matrix();

 

        matrix.postTranslate(imageView_offset, 0);

 

        imageView.setImageMatrix(matrix);

 

    }

 

}

 

 

 

這裡對offset進行的大於0的判斷,因為如上所說,如果bitmap的寬度大於tab的寬度,那麼就需要縮放到和tab一樣大,這個時候offset自然等於0,就避免瞭無用功。

 

 

 

 

 

 

 

添加單擊事件

 

 

public void onClick(View v) {

 

    //從當前項移動到點擊項

 

    moveImageView(cur_index, v.getId()-ID);

 

    //賦值當前項

 

    cur_index = v.getId() – ID;

 

}

 

 

 

這裡就可以看出用id區分tab的好處瞭。為瞭更方便,首先貼出moveImageView的代碼

 

 

 

private void moveImageView(int start,int end){

 

    //要移動的距離

 

    int length = (2 * imageView_offset + bitmap_width) * (end – start);

 

    //初始位置,默認的ImageView在離左邊的imageView_offset處。

 

    int offset = (2 * imageView_offset + bitmap_width)*start;

 

    Animation animation = new TranslateAnimation(offset, offset + length, 0, 0);

 

    //動畫結束後,View停留在結束的位置

 

    animation.setFillAfter(true);

 

    animation.setDuration(300);

 

    imageView.startAnimation(animation);

 

}

 

 

 

這裡的start是指的當前的tab編號,編號是從0開始的,比如tab0、tab1、tab2。

 

end是指的你點擊的tab編號。例如一開始我就點瞭tab3,那麼start=0,end=3。然後我又點瞭tab2,那麼start=3,end=2。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。