《Kotlin從零到精通Android開發》歐陽燊(二)

第六章 Kotlin使用簡單控件

6.1使用按鈕控件

按鈕button

按鈕事件三種Kotlin編碼方式:匿名函數,內部類,接口實現

1.匿名函數方式

btn_click_anonymos.setOnClickListener { v ->
//Kotlin變量類型轉換使用as
toast("${(v as Button).text}")
}

btn_click_anonymos.setOnLongClickListener { v ->
//Kotlin變量類型轉換使用as
longToast("${(v as Button).text}")
true
}

2.內部類方式

private inner class MyClickListener : View.OnClickListener {
override fun onClick(v: View) {
toast("${(v as Button).text}")
}
}
private inner class MyLongClickListener : View.OnLongClickListener {
override fun onLongClick(v: View): Boolean {
longToast("${(v as Button).text}")
return true
}
}

調用的時候:

btn_click_inner.setOnClickListener(MyClickListener())
btn_click_inner.setOnLongClickListener(MyLongClickListener())

3.接口實現方式
內部類的話,每個事件都要定義一個內部類,多了也不好,試試接口實現

class ButtonClickActivity : AppCompatActivity(), OnClickListener, OnLon
gClickListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_button_click)

btn_click_interface.setOnClickListener(this)
btn_click_interface.setOnLongClickListener(this)
}

override fun onClick(v: View) {
if (v.id == R.id.btn_click_interface) {
toast("${(v as Button).text}")
}
}
override fun onLongClick(v: View): Boolean {
if (v.id == R.id.btn_click_interface) {
longToast("${(v as Button).text}")
}
return true
}
}

複選框CheckBox

image.png

單選按鈕RadioButton

同Java

6.2使用頁面佈局

線性佈局LinearLayout

  1. Kotlin允許對屬性orientation直接賦值,從而取代了setOrientation方法;類似的還有屬性gravity取代了setGravity方法;
  2. Kotlin使用關鍵字as進行變量的類型轉換;
  3. Kolin支持調用dip方法將dip數值轉換爲px數值,倘若由Java編碼則需開發者自己實現一個像素轉換的工具類;
    因爲dip方法來自於Kotlin擴展的Anko庫,所以需要在Activity代碼頭部加上下面一行導入語句:
    import org.jetbrains.anko.dip
    image.png

相對佈局 RelativeLayout

rl_params.addRule和rl_params.above
image.png

約束佈局 ConstraintLayout

layout_constraintTop_toTopOf : 該控件的頂部與另一個控件的頂部對齊
layout_constraintTop_toBottompOf : 該控件的頂部與另一個控件的底部對齊
layout_constraintBottom_toTopOf : 該控件的底部與另一個控件的頂部對齊
layout_constraintBottom_toBottomOf : 該控件的底部與另一個控件的底部對齊
layout_constraintLeft_toLeftOf : 該控件的左側與另一個控件的左側對齊
layout_constraintLeft_toRightOf : 該控件的左側與另一個控件的右側對齊
layout_constraintRight_toLeftOf : 該控件的右側與另一個控件的左側對齊
layout_constraintRight_toRightOf : 該控件的右側與另一個控件的右側對齊

若要利用代碼給約束佈局動態添加控件,則可照常調用addView方法,不同之處在於,新控件的佈局參數必須使用約束佈局的佈局參數,即ConstraintLayout.LayoutParams,該參數通過setMargins/setMarginStart/setMarginEnd方法設置新控件與周圍控件的間距,至於新控件與周圍控件的位置約束關係,則可參照ConstraintLayout.LayoutParams的下列屬性說明:
topToTop : 當前控件的頂部與指定ID的控件頂部對齊
topToBottom : 當前控件的頂部與指定ID的控件底部對齊
bottomToTop : 當前控件的底部與指定ID的控件頂部對齊
bottomToBottom : 當前控件的底部與指定ID的控件底部對齊
startToStart : 當前控件的左側與指定ID的控件左側對齊
startToEnd : 當前控件的左側與指定ID的控件右側對齊
endToStart : 當前控件的右側與指定ID的控件左側對齊
endToEnd : 當前控件的右側與指定ID的控件右側對齊

6.3使用圖文控件

###文本視圖TextView
ellipsize+TextUtils.TruncateAt.MARQUEE 跑馬燈效果
image.png
image.png

Java
tv_marquee.setGravity(Gravity.LEFT | Gravity.CENTER);
Kotlin
tv_marquee.gravity = Gravity.LEFT or Gravity.CENTER

image.png

圖像視圖 ImageView

image.png

文本編輯框EditText

image.png

6.4Activity活動跳轉

傳送配對字段數據

Java:
Intent intent = new Intent(MainActivity.this, LinearLayoutActivity.class);
startActivity(intent);

Kotlin anko
startActivity<LinearLayoutActivity>()

帶參數的
Java:
Intent intent = new Intent(this, ActSecondActivity.class);
intent.putExtra("request_time", DateUtil.getNowTime());
intent.putExtra("request_content", et_request.getText().toString());
startActivity(intent);

Kotlin
方式一:用to關鍵字
startActivity<ActSecondActivity>(
"request_time" to DateUtil.nowTime,
"request_content" to et_request.text.toString())

方式二:Pair
startActivity<ActSecondActivity>(
Pair("request_time", DateUtil.nowTime),
Pair("request_content", et_request.text.toString()))

接收方:

val bundle = intent.extras
val request_time = bundle.getString("request_time")
val request_content = bundle.getString("request_content")

傳送序列化數據

//@Parcelize註解表示自動實現Parcelize接口的相關方法
@Parcelize
data class MessageInfo(val content: String, val send_time: String) : Pa
rcelable {
}

//@Parcelize標記需要在build.gradle設置experimental = true
androidExtensions {
experimental = true
}
//傳送序列化數據
val request = MessageInfo(et_request.text.toString(), DateUtil.nowTime)
startActivity<ParcelableSecondActivity>("message" to request)

接收方:

//獲得Parcelable的參數
val request = intent.extras.getParcelable<MessageInfo>("message")
${request.content}

跳轉時指定啓動模式

anko庫取消了intent方法有利有弊,弊端是intent對象的setAction,setData,addCategory,setFlags怎麼設置
那麼下面這種方式就可以拿到intent對象設置了

val intent = intentFor<ActSecondActivity>(
"request_time" to DateUtil.nowTime,
"request_content" to et_request.text.toString())

Android有兩種方式設置啓動模式:清單文件和代碼設置

清單文件設置

image.png

代碼設置

image.png

anko庫仍然簡化了代碼:startActivity(intent.newTask())
image.png

處理返回數據 startActivityForResult

跳轉

val info = MessageInfo(et_request.text.toString(), DateUtil.nowTime)
//ForResult表示需要返回參數
startActivityForResult<ActResponseActivity>(0, "message" to info)

下個頁面返回數據

btn_act_response.setOnClickListener {
val response = MessageInfo(et_response.text.toString(), DateUtil.no
wTime)
val intent = Intent()
intent.putExtra("message", response)
//調用setResult表示參數返回到上個頁面
setResult(Activity.RESULT_OK, intent)
finish()
}

收到數據後:

//返回本頁面時回調onActivityResult
override fun onActivityResult(requestCode: Int, resultCode: Int, data:
Intent?) {
if (data != null) {
//獲得應答參數
val response = data.extras.getParcelable<MessageInfo>
("message")
tv_request.text = " ${response.send_time}${response.content}"
}
}

