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精妙语法之大成。
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
翻页视图ViewPager
Fragment
略
Tablayout
###收发临时广播
1.一人发送广播,多人接收处理
2.对于发送者来说,不需要考虑接收者
3.对于接收者来说,要自行过滤符合条件的广播
sendBroadcast,registerReceiver,unregisterReceiver
静态属性如果是个常量,就还要添加修饰符const
companion object {
//静态属性如果是个常量,就还要添加修饰符const
const val EVENT = "com.example.complex.fragment.BroadcastFragment"
}
编译时常量(const):真正意义的常量
运行时常量(val):可以在声明的时候赋值,运行后被赋值
系统广播
静态注册和动态注册
视图排列:下拉框,列表视图,网格视图,循环视图,适配器的延迟初始化属性
材质设计:状态栏,工具栏
页面切换:ViewPager,fragment,tablayout
广播,两种常量
==============================================================================
共享参数读写模板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", "")
由于上面的语句已经自动从共享参数获取属性值,接下来若要往共享参数保存新的属性值,只需修改委托属性的变量值即可。
数据库帮助器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()
优化记住密码功能
共享参数实现了记住密码的功能,但是只能记住一个账号的。可以采用数据库
###文件保存空间
手机的存储空间分为内部存储和外部存储
内部存储放的是手机系统以及各应用的安装目录
外部存储放的是公共文件,如图片,视频,文档等
为了不影响系统的流畅运行,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之后默认禁止访问公共存储目录
读写文本文件
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操作全局变量
=============================================================================
构造对象
自定义属性的步骤:
- 在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>
- 在模块的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) {
// }
}
- 布局文件的根节点增加自定义的命名空间声明,如“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)
}
}
Notification
三种特殊的通知类型:
1.进度通知 setProgress
2.浮动通知 setFullScreenIntent
3.锁屏通知setVisibility
远程视图RemoteView
消息通知自定义布局,需要借助RemoteView实现
Notification.Builder的setContent
普通启动服务
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.各种系统管理
==============================================================================
Thread {
//具体业务
}.start()
水平进度对话框
val dialog = progressDialog("正在加载中", "请稍后")
dialog.show()
圆圈进度对话框
val dialog = indeterminateProgressDialog("正在加载中", "请稍后")
dialog.show()
异步任务
最后说一下我的学习路线
其实很简单就下面这张图,含概了Android所有需要学的知识点,一共8大板块:
- 架构师筑基必备技能
- Android框架体系架构(高级UI+FrameWork源码)
- 360°Androidapp全方位性能调优
- 设计思想解读开源框架
- NDK模块开发
- 移动架构师专题项目实战环节
- 移动架构师不可不学习微信小程序
- 混合开发的flutter
Android学习的资料
我呢,把上面八大板块的分支都系统的做了一份学习系统的资料和视频,大概就下面这些,我就不全部写出来了,不然太长了影响大家的阅读。
330页PDF Android学习核心笔记(内含上面8大板块)
Android学习的系统对应视频
总结
我希望通过我自己的学习方法来帮助大家去提升技术:
-
1、多看书、看源码和做项目,平时多种总结
-
2、不能停留在一些基本api的使用上,应该往更深层次的方向去研究,比如activity、view的内部运行机制,比如Android内存优化,比如aidl,比如JNI等,并不仅仅停留在会用,而要通过阅读源码,理解其实现原理
-
3、同时对架构是有一定要求的,架构是抽象的,但是设计模式是具体的,所以一定要加强下设计模式的学习
-
4、android的方向也很多,高级UI,移动架构师,数据结构与算法和音视频FFMpeg解码,如果你对其中一项比较感兴趣,就大胆的进阶吧!
希望大家多多点赞,转发,评论加关注,你们的支持就是我继续下去的动力!加油!
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
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()
异步任务
### 最后说一下我的学习路线
**其实很简单就下面这张图,含概了Android所有需要学的知识点,一共8大板块:**
1. **架构师筑基必备技能**
2. **Android框架体系架构(高级UI+FrameWork源码)**
3. **360°Androidapp全方位性能调优**
4. **设计思想解读开源框架**
5. **NDK模块开发**
6. **移动架构师专题项目实战环节**
7. **移动架构师不可不学习微信小程序**
8. **混合开发的flutter**
[外链图片转存中...(img-jsryVg4J-1714526343540)]
**Android学习的资料**
我呢,把上面八大板块的分支都系统的做了一份学习系统的资料和视频,大概就下面这些,我就不全部写出来了,不然太长了影响大家的阅读。
**330页PDF Android学习核心笔记(内含上面8大板块)**
[外链图片转存中...(img-l0MtpxIM-1714526343540)]
**Android学习的系统对应视频**
# 总结
我希望通过我自己的学习方法来帮助大家去提升技术:
* 1、多看书、看源码和做项目,平时多种总结
* 2、不能停留在一些基本api的使用上,应该往更深层次的方向去研究,比如activity、view的内部运行机制,比如Android内存优化,比如aidl,比如JNI等,并不仅仅停留在会用,而要通过阅读源码,理解其实现原理
* 3、同时对架构是有一定要求的,架构是抽象的,但是设计模式是具体的,所以一定要加强下设计模式的学习
* 4、android的方向也很多,高级UI,移动架构师,数据结构与算法和音视频FFMpeg解码,如果你对其中一项比较感兴趣,就大胆的进阶吧!
> 希望大家多多点赞,转发,评论加关注,你们的支持就是我继续下去的动力!加油!
**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**
**[需要这份系统化学习资料的朋友,可以戳这里获取](https://bbs.youkuaiyun.com/topics/618156601)**
**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**