通用式菜單式控件——LineMenuView

菜單式控件——LineMenuView

就目前來說,Android業界各種框架層出不窮,開發時使用通用的框架搭建模型,然後填充數據與業務邏輯即可。
不過對於一些比較“小型”的界面,一般都需要自己封裝類來進行操作,比如一些菜單項,按鈕樣式等等。
這裏基於平常使用菜單類型封裝成了Menu菜單,樣式比較簡單,不過可以明顯加快開發進度。
下面具體介紹框架的結構


目前版本已升級到2.0,添加了anko使用,同時修改了部分源碼,具體可查看版本變更記錄

version.md


anko使用方式或使用步驟可直接查看:

GITHUB

一、顯示效果

針對不同插件類型的效果圖如下:

1、靜態效果

靜態顯示

2、動圖效果

針對一些可以使用的控件,添加響應效果,顯示效果如下:

動圖效果

點擊按鈕則會觸發默認的點擊事件,來切換狀態或進行其他業務處理。

二、插件說明

LineMenuView控件插件類型如下:

  • 無插件形式,只有單獨的左側menu文本,可顯示簡單菜單信息。
  • text插件形式,右側會多出一個TextView,被稱作 “brief”信息,可以設置文本以及 Drawable***
  • transition形式 切換兩種圖片,根據透明度淡入或淡出
  • select形式 切換選中/未選中兩種狀態
  • radio模式 radio形式的選中切換
  • switch_形式,之所以會在最後添加下劃線,是因爲switch屬於關鍵字,無法直接使用;該類型菜單可以切換開關按鈕。

基本插件類型只有這五種(不包括無插件形式),一般 text插件 使用最多,下面就源碼及佈局來說明插件的實現方式:

1、LineMenuView佈局方式

<?xml version="1.0" encoding="utf-8"?>
<merge
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:gravity="center_vertical"
    android:minHeight="@dimen/prefer_view_height_48dp"
    android:orientation="horizontal">
    <!--menu前端爲標題-->
    <com.knowledge.mnlin.linemenuview.MarqueeTextView
        android:id="@+id/tv_menu"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:drawablePadding="@dimen/view_padding_margin_12dp"
        android:ellipsize="marquee"
        android:gravity="start|center_vertical"
        android:marqueeRepeatLimit="marquee_forever"
        android:singleLine="true"
        tools:text="對應的菜單信息"/>

    <!--menu後面爲可選擇部分-->
    <FrameLayout
        android:id="@+id/fl_plugin"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:paddingEnd="0dp"
        android:paddingStart="@dimen/view_padding_margin_8dp">
        <!--使用單個TextView,文本右對齊-->
        <TextView
            android:id="@+id/tv_brief_info"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:drawablePadding="@dimen/view_padding_margin_12dp"
            android:ellipsize="end"
            android:gravity="center_vertical|end"
            android:maxLines="1"
            android:visibility="gone"
            tools:text="右側文本內容"/>

        <!--使用開關按鈕,可切換on/off狀態-->
        <android.support.v7.widget.SwitchCompat
            android:id="@+id/sc_switch"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:gravity="center_vertical|end"
            android:visibility="gone"/>

        <!--使用攜帶了一個Radio的視圖-->
        <RadioButton
            android:id="@+id/rb_check"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:button="@drawable/selector_radio_checked"
            android:checked="true"
            android:gravity="center_vertical|end"
            android:minHeight="@dimen/view_height_16dp"
            android:minWidth="@dimen/view_height_16dp"
            android:visibility="gone"/>

        <!--使用單個圖片,對於需要動態變化的圖像,使用drawable屬性會不方便,/選中/未選中-->
        <ImageView
            android:id="@+id/iv_image"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:gravity="center_vertical|end"
            android:src="@drawable/icon_choose_address"
            android:visibility="gone"/>

        <!--漸變的圖片切換-->
        <ImageView
            android:id="@+id/icon_open_close"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:gravity="center_vertical|end"
            android:src="@drawable/transition_close_open"
            android:visibility="gone"/>
    </FrameLayout>
</merge>

佈局文件很簡單:

  1. 左側有一個MarqueeTextView控件,用於顯示菜單文本,同時該文本支持設定DrawableLeft,也就是icon圖標;單行顯示,若文本過程則以跑馬燈形式顯示。
  2. 右側則是一個FramLayout容器,所有類型插件對應的View都放在裏面,通過visible屬性來控制顯示或隱藏。

通過控件中內置的方法setPlugin就可以看到具體的邏輯:

/**
 * @param plugin 0 表示不顯示任何插件
 *               1 表示顯示textView
 *               2 表示顯示switch
 *               3 表示顯示radio
 *               4 表示select
 *               5 表示transition
 */