6.5實戰項目

6.6總結

1.按鈕控件
2.佈局視圖
3.圖文控件
4.activity跳轉
5.anko庫對話框

第七章 Kotlin操縱複雜控件

7.1使用視圖排列

下拉框spinner

private fun initSpinner() {
val starAdapter = ArrayAdapter(this, R.layout.item_select, starArra
y)
starAdapter.setDropDownViewResource(R.layout.item_dropdown)
//Android 8.0之後的findViewById要求在後面添加"<View>"才能進行類型轉換
val sp = findViewById<View>(R.id.sp_dialog) as Spinner
sp.prompt = "請選擇行星"
sp.adapter = starAdapter
sp.setSelection(0)
sp.onItemSelectedListener = MySelectedListener()
}

private val starArray = arrayOf("水星", "水星", "火星", "木星")

internal inner class MySelectedListener : OnItemSelectedListener {
override fun onItemSelected(arg0: AdapterView<*>, arg1: View, arg2:
Int, arg3: Long) {
toast("${starArray[arg2]}")
}
override fun onNothingSelected(arg0: AdapterView<*>) {}
}

anko庫的簡化:

<TextView
android:id="@+id/tv_spinner"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_toRightOf="@+id/tv_dialog"
android:gravity="center"
android:drawableRight="@drawable/arrow_down"
android:textColor="@color/black"
android:textSize="17sp" />

val satellites = listOf("水星", "水星", "火星", "木星")
tv_spinner.text = satellites[0]
tv_spinner.setOnClickListener {
selector("請選擇行星", satellites) { i ->
tv_spinner.text = satellites[i]
toast("${tv_spinner.text}")
}
}

主要藉助了:import org.jetbrains.anko.selector
anko庫裏面的selector源碼是利用了AlertDialog的setItem方法
###列表視圖listview
Kotlin的擴展視圖selector
Kotlin要求每個變量都要初始化
lateinit延遲初始化屬性,那麼修飾的變量無需賦空值,使用的時候也不用加!!

class PlanetListAdapter(private val context: Context, private val planetList: MutableList<Planet>, private val background: Int) : BaseAdapter() {
 
    override fun getCount(): Int = planetList.size
 
    override fun getItem(position: Int): Any = planetList[position]
 
    override fun getItemId(position: Int): Long = position.toLong()
 
    override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
        var view = convertView
        val holder: ViewHolder
        if (convertView == null) {
            view = LayoutInflater.from(context).inflate(R.layout.item_list_view, null)
            holder = ViewHolder()
            //先聲明視圖持有者的實例,再依次獲取內部的各個控件對象
            holder.ll_item = view.findViewById(R.id.ll_item) as LinearLayout
            holder.iv_icon = view.findViewById(R.id.iv_icon) as ImageView
            holder.tv_name = view.findViewById(R.id.tv_name) as TextView
            holder.tv_desc = view.findViewById(R.id.tv_desc) as TextView
            view.tag = holder
        } else {
            holder = view.tag as ViewHolder
        }
        val planet = planetList[position]
        holder.ll_item.setBackgroundColor(background)
        holder.iv_icon.setImageResource(planet.image)
        holder.tv_name.text = planet.name
        holder.tv_desc.text = planet.desc
        return view!!
    }
 
    //ViewHolder中的屬性使用關鍵字lateinit延遲初始化
    inner class ViewHolder {
        lateinit var ll_item: LinearLayout
        lateinit var iv_icon: ImageView
        lateinit var tv_name: TextView
        lateinit var tv_desc: TextView
    }
}

以上的Kotlin代碼總算有點模樣了,雖然總體代碼還不夠精簡,但是至少清晰明瞭,其中主要運用了Kotlin的以下三項技術:

1、構造函數和初始化參數放在類定義的首行,無需單獨構造,也無需手工初始化;
2、像getCount、getItem、getItemId這三個函數,僅僅返回簡單運算的數值,可以直接用等號取代大括號;
3、對於視圖持有者的內部控件,在變量名稱前面添加lateinit,表示該屬性爲延遲初始化屬性;

網格視圖

在前面的列表視圖一小節中,給出了Kotlin改寫後的適配器類,通過關鍵字lateinit固然避免了麻煩的空校驗,可是控件對象遲早要初始化的呀,晚賦值不如早賦值。翻到前面PlanetListAdapter的實現代碼,認真觀察發現控件對象的獲取其實依賴於佈局文件的視圖對象view,既然如此,不妨把該視圖對象作爲ViewHolder的構造參數傳過去,使得視圖持有者在構造之時便能一塊初始化內部控件。據此改寫後的Kotlin適配器代碼如下所示:

class PlanetGridAdapter(private val context: Context, private val planetList: MutableList<Planet>, private val background: Int) : BaseAdapter() {
 
    override fun getCount(): Int = planetList.size
 
    override fun getItem(position: Int): Any = planetList[position]
 
    override fun getItemId(position: Int): Long = position.toLong()
 
    override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
        var view = convertView
        val holder: ViewHolder
        if (view == null) {
            view = LayoutInflater.from(context).inflate(R.layout.item_grid_view, null)
            holder = ViewHolder(view)
            //視圖持有者的內部控件對象已經在構造時一併初始化了,故這裏無需再做賦值
            view.tag = holder
        } else {
            holder = view.tag as ViewHolder
        }
        val planet = planetList[position]
        holder.ll_item.setBackgroundColor(background)
        holder.iv_icon.setImageResource(planet.image)
        holder.tv_name.text = planet.name
        holder.tv_desc.text = planet.desc
        return view!!
    }
 
    //ViewHolder中的屬性在構造時初始化
    inner class ViewHolder(val view: View) {
        val ll_item: LinearLayout = view.findViewById(R.id.ll_item) as LinearLayout
        val iv_icon: ImageView = view.findViewById(R.id.iv_icon) as ImageView
        val tv_name: TextView = view.findViewById(R.id.tv_name) as TextView
        val tv_desc: TextView = view.findViewById(R.id.tv_desc) as TextView
    }
}

外部調用

gv_planet.adapter = PlanetGridAdapter(this, Planet.defaultList, Color.W
HITE)

循環視圖RecyclerView

RecyclerView可以實現線性列表,網格列表,瀑布流網格
image.png
佈局管理器
LinearLayoutManager,GridLayoutManager,StaggeredGridLayoutManager
循環適配器
循環適配器和其他適配器的區別
1.自帶視圖持有者ViewHolder及其重用功能,無需開發者手工重用ViewHolder
2.未帶點擊和長按功能,需要開發者自己實現
3.增加區分不同列表項的視圖類型
4.可單獨對個別項進行增刪改操作,無須刷新整個列表
Kotlin實現:

//ViewHolder在構造時初始化佈局中的控件對象
class RecyclerLinearAdapter(private val context: Context, private val infos: MutableList<RecyclerInfo>) : RecyclerView.Adapter<ViewHolder>(), OnItemClickListener, OnItemLongClickListener {
    val inflater: LayoutInflater = LayoutInflater.from(context)
 
    //獲得列表項的數目
    override fun getItemCount(): Int = infos.size
 
