泛型
Kotlin 的泛型与 Java 一样,都是一种语法糖,即只在源代码中有泛型定义,到了class级别就被擦除了。 泛型(Generics)其实就是把类型参数化,真正的名字叫做类型参数,它的引入给强类型编程语言加入了更强的灵活性。
泛型的好处
- 增强代码的通用性
- 熟悉泛型有助于理解其他框架源码
- 增加代码的健壮性,避免运行时类型转换异常;
做一个有追求的青年
参考文章
协程概念
本质上,协程可以称为轻量级线程。协程在 CoroutineScope (协程作用域)的上下文中通过 launch、async 等协程构造器(coroutine builder)来启动。
1 | import kotlinx.coroutines.* |
输出结果
1 | Hello, |
协程作用域
作用域构建器
协程构造器
挂起函数
- 本文链接:https://rainmonth.github.io/posts/A220317.html
- 分析版本:Fresco 2.5
将自己的事情交给别人做,自己只要关注这件事情处理的记过就好了,这就是委托的过程。
属性委托通常是为了解决以下场景的问题
为了解决以上几种场景,Kotlin支持属性的委托,基本格式如下:
val/var 属性名:属性类型 by 表达式。在 by 后面的表达式是该 委托, 因为属性对应的
get()
(与set()
)会被委托给它的getValue()
与setValue()
方法。 属性的委托不必实现任何的接口,但是需要提供一个getValue()
函数(与setValue()
——对于 var 属性)。
委托实现的例子
1 | import kotlin.reflect.KProperty |
属性委托的要求:
thisRef
类型必须与属性所有者类型相同(或者是其超类型)(对于扩展属性的委托,必须与扩展属性所在类类型相同,或者是其超类型)注意:lambda表达式返回值总是返回函数体内部最后一行表达式的值
使用示例:var propertyName by lazy lambda
lazy()
是接受一个 lambda 并返回一个 Lazy <T>
实例的函数,返回的实例可以作为实现延迟属性的委托: 第一次调用 get()
会执行已传递给 lazy()
的 lambda 表达式并记录结果, 后续调用 get()
只是返回记录的结果。
lateinit 和 by lazy比较 (结果由 chatGPT提供)
1 | lateinit 和 by lazy 都是延迟初始化属性的方式,但它们有以下区别: |
主要实现是在Delegates.observable()中,例如:
1 | var test0: String by Delegates.observable("Randy") { property, oldValue, newValue -> println("${property.name}:$oldValue->$newValue") } |
输出:
1 | test0:Randy->Jack |
还可以通过Delegate.vetoable来防止属性被更改,例如:
1 | var test: String by Delegates.vetoable("ss") { property: KProperty<*>, oldValue: String, newValue: String -> |
当ss函数返回true是,表示允许修改test属性,反之则不允许修改
从1.4起,支持将一个属性的getter和setter方法委托给另一个属性处理,这种委托对顶层和类的属性(成员属性或扩展属性)都可用。通常这种委托方式在 向后兼容 处理是会非常有用。例如:
1 | var topPro3: String = "ss" |
利用映射来存储 属性的值,非常适用于JSON解析这种场景
1 | class TestUser(var map: Map<String, Any?>) { |
1 | fun example(computeFoo: () -> Foo) { |
上面的代码将局部属性 memoizedFoo 委托给了 computeFoo这个 lambda表达式,这样做的好处是 如果 if 条件不满足,memoizeFoo 根本就不会初始化
委托实现就是通过使用组合的方式来代替通过继承的方式来进行一些业务逻辑的视线。
为什么要这样做呢?继承的缺点:
- 无法通过继承的方式,重用多个类的代码,(当然可以通过接口实现)
- 通过继承,子类必须无条件就收父类的内容,没得选
- 父类继承而来的实现是静态的,不能动态改变,虽然可以通过重写做一些改善,但还不够灵活
所以,后面就提出用组合的方式来取代通过继承来实现代码的复用。
闲话不多扯了,来看看Kotlin 中类委托实现:
1 | interface Base { |
同上上面这种写法,Kotlin编译后会自动生成 Base中定义的方法,同时对象b会存储在 Derived中,看看编译后 decompile来的Java代码
1 | public interface Base { |
可以看到,Derived 中最终处理 print的都是 $$delegate_0这个属性,这个属性就是通过by 指定的由Kotlin自动生成的。
在Kotlin中, 如果属性至少一个访问器使用默认实现,那么Kotlin会自动提供幕后字段,用关键字field
表示,幕后字段主要用于自定义getter和setter中,并且只能在getter 和setter中访问。
对外表现为只读,对内表现为可读可写,我们将这个属性成为幕后属性。
参考文章:什么是幕后字段和幕后属性
只有一个单一抽象方法(但可以有多个非抽象的方法)的接口,这种借口可以直接用lambda函数来实现,从而使代码简洁
Kotlin 能够对一个类或接口扩展新功能而无需继承该类或者使用像装饰者这样的设计模式。 这通过叫做扩展的特殊声明完成。
声明一个扩展函数需用一个接收者类型也就是被扩展的类型来作为他的前缀。 下面代码为 MutableList<Int>
添加一个swap
函数:
1 | fun MutableList<Int>.swap(index1: Int, index2: Int) { |
扩展不能真正的修改他们所扩展的类。通过定义一个扩展,并没有在一个类中插入新成员, 只不过是可以通过该类型的变量用点表达式去调用这个新函数。
扩展函数是静态分发的,即他们不是根据接收者类型的虚方法。 调用的扩展函数是由函数调用所在的表达式的类型来决定的, 而不是由表达式运行时求值结果决定的。例如
1 | /** |
可以为可空的接收者定义扩展,这样的扩展可以在对象变量上调用(即使为空也可以调用),例如:
1 | fun main3() { |
输出结果:
1 | null |
可以为List扩展一个 lastIndex 属性,用来表示最后一个元素的下标
1 | val <T> List<T>.lastIndex: Int |
注意:
- 扩展属性不能有初始化器
- 要注意扩展属性定义的位置(大多数情况下都直接定义在包里)
对象声明
1 | object Singleton { |
伴生对象
1 | class MyClass { |
可以直接用伴生对象所在的类的类名访问伴生对象的扩展方法和扩展属性
1 | class MyCompanion { |
扩展声明为成员
这个好像比较复杂
1 | /** |
生成的.class文件如下:
1 | public final data class User public constructor(id: kotlin.Int, name: kotlin.String) { |
1 | sealed class Error |
密封类用来表示受限的类继承结构:当一个值为有限几种的类型、而不能有任何其他类型时。在某种意义上,他们是枚举类的扩展:枚举类型的值集也是受限的,但每个枚举常量只存在一个实例,而密封类的一个子类可以有可包含状态的多个实例。
密封类作为when的判断条件时,是不需要再最后写else判断的,因为密封类的值集合是受限的
普通类中定义的一个无特殊修饰关键字的类就成为嵌套类、嵌套接口
1 | class Outer { |
嵌套接口
1 | interface OuterInterface { |
类内部定义一个用 inner关键字修饰的类,这个inner关键字修饰的类就成为内部类,同Java一样,内部类会持有外部类的引用
1 | class Outer { |
匿名内部类通常用对象表达式表示
1 | window.addMouseListener(object : MouseAdapter() { |
this
来访问这个上下文对象this
来访问这个对象,同时将lambda的结果返回takeIf
和takeUnless
作为作用域函数的补充,这两个函数配合作用域函数使得可以作用域函数调用链中进行值的检查与筛选
takeIf,如果满足给定的条件,就返回对象本身,否则返回null
takeUnless,和takeIf相反,如果不满足,就返回对象本身,否则返回null
==
运算符,并调用 equals()
来确定两个实例是否相等。===
运算符,以检查两个引用是否指向同一对象。?:,elvis 表达式,例子
1 | var firstName : String? |
假如定义了以下数据类:
1 | data class User(var firstName: String? = null, var lastName: String?) |
那么,主构造函数中的 firstName
参数就是默认参数,具体使用例子如下:
1 | var user1 = User("randy")// 这种写法是错误,由于默认参数将居于未设默认值的参数之前,使用时必须 |
Android 软键盘相关知识点梳理
先说说开发过程中遇到的几个常用的软键盘场景
场景一
屏幕存在多个输入框,点击输入框,软键盘不遮挡输入框
使用adjustPan有效,使用adjustResize无效(找出无效的原因,搜索到的结果都是说改这个)
场景二
WebView界面存在输入框,点击输入框,软键盘不遮挡输入框
场景三
点击非输入框部分,隐藏软键盘
分两步:
- 判断touch的是不是输入框View对应的区域;
- 重写Activity的dispatchTouchEvent方法,触摸的不是输入框对应View的时候,隐藏软键盘;
场景四
进入带输入框的页面,自动弹出软键盘
Android 软键盘的控制主要通过在AndroidManifest.xml文件声明activity时添加 android:windowSoftInputMode属性或者在代码中在Activity
的onCreate
函数调用setContentView
之前通过修改Window
的getWindow().setSoftInputMode(int mode)
方法来控制,代码示例如下:
AndroidManifest.xml文件中
1 | <activity android:name=".module.login.LoginActivity" |
代码中
1 | @Override |
注意:在AndroidMainfest.xml
文件中在此Activity中写入 android:windowSoftInputMode="adjustPan"
可以让界面不被弹出的键盘挤上去;
不管是通过xml文件还是通过代码,效果都是一样的,这个属性控制着软键盘和Activity交互的两种状态:
state
开头的属性值。adjust
开头的属性值。参考文章:
运行老项目,提示No cloud project ID was found by the Analytics SDK
Edit->Project Settings,左侧选择 Service, 然后选择自己的证书,然后创建一个ProjectId 即可。
Unity通过摄像机来实现将三维游戏对象捕获并将其平面化显示到二维屏幕这一过程。主要通过Camera Component来实现这一功能,所以Unity中摄像机的创建都是通过Camera Component来实现的。
Camera组件
先看看摄像机组件在Inspector中支持的属性设置。Unity Camera Inspector会根据当前项目使用的渲染管线(Render-Pipeline)来显示不同的属性。
使用通用渲染管线(URP)
使用高清渲染管线(HDRP)
使用内置渲染管线,Unity会显示以下属性:
ClearFlag,确定将清除屏幕的哪些部分。(在使用多摄像机来绘制不同元素时会非常方便)
Background,在绘制视图中的所有元素之后但没有天空盒的情况下,应用于剩余屏幕部分的颜色。
Culling Mask,剔除蒙版,包含或忽略要由摄像机渲染的对象层。在检视面板中将层分配到对象。
Projection,模拟透视功能,不同的视图会有不同的设置选项,具体如下:
Perspective,透视视图,摄像机将按透视角度渲染对象,有近大远小的特点;
Sensor Size
,模拟传感器的大小,有x和y两个值,可以自己设定,淡然选定Sensor Type后这个会自动更新到合适的值;Orthographic,正交视图,摄像机将均匀暄软对象,无透视感。正交模式下不支持延时渲染,始终是向前渲染;
Size,正交视图事业的大小(仅正交模式下游这个属性)
注意:摄像机高度,Game视图的显示比例决定宽度;
Clipping Planes,直译为裁剪的平面,其实指的就是摄像机渲染的范围,有一个最近值Near(相对摄像机最近绘制点)和最远值Far(相对摄像机最远绘制点)
Viewport Rect,视口范围,显然是一个方位,由x、y、width、height决定;
Depth,摄像机在绘制顺序中的位置。具有更大值的摄像机将绘制在具有更小值的摄像机之上。
Rendering Path,定义摄像机将使用的渲染方法的选项。
Use Player Settings,使用 Player Settings 中设置的任何渲染路径 (Rendering Path);
Vertex Lit,所有对象都将渲染为顶点光照对象。
Forward, 每种材质采用一个通道渲染所有对象。
Deferred Lighting,将在没有光照的情况下一次性绘制所有对象,然后在渲染队列末尾一起渲染所有对象的光照。
注意:如果摄像机的投影模式设置为 Orthographic,则会覆盖该值,并且摄像机将始终使用前向渲染。
Target Texture,将摄像机视图渲染内容输出到指定的渲染纹理。设置此引用将禁用此摄像机的渲染到屏幕功能。
Occlusion Culling,为此摄像机启用遮挡剔除 (Occlusion Culling)。遮挡剔除意味着隐藏在其他对象后面的对象不会被渲染,例如,如果对象在墙后面。请参阅遮挡剔除 (Occlusion Culling) 以了解详细信息。
Allow HDR,用高动态范围渲染。
Allow MSAA,启用多重采样抗锯齿。
Allow Dynamic Resolution,启用动态分辨率渲染;
Target Display,定义要渲染到的外部设备。值为 1 到 8 之间。
摄像机组件,
系列文章
Unity 本身可以制作简单的动画,一般步骤如下:
Project 面板下找到Assets文件夹,创建Animations文件夹用以存放管理动画文件;
Animations文件夹右键,选择Create,新建 Animation;
在Hierarchy 中选择一个GameObject,将新建的 Animationti 添加到这个 GameObject上;
然后选择这个GameObject,在Inspector面板中就可以看到Animator组件了,这个Animator组件就是 动画的播放器,而这个选择的GameObject就成了 动画的控制器;
双击打开这个 动画控制器(即Animator组件的第一个属性),就可以进行动画控制器(其实就是一个状态机)的编辑,可以新建不同的状态;
动画控制器打开 会有几个默认的矩形,代表几种不同的动作,其中:
Entry 表示进入;
Exit 表示退出;
橙黄色的矩形块表示默认的状态;
点击上面关联了动画的GameObject对象,然后打开Animation窗口(Window->Animation->Animation,或者直接快捷键 CMD+6),这样就可以编辑制作 Animation了。
Animation窗口,左边为添加属性窗口,右边为时间线窗口(添加属性前,请先选择要编辑哪个Animation,窗口左边顶部有一个下拉框,可以选择具体哪个Animation),点击Add Property按钮,可以选择要对GameObject下的哪个属性(或者哪个子物体的属性)进行动画
使用Animator
组件可以将动画分配给场景中的游戏对象。
Controller
, 通常是AnimatorController
,用来定义动画要使用那些剪辑(动画片段),以及如何在动画中组织这些剪辑(动画片段),如何过渡;Avatar
,游戏对象为人形角色时,还要在此组件中分配Avatar系列文章
物理系统
Unity可以在游戏中模拟使用物理系统,以确保对象正确加速并对碰撞、重力和各种其他力做出响应。Unity 提供了以下不同的物理引擎实现方案,您可以根据自己的项目需求选用:3D、2D、面向对象或面向数据。
面向对象的项目内置的物理引擎
面向数据的项目物理引擎包
如果项目中使用的时面向数据的技术堆栈(DOTS),则需要安装专用的DOTS物理包,可用的包括:
内置3D物理引擎
Unity内置物理引擎集成的是Nvidia PhysX,使用该引擎,可以方用以下内容来进行物理系统的模拟:
CharacterControl
第一人称和第三人称视角的游戏中,游戏角色通常需要一直基础的物理碰撞属性支持,这样游戏角色就不会从地板上掉下去或者不会穿墙。在3D应用中,可以通过CharacterControl来创建并配置一个这样的游戏角色,主要通过CharacterController
和CharacterControl 组件来配置。
CharacterController
控制器
CharacterController的属性
区别于直接用Transform
或者RigidBody
,CharacterController
有着更好的效果,它拥有RigidBody
的一些重要特性,但是又去掉了很多物理效果,这样可以避免诸如穿模,滑步,被撞飞或者将其他物体撞位移等情况。主要属性如下:
Slope limit,斜度限制,代表的是角色能爬上的最大坡度;
Step offset,每部偏移量,其实就是能走上的台阶的高度。这其实是很有用的一个属性,走上一些高度不是很高的台阶或者石头之类的东西不再需要跳跃,这也更符合实际的移动情况。
这个值有一个限制即该值必须小于等于(高度+半径*2)(上分图片中的最下方两个值),这个设定其实很好理解,毕竟你一个一米七的人不可能一步跨上一米多的台阶吧
Skin width,蒙皮宽度,碰撞体外层再添加一层,用来放置衣服等装饰物品;
要正确调整角色控制器,Skin Width 属性是最重要的属性之一。 如果角色被卡住,那么很可能是因为 Skin Width 设置过小。Skin Width 允许对象轻微穿透控制器,但可消除抖动并防止被卡住。
最好是让 Skin Width 的值至少大于 0.01 并且比 Radius 的值大 10%。
- 两个碰撞体可以穿透彼此且穿透深度最多为皮肤宽度 (Skin Width)。较大的皮肤宽度可减少抖动。较小的皮肤宽度可能导致角色卡住。合理设置是将此值设为半径的 10%。
Min move Distance,最小移动距离
- 建议将 Min Move Distance 保持为 0。
- 此设置可以用来减少抖动。在大多数情况下,此值应保留为 0。
Center,碰撞体的中心
Radius,碰撞体的半径
Height,碰撞体的高度
CharacterController
的操作方法