Wiger代码手记

Code!Code!!Code!!!

AndroidView的坐标系

在Android系统中,我们把屏幕左上角的顶点作为Android坐标系的原点,这个原点向右是X轴正方向,向下是Y轴正方向。 View坐标系 一个View的位置主要由它的四个顶点决定,这四个顶点坐标是基于View的父容器来说的,是一组相对坐标。 左上角纵坐标(top) 左上角横坐标(left) 右下角横坐标(right) 右下角纵坐标(bottom) 获取View自身坐标 通过如下方法可以获得View到其父控件(ViewGroup)的距离。 getTop():获取View自身顶边到其父布局顶边的距离。 getLeft():获取View自身左边到其父布局左边的距离。 getRight():获取View自身右边到其父布局左边的距离。 getBottom():获取View自身底边到其父布局顶边的距离。 获取MotionEvent的坐标 上图那个深蓝色的点,假设就是我们触摸的点,我们知道无论是View还是ViewGroup,最终的点击事件都会由onTouchEvent(MotionEvent event)方法来处理,MotionEvent也提供了各种获取焦点坐标的方法。 getX():获取点击事件距离控件左边的距离,即视图坐标 getY():获取点击事件距离控件顶边的距离,即视图坐标 getRawX():获取点击事件距离整个屏幕左边距离,即绝对坐标 getRawY():获取点击事件距离整个屏幕顶边的的距离,即绝对坐标。 View的宽高 由上图可知,一个View的宽度是 getRight() - getLeft();高度是 getBottom() - getTop()。 /** * Return the width of your view. * * @return The width of your view, in pixels. */ @ViewDebug.ExportedProperty(category = "layout") public final int getWidth() { return mRight - mLeft; } /** * Return the height of your view. * * @return The height of your view, in pixels. */ @ViewDebug.ExportedProperty(category = "layout") public final int getHeight() { return mBottom - mTop; }

September 8, 2025

Android自定义View实现滑动