    //創建整個佈局的視圖持有者
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view: View = inflater.inflate(R.layout.item_recycler_linear, parent, false)
        return ItemHolder(view)
    }
 
    //綁定每項的視圖持有者
    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val vh: ItemHolder = holder as ItemHolder
        vh.iv_pic.setImageResource(infos[position].pic_id)
        vh.tv_title.text = infos[position].title
        vh.tv_desc.text = infos[position].desc
        // 列表項的點擊事件需要自己實現
        vh.ll_item.setOnClickListener { v ->
            itemClickListener?.onItemClick(v, position)
        }
        vh.ll_item.setOnLongClickListener { v ->
            itemLongClickListener?.onItemLongClick(v, position)
            true
        }
    }
 
    //ItemHolder中的屬性在構造時初始化
    inner class ItemHolder(view: View) : RecyclerView.ViewHolder(view) {
        var ll_item = view.findViewById(R.id.ll_item) as LinearLayout
        var iv_pic = view.findViewById(R.id.iv_pic) as ImageView
        var tv_title = view.findViewById(R.id.tv_title) as TextView
        var tv_desc = view.findViewById(R.id.tv_desc) as TextView
    }
 
    private var itemClickListener: OnItemClickListener? = null
    fun setOnItemClickListener(listener: OnItemClickListener) {
        this.itemClickListener = listener
    }
 
    private var itemLongClickListener: OnItemLongClickListener? = null
    fun setOnItemLongClickListener(listener: OnItemLongClickListener) {
        this.itemLongClickListener = listener
    }
 
    override fun onItemClick(view: View, position: Int) {
        val desc = "您點擊了第${position+1}項,標題是${infos[position].title}"
        context.toast(desc)
    }
 
    override fun onItemLongClick(view: View, position: Int) {
        val desc = "您長按了第${position+1}項,標題是${infos[position].title}"
        context.toast(desc)
    }
}

可是這個循環適配器RecyclerLinearAdapter仍然體量龐大,細細觀察發現其實它有着數個與具體業務無關的屬性與方法,譬如上下文對象context、佈局載入對象inflater、點擊監聽器itemClickListener、長按監聽器itemLongClickListener等等,故而完全可以把這些通用部分提取到一個基類,然後具體業務再從該基類派生出特定的業務適配器類。根據這種設計思路,提取出了循環視圖基礎適配器,它的Kotlin代碼如下所示:

//循環視圖基礎適配器
abstract class RecyclerBaseAdapter<VH : RecyclerView.ViewHolder>(val context: Context) : RecyclerView.Adapter<RecyclerView.ViewHolder>(), OnItemClickListener, OnItemLongClickListener {
    val inflater: LayoutInflater = LayoutInflater.from(context)
 
    //獲得列表項的個數,需要子類重寫
    override abstract fun getItemCount(): Int
 
    //根據佈局文件創建視圖持有者,需要子類重寫
    override abstract fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder
 
    //綁定視圖持有者中的各個控件對象,需要子類重寫
    override abstract fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int)
 
    override fun getItemViewType(position: Int): Int = 0
 
    override fun getItemId(position: Int): Long = position.toLong()
 
    var itemClickListener: OnItemClickListener? = null
    fun setOnItemClickListener(listener: OnItemClickListener) {
        this.itemClickListener = listener
    }
 
    var itemLongClickListener: OnItemLongClickListener? = null
    fun setOnItemLongClickListener(listener: OnItemLongClickListener) {
        this.itemLongClickListener = listener
    }
 
    override fun onItemClick(view: View, position: Int) {}
 
    override fun onItemLongClick(view: View, position: Int) {}
}

一旦有了這個基礎適配器,實際業務的適配器即可由此派生而來,真正需要開發者編寫的代碼一下精簡了不少。下面便是個循環視圖的網格適配器,它實現了類似淘寶主頁的網格頻道欄目,具體的Kotlin代碼如下所示:

//把公共屬性和公共方法剝離到基類RecyclerBaseAdapter,
//此處僅需實現getItemCount、onCreateViewHolder、onBindViewHolder三個方法,以及視圖持有者的類定義
class RecyclerGridAdapter(context: Context, private val infos: MutableList<RecyclerInfo>) : RecyclerBaseAdapter<RecyclerView.ViewHolder>(context) {
 
    override fun getItemCount(): Int = infos.size
 
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        val view: View = inflater.inflate(R.layout.item_recycler_grid, parent, false)
        return ItemHolder(view)
    }
 
    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        val vh = holder as ItemHolder
        vh.iv_pic.setImageResource(infos[position].pic_id)
        vh.tv_title.text = infos[position].title
    }
 
    inner class ItemHolder(view: View) : RecyclerView.ViewHolder(view) {
        var ll_item = view.findViewById(R.id.ll_item) as LinearLayout
        var iv_pic = view.findViewById(R.id.iv_pic) as ImageView
        var tv_title = view.findViewById(R.id.tv_title) as TextView
    }
}

然而基類不過是雕蟲小技,Java也照樣能夠運用,所以這根本不入Kotlin的法眼,要想超越Java,還得擁有獨門祕笈才行。注意到適配器代碼仍然通過findViewById方法獲得控件對象,可是號稱在Anko庫的支持之下,Kotlin早就無需該方法就能直接訪問控件對象了呀,爲啥這裏依舊靠老牛拉破車呢?其中的緣由是Anko庫僅僅實現了Activity活動頁面的控件自動獲取,並未實現適配器內部的自動獲取。不過Kotlin早就料到了這一手,爲此專門提供了一個插件名叫LayoutContainer,只要開發者讓自定義的ViewHolder繼承該接口,即可在視圖持有者內部無需獲取就能使用控件對象了。這下不管是在Activity代碼,還是在適配器代碼中,均可將控件名稱拿來直接調用了。這麼神奇的魔法,快來看看Kotlin的適配器代碼是如何書寫的:

//利用Kotlin的插件LayoutContainer,在適配器中直接使用控件對象,而無需對其進行顯式聲明
class RecyclerStaggeredAdapter(context: Context, private val infos: MutableList<RecyclerInfo>) : RecyclerBaseAdapter<RecyclerView.ViewHolder>(context) {
 
    override fun getItemCount(): Int = infos.size
 
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        val view: View = inflater.inflate(R.layout.item_recycler_staggered, parent, false)
        return ItemHolder(view)
    }
 
    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        (holder as ItemHolder).bind(infos[position])
    }
 
    //注意這裏要去掉inner,否則運行報錯“java.lang.NoSuchMethodError: No virtual method _$_findCachedViewById”
    class ItemHolder(override val containerView: View?) : RecyclerView.ViewHolder(containerView), LayoutContainer {
        fun bind(item: RecyclerInfo) {
            iv_pic.setImageResource(item.pic_id)
            tv_title.text = item.title
        }
    }
}

還需要在模塊的build.gradle增加:

androidExtensions {
    experimental = true
}

上面採用了新的適配器插件,似乎已經大功告成,可是依然要書寫單獨的適配器代碼,仔細研究發現這個RecyclerStaggeredAdapter還有三個要素是隨着具體業務而變化的,包括:

1、列表項的佈局文件資源編碼,如R.layout.item_recycler_staggered;
2、列表項信息的數據結構名稱,如RecyclerInfo;
3、對各種控件對象的設置操作,如ItemHolder類的bind方法;