public LineMenuView setPlugin(int plugin) {
    mTvBriefInfo.setVisibility(plugin == 1 ? View.VISIBLE : View.GONE);
    mScSwitch.setVisibility(plugin == 2 ? View.VISIBLE : View.GONE);
    mRbCheck.setVisibility(plugin == 3 ? View.VISIBLE : View.GONE);
    mIvImage.setVisibility(plugin == 4 ? View.VISIBLE : View.GONE);
    mIconOpenClose.setVisibility(plugin == 5 ? View.VISIBLE : View.GONE);
    return this;
}

如此一來,就可以根據插件碼(也可根據後面說到的style屬性來區分)控制插件顯示的類型;

2、插件實際效果與名稱對應關係

這裏給出text插件transition插件顯示效果及對應位置說明:

transition插件:

這裏寫圖片描述

text插件:

這裏寫圖片描述

3、attr屬性(自定義屬性及用途)

LineMenuView定了多種屬性,在使用插件時,可以在xml中使用屬性來定義需要顯示的效果。所有的屬性值如下:

<!--單行菜單對應的參數:switch狀態、menu文本、icon圖標等-->
    <declare-styleable name="LineMenuView">
        <attr name="LineMenuView_plugin" format="enum">
            <enum name="none" value="0"/>
            <enum name="text" value="1"/>
            <enum name="switch_" value="2"/>
            <enum name="radio" value="3"/>
            <enum name="select" value="4"/>
            <enum name="transition" value="5"/>
        </attr>
        <attr name="LineMenuView_switch" format="enum">
            <enum name="off" value="0"/>
            <enum name="on" value="1"/>
        </attr>
        <!--選中/未選中-->
        <attr name="LineMenuView_radio" format="enum">
            <enum name="off" value="0"/>
            <enum name="on" value="1"/>
        </attr>
        <!--開/關-->
        <attr name="LineMenuView_transition" format="enum">
            <enum name="off" value="0"/>
            <enum name="on" value="1"/>
        </attr>
        <!--用於計算,default表示默認:只有在visible時纔會納入計算;on表示納入計算,即便是不可見狀態;off表示不納入計算,即使是可見狀態-->
        <attr name="LineMenuView_for_calculation" format="enum">
            <enum name="bypassed" value="0"/>
            <enum name="on" value="1"/>
            <enum name="off" value="2"/>
        </attr>
        <attr name="LineMenuView_badge" format="reference"/>
        <attr name="LineMenuView_navigation" format="reference"/>
        <attr name="LineMenuView_icon" format="reference"/>
        <attr name="LineMenuView_brief" format="string"/>
        <attr name="LineMenuView_menu" format="string"/>
        <attr name="LineMenuView_brief_text_color" format="color"/>
        <attr name="LineMenuView_menu_text_color" format="color"/>
        <attr name="LineMenuView_brief_text_size" format="dimension"/>
        <attr name="LineMenuView_menu_text_size" format="dimension"/>
    </declare-styleable>

這裏定義了menu以及brief文字大小顏色,還有各個插件的初始狀態;一些名字對應上面的效果圖,很容易分辨出功能來,因此不多做說明。

唯一需要解釋的便是 LineMenuView_for_calculation 屬性,該屬性是服務於點擊事件的。

其實在控件初始化時,其實已經默認綁定了LineMenuView的點擊事件監聽器:

//如果當前view所在的context對象聲明瞭該接口,那麼就直接進行綁定
if (getContext() instanceof LineMenuListener && setListenerIsSelf()) {
    setOnClickListener((LineMenuListener) getContext());
}

在段代碼是在 initData方法 中執行的,而initData方法則是在構造函數中調用的,因此只要LineMenuView所在的上下文環境Context實現了LineMenuListener接口,就無需使用者主動調用監聽器設置方法了。

再看上面的監聽器設置方法,這裏除了判斷Context是否實現了LineMenuListener接口外,還判斷了setListenerIsSelf方法的邏輯。

因此如果使用者不想默認添加監聽事件,只需要自定義一個class類繼承LineMenuView,、將setListenerIsSelf方法重寫返回false即可,就像這樣:

/**
 * 是否設置onClickLisener爲自身this
 *
 * @return true表示設置
 */
protected boolean setListenerIsSelf() {
    return false;
}

接着我們繼續談論 LineMenuView_for_calculation 屬性 的功能,該屬性值其實是爲了控制LineMenuView控件 是否設置表示自身位置的tag屬性,以及自身是否納入計數體系

這個說明起來可能比較拗口,現在先看一下 什麼是計數體系

① LineMenuView的計數處理

前面已經說過,使用該控件時,如果Context已經實現了 LineMenuListener 接口,那麼就無需再主動設置監聽事件了。
那麼使用者怎麼知道被點擊的LineMenuView是哪一個呢(如果一個佈局中出現了多個LineMenuView控件的話)?

可以先查看一下LineMenuListener 接口的回調方法:

