菜單式控件——LineMenuView
就目前來說,Android業界各種框架層出不窮,開發時使用通用的框架搭建模型,然後填充數據與業務邏輯即可。
不過對於一些比較“小型”的界面,一般都需要自己封裝類來進行操作,比如一些菜單項,按鈕樣式等等。
這裏基於平常使用菜單類型封裝成了Menu菜單,樣式比較簡單,不過可以明顯加快開發進度。
下面具體介紹框架的結構
目前版本已升級到2.0,添加了anko使用,同時修改了部分源碼,具體可查看版本變更記錄
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>
佈局文件很簡單:
- 左側有一個MarqueeTextView控件,用於顯示菜單文本,同時該文本支持設定DrawableLeft,也就是icon圖標;單行顯示,若文本過程則以跑馬燈形式顯示。
- 右側則是一個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是否加入計數體系的判斷邏輯:
- 如果 LineMenuView_for_calculation 屬性 取值爲 bypassed(xml不設置的話也是bypassed狀態);則表示採取默認操作;
- 默認操作:如果 LineMenuView可見性爲View.VISIBLE,則加入計數體系。如果爲其他不可見狀態(gone,invisible)則不加入。
- 如果LineMenuView_for_calculation 屬性 取值爲 on,則表示開啓計數,此時無論控件是否可見都會加入計數體系。
- 如果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自定義屬性值:
- 需要使用的插件類型;比如 text,transition等,這個通過app:LineMenuView_plugin屬性指定。
- 插件初始化;比如menu和brief文字,大小,顏色;或者transition默認是選中還是非選中等等。這個通過attr.xml文件中列出的屬性值就可以對照設置。
- 考慮是否需要納入計數系統,如果納入的話,不光自身的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值的控件不能變!!!,否則可能會 出現異常。
如果改動很大,推薦直接拷貝源碼進行修改,代碼量只有數百。