除了以上三個要素,RecyclerStaggeredAdapter內部的其餘代碼都是允許複用的,因此,接下來的工作就是想辦法把這三個要素抽象爲公共類的某種變量。對於第一個的佈局編碼,可以考慮將其作爲一個整型的輸入參數;對於第二個的數據結構,可以考慮定義一個模板類,在外部調用時再指定具體的數據類;對於第三個的bind方法,若是Java編碼早已束手無策,現用Kotlin編碼正好將該方法作爲一個函數參數傳入。依照三個要素的三種處理對策,進而提煉出來了循環適配器的通用類RecyclerCommonAdapter,詳細的Kotlin代碼示例如下:

//循環視圖通用適配器
//將具體業務中會變化的三類要素抽取出來,作爲外部傳進來的變量。這三類要素包括:
//佈局文件對應的資源編號、列表項的數據結構、各個控件對象的初始化操作
class RecyclerCommonAdapter<T>(context: Context, private val layoutId: Int, private val items: List<T>, val init: (View, T) -> Unit): RecyclerBaseAdapter<RecyclerCommonAdapter.ItemHolder<T>>(context) {
 
    override fun getItemCount(): Int = items.size
 
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        val view: View = inflater.inflate(layoutId, parent, false)
        return ItemHolder<T>(view, init)
    }
 
    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        val vh: ItemHolder<T> = holder as ItemHolder<T>
        vh.bind(items.get(position))
    }
 
    //注意init是個函數形式的輸入參數
    class ItemHolder<in T>(val view: View, val init: (View, T) -> Unit) : RecyclerView.ViewHolder(view) {
        fun bind(item: T) {
            init(view, item)
        }
    }
}

有了這個通用適配器,外部使用適配器只需像函數調用那樣傳入這三種變量就好了,具體調用的Kotlin代碼如下所示:

//第二種方式:使用把三類可變要素抽象出來的通用適配器
val adapter = RecyclerCommonAdapter(this, R.layout.item_recycler_staggered, RecyclerInfo.defaultStag,
        {view, item ->
            val iv_pic = view.findViewById(R.id.iv_pic) as ImageView
            val tv_title = view.findViewById(R.id.tv_title) as TextView
            iv_pic.setImageResource(item.pic_id)
            tv_title.text = item.title
        })
rv_staggered.adapter = adapter

最終出爐的適配器僅有十行代碼不到,其中的關鍵技術——函數參數真是不鳴則已、一鳴驚人。至此本節的適配器實現過程終於落下帷幕,一路上可謂是過五關斬六將,硬生生把數十行的Java代碼壓縮到不到十行的Kotlin代碼,經過不斷迭代優化方取得如此彪炳戰績。尤其是最後的兩種實現方式,分別運用了Kotlin的多項綜合技術,才能集Kotlin精妙語法之大成。

7.2使用材質設計

MaterialDesign庫提供了協調佈局CoordinatorLayout,AppBarLayout,CollapsingToolbarLayout

協調佈局CoordinatorLayout

繼承自ViewGroup
對齊方式:layout_gravity,
子視圖位置:app:layout_anchor?app:layout_anchorGravity
行爲:app:layout_behavior

FloatingActionButton 懸浮按鈕
懸浮按鈕會懸浮在其他視圖之上
隱藏和顯示懸浮按鈕時會播放切換動畫,hide和show方法
懸浮按鈕默認會隨着便籤條Snackbar的出現或消失動態調整位置
###工具欄Toolbar
Android 5.0之後使用Toolbar代替ActionBar
不過爲了兼容老版本,ActionBar仍然保留,可是ActionBar和Toolbar都佔着頂部導航欄的位置,所以想引入Toolbar就得關閉ActionBar,具體步驟如下:
1.定義一個style

<style name="AppCompatTheme" parent= "Theme.AppCompat.Light.NoActionBar" />

2.清單文件中的application標籤的

android:theme="@style/AppCompatTheme"

3.創建一個佈局文件

<android.support.v7.widget.Toolbar
android:id="@+id/tl_head"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
extends AppCompatActivity

5.onCreate中setSupportActionBar

Toolbar的常用方法

Toolbar比ActionBar靈活,主要便是它提供了多個方法來修改控件風格,下面是Toolbar的常用方法:
setLogo : 設置工具欄圖標。
setTitle : 設置標題文字。
setTitleTextAppearance : 設置標題的文字風格。
setTitleTextColor : 設置標題的文字顏色。
setSubtitle : 設置副標題文字。副標題在標題下方。
setSubtitleTextAppearance : 設置副標題的文字風格。
setSubtitleTextColor : 設置副標題的文字顏色。
setNavigationIcon : 設置導航圖標。導航圖標在工具欄圖標左邊。
setNavigationOnClickListener : 設置導航圖標的點擊監聽器。
setOverflowIcon : 設置溢出菜單的按鈕圖標。
showOverflowMenu : 顯示溢出菜單圖標。
hideOverflowMenu : 隱藏溢出菜單圖標。
dismissPopupMenus : 關閉已彈出的菜單。

應用欄佈局AppBarLayout

AppBarLayout 繼承自LinearLayout,所以它具備LinearLayout的所有屬性方法

可摺疊工具欄佈局CollapsingToolbarLayout

image.png

7.3實現頁面切換

翻頁視圖ViewPager

image.png

Fragment

Tablayout

image.png

廣播Broadcast

###收發臨時廣播
1.一人發送廣播,多人接收處理
2.對於發送者來說,不需要考慮接收者
3.對於接收者來說,要自行過濾符合條件的廣播
sendBroadcast,registerReceiver,unregisterReceiver

靜態屬性如果是個常量,就還要添加修飾符const

companion object {
//靜態屬性如果是個常量,就還要添加修飾符const
const val EVENT = "com.example.complex.fragment.BroadcastFragment"
}

編譯時常量(const):真正意義的常量
運行時常量(val):可以在聲明的時候賦值,運行後被賦值

系統廣播

靜態註冊和動態註冊

總結

視圖排列:下拉框,列表視圖,網格視圖,循環視圖,適配器的延遲初始化屬性
材質設計:狀態欄,工具欄
頁面切換:ViewPager,fragment,tablayout
廣播,兩種常量

第八章 Kotlin進行數據存儲

8.1使用共享參數SharedPreferences

共享參數讀寫模板Preference

class Perference<T>(val context: Context, val name: String, val default: T)
    : ReadWriteProperty<Any?, T> {

    /***
     * 通過屬性代理初始化共享參數對象
     * lazy:第一次使用時執行初始化
     */
    val prefs: SharedPreferences by lazy { context.getSharedPreferences("default", Context.MODE_PRIVATE) }

    /***
     * 接管屬性值的獲取行爲
     * *:表示一個不確定的類型
     */
    override fun getValue(thisRef: Any?, property: KProperty<*>): T {
        return findPreference(name, default)
    }


    /***
     * 接管屬性值的修改行爲
     */
    override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        putPreference(name, value)
    }

    //利用with函數定義臨時的命名空間
    private fun <T> findPreference(name: String, default: T): T = with(prefs) {
        val res: Any = when (default) {
            is Long -> getLong(name, default)
            is String -> getString(name, default)
            is Int -> getInt(name, default)
            is Boolean -> getBoolean(name, default)
            is Float -> getFloat(name, default)
            else -> throw IllegalArgumentException("This type can be saved into Preferences")
        }
        return res as T
    }

    private fun <T> putPreference(name: String, value: T) = with(prefs.edit()) {
        //putInt、putString等方法返回Editor對象
        when (value) {
            is Long -> putLong(name, value)
            is String -> putString(name, value)
            is Int -> putInt(name, value)
            is Boolean -> putBoolean(name, value)
            is Float -> putFloat(name, value)
            else -> throw IllegalArgumentException("This type can be saved into Preferences")
        }.apply() //commit方法和apply方法都表示提交修改
    }

}