使用 layout() 方法实现滑动 View进行绘制的时候会调用onLayout()方法来设置显示的位置,因此我们同样也可以通过修改View的left、top、right、bottom这四种属性来控制View的坐标。 首先我们要自定义一个View,在onTouchEvent()方法中获取触摸点的坐标: class CustomView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : View(context, attrs, defStyleAttr) { private var lastX: Int = 0 private var lastY: Int = 0 override fun onTouchEvent(event: MotionEvent): Boolean { val x = event.x.toInt() val y = event.y.toInt() when (event.action) { MotionEvent.ACTION_DOWN -> { lastX = x lastY = y } MotionEvent.ACTION_MOVE -> { val offsetX = x - lastX val offsetY = y - lastY //在这里使用layout()方法实现滑动 layout( left + offsetX, top + offsetY, right + offsetX, bottom + offsetY ) } else -> {} } return true } } 使用 offsetLeftAndRight() 与 offsetTopAndBottom()方法实现滑动 class CustomView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : View(context, attrs, defStyleAttr) { private var lastX: Int = 0 private var lastY: Int = 0 override fun onTouchEvent(event: MotionEvent): Boolean { val x = event.x.toInt() val y = event.y.toInt() when (event.action) { MotionEvent.ACTION_DOWN -> { lastX = x lastY = y } MotionEvent.ACTION_MOVE -> { val offsetX = x - lastX val offsetY = y - lastY //在这里使用offsetXXXAndXXX()方法实现滑动 offsetLeftAndRight(offsetX) offsetTopAndBottom(offsetY) } else -> {} } return true } } 使用 scrollTo/scrollBy View类提供了scrollTo和scrollBy的方法来实现View的移动。其中 ...

September 8, 2025

Kotlin协程之函数的挂起

⚠️任何一个协程体或者挂起函数中都有一个隐含的Continuation 实例,编译器能够对这个实例进行正确传递。 挂起函数 用suspend 关键字修饰的函数叫做挂起函数,挂起函数只能在协程体内或其他挂起函数内调用。 挂起函数可以调用任何函数,普通函数只能调用普通函数。 协程的挂起就是程序执行流程发生异步调用时,当前调用流程的执行状态进入等待的状态。 /** * 挂起函数可以像普通函数一样同步返回。 * 函数类型:suspend(Int)->Unit */ suspend fun suspendFunc01(a: Int) { return } /** * 挂起函数可以处理异步逻辑 * 函数类型:suspend(String, String)->Unit */ suspend fun suspendFunc02(a: String, b: String): Int { return suspendCoroutine<Int> { continuation -> thread { continuation.resumeWith(Result.success(5)) } } } 在suspendFunc02的定义中用suspendCoroutine<T>获取当前所在协程体的Continuation<T>的实例作为参数将挂起函数当成异步函数来处理,在1️⃣处执行Continuatation.resumeWith操作,因此协程调用suspendFunc02无法同步执行,会进入挂起状态,直到结果返回。 挂起点 在协程内部挂起函数的调用处被称为挂起点,挂起点如果出现异步调用,那么当前协程就被挂起,直到对应的Continuation的resume函数被调用才会恢复执行。 /** * 不会挂起的挂起函数 */ suspend fun notSuspend() = suspendCoroutine<Int> { continuation -> continuation.resume(100) } 异步调用是否发生的条件 取决于resume函数与对应的挂起函数的调用是否在相同的调用栈上。 切换函数调用栈的方法 切换到其他线程上执行 不切换线程但在当前函数返回之后的某一个时刻再执行。 CPS交换 CPS交换 (Continuation-Passing-Style Transformation),是通过传递Continuation来控制异步调用流程的。 Kotlin 协程挂起时就将挂起点的信息保存到了Continuation对象中。Continuation 携带了协程继续执行所需要的上下文,恢复执行的时候只需要执行它的恢复调用并且把需要的参数或者异常传入即可。 用Java代码调用挂起函数 // 比普通函数多了一个 Continuation 实例的参数。 Object result = K2Kt.notSuspend(new Continuation<>() { @NotNull @Override public CoroutineContext getContext() { return EmptyCoroutineContext.INSTANCE; } @Override public void resumeWith(@NotNull Object o) { } }); suspend() -> Int 类型的函数nonSuspend 在Java 语言看来实际是(Continuation<Integer>)->Object类型,与回调函数相似。 返回值Object两种情况 挂起函数同步返回 作为参数传入的Continuation的resumeWith不会被调用,函数的实际返回值就是它作为挂起函数的返回值。 挂起函数挂起,执行异步逻辑 函数的实际返回值是一个挂起标志, 通过这个标志外部协程就可以知道该函数需要挂起等到异步逻辑执行。 用Kotlin反射调用挂起函数 val ref = ::notSuspend val result = ref.call(object : Continuation<Int> { override val context: CoroutineContext get() = EmptyCoroutineContext override fun resumeWith(result: Result<Int>) { println("resumeWith: ${result.getOrNull()}") } })

August 25, 2025

Kotlin协程的上下文

上下文:是指执行环境相关的通用数据资源的统一提供者。 协程的上下文数据结构特征实现与List、Map集合非常类似,协程的上下文作为一个集合,其元素类型是Element。 协程上下文的内部实现实际上是一个单链表。 Element An element of the CoroutineContext. An element of the coroutine context is a singleton context by itself. public interface Element : CoroutineContext { /** * A key of this coroutine context element. */ public val key: Key<*> //.... } Element接口中有一个属性key,其作用就是协程上下文这个集合中元素的"索引",类似集合元素的下标,但和集合不同的是该“索引”是在数据里面的。 协程上下文元素的实现 除了Element接口外,还有一个AbstractCoroutineContextElement的抽象类,能更加方便实现协程上下文的元素。 public abstract class AbstractCoroutineContextElement(public override val key: Key<*>) : Element 协程名的实现 /** * CoroutineNameElement允许我们为协程绑定一个名字 */ class CoroutineNameElement(name: String) : AbstractCoroutineContextElement(KEY){ companion object KEY : CoroutineContext.Key<CoroutineNameElement> } 协程异常处理器的实现 /** * CoroutineExceptionHandlerElement允许我们在启动协程的时候安装一个统一的异常处理器 */ class CoroutineExceptionHandlerElement(val onErrorAction: (Throwable) -> Unit) : AbstractCoroutineContextElement(KEY) { companion object KEY : CoroutineContext.Key<CoroutineExceptionHandlerElement> fun onError(error: Throwable) { error.printStackTrace() onErrorAction(error) } } 协程上下文的使用 fun main() { var coroutineContext: CoroutineContext = EmptyCoroutineContext //类似集合一样有两种添加上下文的方式 //1️⃣ //coroutineContext += CoroutineNameElement("co-01") //coroutineContext += CoroutineExceptionHandlerElement { // println("error::::${it}") //} //2️⃣ coroutineContext += CoroutineNameElement("co-01") + CoroutineExceptionHandlerElement { println("error::::${it}") } suspend { //在协程内部获取上下文 val nameElement: CoroutineNameElement? = coroutineContext[CoroutineNameElement] println("In Coroutine [${nameElement?.name}].") 100 / 0 }.startCoroutine(object : Continuation<Int> { override val context: CoroutineContext get() = coroutineContext override fun resumeWith(result: Result<Int>) { result.onFailure { context[CoroutineExceptionHandlerElement]?.onError(it) } } }) } 通过resumeWith里的context.get()或者协程体里的coroutineContext.get()获取协程上下文元素。因为有可能没设置CoroutineExceptionHandlerElement、CoroutineNameElement…所以get()返回的结果是可空类型。 context.get(key)中key是协程上下文的key。

August 25, 2025

Kotlin协程的构造

协程的创建 import kotlin.coroutines.* val continuation = suspend { println("5") 5 }.createCoroutine(object : Continuation<Int> { override val context: CoroutineContext get() = EmptyCoroutineContext override fun resumeWith(result: Result<Int>) { println("Coroutine End:$result") } }) 标准库提供了一个createCoroutine函数,可通过它来创建协程,但这个协程并不会立即执行。 标准库也提供了startCoroutine函数,可以创建协程后就立即让它开始执行。 //createCoroutine 函数声明 fun <T> (suspend () -> T).createCoroutine( completion: Continuation<T> ): Continuation<Unit> //startCoroutine 函数声明 fun <T> (suspend () -> T).startCoroutine( completion: Continuation<T> ) suspend () -> T 是 createCoroutine 函数的Receiver,是协程的执行体 ,称为协程体。 参数completion在协程执行完后调用,是协程的完成回调。 createCoroutine函数的返回值是一个Continuation本体|Continuation对象,startCoroutine函数的返回值是Unit。 协程的启动 通过调用continuation.resume(Unit)之后,协程体会立即开始执行。 continuation.resumeWithException(Exception("Custom Exception")) 协程体的Receiver fun <R, T> (suspend R.() -> T).createCoroutine( receiver: R, completion: Continuation<T> ): Continuation<Unit> fun <R, T> (suspend R.() -> T).startCoroutine( receiver: R, completion: Continuation<T> ) suspend R.() -> T多了一个Receiver类型R。R可以为协程体提供一个作用域,即可以在协程体中直接使用作用域内提供的函数或者状态等。 ...

August 25, 2025

简单工厂模式

图解简单工厂模式 定义一个用于创建对象的接口,让子类决定实例化哪一个类。Factory Method使一个类的实例化延迟到其子类。 模块定义 简单工厂模式(Simple Factory Pattern):又称为静态工厂方法(Static Factory Method)模式,它属于类创建型模式。在简单工厂模式中,可以根据参数的不同返回不同类的实例。简单工厂模式专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。 模式结构 简单工厂模式包含如下角色: Factory:工厂角色 工厂角色负责实现创建所有实例的内部逻辑 Product:抽象产品角色 抽象产品角色是所创建的所有对象的父类,负责描述所有实例所共有的公共接口 ConcreteProduct:具体产品角色 具体产品角色是创建目标,所有创建的对象都充当这个角色的某个具体类的实例。 public class SimpleFactoryDemo { public static void main(String[] args) { Operation oper = OperationFactory.createOpreation("+"); oper.numA = 1; oper.numB = 2; try { double result = oper.getResult(); System.out.println(result); } catch (Exception e) { e.printStackTrace(); } } } //工厂角色 class OperationFactory { public static Operation createOpreation(String operate) { Operation oper = null; switch (operate) { case "+": oper = new OperationAdd(); break; case "-": oper = new OperationSub(); break; case "*": oper = new OperationMul(); break; case "/": oper = new OperationDiv(); break; } return oper; } } //抽象产品角色 abstract class Operation { protected double numA; protected double numB; abstract double getResult() throws Exception; } //具体产品角色 class OperationAdd extends Operation { @Override double getResult() { return numA + numB; } } //具体产品角色 class OperationSub extends Operation { @Override double getResult() { return numA - numB; } } //具体产品角色 class OperationMul extends Operation { @Override double getResult() { return numA * numB; } } //具体产品角色 class OperationDiv extends Operation { @Override double getResult() throws Exception { if (numB == 0) throw new Exception("除数不能为0"); return numA / numB; } }

August 20, 2025

Kotlin的可变性与不可变性

基本类型 var a: Int = 1 printInfo("old_a", a) val b: Int = a printInfo("old_b", b) a = 2 printInfo("a", a) printInfo("b", b) int a = 1; printInfo("old_a", a); int b = a; printInfo("old_b", a); a = 2; printInfo("a", a); printInfo("b", b); 【284720968】old_a:1 【284720968】old_b:1 【511754216】a:2 【284720968】b:1 创建一个Int对象值为1,其存放在内存的内存地址是284720968。 创建一个可变引用a指向地址284720968。 创建一个不可变引用b,并赋值a当前所持有的值1的地址284720968。 创建一个Int对象值为2,假设其存放在内存的内存地址是511754216。 a是可变引用,将a=2,它的引用被更新了,现在的a指向的地址是511754216,现在a的值=2。 b是不可变引用,b仍然指向284720968即b的值仍然是1。 集合 Kotlin 的集合库对可变性和不可变性有清晰的区分。它为许多集合类型提供了两个接口:一个只读接口 (不可变) 和一个可写接口 (可变)。 不可变集合 val immutableList: List<Int> = listOf(1, 2, 3) printInfo("immutableList", immutableList) var list1: List<Int> = immutableList printInfo("old_list1", list1) val list2: List<Int> = list1 printInfo("old_list2", list2) list1 += 4 printInfo("list1", list1) printInfo("list2", list2) Integer[] var3 = new Integer[]{1, 2, 3}; List immutableList = CollectionsKt.listOf(var3); printInfo("immutableList", immutableList); printInfo("old_list1", immutableList); printInfo("old_list2", immutableList); List var10 = CollectionsKt.plus((Collection)immutableList, 4); printInfo("list1", var10); printInfo("list2", immutableList); 【930990596】immutableList:[1, 2, 3] 【930990596】old_list1:[1, 2, 3] 【930990596】old_list2:[1, 2, 3] 【94438417】list1:[1, 2, 3, 4] 【930990596】list2:[1, 2, 3] 创建一个不可变的列表immutableList值是[1,2,3],其存放在内存的内存地址是930990596。 创建一个可变引用list1,并赋值其等于immutableList,即指向地址是930990596。 创建一个不可变引用list2,并赋值其等于list1,即指向地址是930990596。 调用list1 += 4,即创建了一个新的不可变列表templist,值是[1,2,3,4],其存放在内存的内存地址是94438417。 list1 是可变引用,将list1赋值等于templist,它的引用被更新了,现在的list1指向的地址是94438417,现在list1的值=[1,2,3,4]。 list2是不可变引用,list2仍然指向930990596即list2的值仍然是[1,2,3]。 从Java代码就能看到,实际上创建了两个不同的不可变列表immutableList和var10,list1就是var10,list2就是immutableList。 可变集合 val mutableList: MutableList<Int> = mutableListOf(1, 2, 3) printInfo("mutableList", mutableList) val list3: MutableList<Int> = mutableList printInfo("old_list3", list3) val list4: MutableList<Int> = list3 printInfo("old_list4", list4) list3 += 4 printInfo("list3", list3) printInfo("list4", list4) Integer[] var6 = new Integer[]{1, 2, 3}; List mutableList = CollectionsKt.mutableListOf(var6); printInfo("mutableList", mutableList); printInfo("old_list3", mutableList); printInfo("old_list4", mutableList); ((Collection)mutableList).add(4); printInfo("list3", mutableList); printInfo("list4", mutableList); 【1109371569】mutableList:[1, 2, 3] 【1109371569】old_list3:[1, 2, 3] 【1109371569】old_list4:[1, 2, 3] 【1109371569】list3:[1, 2, 3, 4] 【1109371569】list4:[1, 2, 3, 4] MutableList是可变集合,val指向的内容如果是可变的话,对象内容可以改。即引用不可变,内容可变。而list3 += 4实际上是调用了Collection.add的方法,并没有生成新的对象引用,仍然指向的是1109371569这个集合实例。 Map Map和集合类似,也分为可变和不可变Map。 不可变Map val immutableMap: Map<String, Int> = mapOf("aaa" to 10) printInfo("immutableMap", immutableMap) var map1 = immutableMap printInfo("old_map1", map1) val map2 = map1 printInfo("old_map2", map2) map1 = mapOf("aaa" to 20) printInfo("map1", map1) printInfo("map2", map2) Map immutableMap = MapsKt.mapOf(TuplesKt.to("aaa", 10)); printInfo("immutableMap", immutableMap); printInfo("old_map1", immutableMap); printInfo("old_map2", immutableMap); Map map1 = MapsKt.mapOf(TuplesKt.to("aaa", 20)); printInfo("map1", map1); printInfo("map2", immutableMap); 【1099983479】immutableMap:{aaa=10} 【1099983479】old_map1:{aaa=10} 【1099983479】old_map2:{aaa=10} 【1268447657】map1:{aaa=20} 【1099983479】map2:{aaa=10} 可变Map val mutableMap: MutableMap<String, Int> = mutableMapOf("aaa" to 10) printInfo("mutableMap", mutableMap) val map3 = mutableMap printInfo("old_map3", map3) val map4 = map3 printInfo("old_map4", map4) map3["aaa"] = 20 printInfo("map3", map3) printInfo("map4", map4) Pair[] var12 = new Pair[]{TuplesKt.to("aaa", 10)}; Map mutableMap = MapsKt.mutableMapOf(var12); printInfo("mutableMap", mutableMap); printInfo("old_map3", mutableMap); printInfo("old_map4", mutableMap); mutableMap.put("aaa", 20); printInfo("map3", mutableMap); printInfo("map4", mutableMap); 【1851691492】mutableMap:{aaa=10} 【1851691492】old_map3:{aaa=10} 【1851691492】old_map4:{aaa=10} 【1851691492】map3:{aaa=20} 【1851691492】map4:{aaa=20} 总结 使用 val 声明不可变引用 (推荐),使用 var 声明可变引用。 val 指向的对象如果是可变的 (如 MutableList),对象内容依然可改。

August 19, 2025

Kotlin内联类(Value Class)

Inline classes 是 Value classes 的子集, Value classes 比 Inline classes 会得到更多优化,现阶段 Value classes 和 Inline classes 一样,只能在构造函数中传入一个参数,参数需要用 val 声明,将来可以在构造函数中添加多个参数,但是每个参数都需要用 val 声明。 声明一个内联类(value class) 声明一个内联类需要下面三个条件: value class 关键字 @JvmInline 注解 主构造函数有且只能有一个属性 // For JVM backends @JvmInline value class Password(private val s: String) // No actual instantiation of class 'Password' happens // At runtime 'securePassword' contains just 'String' val securePassword = Password("Don't try this in production") 成员 value class 允许声明属性(可计算属性、不支持幕后属性)或方法和init 初始块。 @JvmInline value class Name(val s: String){ init { require(s.isNotEmpty()) } val length : Int get() = s.length fun greet(){ println("Hello, $s") } } fun main() { val name = Name("kotlin") name.greet() println(name.length) } 继承 Value Class 编译后将会添加 fianl 修饰符,因此不能被继承,同样也不能继承其他的类;但是可以实现接口。 interface Printable { fun prettyPrint(): String } @JvmInline value class Name(val s: String) : Printable { override fun prettyPrint(): String = "Let's $s!" } 表现形式 Kotlin 编译器会用一个容器(wrapper)包装内联类。 在运行时内联类实体可以表现为包装类或者内联的具体类型。(Inline class instances can be represented at runtime either as wrappers or as the underlying type.) 正是因为内联类有两种表现形式,所以引用相等( === )会被禁止使用。(Because inline classes may be represented both as the underlying value and as a wrapper, referential equality is pointless for them and is therefore prohibited.) Kotlin 编译器会优先使用具体的类型来提高性能和优化代码,但有些场景将会保留包装类。(The Kotlin compiler will prefer using underlying types instead of wrappers to produce the most performant and optimized code. However, sometimes it is necessary to keep wrappers around.) 内联类在被用于其他类型的时候将表现为包装类。(As a rule of thumb, inline classes are boxed whenever they are used as another type.) Foo 和 Foo?不是同样的类型,所以也表现为包装类。 interface I @JvmInline value class Foo(val i: Int) : I fun asInline(f: Foo) {} fun <T> asGeneric(x: T) {} fun asInterface(i: I) {} fun asNullable(i: Foo?) {} fun <T> id(x: T): T = x fun main() { val f = Foo(42) asInline(f) // unboxed: used as Foo itself asGeneric(f) // boxed: used as generic type T asInterface(f) // boxed: used as type I asNullable(f) // boxed: used as Foo?, which is different from Foo // below, 'f' first is boxed (while being passed to 'id') and then unboxed (when returned from 'id') // In the end, 'c' contains unboxed representation (just '42'), as 'f' val c = id(f) } ...

August 15, 2025

Kotlin内联函数

什么是内联函数 在 Kotlin 中通过 inline-functions (内联函数) 实现函数内联。 内联的作用:提升运行效率,调用被 inline 修饰符的函数,会把方法体内的代码放到调用的地方,其主要目的提高性能,减少对象的创建。 inline 修饰的函数适用于以下情况 inline 修饰符适用于把函数作为另一个函数的参数,例如高阶函数 filter 、 map 、 joinToString 或者一些独立的函数 repeat inline 操作符适合和 reified 操作符结合在一起使用 如果函数体很短,使用 inline 操作符提高效率 字节码 内联函数字节码 //Inline Function inline fun inlinePrintByteCode() { println("printByteCode") } fun testInline() { inlinePrintByteCode() } 非函数内联字节码 fun printByteCode() { println("printByteCode") } fun testNoInline() { printByteCode() } 禁用内联 如果希望只内联一部分传给内联函数的 lambda 表达式参数,那么可以用 noinline 修饰符标记不希望内联的函数参数 inline fun inlineF(inlined: () -> Unit, noinline notInlined: () -> Unit) { inlined() notInlined() } fun main() { inlineF({println("Hello")}, {println("World")}) } ...

August 15, 2025

Android动画之淡入淡出动画

在安卓开发中,常用淡入淡出的动画来显示加载中的效果。比如在加载新闻列表时,先显示骨架屏动画,当请求成功后,骨架屏以淡出的形式消失,新闻列表项以淡入的形式出现。或者是加载图片的时候,默认显示一个空的图片或占位图片,当图片加载成功,占位图片以淡出的形式消失,加载的图片以淡入的形式显示。 一个简单的例子。以Android Logo作为占位图,员工头像为请求的图片。 实现淡入淡出动画 1、准备两个ImageView 一个ImageView显示占位图,一个显示ImageView请求的图片。请求图片的ImageView的visibility默认是Gone。 <FrameLayout android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1"> <ImageView android:id="@+id/v_content" android:layout_width="match_parent" android:layout_height="match_parent" android:src="@drawable/profile_picture" android:visibility="gone" /> <ImageView android:id="@+id/v_loading" android:layout_width="match_parent" android:layout_height="match_parent" android:src="@mipmap/ic_launcher" /> </FrameLayout> 2、执行动画 把加载图片的ImageView的visibility设为VISIBLE,并执行从透明到不透明的动画,实现淡入效果。占位图的ImageView执行不透明到透明的动画,实现淡出效果,监听动画回调,当动画执行结束后,将占位图的visibility设为GONE。 //动画时长 val shortAnimationDuration = resources.getInteger(android.R.integer.config_longAnimTime).toLong() private fun crossFade() { content.run { //淡入的View从初始状态的GONE切换成Visible,然后通过透明度0隐藏。 visibility = View.VISIBLE alpha = 0F //执行动画 animate() .alpha(1F) .setDuration(shortAnimationDuration) } loading.animate() .alpha(0F) .setDuration(shortAnimationDuration) .setListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator?) { super.onAnimationEnd(animation) loading.visibility = View.GONE } }) }

April 25, 2022