/**
  * 控件監聽
  */
 public interface LineMenuListener {
     /**
      * 點擊左側文本
      *
      * @param v 被點擊到的v;此時應該是左側的TextView
      * @return 是否消費該點擊事件, 如果返回true, 則performSelf將不會被調用
      */
     boolean performClickLeft(TextView v);

     /**
      * 注:該放置主要針對 text 插件設計,但即便是其他插件模式,也可以通過 v.getTag()方法獲取到位置信息
      * 因爲就menu菜單來說,也只有文本形式纔會取考慮點擊左側和右側時有不同的處理邏輯
      * 
      * @param v 被點擊到的v;此時應該是右側的TextView
      * @return 是否消費該點擊事件, 如果返回true, 則performSelf將不會被調用
      */
     boolean performClickRight(TextView v);

     /**
      * @param v 被點擊到的v;此時應該是該view自身:LineMenuView
      */
     void performSelf(LineMenuView v);
 }

當然,可以通過判斷是否是同一個View,來處理不同邏輯,以 performSelf方法 來說,可以通過判斷 v 與某個佈局中的控件是否是同一個來執行不同的代碼。

但如果xml中有多個這樣的佈局,則處理起來會相當麻煩,因此在這三個回調方法執行時,就已經設置好了一個TAG值,通過該TAG值可以獲取到某個控件在佈局中所處的位置(非LineMenuView的控件不計算在內)。

就像這樣:

/**
 * @param v 被點擊到的v;此時應該是該view自身:LineMenuView
 */
@Override
public void performSelf(LineMenuView v) {
    int position = ((int) v.getTag(LineMenuView.TAG_POSITION));
    switch (position) {
        //因爲第一個LineMenuView設置了LineMenuView_for_calculation爲off,表示不計數,因此會是-1,且不會影響它後面LineMenuView的序號
        //但是,因爲開始時候給mLmvFirst重新設定了onClickListener方法,因此點擊它時根本不會進入該方法(performSelf內)

        case 0://文本形式
            showToast("文本形式:位置 0");
            break;
        case 1://可改變字體顏色大小的文本形式
            showToast("可改變字體顏色大小的文本形式: 位置 1");
            break;
        case 2://帶箭頭(navigation)、badge圖標的形式
            showToast("帶箭頭、badge圖標的形式 : 位置 2");
            break;
        case 3://帶箭頭、icon、badge,且menu信息滾動的形式
            showToast("帶箭頭、icon、badge,且menu信息滾動的形式: 位置 3");
            break;
        case 4://transition模式
            showToast("transition模式: 位置 4");
            v.setTransition(!v.getTransition());
            break;
        case 5://select模式
            showToast("select模式: 位置 5");
            v.setRightSelect(!v.getRightSelect());
            break;
        case 6://radio模式
            showToast("radio模式: 位置 6");
            v.setRadio(!v.getRadio());
            break;
        case 7://switch_模式
            showToast("switch_模式: 位置 7");
            v.setSwitch(!v.getSwitch());
            break;
    }
}

這段代碼即是上面效果圖對應的代碼,使用TAG的話就可以不對xml中控件設置id屬性,直接通過position判斷需要進行的邏輯。

還有一個問題就是,可能某個LineMenuView在某個版本中默認不可見,如果此時不進行額外處理的話,TAG取值會出現偏差,因此就引入了LineMenuView_for_calculation 屬性控制TAG取值效果。

這裏需要說明的是,即便某個LineMenuView不引入計數體系,也不會影響 LineMenuListener 監聽器的設置(如果Context實現了該接口的話),只是這時候在 performSelf中獲取TAG值時爲 -1 罷了;這樣也相當於給不引入計數的控件留下了可操作的空間。

現在給出LineMenuView是否加入計數體系的判斷邏輯:

  1. 如果 LineMenuView_for_calculation 屬性 取值爲 bypassed(xml不設置的話也是bypassed狀態);則表示採取默認操作;
  2. 默認操作:如果 LineMenuView可見性爲View.VISIBLE,則加入計數體系。如果爲其他不可見狀態(gone,invisible)則不加入
  3. 如果LineMenuView_for_calculation 屬性 取值爲 on,則表示開啓計數,此時無論控件是否可見都加入計數體系。
  4. 如果LineMenuView_for_calculation 屬性 取值爲 off,則表示不開啓計數,此時無論控件是否可見都不會加入計數體系。

就像上面的模版那樣,第一個LineMenuView設置LineMenuView_for_calculation屬性爲off,因此雖然控件處於可見狀態,但還是不會納入計數體系;
不過就像之前所說,即便不會納入計數體系也不會影響監聽事件的相應情況,如果第一個LineMenuView沒有調用setOnClickListener方法修改監聽器,那麼點擊該控件在回調方法中還是可以獲取到TAG對應的值的,只不過恆爲 -1 而已。