調用:

private var name: String by Perference(this, "name", "")
private var age: String by Perference(this, "age", "")

後續代碼若給委託屬性賦值,則立即觸發寫入動作

屬性代理等黑科技

這個高大上的Preference用了哪些黑科技?
用到了模板類,委託屬性,lazy修飾符,with函數

模板類:

因爲共享參數允許保存的類型包括,整型,浮點型,字符串等,所以要定義成模板類,參數類型在調用時再指定
除了T,還有Any和*

1、T是抽象的泛型,在模板類中用來佔位子,外部調用模板類時才能確定T的具體類型;
2、Any是Kotlin的基本類型,所有Kotlin類都從Any派生而來,故而它相當於Java裏面的Object;
3、星號表示一個不確定的類型,同樣也是在外部調用時才能確定,這點跟T比較像,但T出現在模板類的定義中,而與模板類無關,它出現在單個函數定義的參數列表中,因此星號相當於Java裏面的問號?;

委託屬性/屬性代理

by表示代理動作,第五章的例子是接口代理或稱類代理,而這裏則爲屬性代理,所謂屬性代理,是說該屬性的類型不變,但是屬性的讀寫行爲被後面的類接管了。類似銀行推出了“委託代扣”的業務,只要用戶跟銀行簽約並指定委託扣費的電力賬戶,那麼在每個月指定時間,銀行會自動從用戶銀行卡中扣費並繳納給指定的電力賬戶,如此省卻了用戶的人工操作。
現實生活中的委託扣費場景,對應到共享參數這裏,開發者的人工操作指的是手工編碼從SharedPreferences類讀取數據和保存數據,而自動操作指的是約定代理的屬性自動通過模板類Preference完成數據的讀取和保存,也就是說,Preference接管了這些屬性的讀寫行爲,接管後的操作則是模板類的getValue和setValue方法。屬性被接管的行爲叫做屬性代理,而被代理的屬性稱作委託屬性。

lazy修飾符

模板類Preference聲明瞭一個共享參數的prefs對象,其中用到了關鍵字lazy,lazy的意思是懶惰,表示只在該屬性第一次使用時執行初始化。聯想到Kotlin還有類似的關鍵字名叫lateinit,意思是延遲初始化,加上lazy可以歸納出Kotlin變量的三種初始化操作,具體說明如下:
1、聲明時賦值:這是最常見的變量初始化,在聲明某個變量時,立即在後面通過等號“=”給它賦予具體的值。
2、lateinit延遲初始化:變量聲明時沒有馬上賦值,但該變量仍是個非空變量,何時初始化由開發者編碼決定。
3、lazy首次使用時初始化:聲明變量時指定初始化動作,但該動作要等到變量第一次使用時才進行初始化。
此處的prefs對象使用lazy規定了屬性值在首次使用時初始化,且初始化動作通過by後面的表達式來指定,即“{ context.getSharedPreferences(“default”, Context.MODE_PRIVATE) }”。連同大括號在內的這個表達式,其實是個匿名實例,它內部定義了prefs對象的初始化語句,並返回SharedPreferences類型的變量值。

with函數

with函數的書寫格式形如“with(函數頭語句) { 函數體語句 }”,看這架勢,with方法的函數語句分爲兩部分,詳述如下:
1、函數頭語句:頭部語句位於緊跟with的圓括號內部。它先於函數體語句執行,並且頭部語句返回一個對象,函數體語句在該對象的命名空間中運行;即體語句可以直接調用該對象的方法,而無需顯式指定該對象的實例名稱。
2、函數體語句:體語句位於常規的大括號內部。它要等頭部語句執行完畢纔會執行,同時體語句在頭部語句返回對象的命名空間中運行;即體語句允許直接調用頭部對象的方法,而無需顯式指定該對象的實例名稱。
綜上所述,在模板類Preference的編碼過程中,聯合運用了Kotlin的多項黑科技,方纔實現了優於Java的共享參數操作方式

實現記住密碼的功能

private var phone: String by Preference(this, "phone", "")
private var password: String by Preference(this, "password", "")

由於上面的語句已經自動從共享參數獲取屬性值,接下來若要往共享參數保存新的屬性值,只需修改委託屬性的變量值即可。

8.2使用數據庫SQLite

數據庫幫助器SQLiteOpenHelper

SQLiteDatabase(數據庫管理類):獲取數據庫實例
SQLiteOpenHelper:操作數據表的API

更安全的ManagedSQLiteOpenHelper

SQLiteOpenHelper開發者需要在操作表之前打開數據庫連接,結束後關閉數據庫連接
於是Kotlin結合anko庫推出了改良版的SQLite管理工具:ManagedSQLiteOpenHelper

use {
        //1、插入記錄
        //insert(...)
        //2、更新記錄
        //update(...)
        //3、刪除記錄
        //delete(...)
        //4、查詢記錄
        //query(...)或者rawQuery(...)
    }
class UserDBHelper(var context: Context, private var DB_VERSION: Int=CURRENT_VERSION) : ManagedSQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) {
    companion object {
        private val TAG = "UserDBHelper"
        var DB_NAME = "user.db" //數據庫名稱
        var TABLE_NAME = "user_info" //表名稱
        var CURRENT_VERSION = 1 //當前的最新版本,如有表結構變更,該版本號要加一
        private var instance: UserDBHelper? = null
        @Synchronized
        fun getInstance(ctx: Context, version: Int=0): UserDBHelper {
            if (instance == null) {
                //如果調用時沒傳版本號,就使用默認的最新版本號
                instance = if (version>0) UserDBHelper(ctx.applicationContext, version)
                            else UserDBHelper(ctx.applicationContext)
            }
            return instance!!
        }
    }
 
    override fun onCreate(db: SQLiteDatabase) {
        Log.d(TAG, "onCreate")
        val drop_sql = "DROP TABLE IF EXISTS $TABLE_NAME;"
        Log.d(TAG, "drop_sql:" + drop_sql)
        db.execSQL(drop_sql)
        val create_sql = "CREATE TABLE IF NOT EXISTS $TABLE_NAME (" +
            "_id INTEGER PRIMARY KEY  AUTOINCREMENT NOT NULL," +
            "name VARCHAR NOT NULL," + "age INTEGER NOT NULL," +
            "height LONG NOT NULL," + "weight FLOAT NOT NULL," +
            "married INTEGER NOT NULL," + "update_time VARCHAR NOT NULL" +
            //演示數據庫升級時要先把下面這行註釋
            ",phone VARCHAR" + ",password VARCHAR" + ");"
        Log.d(TAG, "create_sql:" + create_sql)
        db.execSQL(create_sql)
    }
 
