《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

完。

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