之前已寫過關於LineMenuView的使用說明,主要針對 xml+java
這種經典的形式,也就是1.0
版本:通用式菜單式控件——LineMenuView(一);
隨着kotlin的興起,很多項目也慢慢的轉向了這位新寵
同樣的,針對一些簡單佈局,會有這種轉變:xml-> anko
LineMenuView升級到版本二後,也集成了anko
功能,這裏會先介紹2.0
的使用方法,然後根據源碼來簡單的說明一下anko的原理
.
一 使用步驟
雖然LineMenuView升級到2.0
有比較大的改動,但很多情況是兼容1.0
的,比如有關xml佈局
文件,就沒有任何改變.
不同的是,如果想使用2.0
提供的anko
功能,需要先添加kotlin+anko
環境.
幸運的,androidstudio
在 3.0
之後,默認會提供kotlin環境,至於anko,則可以參照anko-github地址來配置
1. gradle 引入
和1.0
一樣,添加依賴
compile 'com.knowledge.mnlin:linemenuview:2.0.2'
2. 選擇提供的anko方法
LineMeneView 提供的 anko方法
放在 AnkoCreate.kt
主要包括以下幾種方法:
* lmv_none 適用於無插件情況
* lmv_text 適用於 text 插件形式
* lmv_switch 適用於 switch 形式
* lmv_radio 適用於 radio 形式
* lmv_select 適用於 select 形式
* lmv_transition 適用於 transition 形式
分別對應attr
屬性,即plugin類型
<!--單行菜單對應的參數: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>
<!--brief部分-->
<attr name="LineMenuView_badge" format="reference"/>
<attr name="LineMenuView_navigation" format="reference"/>
<attr name="LineMenuView_brief" format="string"/>
<attr name="LineMenuView_brief_text_color" format="color"/>
<attr name="LineMenuView_brief_text_size" format="dimension"/>
<!--menu部分-->
<attr name="LineMenuView_icon" format="reference"/>
<attr name="LineMenuView_menu" format="string"/>
<attr name="LineMenuView_menu_text_color" format="color"/>
<attr name="LineMenuView_menu_text_size" format="dimension"/>
</declare-styleable>
關於插件類型,在上一篇裏已有詳細說明
在選擇好對應的方法後,就可以直接在代碼中進行調用:
3. 方法調用
2.0
中anko
提供的方法,都是在1.0
中以有的xml屬性值,這裏針對各種插件都進行一次調用,實例代碼可以直接在這裏找到:kotlin代碼實例
verticalLayout {
//toolbar
include<AppBarLayout>(R.layout.layout_top_bar)
//scroll_view
scrollView {
overScrollMode = OVER_SCROLL_ALWAYS
isVerticalScrollBarEnabled = false
//佈局
verticalLayout {
bottomPadding = dp16
topPadding = dp16
//無插件
var a = lmv_none(menuText = "無插件") {
setCalculation(2)
setOnClickListener(object : LineMenuListener {
override fun performClickLeft(v: TextView): Boolean {
showToast("我被點擊了左邊的控件,並且返回了false讓performSelf方法得以執行")
return false
}
override fun performClickRight(v: View): Boolean {
//這裏mLmvFirst屬於無插件形式,右側的範圍極小,很難點擊到
showToast("我被點擊了右邊的控件,並且阻止了performSelf方法的執行")
return true
}
override fun performSelf(v: LineMenuView) {
showToast("只有點擊左邊我纔會執行")
}
})
}.lparams(width = matchParent) { topMargin = dp12 }
//文本形式
lmv_text(menuText = "文本形式", briefText = "簡要信息").lparams(width = matchParent) { topMargin = dp12 }
//文本大小/顏色/改變
lmv_text(menuText = "文本大小/顏色/改變", menuTextSizeRes = R.dimen.text_size_large_18sp, menuTextColorRes = R.color.yellow,
briefText = "簡要信息", briefTextColorRes = R.color.blue, briefTextSize = dimen(R.dimen.text_size_10sp)).lparams(width = matchParent) { topMargin = dp12 }
//帶上箭頭形式
lmv_text(menuText = "帶上箭頭形式",
briefText = "簡要信息", briefBadgeRes = R.mipmap.mobile_black, briefNavigation = dispatchGetDrawable(R.drawable.icon_arrow_right))
.lparams(width = matchParent) { topMargin = dp12 }
//帶icon的簡要信息,且信息太長需要一直滾動滾動滾動滾動滾動滾動滾動滾動滾動滾動
lmv_text(menuText = "帶icon的簡要信息,且信息太長需要一直滾動滾動滾動滾動滾動滾動滾動滾動滾動滾動", menuIconRes = R.mipmap.mobile_blue,
briefText = "簡要信息", briefBadgeRes = R.mipmap.mobile_black, briefNavigation = dispatchGetDrawable(R.drawable.icon_arrow_right))
.lparams(width = matchParent) { topMargin = dp12 }
//切換模式
lmv_transition(menuText = "切換模式",
transition = true).lparams(width = matchParent) { topMargin = dp12 }
//選中/未選中模式
lmv_select(menuText = "選中/未選中模式",
select = false).lparams(width = matchParent) { topMargin = dp12 }
//radio顯示模式
lmv_radio(menuText = "選中/未選中模式",
radio = true).lparams(width = matchParent) { topMargin = dp12 }
//radio顯示模式
lmv_switch(menuText = "switch顯示模式",
switch = true).lparams(width = matchParent) { topMargin = dp12;leftMargin = dp16;rightMargin = dp16 }
}.lparams(width = matchParent).applyRecursively {
if (it is LineMenuView) {
it.common_padding_bg()
}
}
}.lparams(width = matchParent, height = matchParent)
}
這段anko
代碼對應 xml
如下(源碼對應activity_test_activity.xml):
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include layout="@layout/layout_top_bar"/>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="none">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingBottom="@dimen/view_padding_margin_16dp"
android:paddingTop="@dimen/view_padding_margin_16dp">
<com.knowledge.mnlin.linemenuview.LineMenuView
android:id="@+id/lmv_first"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white_background_5"
android:paddingEnd="@dimen/view_padding_margin_16dp"
android:paddingStart="@dimen/view_padding_margin_16dp"
app:LineMenuView_for_calculation="off"
app:LineMenuView_menu="無插件"
app:LineMenuView_plugin="none"/>
<!--text插件則會包含brief信息-->
<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_brief="簡要信息"
app:LineMenuView_menu="文本形式"
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_brief="簡要信息"
app:LineMenuView_brief_text_color="@color/blue"
app:LineMenuView_brief_text_size="@dimen/text_size_10sp"
app:LineMenuView_menu="文本大小/顏色/改變"
app:LineMenuView_menu_text_color="@color/yellow"
app:LineMenuView_menu_text_size="@dimen/text_size_large_18sp"
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_badge="@mipmap/mobile_black"
app:LineMenuView_brief="簡要信息"
app:LineMenuView_menu="帶上箭頭形式"
app:LineMenuView_navigation="@drawable/icon_arrow_right"
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_badge="@mipmap/mobile_black"
app:LineMenuView_brief="簡要信息"
app:LineMenuView_icon="@mipmap/mobile_blue"
app:LineMenuView_menu="帶icon的簡要信息,且信息太長需要一直滾動滾動滾動滾動滾動滾動滾動滾動滾動滾動"
app:LineMenuView_navigation="@drawable/icon_arrow_right"
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"/>
<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="select"/>
<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="radio顯示模式"
app:LineMenuView_plugin="radio"
app:LineMenuView_radio="on"/>
<!--注意下劃線-->
<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="switch顯示模式"
app:LineMenuView_plugin="switch_"
app:LineMenuView_switch="on"/>
</LinearLayout>
</ScrollView>
</LinearLayout>
這裏列出了各種plugin
的各種使用方式,在針對 簡單佈局時,使用anko可以省略大量的代碼.
以上代碼顯示效果和通用式菜單式控件——LineMenuView(一)中顯示效果相同
二 anko 原理淺析
從上面代碼可以看到,anko
使用起來有些像是web,或者說其實和xml
表現形式相同,那麼系統是如何處理anko呢?對於通用佈局,如何使用anko來定義LineMenuView
模版?
1 anko到底是什麼格式?
以一個簡短的anko
代碼爲例:
verticalLayout {
include<AppBarLayout>(R.layout.layout_top_bar)
scrollView {
overScrollMode = View.OVER_SCROLL_ALWAYS
isVerticalScrollBarEnabled = false
verticalLayout {
//系統存儲的值
val locale = DefaultPreferenceUtil.getInstance().localeLanguageSwitch
val menus = getStringArray(R.array.array_locale_language)
//初始化界面
for (i in lmvs.indices) {
//lmv
lmvs[i] = lmv_select(menuText = menus[i]) {
rightSelect = i == locale
}.lparams(width = matchParent) {
if (i == 0) {
topMargin = dimen(R.dimen.view_padding_margin_10dp)
}
}
//分隔符divider
if (i < lmvs.size - 1) {
dv_line().lparams(width = matchParent)
}
}
}.lparams(width = matchParent).applyRecursively {
if (it is LineMenuView || it is DividerView) {
it.horizontalPadding = dimen(R.dimen.view_padding_margin_16dp)
it.backgroundColorResource = R.color.main_color_white
}
}
}.lparams(matchParent, matchParent)
}
它的效果圖是這樣的:
只是一個簡單的仿微信應用語言切換的界面,都是LineMenuView的select類型使用
,這裏使用for循環
創建LineMenuView控件,然後使用applyRecursively
統一處理style
要知道的是,anko 也是符合(必須符合) kotlin語法的,代碼最前的 verticalLayout
以及 include
和 scrollView
等,其實是真正的方法:fun
,只是由於kotlin最後一個參數爲lam時可以簡寫
的規則,看起來比較抽象,例如:
inline fun Activity.verticalLayout(theme: Int = 0, init: (@AnkoViewDslMarker _LinearLayout).() -> Unit): LinearLayout {
return ankoView(`$$Anko$Factories$CustomViews`.VERTICAL_LAYOUT_FACTORY, theme, init)
}
那麼,這裏就不難理解了,其實上面的anko換成kotlin非簡寫形式
,應該是這樣:
this.verticalLayout(theme = 0, init = {
// ... 初始化創建好的 LinearLayout
})
只是一個簡單的方法調用而已,該方法主要做的功能(在該Activity中)有三個:
1. 方法內部創建一個 LinearLayout (orientation=Vertical)
2. 使用我們傳入的lam表達式 **init** 來初始化生成的 LinearLayout
3. 調用 setContentView 方法,將生成的佈局添加到主界面中
那這三個步驟,是怎麼實現的呢?
# 2. anko的實現
事實上,anko中針對每個view在不同的類上都添加了實現方法,比如vertialLayout
的實現:
inline fun ViewManager.verticalLayout(theme: Int = 0): LinearLayout = verticalLayout(theme) {}
inline fun ViewManager.verticalLayout(theme: Int = 0, init: (@AnkoViewDslMarker _LinearLayout).() -> Unit): LinearLayout {
return ankoView(`$$Anko$Factories$CustomViews`.VERTICAL_LAYOUT_FACTORY, theme, init)
}
inline fun Context.verticalLayout(theme: Int = 0): LinearLayout = verticalLayout(theme) {}
inline fun Context.verticalLayout(theme: Int = 0, init: (@AnkoViewDslMarker _LinearLayout).() -> Unit): LinearLayout {
return ankoView(`$$Anko$Factories$CustomViews`.VERTICAL_LAYOUT_FACTORY, theme, init)
}
inline fun Activity.verticalLayout(theme: Int = 0): LinearLayout = verticalLayout(theme) {}
inline fun Activity.verticalLayout(theme: Int = 0, init: (@AnkoViewDslMarker _LinearLayout).() -> Unit): LinearLayout {
return ankoView(`$$Anko$Factories$CustomViews`.VERTICAL_LAYOUT_FACTORY, theme, init)
}
源碼中同時對三個對象 (ViewManager
,Context
,Activity
) 動態添加了verticalLayout
方法
在觀察源碼之前,我們可以猜測一下這三種方式大概功能,首先以一般的xml佈局來說,主要用處就幾個:
1. activity直接進行加載,然後界面顯示
2. inflate出View,然後當做參數進行傳遞或者其他作用
3. 通過 `include` 或者通過 ViewStub 方式進行引用
4. 其他
雖然說用處有多重,但說到底,最後還是需要添加到activity中進行顯示的(特殊情況不考慮),不然view拿出來也沒什麼意義
安卓中View的創建有個參數不可缺少:Context
,那麼可想而知,即便anko中要創建出View來,這個參數也是必不可少的
上面的代碼是在Activity
中執行的,this
爲Activity
,我們可以追蹤源碼查看:
inline fun Activity.verticalLayout(theme: Int = 0, init: (@AnkoViewDslMarker _LinearLayout).() -> Unit): LinearLayout {
return ankoView(`$$Anko$Factories$CustomViews`.VERTICAL_LAYOUT_FACTORY, theme, init)
}
然後:
inline fun <T : View> Activity.ankoView(factory: (ctx: Context) -> T, theme: Int, init: T.() -> Unit): T {
val ctx = AnkoInternals.wrapContextIfNeeded(this, theme)
val view = factory(ctx)
view.init()
AnkoInternals.addView(this, view)
return view
}
第一行代碼:AnkoInternals.wrapContextIfNeeded(this, theme)
傳入了activity
以及theme
,返回一個Context
,根據方法的說明,可以看到它的功能是獲取到一個包含theme的上下文
第二行代碼:factory(ctx)
,根據Context
創建一個需要的View,對於verticalLayout
方法,系統默認提供的view是這樣的:
val VERTICAL_LAYOUT_FACTORY = { ctx: Context ->
val view = _LinearLayout(ctx)
view.orientation = LinearLayout.VERTICAL
view
}
這也印證了上面所說:verticalLayout
是一個orientation = LinearLayout.VERTICAL
的LinearLayout
第三行代碼:view.init()
:這裏對剛創建好的View進行初始化,init
是lam表達式,使我們主要進行邏輯處理的地方,類型爲:T.()
上面三行代碼以及針對view進行了創建,初始化等操作,接下來就是anko
的核心部分:View的使用
創建好的view總是要加載到activity中的,或者作爲根佈局添加到activity
(其實也是作爲子佈局),要麼就是作爲子View添加到ViewGroup
中,接下來詳細分析一下第四行代碼:AnkoInternals.addView(this, view)
fun <T : View> addView(manager: ViewManager, view: T) = when (manager) {
is ViewGroup -> manager.addView(view)
is AnkoContext<*> -> manager.addView(view, null)
else -> throw AnkoException("$manager is the wrong parent")
}
fun <T : View> addView(ctx: Context, view: T) {
ctx.UI { AnkoInternals.addView(this, view) }
}
fun <T : View> addView(activity: Activity, view: T) {
createAnkoContext(activity, { AnkoInternals.addView(this, view) }, true)
}
在這裏,addView
已經有了區別,如果verticalLayout{*}
是在activity中調用(根節點部分),那麼代碼會執行createAnkoContext(activity, { AnkoInternals.addView(this, view) }, true)
,三個參數表達的含義是這樣的:
inline fun <T> T.createAnkoContext(
ctx: Context,
init: AnkoContext<T>.() -> Unit,
setContentView: Boolean = false
)
也就是說,當this
是activity
時,setContentView
爲true
,因此我們在activity
中使用anko時,不需要主動去調用setContentView
方法(同樣的,在Fragment中,系統也幫我們做了處理)
如果我們現在this
不是activity
,而是View
,或者說是ViewGroup
,也就是說,現在我們想對ViewGroup
添加子佈局,那麼系統是如何實現的呢?
首先我們要知道,什麼是ViewManager
用官方api的解釋說明:
然後可以發現,ViewGroup
的繼承關係是這樣的:
public abstract class ViewGroup extends View implements ViewParent, ViewManager
這樣的話,我們在非根部局
調用verticalLayout
等方法時,系統走這條邏輯:
fun <T : View> addView(manager: ViewManager, view: T) = when (manager) {
is ViewGroup -> manager.addView(view)
is AnkoContext<*> -> manager.addView(view, null)
else -> throw AnkoException("$manager is the wrong parent")
}
因爲我們此時this
是ViewGroup
,因此,會直接調用ViewGroup
的addView
方法:
/**
* <p>Adds a child view. If no layout parameters are already set on the child, the
* default parameters for this ViewGroup are set on the child.</p>
*
* <p><strong>Note:</strong> do not invoke this method from
* {@link #draw(android.graphics.Canvas)}, {@link #onDraw(android.graphics.Canvas)},
* {@link #dispatchDraw(android.graphics.Canvas)} or any related method.</p>
*
* @param child the child view to add
*
* @see #generateDefaultLayoutParams()
*/
public void addView(View child) {
addView(child, -1);
}
對於this
是context
的情況,其實和activity
類似,只是在AnkoContextImpl<T>
中方法addView
有所區別:
override fun addView(view: View?, params: ViewGroup.LayoutParams?) {
if (view == null) return
if (myView != null) {
alreadyHasView()
}
this.myView = view
if (setContentView) {
doAddView(ctx, view)
}
}
三 LineMenuView 自定義實現 anko
庫中爲LIneMenuView添加anko實現其實很簡單,只是針對不同的方法或者參數生成屬性異同的LineMenuView
對象而已.
以lmv_transition
插件爲例,方法調用邏輯是這樣:
/**
* 默認爲transition形式
*/
fun ViewManager.lmv_transition(
//menu參數部分
menuText: String? = null, menuIcon: Drawable? = null, @DrawableRes menuIconRes: Int? = null, @Dimension(unit = PX) menuTextSize: Int? = null, @DimenRes menuTextSizeRes: Int? = null, @ColorInt menuTextColor: Int? = null, @ColorRes menuTextColorRes: Int? = null,
//transition參數,true表示開
transition: Boolean = false,
//自定義初始化方法
init: (LineMenuView.() -> Unit) = {}): LineMenuView {
return this.lmv(menuText, menuIcon, menuIconRes, menuTextSize, menuTextSizeRes, menuTextColor, menuTextColorRes, init) {
setPlugin(5)
this.transition = transition
}
}
然後調用通用的方法:
/**
* 相同的添加menu
*/
private fun ViewManager.lmv(menuText: String? = null, menuIcon: Drawable? = null, @DrawableRes menuIconRes: Int? = null, @Dimension(unit = PX) menuTextSize: Int? = null, @DimenRes menuTextSizeRes: Int? = null, @ColorInt menuTextColor: Int? = null, @ColorRes menuTextColorRes: Int? = null, init: (LineMenuView.() -> Unit) = {}, pluginInt: LineMenuView.(ctx: Context) -> Unit): LineMenuView {
return ankoView({ ctx ->
LineMenuView(ctx, null, 0).apply {
this.menuText = menuText
menuIcon?.let {
setIcon(it)
}
menuIconRes?.let {
setIcon(getDrawable(ctx, it))
}
menuTextSize?.let {
setMenuTextSize(TypedValue.COMPLEX_UNIT_PX, it)
}
menuTextSizeRes?.let {
setMenuTextSize(TypedValue.COMPLEX_UNIT_PX, ctx.dimen(it))
}
menuTextColor?.let {
setMenuTextColor(it)
}
menuTextColorRes?.let {
setMenuTextColor(getColor(ctx, it))
}
//初始化插件
pluginInt(ctx)
}
}, 0, init)
}
很簡單就完成了anko的自定義
更多使用方式請參考:GITHUB-LineMenuView