    override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
        Log.d(TAG, "onUpgrade oldVersion=$oldVersion, newVersion=$newVersion")
        if (newVersion > 1) {
            //Android的ALTER命令不支持一次添加多列,只能分多次添加
            var alter_sql = "ALTER TABLE $TABLE_NAME ADD COLUMN phone VARCHAR;"
            Log.d(TAG, "alter_sql:" + alter_sql)
            db.execSQL(alter_sql)
            alter_sql = "ALTER TABLE $TABLE_NAME ADD COLUMN password VARCHAR;"
            Log.d(TAG, "alter_sql:" + alter_sql)
            db.execSQL(alter_sql)
        }
    }
 
    fun delete(condition: String): Int {
        var count = 0
        use {
            count = delete(TABLE_NAME, condition, null)
        }
        return count
    }
 
    fun insert(info: UserInfo): Long {
        val infoArray = mutableListOf(info)
        return insert(infoArray)
    }
 
    fun insert(infoArray: MutableList<UserInfo>): Long {
        var result: Long = -1
        for (i in infoArray.indices) {
            val info = infoArray[i]
            var tempArray: List<UserInfo>
            // 如果存在同名記錄,則更新記錄
            // 注意條件語句的等號後面要用單引號括起來
            if (info.name.isNotEmpty()) {
                val condition = "name='${info.name}'"
                tempArray = query(condition)
                if (tempArray.size > 0) {
                    update(info, condition)
                    result = tempArray[0].rowid
                    continue
                }
            }
            // 如果存在同樣的手機號碼,則更新記錄
            if (info.phone.isNotEmpty()) {
                val condition = "phone='${info.phone}'"
                tempArray = query(condition)
                if (tempArray.size > 0) {
                    update(info, condition)
                    result = tempArray[0].rowid
                    continue
                }
            }
            // 不存在唯一性重複的記錄,則插入新記錄
            val cv = ContentValues()
            cv.put("name", info.name)
            cv.put("age", info.age)
            cv.put("height", info.height)
            cv.put("weight", info.weight)
            cv.put("married", info.married)
            cv.put("update_time", info.update_time)
            cv.put("phone", info.phone)
            cv.put("password", info.password)
            use {
                result = insert(TABLE_NAME, "", cv)
            }
            // 添加成功後返回行號,失敗後返回-1
            if (result == -1L) {
                return result
            }
        }
        return result
    }
 
    @JvmOverloads
    fun update(info: UserInfo, condition: String = "rowid=${info.rowid}"): Int {
        val cv = ContentValues()
        cv.put("name", info.name)
        cv.put("age", info.age)
        cv.put("height", info.height)
        cv.put("weight", info.weight)
        cv.put("married", info.married)
        cv.put("update_time", info.update_time)
        cv.put("phone", info.phone)
        cv.put("password", info.password)
        var count = 0
        use {
            count = update(TABLE_NAME, cv, condition, null)
        }
        return count
    }
 
    fun query(condition: String): List<UserInfo> {
        val sql = "select rowid,_id,name,age,height,weight,married,update_time,phone,password from $TABLE_NAME where $condition;"
        Log.d(TAG, "query sql: " + sql)
        var infoArray = mutableListOf<UserInfo>()
        use {
            val cursor = rawQuery(sql, null)
            if (cursor.moveToFirst()) {
                while (true) {
                    val info = UserInfo()
                    info.rowid = cursor.getLong(0)
                    info.xuhao = cursor.getInt(1)
                    info.name = cursor.getString(2)
                    info.age = cursor.getInt(3)
                    info.height = cursor.getLong(4)
                    info.weight = cursor.getFloat(5)
                    //SQLite沒有布爾型,用0表示false,用1表示true
                    info.married = if (cursor.getInt(6) == 0) false else true
                    info.update_time = cursor.getString(7)
                    info.phone = cursor.getString(8)
                    info.password = cursor.getString(9)
                    infoArray.add(info)
                    if (cursor.isLast) {
                        break
                    }
                    cursor.moveToNext()
                }
            }
            cursor.close()
        }
        return infoArray
    }
 
    fun queryByPhone(phone: String): UserInfo {
        val infoArray = query("phone='$phone'")
        val info: UserInfo = if (infoArray.size>0) infoArray[0] else UserInfo()
        return info
    }
 
    fun deleteAll(): Int = delete("1=1")
 
    fun queryAll(): List<UserInfo> = query("1=1")
 
}

記得加:

compile "org.jetbrains.anko:anko-sqlite:$anko_version"

調用:

var helper: UserDBHelper = UserDBHelper.getInstance(this)
val userArray = helper.queryAll()

優化記住密碼功能

共享參數實現了記住密碼的功能,但是隻能記住一個賬號的。可以採用數據庫

8.3 文件I/O操作

###文件保存空間
手機的存儲空間分爲內部存儲和外部存儲
內部存儲放的是手機系統以及各應用的安裝目錄
外部存儲放的是公共文件,如圖片,視頻,文檔等
爲了不影響系統的流暢運行,App運行過程中要處理的文件都保存在外部存儲空間
爲保證App正常讀寫外部存儲,需要在AndroidManifest中增加權限

<!-- SD讀寫權限 -->
<uses-
permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-
permission android:name="android.permission.READ_EXTERNAL_STORAG" />
<uses-
permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"
/>

Android7.0加強了SD卡的權限管理,系統默認關閉了外部存儲的公共空間,外部存儲的私有空間依然可以正常讀寫
因爲Android把外部存儲分成了2塊區域,一塊是所有應用都能訪問的公共空間,一塊是隻有應用自己纔可以訪問的專享空間。當然,您的應用被卸載,那麼這塊專享空間文件目錄也就沒有了
獲取公共空間的存儲路徑:Environment.getExternalStoragePublicDirectory
獲取應用私有空間的存儲路徑:getExternalFilesDir

//公共路徑
val publicPath = Environment.getExternalStoragePublicDirectory
(Environment.DIRECTORY_DOWNLOADS).toString();
//私有路徑
val privatePath = getExternalFilesDir(Environment. DIRECTORY_DO
WNLOADS).toString();
tv_file_path.text = "{publicPath}" ,${privatePath}" 
"Android7.0之後默認禁止訪問公共存儲目錄

image.png

讀寫文本文件

Kotlin利用拓展函數功能添加了一些常用的文件內容讀寫方法,一行代碼搞定問題

//比如文本寫入文件
File(file_path).writeText(content)

若要往源文件追加文本,則可調用appendText方法

readText:讀取文本形式的文件內容
readLines:按行讀取文件內容
val content = File(file_path).readText()

讀寫圖片文件

寫入

fun saveImage(path: String, bitmap: Bitmap) {
try {
val file = File(path)
val fos: OutputStream = file.outputStream()
//壓縮格式爲JPEG,壓縮質量爲80%
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, fos)
fos.flush()
fos.close()
} catch (e: Exception) {
e.printStackTrace()
}
}

讀取:

//方式一
val bytes = File(file_path).readBytes()
val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)

//方式二
val fis = File(file_path).inputStream()
val bitmap = BitmapFactory.decodeStream(fis)
fis.close()

//方式三
val bitmap = BitmapFactory.decodeFile(file_path)

遍歷文件目錄

Kotlin新概念:文件樹

var fileNames: MutableList<String> = mutableListOf()
//在該目錄下走一圈,得到文件目錄樹結構
val fileTree: FileTreeWalk = File(mPath).walk()
fileTree.maxDepth(1) //需遍歷的目錄層級爲1,即無需檢查子目錄
.filter { it.isFile } //只挑選文件,不處理文件夾
.filter { it.extension == "txt" } //選擇拓展名爲txt的文本文件
.forEach { fileNames.add(it.name) } //循環處理符合條件的文件

