数理流程
1.界⾯ - 2.逻辑(数据)
登录
界面加载起来就加载用户信息:从哪里加载读取⽂件
保存⽤户信息(设置密码) :写⼊⽂件
获取所有⽤户信息:读取⽂件
注册
在文件中添加新数据
切换
文件存取用户信息
1. ⽤户→ model → UserModel: name pin pattern type(区别是什么类型的密码)
2. type: 0(TYPE_PIN) 1(Type_PATTERN)
3. ⽤户操作→ UserManager
a. 加载所有⽤户信息 loadAllUsersInfo()
b. 检测是否有⽤户 hasUserInfo():Boolean //有用户就可以显示切换图案解锁
c. 判断⽤户是否存在 checkUser(name):Boolean //先根据输入的用户名判断用户是否存在 不存在直接爆红
d. 判断⽤户名和密码是否正确 checkUser(name,password,type):Boolean //能到这一步判断说明用户名已经存在了
e. 获取所有⽤户名 getAllUserName():List<String> //滑动解锁显示的弹窗
f. 添加⽤户信息 saveUser(name,pin,pattern) //注册后存用户信息
4. ⽂件操作→ FileManager
a. 创建⽂件creatFile()
b. 读取⽂件内容readData():List<List> //加载用户信息 判断 获取
c. 写⼊⽂件内容writeData(userList) //添加用户信息
看不见的:文件的路径filePath():String
UML类图
用户信息功能
建立两个包file、user,
创建User数据类[创建model对象]
data class User( val name:String, val pin:String, val pattern:String, var isLogin:Boolean)
创建FileManager的单例对象
先写fileManager,因为用户操作里面有好多要依赖文件操作
创建FileManager类
首先要提供文件的单例对象,外部不能直接访问我的构造函数了,要私有化构造函数,加伴生对象,伴随着这个类的产生而产生的对象。instance保存唯一对象又不希望外部直接访问,并给外部暴露一个方法出来用于得到这个对象,如果instance不为空则不用再创建一个对象,直接把当前的instance返回出去。
synchronized:涉及线程之间的安全问题(多个对象去抢夺这一个对象的时候可能会出现线程不安全问题)如果instance没有就去创建,创建过程中加把锁,先别急着访问,先把这个对象创建好了再来访问,进来之后再来判断一次(有可能刚刚进来的时候刚好已经创建好了,创好了就没有必要去再做一次了),即将要创建了还是空则去创建这个对象
(当存在多个线程操作共享数据时,需要保证同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再进行,这种方式有个高尚的名称叫互斥锁,即能达到互斥访问目的的锁,也就是说当一个共享数据被当前正在访问的线程加上互斥锁后,在同一个时刻,其他线程只能处于等待的状态,直到当前线程处理完毕释放该锁。
在Java中,关键字 synchronized 可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块(主要是对方法或者代码块中存在共享数据的操作),同时我们还应该注意到synchronized另外一个重要的作用,synchronized可保证一个线程的变化(主要是共享数据的变化)被其他线程所看到(保证可见性,完全可以替代Volatile功能),这点确实也是很重要的。)
class FileManager private constructor({ companion object{ private var instance:FileManager?= null fun sharedInstance():FileManager{//只有调用这个方法的时候才去创建这个对象 if (instance == null){ synchronized(this){//每一个类、每个对象都有一把锁, if (instance == null){ instance = FileManager() } } } return instance!! } }}
获取保存用户信息的文件路径filePath
filesDir只有Activity(顶级父类是context)或者其父类才能访问到,为了访问到filesDir,方法里要传入context过来。有了filesDir再拼上文件名就形成路径了private fun filePath(context: Context):String{ return "${context.filesDir.path}/userInfo"}
从文件中读取用户信息readData
存进去的时候是Json格式的字符串,但是读取的时候想得到的是对象,而且有可能有多个对象,所以返回User的数组,为了得到路径,所以要传入context。
将所有信息读进来readText(),Json格式的字符串对象]用Gson将其转化为对象。
导入依赖//Gson implementation 'com.google.code.gson:gson:2.10.1'
还要得到Gson的对象
创建一个对象表达式,从该对象表达式继承TypeToken,然后从中获取Java Type。
TypeToken是一个用于类型推断和类型安全性的技术,可以获得一个对象的实际类型,而不仅仅是其类[就是我们想得到的是List<User>而不仅仅是List<String>或者其他类型],TypeToken是一个抽象类,因此需要用匿名子类来创建它。
注意:如果是第一次安装则是没有这个文件的,是读不出来的,要判断文件是否存在再进行文件的读取
fun readData(context: Context):List<User>{ if (File(filePath(context)).exists()){//要首先确保文件是存在的才能读取 FileReader(filePath(context)).use { val jsonString = it.readText() val token = object : TypeToken<List<User>>(){}.type return Gson().fromJson(jsonString,token) } } return emptyList()//没有该文件就返回空}
将用户数据写入文件writeData
参数传入需要写入的users类型为List<User>
我们需要从List<User>类型转化为json格式的字符串
用write写入
fun writeData(context: Context,users:List<User>){ FileWriter(filePath(context)).use { val jsonString = Gson().toJson(users) it.write(jsonString) }}
测试:
在MainActivity里面尝试写一个进去(因为只有MainActivity继承于context所以在此测试)
先将假数据写进去
val users = listOf( User("jack","123","456",false), User("rose","252","247",false))FileManager.sharedInstance().writeData(this,users)
没有报错就好,在Device File Explorer里面
data -> data -> com.example.loginannie -> file -> userInfo里面就有我们存入的用户信息
其具有一定的格式
[//中括号[ ] -> 集合 数组
{"isLogin":false,"name":"jack","pattern":"456","pin":"123"},//大括号{ } -> 对象
{"isLogin":false,"name":"rose","pattern":"247","pin":"252"}
]
读取改为
FileManager.sharedInstance().readData(this).also { it.forEach { user-> Log.v("annie","$user") }}
Logcat中可以打印出这个
创建用户管理器UserManager
新建UserManager类,管理用户操作
构建单例静态对象:私有化构造函数,阻止外部直接创建这个对象,私有化之后只有在自己内部才能创建对象
class UserManager private constructor(){ companion object{ private var instance:UserManager? = null fun sharedInstance():UserManager{ if (instance == null){ synchronized(this){ if (instance == null){ instance = UserManager() } } } return instance!! } }}
进行用户的数据存取需要用到context,UserManager里面需要传一个context进来,构建instance对象的时候就传入一个context进来,多地方要用context就将它作为全局的属性,既然是全局的属性那就把它放在构造函数里面。
但是如果直接val context:Context会出现内存泄漏(不用了的东西没有删掉)
一个对象是否被删除就看它还要不要用,如果它一直被其他对象持有,则永远不能被删掉
单例
companion object里面是静态属性和静态方法,静态对象一直存在,其属性context一直存在,在MainActivity中用this传入自身则不能被销毁,外部无法释放,直到程序退出
使用弱引用class UserManager private constructor(val context: WeakReference<Context>){ companion object{ private var instance:UserManager? = null fun sharedInstance(context:Context):UserManager{ if (instance == null){ synchronized(this){ if (instance == null){ instance = UserManager(WeakReference(context)) } } } return instance!! } }}
加载所有用户信息loadAllUserInfo
定义保存用户信息的变量private val users = arrayListOf<User>()
用户信息的存与取都是靠FileManager来做的,每次访问都要用FileManager.sharedInstance(),可以起一个临时变量记录一下
删掉MainActivity里面的测试,在MainActivity里面希望其onCreat时加载所有用户信息
加载用户信息即可,不需要返回值,加载完信息就存在users 里面
取出弱引用里面包含的context,调用get()方法即可
把多个东西加到数组里面用addAll(),加到users数组里面
测试:在MainActivity加载起来的时候调用这个方法
fun loadAllUserInfo(){ fileManager.readData(context.get()!!).also { users.addAll(it) Log.v("annie","$users") }}
在MainActivity中调用UserManager.sharedInstance(this).loadAllUserInfo()
是否有用户hasUser
判断存储用户信息的数组是否为空即可
fun hasUser():Boolean{ return users.size>0 } fun hasUser() = users.size > 0
用户名是否存在checkUser
循环遍历users取其name相比较
fun checkUser(name:String):Boolean{ users.forEach { if (it.name == name){ return true } } return false}
判断用户名和密码是否正确checkUser
写type的方式(后面改成枚举了)
创建一个静态类,更容易让别人看懂
object PasswordType { const val LoginType_Pin = 1 const val LoginType_Pattern = 2}
先找到用户,再判断是什么类型的密码,再比较密码是否正确
fun checkUser(name: String,password:String,type:Int):Boolean{ users.forEach { if (it.name == name){ //找用户 if (type == PasswordType.LoginType_Pin){ return it.pin == password //比较pin密码是否相同 }else{ return it.pattern == password //比较pattern密码是否相同 } } } return false //没有这个用户}
获取所有的用户名
在pattern密码的时候,如何当前没有登录的用户,那么我们就要获取我们要登录的是哪一个
还要知道当前有没有登录的用户信息
获取当前登录用户currentUser
当前登录的用户不一定有,但是有就只有一个没有就没有(同一时间只有一个登录用户)
filter{}过滤完之后得到的是List<User>
users.filter { if (it.isLogin){ true }else{ false }}//过滤条件
先过滤出isLogin == true的user放在List<User>数组里去,如果这个数组不为空则代表当前有登录的用户,有就把这个User()取出来
fun currentUser():User?{ users.filter { it.isLogin }.also { return if (it.isNotEmpty()){ it[0] }else{ null } }}
后面该怎么用涉及界面的逻辑
保存注册用户信息registerUser
首先添加到users数组里面(将注册的用户添加到用户信息中),注册完之后需要重新登录
登录过后再将信息写入
fun registerUser(name: String,pin:String,pattern:String){ users.add(User(name,pin,pattern,false)) //注册后需要再次登录 fileManager.writeData(context.get()!!,users)}
登录login
可以不checkUser直接点击登录
先找到用户名和密码对应的用户名再去判断
用一个变量存储要找到的那个用户,整个forEach结束就是为了找到对应的用户,将找到的该用户的isLogin改为true就表示是登录了,此处是把数组里面的用户信息的isLogin改为true,文件里的也要改,即还要写入数据
fun login(name: String,password: String,type: Int):Boolean{ var user:User? = null users.forEach { if (it.name == name){ if (type == PasswordType.LoginType_Pin){ if (it.pin == password){ user = it //找到当前用户名和密码对应的用户 } }else{ if (it.pattern == password){ user = it } } } } //forEach结束找到用户名和密码对应的用户user return if (user != null){ user!!.isLogin = true //将对应用户数组里面的用户信息的isLogin改为true fileManager.writeData(context.get()!!,users)//将修改后的所有用户信息重新写入文件 true }else{ false }}
在UserManager()中添加getUserInfo()方法,根据用户名找到用户信息
fun getUserInfo(name:String):User?{ users.forEach { if (it.name == name){ return it } } return null}
测试:用Jack来试一试,在MainActivity中加入
UserManager.sharedInstance(this).login("jack","123",PasswordType.LoginType_Pin)
去file中看看,isLogin变成true了
在登录之前要先取消掉上一个用户的登录状态
------------------------------------------------------------------------------------------------------------------------------------------
17
思考用户的信息应该在哪个地方加载?
搭建界面流程(把这几个界面串联起来)
MyApplication
在此之前先做一个合理化的事情:
程序运行起来就需要立刻拥有的东西(加载用户信息)就可以把它放在application里面来
创建类MyApplication继承于Application() [生命周期:从程序开始到结束]
专门用于管理我们这个应用程序的(用于保持整个程序的全局状态)
当这个应用程序被点击然后要加载起来之前,优先调用这个application,界面还未加载
在Manifest的application下面加入
android:name=".MyApplication"告诉系统加载应用时,去加载MyApplication(声明优先调用这个 不用系统的)
①创建Application的⼦类
②实现onCreate⽅法
③方法内部实现自己需要的配置
④在AndroidManifest文件的<Application>中使⽤android:name绑定自己的MyApplication
class MyApplication:Application() { override fun onCreate() { super.onCreate() //加载所有用户信息 UserManager.sharedInstance(this).loadAllUserInfo() }}
运行起来有没有打印就知道有没有被调用了
对log⽇志输出进⾏简单封装MyLog
保留Log(只有写代码的时候才需要这个Log)
使用一个开关控制是否需要输出日志
IS_RELEASE是否发布状态 作为一个开关(调试状态[写代码] 发布状态)
private const val IS_RELEASE = false //默认非发布状态 即调试状态
如果需要所有的Log无法打印 将其改为true状态即可
v i d e w五种常用的
object MyLog {//静态类 private const val IS_RELEASE = false //静态属性 fun v(tag:String = "annieTry",content:String = ""){//静态方法 注意两个参数 给默认值的先后顺序 if (!IS_RELEASE){ Log.v(tag,content) } }
}
注意包的合理分配
梳理跳转流程
建立程序执⾏流程界⾯
分析整个项⽬Activity(如果这个界面需要提供给外部一个服务,需要外部其他应用也能调用我这个应用程序,则需要独立有一个Activity)和Fragment的数量(有几个界面就有几个Fragment)
创建Fragment 5个
PinLoginFragment
PatternLoginFragment
PinRegisterFragment
PatternRegisterFragment
ChooseUserBottomSheetFragment
删除不需要的东西
注意包的合理分配
在activity_main.xml添加FragmentContainerView
首页默认显示PinLoginFragment界面,调整大小,铺满整个屏幕
将每一个Fragment默认的FrameLayout改为ConstraintLayout
为了方便调试可以改一改对应的背景颜色和文字
修改颜色后但是看不到默认界面?
①在activity_main.xml中点击右上角有一个红色叹号警告
②点击Unknown fragments
③(Use@layout/fragment_pin_login,Pick Layout)
添加Fragment之间的切换效果
添加平移动画效果enter exit popEnter popExit
1.先添加资源文件
res -> Android Resource Directory[安卓资源目录] -> anim
anim -> Animation Resource File[动画资源文件]
①enter_from_right从右边进入 <translate平移动画 fromXDelta toXDelta duration(从上面写)
②exit_to_left左边出去
③pop_enter_from_left从左边弹回来
④pop_exit_to_right弹到右边去
⑤enter_from_borrom 从下面进入(给底部用户选择弹窗添加进入/出去动画)
⑥exit_to_borrom 从底部消失
<set xmlns:android="http://schemas.android.com/apk/res/android" android:duration="500"> <translate android:fromXDelta="-100%" android:toXDelta="0%"/></set>
2.实现对应切换效果PinLoginFragment
在PinLoginFragment里面要得到View,添加点击事件
onViewCreated
fragment自己不能切换要找FragmentManager做切换
导入依赖
//FragmentManager切换拥有commit{}扩展函数
implementation "androidx.fragment:fragment-ktx:1.6.0"
用commit就可以得到FragmentTransaction
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) view.setOnClickListener { parentFragmentManager.commit { setCustomAnimations( //设置动画 R.anim.enter_from_right, R.anim.exit_to_left, R.anim.pop_enter_from_left, R.anim.pop_exit_to_righ ) replace(R.id.fragmentContainerView,PinRegisterFragment()) //切换到那个界面 setReorderingAllowed(true) //允许重新排序 addToBackStack(null) // 是否加到栈里可以回来 } }}
测试:运行
3.给Fragment添加扩展方法Tools(封装Fragment切换的方法)
但是切换非常频繁不适合在Fragment里直接全写
应把commit封装成对应的方法(动画固定 不一定切换到哪里去 不一定入栈)
只有在Fragment里用,可以给其加拓展方法
fun Fragment.navigateTo(target: Fragment,addToStack:Boolean){ parentFragmentManager.commit { setCustomAnimations( //设置动画 R.anim.enter_from_right, R.anim.exit_to_left, R.anim.pop_enter_from_left, R.anim.pop_exit_to_righ ) replace(R.id.fragmentContainerView, target) //切换到那个界面 setReorderingAllowed(true) //允许重新排序 if (addToStack){ // 是否加到栈里入栈 addToBackStack(null) } }}
在PinLoginFragmen中即可这样简写
navigateTo(PinRegisterFragment(),true)
2023.7.19
再次完善封装Fragment.navigateTo方法
传入的参数addToStack默认为true
为了方便从下往上弹的动画,为进入和进出动画提供默认值(一种是左右进出、另一种是上下进出)
参数将必须指定的放在前面,后面提供默认值,若不填写则使用默认
fun Fragment.navigateTo( target: Fragment, enterAnim:Int = R.anim.enter_from_right, exitAnim:Int = R.anim.exit_to_left, popEnter:Int = R.anim.pop_enter_from_left, popExit:Int = R.anim.pop_exit_to_righ, addToStack:Boolean = true){ parentFragmentManager.commit { setCustomAnimations(enterAnim,exitAnim,popEnter,popExit)//设置动画 replace(R.id.fragmentContainerView, target) //切换到那个界面 setReorderingAllowed(true) //允许重新排序 if (addToStack){ // 是否加到栈里入栈 addToBackStack(null) } }}
到此为止Fragment添加好了,其之间的切换关系也搞好了(框架搭建完毕)
下一步:搭建界面
------------------------------------------------------------------------------------------------------------------------------------------
界面搭建
res -> values ->colors统一颜色管理
文字白
小主体灰
提示文字深灰
切换蓝
<resources> <color name="text_white">#E5E5E5</color> <color name="text_gray">#999999</color> <color name="text_dark_gray">#5B5E63</color> <color name="text_black">#424242</color> <color name="red">#FF3333</color> <color name="light_blue">#6375FE</color> <color name="alpha_blue">#446375FE</color>
<color name="dark_blue">#000E45</color></resources>
res -> values ->thems全屏显示
去掉状态栏和
Mainfest里面有主题
android:theme="@style/Theme.LoginAnnie"
①该版本默认NoActionBar(不显示标题 标题栏(导航栏))
②去掉状态栏:
<item name="android:windowFullscreen">true</item>
运行后则全屏显示
改Fragment背景颜色为dark_blue并删掉textView
Bottom_sheet为alpha_blue
android:background="@color/dark_blue"
res -> values ->strings统一管理字符串(文本内容)
Fragment_pin_login
中添加一个textView 顶部64 id:titleTextView
字符串内容统一管理
strings里面可以更改app_name名称
<resources> <string name="welcome_title">欢迎回来</string> <string name="welcome_register">欢迎注册</string> <string name="login_subtitle">让我们一起努力学好Android开发拿到offer</string> <string name="register_subtitle">所以的结果都是从一个决定开始的</string> <string name="login_title">登录</string> <string name="register_title">注册</string></resources>
res -> values ->styles设置字体大小颜色粗细
<resources> <!--主标题--><style name="TextTitle"> <item name="android:textSize">36sp</item> <item name="android:textColor">@color/text_white</item> <item name="android:textStyle">bold</item></style><!--副标题--><style name="SubTextTitle"> <item name="android:textSize">10sp</item> <item name="android:textColor">@color/text_gray</item></style><!--提示用户操作的文本样式--><style name="AlertTextTitle"> <item name="android:textSize">18sp</item> <item name="android:textColor">@color/light_blue</item></style></resources>
调用:在xlm对应textView控件中
style="@style/TextTitle"
2023.8.26
添加白色背景
在fragment_pin_login.xml中添加View roundBgView
<View android:id="@+id/roundBgView" android:layout_width="0dp" android:layout_height="250dp" android:layout_marginStart="22dp" android:layout_marginTop="56dp" android:layout_marginEnd="22dp" android:background="@drawable/shape_round_corner" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/switchToPatternView" />
Background用绘制资源设置圆角矩形
res -> drawable -> Drawable Resource File -> shape_round_corner
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <corners android:radius="13dp"/> <solid android:color="@color/white"/></shape>
手动添加Button Button -> TextView
<TextViewTextView扮演按钮 android:id="@+id/button" android:layout_width="0dp" android:layout_height="58dp" android:layout_marginStart="10dp" android:layout_marginEnd="10dp" android:background="@drawable/shape_button_round_corner" android:text="@string/login_title" android:textColor="@color/text_white" android:textStyle="bold" android:gravity="center" android:textSize="18sp" />
res -> drawable ->Drawable Resource File -> shape_button_round_corner_blue
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <corners android:radius="29dp"/> <solid android:color="@color/light_blue"/></shape>
添加用户输入框
res -> layout -> New Resource File -> layout_user_input xml文件统一管理//最终未使用这个
①文本 TextView -> title
②输入框 EditText -> 输入
③承载输入内容的分割线 View -> 分割线(出现了5次)
为了可以重复利用且更灵活,三者作为一个整体放在容器ViewGroup中
[从上至下 -> LinearLayout]
1.xml方式让控件形成一个统一整体然后再进行复用
res -> layout -> New Resource File -> UserInputView
Root element:LinearLayout
---EditText输入框:默认就有一条直线,这条线是贴在输入文字上的,而且和该控件底部存在内间距,去掉方法:background = “null”
---额外添加一条线:添加一个View,高度为1dp,再调整一下上间距即可
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="wrap_content"> <TextView
android:id="@+id/titleTextView"
android:layout_width="match_parent" android:layout_height="wrap_content" android:text="用户名" android:textColor="@color/text_black" android:textSize="14sp" /> <EditText
android:id="@+id/inputEditText" android:layout_width="match_parent" android:layout_height="45dp" android:hint="请输入用户名" android:textSize="15sp" android:background="@null"/>//去掉自带的下划线 <View android:layout_width="match_parent" android:layout_height="1dp" android:background="@color/text_gray" android:layout_marginTop="5dp"/></LinearLayout>
在fragment_pin_login中引用<include layout = “@layout/layout_user_input”/>引用
注意要重新写layout_width和layout_height
用xml的局限:
只能实现布局,不能灵活配置
不能灵活改变里面的文字,需要用代码来操作
优点:布局方便-> xml中拖拽控件 快速设置对应属性
使用UserInputView类关联layout_user_input.xml布局文件
使用一个类来关联这个layout布局文件:
xml完成布局
代码实现逻辑(用一个界面来管理视图View/ViewGroup)
①建一个UserInputView类继承于LinearLayout,重写构造方法
次构造函数/**使用代码创建一个控件时调用这个构造方法*/ constructor(context:Context):super(context){}
/**在xml中添加一个控件,并设置对应属性就调用这个构造方法*/ constructor(context: Context, attrs: AttributeSet?):super(context,attrs){}/**在xml中添加一个控件并设置了style样式就会调用这个构造方法*/ constructor(context: Context, attrs: AttributeSet?, style:Int):super(context, attrs,style){}
提供主构造方法
class UserInputView(context: Context,attrs:AttributeSet?) :LinearLayout(context,attrs){}
当一个对象被创建时:1.构造函数2.init方法
②在init方法中实现View和xml中布局视图关联
创建一个对象又想做额外的事情就到init里面
将layout布局文件和当前这个类相关联
//ViewGroup
class UserInputView(context: Context,attrs:AttributeSet?) :LinearLayout(context,attrs){ //找到控件外部不能直接访问 private var titleTextView:TextView private var inputEditText: EditText init { val layoutInflater = LayoutInflater.from(context) val view = layoutInflater.inflate(R.layout.layout_user_input,null,false) //创建布局参数 view在FrameLayout中如何显示 val lp = FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT ) //将解析出来的View添加到当前容器中显示出来 addView(view,lp) //获取对应控件解析出View里面所有需要配置的控件 titleTextView = view.findViewById(R.id.titleTextView) inputEditText = view.findViewById(R.id.inputEditText) } //暴露给外部使用这些方法配置信息 fun setTitle(title:String){ titleTextView.text = title } fun setPlaceholder(text:String){ inputEditText.hint = text }}
Fragment_pin_login中就不用<include了
在PinLoginFragment中配置用户名和密码
使用viewBinding绑定
binding.nameInputView.setTitle("用户名")binding.passwordInputView.setTitle("请输入用户名")binding.nameInputView.setPlaceholder("密码")binding.passwordInputView.setPlaceholder("请输入密码")
使用绑定后
class UserInputView(context: Context,attrs:AttributeSet?) :LinearLayout(context,attrs){ //使用的时候再去解析懒加载 private val binding:LayoutUserInputBinding by lazy { LayoutUserInputBinding.inflate(LayoutInflater.from(context)) } init { val layoutInflater = LayoutInflater.from(context) val view = layoutInflater.inflate(R.layout.layout_user_input,null,false) //创建布局参数 view在FrameLayout中如何显示 val lp = FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT ) //将解析出来的View添加到当前容器中显示出来 addView(binding.root,lp) } //暴露给外部使用这些方法配置信息 fun setTitle(title:String){ binding.titleTextView.text = title } fun setPlaceholder(text:String){ binding.inputEditText.hint = text }}
*************************
2023.8.27
自定义View
给自己定义的控件在xml中设置对应属性
自定义属性:让这些属性在xml中可以直接使用,就像使用系统的一样
//不用↓以下方式
Fragment_pin_login中就不用<include了
在PinLoginFragment中配置用户名和密码
使用viewBinding绑定
binding.nameInputView.setTitle("用户名")binding.passwordInputView.setTitle("请输入用户名")binding.nameInputView.setPlaceholder("密码")binding.passwordInputView.setPlaceholder("请输入密码")
res ->valuse -> New Resource File -> attrs
1.创建attr.xml文件 管理自己定义的属性res ->valuse -> New Resource File -> attrs
声明样式----><declare-styleable name="UserInputView">
添加属性设置name和对应的类型
输入的密码是不能看见的,再定义一个input_type用枚举
<resources> <declare-styleable name="UserInputView"> <!--标题--> <attr name="title" format="string|reference"/> <!--默认提示内容--> <attr name="placeholder" format="string|reference"/> <!--设置类型:密码输入 or 正常输入--> <attr name="input_type" format="integer"> <enum name="password" value="1"/> <enum name="normal" value="2"/> </attr> </declare-styleable></resources>
2.fragment_pin_login.xml中使用自定义的属性
app:title="密码"app:placeholder="请输入密码"app:input_type="password"
3.把自定义的属性关联上去,在自定义的View中 解析对应的属性//解析属性的值
只有在init中可以直接访问attrs, 在init中解析对应的属性(拆包)
//解析xml中自定义的属性 -> 拆包//从attrs里面解析出R.styleable.UserInputView里面自定义的对应的属性和值 val typedArray = context.obtainStyledAttributes(attrs,R.styleable.UserInputView)//从typedArray中解析每一个属性的值取出attrs.xml中定义的值并使用 binding.titleTextView.text = typedArray.getString(R.styleable.UserInputView_title) binding.inputEditText.hint = typedArray.getString(R.styleable.UserInputView_placeholder) val inputType = typedArray.getInteger(R.styleable.UserInputView_input_type,2) if (inputType == 1){ //设置密码不可见 binding.inputEditText.inputType =
InputType.TYPE_TEXT_VARIATION_PASSWORD
or InputType.TYPE_CLASS_TEXT }//回收 typedArray.recycle()
运行一下木有问题
切换到图案解锁:有用户显示,无用户不显示
PinLoginFragment的onViewCreated中
//设置切换到图案解锁的显示与隐藏binding.switchToPatternView.visibility = if (UserManager.sharedInstance(requireContext()).hasUser()){ View.VISIBLE}else{ View.INVISIBLE}
测试:删除userInfo文件,默认无用户状态,运行不显示切换到图案密码解锁
登录按钮:无用户不能点击
binding.button.isEnabled = UserManager.sharedInstance(requireContext()).hasUser()
根据状态切换背景
res->drawable -> New Resource File -> selector_login_btnselector自动切换drawable资源
<!--selector 选择器给系统使用注意:必须给drawable资源--><selector xmlns:android="http://schemas.android.com/apk/res/android"> <!--当控件的enabled状态是false 显示color为灰色--> <item android:drawable= "@color/text_gray" android:state_enabled="false"/> <!--true 显示color为蓝色--> <item android:drawable= "@color/light_blue" android:state_enabled="true"/></selector>
运行一下看一看
但是用selector之后圆角就不能用了
可以在drawable资源中再加一套灰色圆角矩形
res -> drawable ->Drawable Resource File -> shape_button_round_corner_gray
再把selector改为:
<selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:drawable = "@drawable/shape_button_round_corner_gray" android:state_enabled="false"/> <item android:drawable = "@drawable/shape_button_round_corner_blue" android:state_enabled="true"/></selector>
还没注册?现在注册
前灰后黑两个textView
defaultTextView registerTextView
//跳转到注册页面binding.registerTextView.setOnClickListener { navigateTo(PinRegisterFragment())}
------------------------------------------------------------------------------------------------------------------------------------------
滑动解锁:九个点自定义绘制View实现
UnlockView
分析:
①自定义View有两种情况:告诉我们大小,未告知大小所以要先确定正方形区域
②绘制圆点背景
③确定圆的半径:
上中下都有间距space
(width-4*space)/6
23.8.28
Tools -> 加方法View.dp2px
View里面就有context直接给View加扩展即可
fun View.dp2px(dp:Int):Int{ return (context.resources.displayMetrics.density*dp).toInt()}
测量尺寸(测量宽度、高度):onMeasure
在onMeasure中进行
确定宽度:else需要自己算对应尺寸 -->需要用到默认的尺寸
宽度由半径和间距确定的
提供默认半径和默认间距//全局的
确定高度:相同复制粘贴即可
private var mRadius = dp2px(40)//默认半径private var mSpace = dp2px(40)//默认间距override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { super.onMeasure(widthMeasureSpec, heightMeasureSpec) var mWidth = 0 var mHeight = 0 //确定高度 val widthMode = MeasureSpec.getMode(widthMeasureSpec) val widthSize = MeasureSpec.getSize(widthMeasureSpec) mWidth = when(widthMode){ MeasureSpec.EXACTLY -> widthSize else -> 6*mRadius + 4*mSpace } //确定宽度 val heightMode = MeasureSpec.getMode(heightMeasureSpec) val heightSize = MeasureSpec.getSize(heightMeasureSpec) mHeight = when(heightMode){ MeasureSpec.EXACTLY -> heightSize else -> 6*mRadius + 4*mSpace }
setMeasuredDimension(mWidth,mHeight)
}
确定正方形区域:onSizeChanged
宽度和半径不一定就是我们刚给的值,View不一定刚好是一个矩形区域——要确定正方形区域就是在确定绘制点的起始坐标点x,y
起始点x,起始点y确定每个圆在哪个位置画
定义变量:就是绘制点的起始点
当尺寸发生变化:即从无到有那一刻,一旦确定下来,我的正方形也就确定下来了
起始点坐标:
mRadius+mSpace 空+mRadius+mSpace
①半径不确定:可能会变大变小
所以首先要计算半径——以最小边为正方形来进行计算:(min-4*mSpace)/6
②确定起始点的中心点cx,cy
得到最小边squareSize()
会重复多次用到,就直接提取该方法
***
private fun squareSize() = Math.min(width,height)
***
private var mStart_cx = 0f //第一个点的起始坐标点全局变量private var mStart_cy = 0f
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { super.onSizeChanged(w, h, oldw, oldh) //计算半径 mRadius = (squareSize()-4*mSpace)/6 //确定起始点的中心点cx,cy mStart_cx = (width-squareSize())/2 + mSpace + mRadius.toFloat() mStart_cy = (height-squareSize())/2 + mSpace + mRadius.toFloat()}
绘制:onDraw
尺寸确定完了就可以去绘制,调用onDraw方法
绘制九个圆点drawNineDot()
先提供绘制圆点的画笔——抗锯齿、颜色(在color中统一管理)、填充(style)
********
//默认状态时的背景画笔private val mDotPaint:Paint by lazy { Paint().apply { isAntiAlias = true color = context.resources.getColor(R.color.dot_bg_color,null) style = Paint.Style.FILL }}
九个圆点用循环就可以了,一行一行地画,三行三列
九个点cx,cy不相同,cx和cy确定即可
*********
private fun drawNineDot(canvas: Canvas?){ for (i in 0 until 3){ //控制行数 for (j in 0 until 3){ //控制列数 val cx = mStart_cx + j*(2*mRadius + mSpace) val cy = mStart_cy + i*(2*mRadius + mSpace) canvas?.drawCircle(cx,cy,mRadius.toFloat(),mDotPaint) } }}
运行一下:在fragment_pattern_login里面显示unlockView
<com.example.loginannie.view.views.UnlockView android:id="@+id/unlockView" android:layout_width="0dp" android:layout_height="0dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintDimensionRatio="h,1:1"
PinLoginFragment中默认先不要判断有没有用户(注释掉),因为前面为了测试把用户信息删了
先直接给”切换到图案密码解锁”添加点击事件直接跳转到图案登录
binding.switchToPatternView.setOnClickListener { navigateTo(PatternLoginFragment())}
——————
思考:如何记录密码:用编号
怎么知道一个点被点亮了:判断触摸点在哪儿
点亮:就是在上面再重新绘制圆
怎么知道圆心在哪儿?
九个点因为是被画出来的,所以在代码层面是不存在的,
但是我们要知道它们是存在的,那就进行封装
[但凡遇到要把多个东西集中到一个点/控件上去就对它进行封装]
DotModel() 封装数据模型(封装圆点信息)把分散的数据集中化统一管理
包models(放所有的模型)
没有控件但是我们可以每绘制一个点就产生一个模型和它一一对应就可以了
用枚举记录是否点亮(点亮的状态)
data class DotModel ( val num:Int ,//保存编号记录密码 val cx:Float,//中心点坐标x,y -> 绘制点亮时的圆 val cy:Float, val radius:Float,//半径 var state:DotState = DotState.Normal//选中状态)//枚举记录点的状态正常选中错误enum class DotState{ Normal,Selected,Error}
————————————————————————
一旦绘制好了一个圆就应该把这个圆的模型数据给存好
有圆的模型数据就要有对应的属性与之相关联
*******private val mDotModels = arrayListOf<DotModel>() //存放九个点的模型数据
一旦绘制就封装当前这个点模型数据
需要计算点的编号
******
var index = 1 //记录点的序号
加到封装的模型数组里面
******mDotModels.add( //封装点的模型数据
//++在后面 -> 先拿去做事再++ DotModel(index++,cx,cy,mRadius.toFloat()))
onTouchEvent() 触摸状态
它本身就是一个View可以直接重写onTouchEvent
直接return true 直接消费了,不需要继续往下传
touchBegin() 触摸开始 要知道触摸点x,y(DOWN和MOVE)
DOWN按下去和MOVE移动的时候都会产生x和y
触摸开始就要判断触摸点有没有在一个圆点内部
↓
isInDot() 是不是在某一个圆点内
要知道:是否在某个圆点内部、这个点有没有点亮、和上一个点链接的编号值(需要知道具体内容)
具体的信息在mDotModels里面存着,所以就直接返回DotModel模型即可(可选)
去做一个遍历即可
需要知道圆点的矩形区域
↓
在DotModel模型数据内部添加一个方法containsPoint()
让模型自己判断一个点是否在自己内部,外部不知道,自己是最知道的
data class DotModel (
val num:Int,val cx:Float,val cy:Float,val radius:Float,var state:DotState = DotState.Normal){ //判断是否包含某个点 fun containsPoint(x:Float,y:Float):Boolean{ val rect = RectF(cx-radius,cy-radius,cx+radius,cy+radius) return rect.contains(x,y) }}
循环遍历每一个圆点,判断这个圆点是否包含触摸点x,y,如果是就把model返回出去
*****
private fun isInDot(x:Float,y:Float):DotModel?{ mDotModels.forEach { if (it.containsPoint(x,y)){ return it } } return null}
——————isInDot()全
触摸上来了就找到是否有那个被触摸的圆点
不为空则已经进入到某个触摸点内部了
判断是否点亮,没点亮就点亮
点亮->绘制一个圆
调用invalidate()重新绘制
怎么知道要画个圆呢?画哪个圆呢?——有一个容器专门用来保存我要绘制哪些圆
保存选中的区域(这个区域保存了那肯定要绘制它)
点亮:画边框和小圆点需要改变画笔
绘制完了怎么知道是第一次绘制还是后面在绘制
1.绘制背景
2.判断状态
第一次绘制:绘制背景加到mDotModels
注意调用invalidate()重新绘制不能重复加到数组里面,只能加一次
怎么知道加进数组还是不加:
根据目前数组的size长度是否小于9判断要不要加
如果index < mDotModels.size 就不要加进去了
如果index > mDotModels.size 说明是新的未加进去过的点,加进去
直到加到9
如果已经加满都不用加进数组了,如果又重新绘制圆说明其状态可能改变了,就要把该点拿过来判断它的状态
正常状态啥都不用做
选中状态:就加个边框、中间加圆
改状态就是在绘制圆更改画笔
****************************
//选中时空心边框圆private val mDotSelectPaint:Paint by lazy { Paint().apply { isAntiAlias = true color = context.resources.getColor(R.color.light_blue,null) style = Paint.Style.STROKE //只描边 strokeWidth = dp2px(2).toFloat() //描边涉及边的粗细 }}//选中时中心小圆private val mDotCenterPaint:Paint by lazy { Paint().apply { isAntiAlias = true color = context.resources.getColor(R.color.light_blue,null) style = Paint.Style.FILL //中心实心圆 }}
根据状态绘制不同的圆
中心点圆的半径不写死,和外面大圆成一定的比例
************************************
private var mCenterDotRadius = 0f //中间小圆点的默认半径
mCenterDotRadius = mRadius.toFloat()/6
when(mDotModels[index-1].state){ DotState.Selected -> { //选中状态 canvas?.drawCircle(cx,cy,mRadius.toFloat(),mDotSelectPaint) canvas?.drawCircle(cx,cy,mCenterDotRadius,mDotCenterPaint) } DotState.Error -> { //错误状态 mDotSelectPaint.color = resources.getColor(R.color.red,null) canvas?.drawCircle(cx,cy,mRadius.toFloat(),mDotSelectPaint) mDotCenterPaint.color = resources.getColor(R.color.red,null) canvas?.drawCircle(cx,cy,mCenterDotRadius.toFloat(),mDotCenterPaint) } else -> {}}
private fun drawNineDot(canvas: Canvas?){ var index = 1 //记录点的序号 for (i in 0 until 3){ //控制行数 for (j in 0 until 3){ //控制列数 val cx = mStart_cx + j*(2*mRadius + mSpace) val cy = mStart_cy + i*(2*mRadius + mSpace) canvas?.drawCircle(cx,cy,mRadius.toFloat(),mDotPaint)
//判断dot状态 if (index > mDotModels.size) { //说明还不足9个点第一次绘制加进去 //封装点的模型数据 mDotModels.add( DotModel(index, cx, cy, mRadius.toFloat()) ) }else{ //已经绘制过了无需再加进数组需要判断状态 //根据状态画圆 when(mDotModels[index-1].state){ DotState.Selected -> { //选中状态 canvas?.drawCircle(cx,cy,mRadius.toFloat(),mDotSelectPaint) canvas?.drawCircle(cx,cy,mCenterDotRadius,mDotCenterPaint) } DotState.Error -> { //错误状态 mDotSelectPaint.color = resources.getColor(R.color.red,null) canvas?.drawCircle(cx,cy,mRadius.toFloat(),mDotSelectPaint) mDotCenterPaint.color = resources.getColor(R.color.red,null) canvas?.drawCircle(cx,cy,mCenterDotRadius.toFloat(),mDotCenterPaint) } else -> {} } } index++ } }}
九个点绘制完毕
***********************
2023.8.29
记录密码
点亮记录密码是在up之后
绘制路径
绘制线drawLine()
点的内部直接连接线
需要用path连接:线长什么样子
滑到空白的地方无线,除非已经点亮一个点了
先画线再画点,让点盖住线
①先创建一条线↓
private var mPath = Path()
②画线的笔↓
private val mLinePaint:Paint by lazy { Paint().apply { isAntiAlias = true color = resources.getColor(R.color.light_blue,null) style = Paint.Style.STROKE strokeWidth = dp2px(2).toFloat() }}
线不为空才去画要不然不画如果路线为空啥都不用做,直接return结束
if (mPath.isEmpty) return
*****
绘制里面onTouchEvent()方法最重要,因为我们在随时随地在移动
当点亮第一个点的时候我们需要记录一下当前这个点
当点亮第二个点就要在此之间画一个线
记录上一个点,没有控件但是有model
记录上一个被点亮的点的model即可
可能为空,一开始运行起来就是空
private var lastSelectedDotModel:DotModel? = null
就可以直接用点的中心点x,y画线即可
在onTouchEvent()方法里面会调用touchBegin(),更重要的事情都在touchBegin()里面做
点到某个点的内部:
1.改变点的状态 重新绘制
2.判断有没有上一个点
①没有:该点就是起始点,是第一个点,记录一下当前点
把路径的起点设到这个点
if (lastSelectedDotModel == null){ //是第一个点记录 lastSelectedDotModel = dot //当前第一个点就是路径的起点 mPath.moveTo(dot.cx,dot.cy) }
②有:拉一条线,连接两个点
mPath.lineTo(dot.cx,dot.cy)
运行一下:选中过的点无法在之间连线
只有是normal的才可以点亮
如果已经选中过了,变成非normal,则已选中的两点无法连线
如果想让其之间也可以连线,则应去掉↓判断
if(dot.state == DotState.Normal)
运行一下:可以都连接了,但是出现重复连接,会导致密码记录出现问题(可能看到的是123 但是密码可能是123321123321123)虽然看不到但是有在记录
细节问题:已经点亮了某个点了,手指继续在点里面滑动,上一个点和当前点永远是自己,在自己点的内部中心点反复持续划线
所以非第一个点时不能直接lineTo划线,自己在自己内部就没有必要了
if (lastSelectedDotModel != dot) {
MyLog.v("点亮:${dot.num}") mPath.lineTo(dot.cx, dot.cy)}
运行一下:用MyLog.v("点亮:${dot.num}")打印看一下效果,出现一直点亮的问题
这是没有切换到上一个点到当前点,应↓
if (lastSelectedDotModel != dot) { //只有两个点不相同时再画 MyLog.v("点亮:${dot.num}") mPath.lineTo(dot.cx, dot.cy) lastSelectedDotModel = dot}
运行一下:此时已经解决同一点反复点亮的问题,但是会一条线反复来回点亮
思考:密码该如何记录
2023.9.11周一
两点之间已经连过线就不要再重复连线了
线有过就不要再连了
用一个变量记录已经连过的线
private var mSelectedLines = arrayListOf<Int>() //保存已经点亮的线的数值
此数值就是12 56 小值的点的num在前 大的在后
需要两个点的num值形成一个数据,到数组里面判断是否已经有了
独立出一个方法
得到两点之间的线lineValue()
先得到线的值,再判断mSelectedLines数组里面是否已经包含这个线了
private fun lineValue(first:DotModel,second:DotModel):Int{ return Math.min(first.num,second.num)*10 + Math.max(first.num,second.num)}
if (!mSelectedLines.contains(lineValue(lastSelectedDotModel!!,dot)))
才可连接
连完之后将连接完的加到mSelectedLines数组里面
val lineValue = lineValue(lastSelectedDotModel!!,dot)if (!mSelectedLines.contains(lineValue)) { //两点之间连线 MyLog.v("连接:${dot.num}") mPath.lineTo(dot.cx, dot.cy) lastSelectedDotModel = dot mSelectedLines.add(lineValue)}
运行一下: MyLog.v("连接:${dot.num}")可以解决重复连线的问题了
实现线跟着手一起拽动的效果
将上一个点和滑动过程中产生的新的点连接起来就可以了
touchBegin()
else{} ——> 触摸点没有在某个圆点内部
分析:先判断有没有上一个点,没有就在旁白处空划,不做任何事
有上一个点就以该点中心为起始点连一条线
lastSelectedDotModel != null
else{ //触摸点没有在某个点的内部 if (lastSelectedDotModel != null) { //从上一个点和当前点连成一条线 mPath.lineTo(x,y) invalidate() }}
运行一下:出现画曲线的情况
应重新设置起点,将起点改为lastSelectedDotModel
mPath.moveTo(lastSelectedDotModel!!.cx,lastSelectedDotModel!!.cy)
再mPath.lineTo(x,y)
运行一下:出现扇形
因为永远都是从中心点不断连线
但是我们先只要一条线
解决:开一条新的从上一个点和在活动点的Path即可
即开一条移动的线的路径
private var mMoveLinePath = Path() //移动的线的路径
永远只需要一个从上一个点到现在的线
先重置一下,再移动到下一个点
需要再draw里面画这个路径
private fun drawLine(canvas: Canvas?){ if (mPath.isEmpty) return canvas?.drawPath(mPath,mLinePaint) //绘制圆点之间的连接线 if (mMoveLinePath.isEmpty) return canvas?.drawPath(mMoveLinePath,mLinePaint) //绘制移动时跟着手移动的线}
运行一下:可以跟着手移动了,但是会出现同时出现两条线的那一瞬间
说明:未将移动的线的起始点移到当前点
两点直接连接线之后要清一下mMoveLinePath,且移到当面点的中心点
在连接连个点之间的线之后↓
//移动的线mMoveLinePath.reset()mMoveLinePath.moveTo(dot.cx,dot.cy)mMoveLinePath.lineTo(x,y)
运行一下:正常了,但是不能在圆点内部实现线随着手在内部移动
圆点内部实现线随着手在内部移动
在圆点内部需要做
封装一下
mMoveLinePath.reset()mMoveLinePath.moveTo(lastSelectedDotModel!!.cx,lastSelectedDotModel!!.cy)mMoveLinePath.lineTo(x,y)
[if !supportLists]1. [endif]直接点到点的外面,没有上一个圆点return
[if !supportLists]2. [endif]触摸点没有在某个圆点内部,且有上一个点,则从上一个点和当前点连成一条线
[if !supportLists]3. [endif]触摸点在圆点内部滑动,线也要跟着手移动
[if !supportLists]4. [endif]两点之间连完线要接着确定拉伸线的路径
到此图案解锁的绘制的逻辑完成
下一步完成松手的逻辑
****************
private fun touchBegin(x:Float,y:Float){ val dot = isInDot(x,y) if (dot != null){ //在某个圆点内部 //判断这个点是不是第一个点 if (lastSelectedDotModel == null){ //是第一个点记录 lastSelectedDotModel = dot //当前第一个点的中心点就是路径的起点 mPath.moveTo(dot.cx,dot.cy) }else{ //非第一个点连线 && 上一个点非自己在自己内部就不要画 if (lastSelectedDotModel != dot) { //只有两个点不相同时再画线 //判断这条线是否已经画过了 val lineValue = lineValue(lastSelectedDotModel!!,dot) if (!mSelectedLines.contains(lineValue)) { //两点之间连线 mPath.lineTo(dot.cx, dot.cy) lastSelectedDotModel = dot mSelectedLines.add(lineValue) //移动的线 addMoveLine(x,y) } }else{ //圆点内部,线随着手移动 addMoveLine(x,y) } } dot.state = DotState.Selected //点亮 -> 绘制一个圆 invalidate() }else{ //触摸点没有在某个点的内部没有点到某个点上 if (lastSelectedDotModel != null) { //从上一个点和当前点连成一条线 addMoveLine(x,y) invalidate() } }}
2023.9.12周二
松手逻辑touchEnd()
获取密码
清空
注意:这个View只是完成绘制这一单一功能,密码产生
至于外部拿密码做什么(根据密码改变颜色)那就是外部的事情了(判断密码逻辑)
先做密码:
有一个变量记录密码
private val mPasswordBuilder = StringBuilder()
在touchBegin()里面但凡有点的点亮就把它记录起来
①第一个点
②有lineTo()的地方
//记录当前点的值mPasswordBuilder.append(dot.num)
touchEnd()中
//获取密码val password = mPasswordBuilder.toString()
清空单独写一个方法,因为不只有密码记录结束后需要清空,红色的点亮完后也需要清空
清空clear()
需要清空密码、重置线、清空已经连接的线、已经记录的点,并重新绘制
*********
private fun clear(){
postDelayed({ //密码 mPasswordBuilder.clear() //重置线 mPath.reset() mMoveLinePath.reset()
//清空上一个记录的点
lastSelectedDotModel = null //清空已经连接的线 mSelectedLines.clear() //清空已经记录的点 mDotModels.clear()
//清理完需要重新绘制 invalidate()
},500)}
不能立刻清空,需要时间延迟postDelayed()
运行一下:可实现抬手清空
回调数据给外部
抬手之后清空之前,需要把密码回调给调用者,把当前这个控件也传给调用者(因为需要用控件里面的方法)
private var mCallBack:((UnlockView,String)->Unit)? = null //回调数据
给外部提供一个公开方法用于调用
//外部监听密码绘制结束的监听器fun addDrawFinishedListener(callback:(UnlockView,String) -> Unit){ mCallBack = callback}
**********private fun touchEnd(){ //获取密码 val password = mPasswordBuilder.toString() //清空 clear() //数据回调 mCallBack?.let { it(this,password) }}
回调数据给外部:
①函数声明:将外部传给我的实现保存住
private var mCallBack:((UnlockView,String)->Unit)? = null //回调数据
②给外部一个接口(一个方法),通过这种方式传给我的
//外部监听密码绘制结束的监听器fun addDrawFinishedListener(callback:(UnlockView,String) -> Unit){ mCallBack = callback}
③真正回调:调用这个函数
//数据回调 mCallBack?.let { it(thiss,password) //调用这个函数,password是我的参数 }
这样就传过去了
数据是回调给PatternLoginFragment的
给PatternLoginFragment添加binding
view创建完之后是需要获取其事件的,PatternLoginFragment通过调用addDrawFinishedListener{}得到密码
只有密码还不够,还要支配你,可能需要你额外做一些事情,如让你显示错误
显示错误showError()
但是在此之前的做法是还未显示错误就已经全部清空了
提供两种状态,正常状态和错误状态
做完之后需要清一下但是又不能全部给清空了
mPath
mDotModels需要保存
矛盾:不清的话就会一直显示
清的话就无法显示红色了
做一个备份,将要的东西存着
//备份private var mBackupPath:Path? = null //备份连接线的路径private var mBackupModels:ArrayList<DotModel>? = null //备份圆点的model
圆点不用在此处更画笔改为红色,因为在绘制圆点drawNineDot()的时候会根据其状态改变颜色的
在PatternLogin里面调用showError()
**********
fun showError(){ //恢复数据 mPath = mBackupPath?:Path() //没有路径就创建一个空的 mDotModels = mBackupModels?: arrayListOf() //没有对象就创建空的数组 //显示错误颜色改变画笔颜色 mLinePaint.color = resources.getColor(R.color.red,null) //恢复完重新绘制一下 invalidate()}
测试一下:不可以
******
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding.unlockView.addDrawFinishedListener {unlockView, s -> unlockView.showError() }}
在touchEnd()中先清空再保存
测试一下:
应该在清理之前,一抬手就先做个备份,不能delay五毫秒再备份
运行一下:出现红线后又立马消失,是时机的问题
,touch()中先备份,备份完就去清空了,清空会延迟500毫秒做,调用clear()
还没开始执行清空的时候就去回调给外部了,外部立刻就调用showError(),showError()里面就会恢复线点,显示红,此刻0.5秒到了就clear()全部清空了
---------
private fun touchEnd(){ //获取密码 val password = mPasswordBuilder.toString()
//备份 mBackupPath = Path(mPath) mBackupModels = ArrayList(mDotModels) //清空 clear() //数据回调 mCallBack?.let { it(this,password) }}
运行一下:不要延迟500毫秒了,再运行会持续留下,不会消失,为什么点没有变红?:因为我们没有改变点的状态值
showError()中添加
//改变点的状态值使其变成红色mDotModels.forEach { if (it.state == DotState.Selected){ it.state = DotState.Error }}
运行一下:再运行就是红点了,但是会可以再画,应该控制一下,任何操作都不能再做了
如果控制不能再做其他任何操作了:
给一个开关
private var canTouch = true
但是进入到canTouch ()中之后就不能Touch 了
同样的在touchBegin()中也需要做判断
if (!canTouch) return
运行一下:这样变红之后就不能再画了,但是不能一直红着如何恢复呢
showError()之后就将canTouch改为true,再clear()一下,且备份的东西也不要
运行一下:可以消失并进行下一次滑动了,但是下一次滑动开始也是红色的
因为线的画笔还是红色的,再改一次,点还是红色的是因为上面画九个点的时候没有改selected状态时的画笔颜色就直接绘制了(错误的时候就一直保持错误时画笔的颜色了)
******************
fun showError(){ canTouch = false //恢复数据 mPath = mBackupPath?:Path() //没有路径就创建一个空的 mDotModels = mBackupModels?: arrayListOf() //没有对象就创建空的数组 //改变点的状态值使其变成红色 mDotModels.forEach { if (it.state == DotState.Selected){ it.state = DotState.Error } } //显示错误颜色改变画笔颜色 mLinePaint.color = resources.getColor(R.color.red,null) //恢复完重新绘制一下 invalidate() //变红之后恢复 postDelayed({ mLinePaint.color = resources.getColor(R.color.light_blue,null) mBackupModels = null mBackupPath = null canTouch = true clear() },500)}
自定义View部分结束
------------------------------------------------------------------------------------------------------------------------------------------
2023.9.15周五
SharedViewModel()
分析项目需不需要ViewModel,是否需要共享ViewModel
应该共享users数据
在loadAllUserInfo的时候其实应该有一个返回值,返回所有用户信息,然后在ViewModel里面存着
ViewModel的更新更界面的监听有什么关系?
需要监听切换到图案解锁:它的显示与隐藏的状态是需要实时更新的
登录按钮:监听用户名和密码两个数据是否输入完毕,只有全部输入完毕才能点击,否则不能
view -> viewmodels(包)
新建一个需要共享的ViewModel:SharedViewModel,继承于ViewModel()
图案解锁的显示与否需要监听:它的状态是由数据来更改的
把它定义成一个类型,这个状态是可以改变的,默认值是false(注意不能私有化,需要给外部监听)
val showChange = MutableLiveData(false)
怎么知道是true还是false呢?
切换到图案解锁只有存在用户信息的时候才会显示
loadAllUserInfo在MyApplication时加载,此时还没有界面,SharedViewModel无法存对应的数据
应对方法:写一个init方法,在初始化创建我这个对象的时候就确定”切换到图案解锁”要不要显示
init { UserManager.sharedInstance()}
这个需要context,所以我们就使用AndroidViewModel()这个需要外部传入application的
一会可能会多次调用UserManager里面的属性或者方法,所以将其单独摘出来,方便使用,就不用每次都写一次了
private var userManager = UserManager.sharedInstance(application.applicationContext)
程序运行起来就要进入这个init方法里面来,在init中调用hasUser()方法,如果有用户showChange 的值是true,可以直接这么写↓
init { showChange .postValue(userManager.hasUser())
//如果有用户就直接把hasUser()的true post给showChange }
此时showChange 的状态已经确定,下一步→监听
谁来监听:Fragment来监听→PinLoginFragment来监听
此时数据在ViewModel中,去监听ViewModel
①共享数据
private val viewModel:SharedViewModel by activityViewModels()
//可以获得当前activity所管理的ViewModel
注意只能使用val
②监听
//监听是否需要显示切换图案解锁的textViewviewModel.showChange.observe(viewLifecycleOwner){ binding.switchToPatternView.visibility = if (it) View.VISIBLE else View.INVISIBLE}
运行一下:不能显示切换到图案密码解锁,因为之前已经把加载的数据删除了,正确
按钮是否能点击的状态是切换
根据用户是否输入完毕,默认false不能点击
//记录按钮是否可以点击val loginBtnIsEnabled = MutableLiveData(false)
//监听登录按钮是否可以点击viewModel.loginBtnIsEnabled.observe(viewLifecycleOwner){ binding.button.isEnabled = it}
运行一下:
写界面:一个界面对应一个ViewModel,管理这个界面的所有数据
ViewModel可以实现对拥有者的生命周期的监听,其生命周期没有结束的时候ViewModel不会消失,好处:对一些我们需要长时间存在的数据,可以把数据放在ViewModel里面,如果数据的变化需要外面能感知到,那么可以使用LiveData(MutableLiveData是LiveData的实现子类),postVale设置它的值,一旦设置就会调给监听者,告知其发生变化
2023.9.16周六晚
共享ViewModel
在Fragment中想和activity(或者其他的Fragment)共享ViewModel就使用by activityViewModels(),就可以获取到当前这个activity管理的ViewModel,如果这个activity上面依附了很多Fragment那么就可以确定他们创建的ViewModel都是一样的
按钮是否能点击
只有用户名和密码都输入了才能点击
UserInputView
需要把正在输入的这件事情回调给外部(告诉外部这个地方已经开始输入了)
要知道输入框正在输入,要关注一下EditText
addTextChangedListener
监听输入框,内容改变了就把事件传出去
2023.9.17周日
输入状态的监听
正在发生改变,要把这个事件实时往外传
怎么回传数据:
想要外部传递东西就是回调数据
[if !supportLists]1. [endif]告诉外部发生变化的文本
[if !supportLists]2. [endif]外部只关心结果,不关系正在输入或删除的过程
[if !supportLists]3. [endif]当前这个对象本身也要传出去(可能需要改变显示错误的状态)
我调用你的时候不需要你返回给我,但是我要给你传参数(对象本身,文本内容的字符串)
事件回调
外部textChangedListener通过监听
//定义回调的高阶函数类型
var textChangedListener:((UserInputView,String) -> Unit)? = null
一旦发生改变就立刻回调出去,将当前文本内容传递给外部
textChangedListener?有没有,如果有就立刻调用自己textChangedListener()这个函数,函数的参数传(this本身,传当前文本内容text.toString)
******
binding.inputEditText.addTextChangedListener( //正在发生改变 onTextChanged = {text: CharSequence?, start: Int, before: Int, count: Int -> //将当前文本内容传递给外部 textChangedListener?.let { it(this,text.toString()) } })
谁要监听谁就去设置这个监听器就可以了
找到pin登录的界面PinLoginFragment
就要去监听了
外部监听到了这个事件要去干什么那就是外部的事情了
nameInputView和passwordInputView两个结合起来就要去判断登录按钮是否显示
两个同时必须同时有内容才可以显示
得到对应的输入内容
如果在nameInputView可以得到自己的text,只要知道password的就可以了
需要通过一个方法得到输入的内容
UserInputView还没有把当前输入的内容暴露给外部的方法
可以定义一个属性text(文本内容)
--
var text:String = “”//记录输入的文本内容
用var还是val产生的矛盾:text属性外部只能访问不能改变数据,内部可以改变数据
通常用两个属性
private var _text:String = "" //内部使用val text:String = "" //外部使用
当外部通过text访问的时候
要经过get方法,把_text内容给text即可
get() = _text
//但是没有必要这么写
可以通过binding直接找得到text(直接访问输入框里面的内容)↓
val text:String //外部使用
get() = binding.inputEditText.text.toString()
text.isNotEmpty() && binding.passwordInputView.text.isNotEmpty()
两个条件同时成立即可显示
那么如何改变按钮的状态呢?
显示与否的状态是SharedViewModel来做的
更改状态的话单独写一个方法出来
SharedViewModel中更改登录按钮状态changeBtnState()
只有两种状态两个值,用枚举类
***
//给外部提供改变状态的方法
fun changeBtnState(state: ButtonState){ loginBtnIsEnabled.postValue(state == ButtonState.Enabled)}
enum class ButtonState{ Enabled,UnEnabled}
****
PinLoginFragment
//监听用户名or密码输入框文本改变事件binding.nameInputView.textChangedListener = {userInputView, text -> if (text.isNotEmpty() && binding.passwordInputView.text.isNotEmpty()){ viewModel.changeBtnState(ButtonState.Enabled) }else{ viewModel.changeBtnState(ButtonState.UnEnabled) }}binding.passwordInputView.textChangedListener = {userInputView, text -> if (text.isNotEmpty() && binding.nameInputView.text.isNotEmpty()){ viewModel.changeBtnState(ButtonState.Enabled) }else{ viewModel.changeBtnState(ButtonState.UnEnabled) }}
运行一下:可以正常显示
再封装一下
initUI()
initListener()
登录按钮的操作
现在已经实现输入了
一点登录就要做登录要做的事情了
点了登录就要去登录用户名,把用户名和密码拿出来做登录操作
点击:完成对应的登录状态进行登录验证
注意:有了ViewModel所有的操作都是从ViewModel发出去,ViewModel就是管理数据的
想要做什么事就先在ViewModel中添加什么方法
(实际的应用里面不是在本地登录的,肯定是在网络中登录的,要把用户名拿到服务器上面去,从服务器上返回一个结果出来,才能返回一个结果给调用者,才能拿去做其他事情
注意:一个线程同一时间只能做一个事情)
//记录登录结果是否成功的状态(成功状态 or 失败状态)val loginState = MutableLiveData(LoginState.Default)
枚举三种状态:Success,Failure,Default
↓
//登录状态enum class LoginState{ Success,Failure,Default}
登录逻辑:一旦输入后点击登录按钮就进入login()方法里面来,login()要去找UserManager,UserManager整完得到结果给login(),再更改loginState的状态值,Fragment再监听状态值。(最终只关心实时更新的loginState的状态值)
再PinLoginFragment中任何东西的变化、任何的操作都要去找ViewModel
↓
//登录按钮的点击事件binding.button.setOnClickListener { //登录验证 viewModel.login(binding.nameInputView.text,binding.passwordInputView.text
,PasswordType.Pin)
}
要得到它的结果,去监听loginState
↓
//实时观察登录状态
viewModel.loginState.observe(viewLifecycleOwner){ Toast.makeText(context,"$it",Toast.LENGTH_LONG).show()}
在SharedViewModel的login()中真正的登录是在UserManager中完成的
↓
fun login(name:String,password:String,type:PasswordType){ val result = userManager.login(name,password,type) val state = if (result) LoginState.Success else LoginState.Failure loginState.postValue(state)}
运行一下:是登录不了的,因为没有文件,默认弹出default
登录按钮登录失败后的显示:
显示提示文本、文字显示红色、登录按钮震动
以属性的方式显示错误后的提示文本
添加自定义属性
1.values -> attrs
<!--错误时提示内容--><attr name="error_title" format="string|reference"/>
[if !supportLists]2. [endif]xml中配置属性
app:error_title="用户名不存在"
app:error_title="密码错误"
[if !supportLists]3. [endif]UserInputView中
(1)解析error_title
private lateinit var errorTitle:String
是不是空,不是空就返回,如果外部没有设置是空就显示原来title的内容
initUI()中↓
errorTitle = (typedArray.getString(R.styleable.UserInputView_error_title)?:binding.titleTextView.text) as String
(2)
还要给外部配置一下,显示正常还是显示提示错误
不做任何配置,显示的是正常状态
给外部提供一个方法配置状态
切换错误状态showError()
把标题和颜色改变一下
内部再写一个方法
得到正常or错误的状态showState()
注意:title要存一下,切换提示字的时候要用到
↓
private lateinit var title:String//记录提示文本
fun showError(){ showState(false)}
private fun showState(isNormal:Boolean){ if (isNormal){ binding.titleTextView.text = title binding.titleTextView.setTextColor(resources.getColor(R.color.text_dark_gray,null)) binding.inputEditText.setTextColor(resources.getColor(R.color.text_dark_gray,null)) }else{ binding.titleTextView.text = errorTitle binding.titleTextView.setTextColor(resources.getColor(R.color.red,null)) binding.inputEditText.setTextColor(resources.getColor(R.color.red,null)) }}
怎么切换呢
PinLoginFragment中就不用Toast了
只有错误状态才切换
登录成功就直接跳转到主界面去了
↓
viewModel.loginState.observe(viewLifecycleOwner){ //Toast.makeText(context,"$it",Toast.LENGTH_LONG).show() when(it){ SharedViewModel.LoginState.Success -> {} SharedViewModel.LoginState.Failure -> { binding.nameInputView.showError() binding.passwordInputView.showError() } SharedViewModel.LoginState.Default -> {} }}
运行一下:显示红色了,但是无法恢复正常输入状态
应该延时一会儿再变回来
①显示正常提示状态
②清空输入框
↓
fun showError(){ showState(false) postDelayed({ showState(true) binding.inputEditText.setText("") },1000)}
2023.9.18周一
登录失败后:登录按钮作用震动
给控件添加晃动的效果
tools -> AnimTools(静态类)
左右摆动动画startSwingAnim()
传入:①View给哪个控件添加这个动画 ②摆动幅度,给默认值 ③晃动的时间
④动画开始了要做什么(开始时事件回调) ⑤动画结束了要做什么 不需要参数和返回值,默认啥都不做
左右摆动是改变x坐标,看一下要改变的x坐标是什么类型的
用属性动画:就是给某一个控件的某一个属性做动画
↓ 可以通过以下方式查看
view.translationX
所以这里的of只能用float类型
vararg理解:注意要给的是px像素值
可调用之前已经写过多的view.dp2px
先让动画动起来,先不管onStart、onEnd
什么时候做动画?
登录失败的时候做动画
PinLoginFragment中的when Failure
AnimTools.startSwingAnim( binding.button, time = 100)}
运行一下:可正常晃动
如果要额外做事,就监听一下这个动画事件
addListener(onStart = {onStart()}, onEnd = {onEnd()}) //将动画事件传递给外部
注册功能
还没有用户信息
先搭建Pin注册界面
PinRegisterFragment中,binding,viewModel
↓
private lateinit var binding:FragmentPinRegisterBinding
//Pin登录界面的数据会影响Pin注册界面,所以单独用一个ViewModel,不数据共享private val viewModel:SharedViewModel by activityViewModels()
监听注册按钮的状态
viewModel.loginBtnIsEnabled.observe(viewLifecycleOwner){ binding.button.isEnabled = it}
当三个输入框都有内容的时候才可以点击
就看这三个东西是否为空
但是三个东西同时判断不方便判断
用变量实时保存输入框内容,一旦发生改变就把内容存下来
private var name = ""private var password = ""private var confirmPassword = ""
内容存下来就去判断它的状态checkEnabled()
private fun checkEnabled(){ val state = if (name.isNotEmpty() && password.isNotEmpty() && confirmPassword.isNotEmpty()){ SharedViewModel.ButtonState.Enabled }else{ SharedViewModel.ButtonState.UnEnabled } viewModel.changeBtnState(state)}
↓
//监听输入框内容改变的事件binding.nameInputView.textChangedListener = {userInputView, text -> name = text checkEnabled()}binding.passwordInputView.textChangedListener = {userInputView, text -> password = text checkEnabled()}binding.confirmPasswordInputView.textChangedListener = {userInputView, text -> confirmPassword = text checkEnabled()}
点击注册实现注册功能
确保两次密码输入一致checkPassword()
当注册按钮被点击的时候调用该方法or
确认密码输入完之后就判断(未采用)
想知道密码输入完毕
确认密码是否输入完毕事件监听
2023.9.19周二
点击登录就去判断两次密码是否输入一致
注册按钮一点击就调用checkPassword()
两个相同就注册不同就提示错误showError、按钮震动
先做错误时
运行一下:正常显示红色,可震动
private fun checkPassword(){ if (password == confirmPassword){ //注册 }else{ //提示错误 binding.passwordInputView.showError() binding.confirmPasswordInputView.showError()
AnimTools.startSwingAnim(binding.button,time = 100)
}}
注册找ViewModel,这种行为动作就去找ViewModel
在UserManager中的registerUser()要同时有两种密码才能注册成功
注意:Pin注册成功后就去进入下一个页面进行Pattern的注册
当前界面需要保存用户和设置的Pin密码,并进入到下一个界面
保存Pin密码的话需要和图案密码的界面用统一个ViewModel,因为需要共享用户名和密码
在SharedViewModel中↓
创建一个User对象,val user = User(),直接暴露给外部就性,要不然还要提供一个方法供外部访问
需要更改User()类,都改成var,给默认值为空,isLogin默认为false
private fun checkPassword(){ if (password == confirmPassword){ //注册:保存当前用户的用户名和密码 viewModel.user.name = name viewModel.user.pin = password //进入到图案密码界面 navigateTo(PatternRegisterFragment(), addToStack = false) }else{ //提示错误 binding.passwordInputView.showError() binding.confirmPasswordInputView.showError() AnimTools.startSwingAnim(binding.button,time = 100) }}
运行一下:运行正常,可以跳转到Pattern注册界面
-----------------------------------------------------------
PatternRegister界面搭建 数据监听
给PatternRegister添加binding
监听unLockView返回的数据
binding.unlockView.addDrawFinishedListener { unlockView, password ->
unLockView中用addDrawFinishedListener()实现数据外传
得到unLockView和password
第一次的时候肯定要记录密码
private var mPassword = ""
要判断两次密码是否一致
1.先判断mPassword是否为空,为空就是第一次,将第一次密码的值赋给mPassword,再进行第二次的滑动
2.不为空就说明是第二次了要进行两次密码是否一致的判断
提示textView显示对应文字
(1)①两次密码不一致要清空mPassword
②showError()
[if !supportLists](2)[endif]密码一致就要保存设置的图案密码注册用户
注册成功后就要跳转到PinLogin界面进行登录操作navigateTo(PinLoginFragment()
数据在ViewModel中,添加ViewModel
PinRegister和PatternRegister需要数据的共享
但是在此之前,PinRegister用的是by viewModels
解决:
方法一:如果只有PinRegister和PatternRegister两个之间需要数据共享,不跟外部共享SharedViewModel,自己再创建一个SharedViewModel
方法二:二者之间需要共享的只是user的用户名和密码,可以把用户名和密码作为参数传给Fragment
如果都共享的话,loginBtnEnabled会有影响:如果在PinLogin都填入信息了,则登录按钮可以点击,如果这个时候切换到PinRegister注册界面,那么注册按钮就可以立即点击了
干脆重新单独弄一个registerBtnIsEnabled,changeRegisterBtnState()
此时需要修改PinRegisterFragment中就不要监听loginBtnEnabled了,改成registerBtnIsEnabled
//保存注册用户信息viewModel.user.pattern = mPassword
↓
binding.unlockView.addDrawFinishedListener { unlockView, password -> if(mPassword.isEmpty()){ mPassword = password }else{ if (mPassword == password){ //密码设置成功 binding.alertPatternView.text = "密码设置成功"
binding.alertPatternView.text = "请确认密码图案" //保存注册用户信息 viewModel.user.pattern = mPassword viewModel.register() navigateTo(PinLoginFragment(), addToStack = false) }else{ //密码设置失败 binding.alertPatternView.text = "两次密码不一致请重新设置" mPassword = "" unlockView.showError() } }}
此时user的信息已经全了,就可以去注册,注册操作就要去找ViewModel
ViewModel中要提供注册功能:
①调用UserManager里面的registerUser()
②注册成功后,文件中就存在用户信息了,需要显示切换到图案解锁,注意重新post更新showChange的值
ViewModel注册功能register()
fun register(){ userManager.registerUser(user.name,user.pin,user.pattern) showChange.postValue(userManager.hasUser())}
运行一下:正常
PinLogin登录成功之后Success
提供一个activity
view -> New -> Activity -> Empty Views Activity -> RootActivity
登录成功就直接进入RootActivity
此项目一进来加载的就是MainActivity主界面,后面切换到RootActivity的,主界面不能销毁,所以只能可以返回
2023.9.20周三
PatternLogin图案密码解锁登录
解锁密码成功进入主界面
PatternLoginFragment中监听绘制解锁事件
要确定登录的用户名是谁
底部弹窗显示选择用户
ChooseUserBottomSheetFragment()继承于BottomSheetDialogFragment()
先不用RecyclerView处理,先弹两个出来,有请选择用户,取消弹窗就消失
选则用户弹窗界面搭建
fargment_choose_user_bottom_user_sheet界面搭建
上下间距10dp
字体18sp
注意背景是(左上和右下)圆角
要用drawable资源 bottom_sheet_shape
<corners android:topLeftRadius="15dp" android:topRightRadius="15dp" /><solid android:color="@color/alpha_blue"/>
在ChooseUserBottomSheetFragment中解析,添加binding
一点击切换到图案密码解锁,跳转到PatternLogin界面,立刻从底部弹出
ChooseUserBottomSheetFragment().show(parentFragmentManager,"")
运行一下:可显示弹窗
什么时候可以弹窗:没有登录用户的信息就弹出(让用户选择具体是那个用户要登录)
在SharedViewModel中,一旦有登录的用户信息就放在loginedUser
val loginedUser = MutableLiveData(User())
希望在PatternLogin界面显示出来之后再出现弹窗,所以在onResume()中监听loginedUser
如果不存在用户还没有登录就出现弹窗
为了测试可以手动添加User(name = “jack”)
override fun onResume() { super.onResume() viewModel.loginedUser.observe(viewLifecycleOwner){ if (it.name.isEmpty()){ ChooseUserBottomSheetFragment().show(parentFragmentManager,null) }else{ } }}
点击用户名之后就都要消失
在ChooseUserBottomSheetFragment添加点击事件
点击后弹窗要消失,在消失dismis()之前要将用户信息回调出去
需要根据用户名返回这个用户的所有信息
在UserManager()中添加getUserInfo()方法,根据用户名找到用户信息
fun getUserInfo(name:String):User?{ users.forEach { if (it.name == name){ return it } } return null}
在SharedViewModel中提供下载用户信息的方法loadUserInfo()
如果为空就啥也不做
不为空,则用户信息就有了,LiveData更新用户信息,更新loginedUser
loginUser.postValue(user)
fun loadUserInfo(name:String){ val user = userManager.getUserInfo(name) ?: return loginedUser.postValue(user)}
PatternLogin
PatternLogin里面就能监听到用户信息更新了,马上去登录即可
输完图案密码就去比较即可
用一个变量记录是否有用户信息hasUser默认false
↓
override fun onResume() { super.onResume() viewModel.loginedUser.observe(viewLifecycleOwner){ if (it.name.isEmpty()){ //还没有用户信息要弹出供用户选择用户名 ChooseUserBottomSheetFragment().show(parentFragmentManager,null) }else{ //有用户了去登录 hasUser = true } }}
有了用户信息就去调用viewModel.login(),login需要传入用户信息,用户信息都保存在了loginedUser里面了
取出登录用户信息,再做登录
val user = viewModel.loginedUser.value!!
↓
binding.unlockView.addDrawFinishedListener {unlockView, password -> if (hasUser){ val user = viewModel.loginedUser.value!! viewModel.login(user.name,password,PasswordType.Pattern) }}
登录的话有成功有失败需要监听登录是否成功的状态loginState,登录Success就跳转到RootActivity
viewModel.loginState.observe(viewLifecycleOwner){ if (it == SharedViewModel.LoginState.Success){ startActivity(Intent(requireContext(),RootActivity::class.java)) } if (it == SharedViewModel.LoginState.Failure){ binding.unlockView.showError() binding.switchToPatternView.text = "图案密码错误请重新绘制" }}
运行一下:出现滑动密码错误(不管密码是啥)但是直接跳转到RootActivity了,错误,已经修改标红,传给login的应该是unlockView传出的password,不是正确的密码