4、監聽器LineMenuListener

上面已經基本介紹了LineMenuListener回調如果使用。即通過TAG取值判斷對應的控件(不僅僅是performSelf方法,其他兩個perform***方法也可以通過TAG來獲取位置值)。

還有一些沒提到的就是這監聽器其實是三個回調,分別爲:

  • 點擊左側 menu菜單信息,會調用performClickLeft方法,如果該方法返回true,performSelf方法將不會執行。
  • 點擊右側 各種插件 ,會調用performClickRight方法,如果該方法返回true,performSelf方法將不會執行。
  • 當以上兩個方法返回false時,該方法纔有得以執行的機會。該方法沒有返回值,通常用於處理點擊左側和右側都具有相同邏輯的事情。

三、使用步驟

接下來說明如何使用該控件,事實上這很簡單:

1、在xml中聲明控件

<com.knowledge.mnlin.linemenuview.LineMenuView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginTop="@dimen/view_padding_margin_12dp"
    app:LineMenuView_badge="@mipmap/mobile_black"
    app:LineMenuView_navigation="@drawable/icon_arrow_right"
    android:background="@color/white_background_5"
    android:paddingEnd="@dimen/view_padding_margin_16dp"
    android:paddingStart="@dimen/view_padding_margin_16dp"
    app:LineMenuView_brief="簡要信息"
    app:LineMenuView_icon="@mipmap/mobile_blue"
    app:LineMenuView_menu="帶icon的簡要信息,且信息太長需要一直滾動滾動滾動滾動滾動滾動滾動滾動滾動滾動"
    app:LineMenuView_plugin="text"/>

<com.knowledge.mnlin.linemenuview.LineMenuView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginTop="@dimen/view_padding_margin_12dp"
    android:background="@color/white_background_5"
    android:paddingEnd="@dimen/view_padding_margin_16dp"
    android:paddingStart="@dimen/view_padding_margin_16dp"
    app:LineMenuView_menu="切換模式"
    app:LineMenuView_plugin="transition"
    app:LineMenuView_transition="on"/>

上面代碼展示的是: “二:2、插件實際效果與名稱對應關係” 中兩種插件顯示效果對應的代碼;一般分三個步驟考慮設置LineMenuView自定義屬性值:

  1. 需要使用的插件類型;比如 text,transition等,這個通過app:LineMenuView_plugin屬性指定。
  2. 插件初始化;比如menu和brief文字,大小,顏色;或者transition默認是選中還是非選中等等。這個通過attr.xml文件中列出的屬性值就可以對照設置。
  3. 考慮是否需要納入計數系統,如果納入的話,不光自身的TAG取值會有變化,連同其他兄弟節點(僅指LineMenuView類型)的TAG值也可能會發生變化。這個通過app:LineMenuView_for_calculation屬性來設置。

此時,基本的顯示效果已經完成。

2、對自己的 **Activity 聲明實現LineMenuListener接口

接下來,就需要讓Context實現接聽接口了。
當然也可以不使用默認的方法,但那樣就需要自己去聲明LineMenuView**對應成員變量,爲xml中LineMenuView添加 **id 屬性,然後通過ButterKnife或者findViewById方法爲成員變量賦值。
然後還對每一個LineMenuView控件調用setOnClickListener方法設置監聽器。

這樣實在是太麻煩了,使用框架的目的就是爲了簡單方便,如果需要做那麼多操作,還不如直接手搓代碼來的爽快。

public class TestActivityActivity extends BaseActivity<TestActivityPresenter> implements TestActivityContract.View, LineMenuView.LineMenuListener {
    //...
}

就像這樣,然後再實現三個方法即可。

3、填充處理邏輯

就像上面所說,通過TAG標籤來獲取所處的位置,然後通過switch 處理邏輯:

int position = ((int) v.getTag(LineMenuView.TAG_POSITION));
switch (position) {
    //...
}

四、自定義顯示效果

因爲自身控件代碼量很小,即便拷貝粘貼到自己的項目中也不會花費什麼時間,因此這裏只提供了修改全局xml文件的方法:

/**
 * 位置信息情況
 */
public static final int TAG_POSITION = R.id.LINE_MENU_VIEW_TAG_POSITION;

/**
 * 佈局文件
 */
public static final int LAYOUT_SELF = R.layout.layout_line_menu;

看到這個應該都明白了吧,想要修改整個佈局方法很簡單,直接修改靜態變量就可以了,一般來說一個項目中代碼風格都是統一的,應該不會出現變來變去需要多套場景的情況。

不過需要注意的時,自定義xml文件時一定是要把源xml文件給拷貝下來,必須裏面的有id值的控件不能變!!!,否則可能會 出現異常。

如果改動很大,推薦直接拷貝源碼進行修改,代碼量只有數百。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章