改進後:

var fileNames: MutableList<String> = mutableListOf()
//在該目錄下走一圈,得到文件目錄樹結構
val fileTree: FileTreeWalk = File(mPath).walk()
fileTree.maxDepth(1) //需遍歷的目錄層級爲1,即無需檢查子目錄
.filter { it.isFile } //只挑選文件,不處理文件夾
.filter { it.extension in listOf("png","jpg") } //選擇拓展名爲png和jpg的圖片文件
.forEach { fileNames.add(it.name) } //循環處理符合條件的文件

8.4 Application全局變量
方式一:

class MainApplication : Application() {
override fun onCreate() {
super.onCreate()
instance = this
}
//單例化的第一種方式:聲明一個簡單的Application屬性
companion object {
//聲明可空屬性
private var instance: MainApplication? = null
fun instance() = instance!!
//聲明延遲初始化屬性
//private lateinit var instance: MainApplication
//fun instance() = instance
}
}

方式二

class MainApplication : Application() {
override fun onCreate() {
super.onCreate()
instance = this
}
//單例化的第二種方式:利用系統自帶的Delegates生成委託屬性
companion object {
private var instance: MainApplication by Delegates.notNull()
fun instance() = instance
}
}

前兩種單例都只完成了非空校驗,還不是嚴格意義上的單例化,真正的單例化有且僅有一次的賦值操作,儘管前兩種單例化並未實現唯一賦值功能,不多大多數場合已經夠用了。
那麼怎實現唯一賦值的的單例化?需要開發者自己寫一個校驗賦值次數的行爲類

class MainApplication : Application() {
override fun onCreate() {
super.onCreate()
instance = this
}
//單例化的第三種方式:自定義一個非空且只能一次性賦值的委託屬性
companion object {
private var instance: MainApplication by NotNullSingleValueVar(
)
fun instance() = instance
}
//定義一個屬性管理類,進行非空和重複賦值的判斷
private class NotNullSingleValueVar<T>
() : ReadWriteProperty<Any?, T> {
private var value: T? = null
override fun getValue(thisRef: Any?, property: KProperty<*>): T
{
return value ?: throw IllegalStateException("application no
t initialized")
}
override fun setValue(thisRef: Any?, property: KProperty<*>, va
lue: T) {
this.value = if (this.value == null) value
else throw IllegalStateException("application already initi
alized")
}
}
}

上述代碼,自定義的代理行爲在getValue方法中進行非空校驗,在setValue方法中進行重複賦值的校驗,按照要求接管了委託屬性的讀寫行爲。

利用Application實現全局變量

適合在Application中保持的全局變量主要有一下幾類數據
1.頻繁讀取的信息,如用戶名,手機號等
2.網絡上獲取的臨時數據,節約流量也爲了減少用戶的等待時間,暫時放在內存中供下次使用,例如logo,圖片
3.容易因頻繁分配內存而導致內存泄漏的對象,例如Handle線程池ThreadPool等

總結

1.利用工具類Preference進行共享參數的鍵值對管理工作,並掌握委託屬性,lazy修飾符,with函數的基本用法
2.使用Kotlin的ManagedSQLiteOpenHelper管理數據庫
3.使用Kotlin的文件I/O函數進行文件處理,包括文本文件讀寫,圖片文件讀寫,文件目錄遍歷等
4.Kotlin實現Application單例化,通過Application操作全局變量

第九章 Kotlin自定義控件

9.1自定義普通視圖

構造對象

自定義屬性的步驟:

  1. 在res\values目錄下創建attrs.xml,文件內容如下所示,其中declare-styleable的name屬性值表示新視圖的名稱,兩個attr節點表示新增的兩個屬性分別是textColor和textSize:
<resources>
    <declare-styleable name="CustomPagerTab">
        <attr name="textColor" format="color" />
        <attr name="textSize" format="dimension" />
    </declare-styleable>
</resources>
  1. 在模塊的widget目錄下創建CustomPagerTab.java,填入以下自定義視圖的代碼:
public class CustomPagerTab extends PagerTabStrip {
    private int textColor = Color.BLACK;
    private int textSize = 15;
 
    public CustomPagerTab(Context context) {
        super(context);
    }
    
    public CustomPagerTab(Context context, AttributeSet attrs) {
        super(context, attrs);
        //構造函數從attrs.xml讀取CustomPagerTab的自定義屬性
        if (attrs != null) {
            TypedArray attrArray=getContext().obtainStyledAttributes(attrs, R.styleable.CustomPagerTab);
            textColor = attrArray.getColor(R.styleable.CustomPagerTab_textColor, textColor);
            textSize = attrArray.getDimensionPixelSize(R.styleable.CustomPagerTab_textSize, textSize);
            attrArray.recycle();
        }
        setTextColor(textColor);
        setTextSize(TypedValue.COMPLEX_UNIT_SP, textSize);
    }
    
//    //PagerTabStrip沒有三個參數的構造函數
//    public PagerTab(Context context, AttributeSet attrs, int defStyleAttr) {
//    }
}


  1. 佈局文件的根節點增加自定義的命名空間聲明,如“xmlns:app=“http://schemas.android.com/apk/res-auto””;並把android.support.v4.view.PagerTabStrip的節點名稱改爲自定義視圖的全路徑名稱如“com.example.custom.widget.PagerTab”,同時在該節點下指定新增的兩個屬性即app:textColor與app:textSize。修改之後的佈局文件代碼如下:
<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"
    android:padding="10dp" >
 
    <android.support.v4.view.ViewPager
        android:id="@+id/vp_content"
        android:layout_width="match_parent"
        android:layout_height="400dp" >
 
        <com.example.custom.widget.CustomPagerTab
            android:id="@+id/pts_tab"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:textColor="@color/red"
            app:textSize="17sp" />
    </android.support.v4.view.ViewPager>
</LinearLayout>

上述自定義屬性的三個步驟,其中第二步驟涉及到Java代碼,接下來用Kotlin改寫CustomPagerTab類的代碼,主要改動有以下兩點:

1、原來的兩個構造函數,合併爲帶默認參數的一個主構造函數,並且直接跟在類名後面;
2、類名後面要加上註解“@JvmOverloads constructor”,表示該類支持被Java代碼調用。因爲佈局文件中引用了自定義視圖的節點,系統是通過SDK裏的Java代碼找到自定義視圖類,所以凡是自定義視圖都要加上該註解,否則App運行時會拋出異常。
下面是CustomPagerTab類改寫之後的Kotlin代碼:

//自定義視圖務必要在類名後面增加“@JvmOverloads constructor”,因爲佈局文件中的自定義視圖必須兼容Java
class CustomPagerTab @JvmOverloads constructor(context: Context, attrs: AttributeSet?=null) : PagerTabStrip(context, attrs) {
    private var txtColor = Color.BLACK
    private var textSize = 15
    
    init {
        txtColor = Color.BLACK
        textSize = 15
        //初始化時從attrs.xml讀取CustomPagerTab的自定義屬性
        if (attrs != null) {
            val attrArray = getContext().obtainStyledAttributes(attrs, R.styleable.CustomPagerTab)
            txtColor = attrArray.getColor(R.styleable.CustomPagerTab_textColor, txtColor)
            textSize = attrArray.getDimensionPixelSize(R.styleable.CustomPagerTab_textSize, textSize)
            attrArray.recycle()
        }
        setTextColor(txtColor)
        setTextSize(TypedValue.COMPLEX_UNIT_SP, textSize.toFloat())
    }
}

測量尺寸

繪製

完整的自定義視圖有3部分組成:
1.定義構造函數,讀取自定義屬性值並初始化
2.重寫測量函數onMesure,計算該視圖的寬高尺寸
3.重寫繪圖函數onDraw(控件+佈局)或者dispatchDraw(佈局),在當前視圖內部繪製指定形狀

public class RoundTextView extends TextView {
public RoundTextView(Context context) {
super(context);
}
public RoundTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public RoundTextView(Context context, AttributeSet attrs, int defSt
yle) {
super(context, attrs, defStyle);
}
//控件只能重寫onDraw方法
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint paint = new Paint();
paint.setColor(Color.RED); 
paint.setStrokeWidth(2); 
paint.setStyle(Style.STROKE); 
paint.setAntiAlias(true);
RectF rectF = new RectF(1, 1, this.getWidth()-1, this.getHeight()-1);

canvas.drawRoundRect(rectF, 10, 10, paint);
}
}

//“@JvmOverloads constructor”
class RoundTextView @JvmOverloads constructor(context: Context, attrs:
AttributeSet?
=null, defStyle: Int=0) : TextView(context, attrs, defStyle) {

override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)

val paint = Paint()
paint.color = Color.RED 
paint.strokeWidth = 2f 
paint.style = Style.STROKE 
paint.isAntiAlias = true 
val rectF = RectF(1f, 1f, (this.width - 1).toFloat(), (this.hei
ght - 1). toFloat())
canvas.drawRoundRect(rectF, 10f, 10f, paint)
}
}

public class RoundLayout extends LinearLayout {
public RoundLayout(Context context) {
super(context);
}
public RoundLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public RoundLayout(Context context, AttributeSet attrs, int defStyl
e) {
super(context, attrs, defStyle);
}
//佈局一般重寫dispatchDraw方法,防止繪圖效果 被上面的控件覆蓋
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
Paint paint = new Paint();
paint.setColor(Color.BLUE);
paint.setStrokeWidth(2);
paint.setStyle(Style.STROKE);
paint.setAntiAlias(true);
RectF rectF = new RectF(1, 1, this.getWidth()-1, this.getHeight
()-1);
canvas.drawRoundRect(rectF, 10, 10, paint);
}
}

用Kotlin改寫後:

//自定義視圖要在類名後增加“@JvmOverloads constructor”
class RoundLayout @JvmOverloads constructor(context: Context, attrs: At
tributeSet?
=null, defStyle: Int=0) : LinearLayout(context, attrs, defStyle) {
override fun dispatchDraw(canvas: Canvas) {
super.dispatchDraw(canvas)
val paint = Paint()
paint.color = Color.BLUE
paint.strokeWidth = 2f 
paint.style = Style.STROKE
paint.isAntiAlias = true 
val rectF = RectF(1f, 1f, (this.width - 1).toFloat(), (this.height - 1).toFloat())
canvas.drawRoundRect(rectF, 10f, 10f, paint)
}
}

9.3自定義通知欄

Notification

image.png
image.png
三種特殊的通知類型:
1.進度通知 setProgress
2.浮動通知 setFullScreenIntent
3.鎖屏通知setVisibility
image.png

遠程視圖RemoteView

消息通知自定義佈局,需要藉助RemoteView實現
Notification.Builder的setContent
image.png

9.4 Service服務

普通啓動服務

startService<NormalService>()
//帶參數
startService<NormalService>
("request_content" to et_request.text.toString())


val intent = intentFor<NormalService>
("request_content" to et_request.text.toString())
startService(intent)

val intent = intentFor<NormalService>
(Pair("request_content", et_request.text.toString()))
startService(intent)

綁定方式啓動服務

val bindFlag = bindService(intentBind, mFirstConn, Context.BIND_AUTO_CREATE)
//解除綁定
if (mBindService != null) {
unbindService(mFirstConn)
mBindService = null
}

推送服務到前臺

不要讓服務依附於任何頁面,而Android允許服務以某種形式出現在屏幕上,那就是通知欄。
startForeground:服務切到前臺
stopForeground:停止前臺運行(true表示清除通知,false不清除)

<!-- 震動權限-->
<uses-permission android:name="android.permission.VIBRATE" />
val vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
vibrator.vibrate(3000)

其他服務管理器:

//下載管理器
val Context.downloader: DownloadManager
get() = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
//定位管理器
val Context.locator: LocationManager
get() = getSystemService(Context.LOCATION_SERVICE) as LocationManager
//連接管理器
val Context.connector: ConnectivityManager
get() = getSystemService(Context.CONNECTIVITY_SERVICE) as Connectiv
ityManager
//電話管理器
val Context.telephone: TelephonyManager
get() = getSystemService(Context.TELEPHONY_SERVICE) as TelephonyMan
ager
//無線管理器
val Context.wifi: WifiManager
get() = getSystemService(Context.WIFI_SERVICE) as WifiManager
//鬧鐘管理器
val Context.alarm: AlarmManager
get() = getSystemService(Context.ALARM_SERVICE) as AlarmManager
//音頻管理器
val Context.audio: AudioManager
get() = getSystemService(Context.AUDIO_SERVICE) as AudioManager

總結

1.自定義視圖
2.消息通知
3.Service
4.各種系統管理

第十章 Kotlin實現網絡通信

10.1多線程

Thread {
//具體業務
}.start()

水平進度對話框

val dialog = progressDialog("正在加載中", "請稍後")
dialog.show()

圓圈進度對話框

val dialog = indeterminateProgressDialog("正在加載中", "請稍後")
dialog.show()

異步任務

doAsync {
//業務代碼
uiThread { 
//回到主線程
 }
}

有時候,APP會啓動多個分線程,然後對這些線程對象進行調度,動態控制 線程的運行狀態,那麼就用上doAsyncResult了

//doAsyncResult返回一個異步對象
val future : Future<String> = doAsyncResult(null, longTask)

10.2訪問HTTP接口

移動數據格式JSON

普通解析:Kotlin和Java代碼差不多
image.png
可以利用data數據類+Gson處理

HTTP接口調用

doAsync{
//網絡請求
uiThread{
//更新數據
}
}

10.3文件下載操作

DownloadManager
image.png
image.png
image.png
Android7.0增強了文件 訪問權限,造成字段DownloadManager.COLUMN_LOCAL_FILENAME被廢棄,所以7.0以上版本訪問該字段會拋出java.lang.SecurityException異常,此時獲取下載任務對應的文件路徑可以通過DownloadManager.COLUMN_LOCAL_URI獲得。
image.png

10.4 ContentProvider

內容提供者 ContentProvider

ContentProvider爲app存取內部數據提供了統一的外部接口,讓不同的應用之間共享數據

內容解析器:ContentResolver

內容觀察器:ContentObserver

SpannableString

SpannableString可以對文本樣式分段處理

val str: Spanned = buildSpanned {
append("爲", Bold) //文字字體使用粗體
append("人民", RelativeSizeSpan(1.5f)) //文字大小
append("服務", foregroundColor(Color.RED)) //文字顏色

}

常見的anko簡寫:
image.png

完。

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