數(shù)理流程
1.界? - 2.邏輯(數(shù)據(jù))
登錄
界面加載起來(lái)就加載用戶信息:從哪里加載讀取?件
保存?戶信息(設(shè)置密碼) :寫??件
獲取所有?戶信息:讀取?件
注冊(cè)
在文件中添加新數(shù)據(jù)
切換
文件存取用戶信息
1. ?戶→ model → UserModel: name pin pattern type(區(qū)別是什么類型的密碼)
2. type: 0(TYPE_PIN) 1(Type_PATTERN)
3. ?戶操作→ UserManager
a. 加載所有?戶信息 loadAllUsersInfo()
b. 檢測(cè)是否有?戶 hasUserInfo():Boolean //有用戶就可以顯示切換圖案解鎖
c. 判斷?戶是否存在 checkUser(name):Boolean //先根據(jù)輸入的用戶名判斷用戶是否存在 不存在直接爆紅
d. 判斷?戶名和密碼是否正確 checkUser(name,password,type):Boolean //能到這一步判斷說(shuō)明用戶名已經(jīng)存在了
e. 獲取所有?戶名 getAllUserName():List<String> //滑動(dòng)解鎖顯示的彈窗
f. 添加?戶信息 saveUser(name,pin,pattern) //注冊(cè)后存用戶信息
4. ?件操作→ FileManager
a. 創(chuàng)建?件creatFile()
b. 讀取?件內(nèi)容readData():List<List> //加載用戶信息 判斷 獲取
c. 寫??件內(nèi)容writeData(userList) //添加用戶信息
看不見(jiàn)的:文件的路徑filePath():String
UML類圖
用戶信息功能
建立兩個(gè)包file土童、user,
創(chuàng)建User數(shù)據(jù)類[創(chuàng)建model對(duì)象]
data class User(? ? val name:String,? ? val pin:String,? ? val pattern:String,? ? var isLogin:Boolean)
創(chuàng)建FileManager的單例對(duì)象
先寫fileManager,因?yàn)橛脩舨僮骼锩嬗泻枚嘁蕾囄募僮?/p>
創(chuàng)建FileManager類
首先要提供文件的單例對(duì)象,外部不能直接訪問(wèn)我的構(gòu)造函數(shù)了国旷,要私有化構(gòu)造函數(shù)误趴,加伴生對(duì)象薇芝,伴隨著這個(gè)類的產(chǎn)生而產(chǎn)生的對(duì)象蓬抄。instance保存唯一對(duì)象又不希望外部直接訪問(wèn),并給外部暴露一個(gè)方法出來(lái)用于得到這個(gè)對(duì)象夯到,如果instance不為空則不用再創(chuàng)建一個(gè)對(duì)象嚷缭,直接把當(dāng)前的instance返回出去。
synchronized:涉及線程之間的安全問(wèn)題(多個(gè)對(duì)象去搶奪這一個(gè)對(duì)象的時(shí)候可能會(huì)出現(xiàn)線程不安全問(wèn)題)如果instance沒(méi)有就去創(chuàng)建耍贾,創(chuàng)建過(guò)程中加把鎖阅爽,先別急著訪問(wèn),先把這個(gè)對(duì)象創(chuàng)建好了再來(lái)訪問(wèn)荐开,進(jìn)來(lái)之后再來(lái)判斷一次(有可能剛剛進(jìn)來(lái)的時(shí)候剛好已經(jīng)創(chuàng)建好了优床,創(chuàng)好了就沒(méi)有必要去再做一次了),即將要?jiǎng)?chuàng)建了還是空則去創(chuàng)建這個(gè)對(duì)象
(當(dāng)存在多個(gè)線程操作共享數(shù)據(jù)時(shí)誓焦,需要保證同一時(shí)刻有且只有一個(gè)線程在操作共享數(shù)據(jù)胆敞,其他線程必須等到該線程處理完數(shù)據(jù)后再進(jìn)行,這種方式有個(gè)高尚的名稱叫互斥鎖杂伟,即能達(dá)到互斥訪問(wèn)目的的鎖移层,也就是說(shuō)當(dāng)一個(gè)共享數(shù)據(jù)被當(dāng)前正在訪問(wèn)的線程加上互斥鎖后,在同一個(gè)時(shí)刻赫粥,其他線程只能處于等待的狀態(tài)观话,直到當(dāng)前線程處理完畢釋放該鎖。
在Java中越平,關(guān)鍵字 synchronized 可以保證在同一個(gè)時(shí)刻频蛔,只有一個(gè)線程可以執(zhí)行某個(gè)方法或者某個(gè)代碼塊(主要是對(duì)方法或者代碼塊中存在共享數(shù)據(jù)的操作),同時(shí)我們還應(yīng)該注意到synchronized另外一個(gè)重要的作用秦叛,synchronized可保證一個(gè)線程的變化(主要是共享數(shù)據(jù)的變化)被其他線程所看到(保證可見(jiàn)性晦溪,完全可以替代Volatile功能),這點(diǎn)確實(shí)也是很重要的挣跋。)
class FileManager private constructor({? companion object{? ? ? ? private var instance:FileManager?= null? ? ? ? fun sharedInstance():FileManager{//只有調(diào)用這個(gè)方法的時(shí)候才去創(chuàng)建這個(gè)對(duì)象? ? ? ? ? ? if (instance == null){? ? ? ? ? ? ? ? synchronized(this){//每一個(gè)類三圆、每個(gè)對(duì)象都有一把鎖,? ? ? ? ? ? ? ? ? ? if (instance == null){? ? ? ? ? ? ? ? ? ? ? ? instance = FileManager()? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? }? ? ? ? ? ? }? ? ? ? ? ? return instance!!? ? ? ? }? ? }}
獲取保存用戶信息的文件路徑filePath
filesDir只有Activity(頂級(jí)父類是context)或者其父類才能訪問(wèn)到避咆,為了訪問(wèn)到filesDir,方法里要傳入context過(guò)來(lái)舟肉。有了filesDir再拼上文件名就形成路徑了private fun filePath(context: Context):String{? ? return "${context.filesDir.path}/userInfo"}
從文件中讀取用戶信息readData
存進(jìn)去的時(shí)候是Json格式的字符串,但是讀取的時(shí)候想得到的是對(duì)象查库,而且有可能有多個(gè)對(duì)象路媚,所以返回User的數(shù)組,為了得到路徑樊销,所以要傳入context整慎。
將所有信息讀進(jìn)來(lái)readText()适荣,Json格式的字符串對(duì)象]用Gson將其轉(zhuǎn)化為對(duì)象。
導(dǎo)入依賴//Gson? implementation 'com.google.code.gson:gson:2.10.1'
還要得到Gson的對(duì)象
創(chuàng)建一個(gè)對(duì)象表達(dá)式,從該對(duì)象表達(dá)式繼承TypeToken,然后從中獲取Java Type院领。
TypeToken是一個(gè)用于類型推斷和類型安全性的技術(shù),可以獲得一個(gè)對(duì)象的實(shí)際類型够吩,而不僅僅是其類[就是我們想得到的是List<User>而不僅僅是List<String>或者其他類型]比然,TypeToken是一個(gè)抽象類,因此需要用匿名子類來(lái)創(chuàng)建它周循。
注意:如果是第一次安裝則是沒(méi)有這個(gè)文件的强法,是讀不出來(lái)的,要判斷文件是否存在再進(jìn)行文件的讀取
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()//沒(méi)有該文件就返回空}
將用戶數(shù)據(jù)寫入文件writeData
參數(shù)傳入需要寫入的users類型為L(zhǎng)ist<User>
我們需要從List<User>類型轉(zhuǎn)化為json格式的字符串
用write寫入
fun writeData(context: Context,users:List<User>){? FileWriter(filePath(context)).use {? ? ? ? val jsonString = Gson().toJson(users)? ? ? ? it.write(jsonString)? ? }}
測(cè)試:
在MainActivity里面嘗試寫一個(gè)進(jìn)去(因?yàn)橹挥蠱ainActivity繼承于context所以在此測(cè)試)
先將假數(shù)據(jù)寫進(jìn)去
val users = listOf(? ? User("jack","123","456",false),? ? User("rose","252","247",false))FileManager.sharedInstance().writeData(this,users)
沒(méi)有報(bào)錯(cuò)就好湾笛,在Device File Explorer里面
data -> data -> com.example.loginannie -> file -> userInfo里面就有我們存入的用戶信息
其具有一定的格式
[//中括號(hào)[ ] -> 集合 數(shù)組
{"isLogin":false,"name":"jack","pattern":"456","pin":"123"},//大括號(hào){ } -> 對(duì)象
{"isLogin":false,"name":"rose","pattern":"247","pin":"252"}
]
讀取改為
FileManager.sharedInstance().readData(this).also {? ? it.forEach { user->? ? ? ? Log.v("annie","$user")? ? }}
Logcat中可以打印出這個(gè)
創(chuàng)建用戶管理器UserManager
新建UserManager類饮怯,管理用戶操作
構(gòu)建單例靜態(tài)對(duì)象:私有化構(gòu)造函數(shù),阻止外部直接創(chuàng)建這個(gè)對(duì)象嚎研,私有化之后只有在自己內(nèi)部才能創(chuàng)建對(duì)象
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!!? ? ? ? }? ? }}
進(jìn)行用戶的數(shù)據(jù)存取需要用到context蓖墅,UserManager里面需要傳一個(gè)context進(jìn)來(lái),構(gòu)建instance對(duì)象的時(shí)候就傳入一個(gè)context進(jìn)來(lái)临扮,多地方要用context就將它作為全局的屬性论矾,既然是全局的屬性那就把它放在構(gòu)造函數(shù)里面。
但是如果直接val context:Context會(huì)出現(xiàn)內(nèi)存泄漏(不用了的東西沒(méi)有刪掉)
一個(gè)對(duì)象是否被刪除就看它還要不要用杆勇,如果它一直被其他對(duì)象持有贪壳,則永遠(yuǎn)不能被刪掉
單例
companion object里面是靜態(tài)屬性和靜態(tài)方法,靜態(tài)對(duì)象一直存在蚜退,其屬性context一直存在闰靴,在MainActivity中用this傳入自身則不能被銷毀,外部無(wú)法釋放钻注,直到程序退出
使用弱引用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來(lái)做的蚂且,每次訪問(wèn)都要用FileManager.sharedInstance(),可以起一個(gè)臨時(shí)變量記錄一下
刪掉MainActivity里面的測(cè)試,在MainActivity里面希望其onCreat時(shí)加載所有用戶信息
加載用戶信息即可幅恋,不需要返回值膘掰,加載完信息就存在users 里面
取出弱引用里面包含的context,調(diào)用get()方法即可
把多個(gè)東西加到數(shù)組里面用addAll()佳遣,加到users數(shù)組里面
測(cè)試:在MainActivity加載起來(lái)的時(shí)候調(diào)用這個(gè)方法
fun loadAllUserInfo(){? ? fileManager.readData(context.get()!!).also {? ? ? ? users.addAll(it)? ? ? ? Log.v("annie","$users")? ? }}
在MainActivity中調(diào)用UserManager.sharedInstance(this).loadAllUserInfo()
是否有用戶hasUser
判斷存儲(chǔ)用戶信息的數(shù)組是否為空即可?
fun hasUser():Boolean{? ? ? ? return users.size>0? }? ? fun hasUser() = users.size > 0
用戶名是否存在checkUser
循環(huán)遍歷users取其name相比較
fun checkUser(name:String):Boolean{? ? users.forEach {? ? ? ? if (it.name == name){? ? ? ? ? ? return true? ? ? ? }? ? }? ? return false}
判斷用戶名和密碼是否正確checkUser
寫type的方式(后面改成枚舉了)
創(chuàng)建一個(gè)靜態(tài)類识埋,更容易讓別人看懂
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 //沒(méi)有這個(gè)用戶}
獲取所有的用戶名
在pattern密碼的時(shí)候窒舟,如何當(dāng)前沒(méi)有登錄的用戶,那么我們就要獲取我們要登錄的是哪一個(gè)
還要知道當(dāng)前有沒(méi)有登錄的用戶信息
獲取當(dāng)前登錄用戶currentUser
當(dāng)前登錄的用戶不一定有诵盼,但是有就只有一個(gè)沒(méi)有就沒(méi)有(同一時(shí)間只有一個(gè)登錄用戶)
filter{}過(guò)濾完之后得到的是List<User>
users.filter {? ? if (it.isLogin){? ? ? ? true? ? }else{? ? ? ? false? ? }}//過(guò)濾條件
先過(guò)濾出isLogin == true的user放在List<User>數(shù)組里去惠豺,如果這個(gè)數(shù)組不為空則代表當(dāng)前有登錄的用戶银还,有就把這個(gè)User()取出來(lái)
fun currentUser():User?{? ? users.filter { it.isLogin }.also {? ? ? ? return if (it.isNotEmpty()){? ? ? ? ? ? it[0]? ? ? ? }else{? ? ? ? ? ? null? ? ? ? }? ? }}
后面該怎么用涉及界面的邏輯
保存注冊(cè)用戶信息registerUser
首先添加到users數(shù)組里面(將注冊(cè)的用戶添加到用戶信息中),注冊(cè)完之后需要重新登錄
登錄過(guò)后再將信息寫入
fun registerUser(name: String,pin:String,pattern:String){? ? users.add(User(name,pin,pattern,false)) //注冊(cè)后需要再次登錄? ? fileManager.writeData(context.get()!!,users)}
登錄login
可以不checkUser直接點(diǎn)擊登錄
先找到用戶名和密碼對(duì)應(yīng)的用戶名再去判斷
用一個(gè)變量存儲(chǔ)要找到的那個(gè)用戶洁墙,整個(gè)forEach結(jié)束就是為了找到對(duì)應(yīng)的用戶蛹疯,將找到的該用戶的isLogin改為true就表示是登錄了,此處是把數(shù)組里面的用戶信息的isLogin改為true热监,文件里的也要改捺弦,即還要寫入數(shù)據(jù)
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 //找到當(dāng)前用戶名和密碼對(duì)應(yīng)的用戶? ? ? ? ? ? ? ? }? ? ? ? ? ? }else{? ? ? ? ? ? ? ? if (it.pattern == password){? ? ? ? ? ? ? ? ? ? user = it? ? ? ? ? ? ? ? }? ? ? ? ? ? }? ? ? ? }? ? } //forEach結(jié)束找到用戶名和密碼對(duì)應(yīng)的用戶user? ? return if (user != null){? ? ? ? user!!.isLogin = true //將對(duì)應(yīng)用戶數(shù)組里面的用戶信息的isLogin改為true? ? ? ? fileManager.writeData(context.get()!!,users)//將修改后的所有用戶信息重新寫入文件? ? ? ? true? ? }else{? ? ? ? false? ? }}
在UserManager()中添加getUserInfo()方法,根據(jù)用戶名找到用戶信息
fun getUserInfo(name:String):User?{? ? users.forEach {? ? ? ? if (it.name == name){? ? ? ? ? ? return it? ? ? ? }? ? }? ? return null}
測(cè)試:用Jack來(lái)試一試,在MainActivity中加入
UserManager.sharedInstance(this).login("jack","123",PasswordType.LoginType_Pin)
去file中看看孝扛,isLogin變成true了
在登錄之前要先取消掉上一個(gè)用戶的登錄狀態(tài)
------------------------------------------------------------------------------------------------------------------------------------------
17
思考用戶的信息應(yīng)該在哪個(gè)地方加載列吼?
搭建界面流程(把這幾個(gè)界面串聯(lián)起來(lái))
MyApplication
在此之前先做一個(gè)合理化的事情:
程序運(yùn)行起來(lái)就需要立刻擁有的東西(加載用戶信息)就可以把它放在application里面來(lái)
創(chuàng)建類MyApplication繼承于Application()? [生命周期:從程序開(kāi)始到結(jié)束]
專門用于管理我們這個(gè)應(yīng)用程序的(用于保持整個(gè)程序的全局狀態(tài))
當(dāng)這個(gè)應(yīng)用程序被點(diǎn)擊然后要加載起來(lái)之前,優(yōu)先調(diào)用這個(gè)application苦始,界面還未加載
在Manifest的application下面加入
android:name=".MyApplication"告訴系統(tǒng)加載應(yīng)用時(shí)寞钥,去加載MyApplication(聲明優(yōu)先調(diào)用這個(gè) 不用系統(tǒng)的)
①創(chuàng)建Application的?類
②實(shí)現(xiàn)onCreate?法
③方法內(nèi)部實(shí)現(xiàn)自己需要的配置
④在AndroidManifest文件的<Application>中使?android:name綁定自己的MyApplication
class MyApplication:Application() {? ? override fun onCreate() {? ? ? ? super.onCreate()? ? ? ? //加載所有用戶信息? ? ? ? UserManager.sharedInstance(this).loadAllUserInfo()? ? }}
運(yùn)行起來(lái)有沒(méi)有打印就知道有沒(méi)有被調(diào)用了
對(duì)log?志輸出進(jìn)?簡(jiǎn)單封裝MyLog
保留Log(只有寫代碼的時(shí)候才需要這個(gè)Log)
使用一個(gè)開(kāi)關(guān)控制是否需要輸出日志
IS_RELEASE是否發(fā)布狀態(tài) 作為一個(gè)開(kāi)關(guān)(調(diào)試狀態(tài)[寫代碼] 發(fā)布狀態(tài))
private const val IS_RELEASE = false //默認(rèn)非發(fā)布狀態(tài) 即調(diào)試狀態(tài)
如果需要所有的Log無(wú)法打印 將其改為true狀態(tài)即可
v i d e w五種常用的
object MyLog {//靜態(tài)類? ? private const val IS_RELEASE = false //靜態(tài)屬性? ? fun v(tag:String = "annieTry",content:String = ""){//靜態(tài)方法? 注意兩個(gè)參數(shù) 給默認(rèn)值的先后順序? ? ? ? if (!IS_RELEASE){? ? ? ? ? ? Log.v(tag,content)? ? ? ? }? ? }
}
注意包的合理分配
梳理跳轉(zhuǎn)流程
建立程序執(zhí)?流程界?
分析整個(gè)項(xiàng)?Activity(如果這個(gè)界面需要提供給外部一個(gè)服務(wù),需要外部其他應(yīng)用也能調(diào)用我這個(gè)應(yīng)用程序陌选,則需要獨(dú)立有一個(gè)Activity)和Fragment的數(shù)量(有幾個(gè)界面就有幾個(gè)Fragment)
創(chuàng)建Fragment 5個(gè)
PinLoginFragment
PatternLoginFragment
PinRegisterFragment
PatternRegisterFragment
ChooseUserBottomSheetFragment
刪除不需要的東西
注意包的合理分配
在activity_main.xml添加FragmentContainerView
首頁(yè)默認(rèn)顯示PinLoginFragment界面理郑,調(diào)整大小,鋪滿整個(gè)屏幕
將每一個(gè)Fragment默認(rèn)的FrameLayout改為ConstraintLayout
為了方便調(diào)試可以改一改對(duì)應(yīng)的背景顏色和文字
修改顏色后但是看不到默認(rèn)界面咨油?
①在activity_main.xml中點(diǎn)擊右上角有一個(gè)紅色嘆號(hào)警告
②點(diǎn)擊Unknown fragments
③(Use@layout/fragment_pin_login,Pick Layout)
添加Fragment之間的切換效果
添加平移動(dòng)畫效果enter? exit? popEnter? popExit
1.先添加資源文件
res -> Android Resource Directory[安卓資源目錄](méi) -> anim
anim -> Animation Resource File[動(dòng)畫資源文件]
①enter_from_right從右邊進(jìn)入? <translate平移動(dòng)畫 fromXDelta? toXDelta? duration(從上面寫)
②exit_to_left左邊出去
③pop_enter_from_left從左邊彈回來(lái)
④pop_exit_to_right彈到右邊去
⑤enter_from_borrom 從下面進(jìn)入(給底部用戶選擇彈窗添加進(jìn)入/出去動(dòng)畫)
⑥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.實(shí)現(xiàn)對(duì)應(yīng)切換效果PinLoginFragment
在PinLoginFragment里面要得到View香浩,添加點(diǎn)擊事件
onViewCreated
fragment自己不能切換要找FragmentManager做切換
導(dǎo)入依賴
//FragmentManager切換擁有commit{}擴(kuò)展函數(shù)
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( //設(shè)置動(dòng)畫? ? ? ? ? ? ? ? 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()) //切換到那個(gè)界面? ? ? ? ? ? setReorderingAllowed(true) //允許重新排序? ? ? ? ? ? addToBackStack(null) // 是否加到棧里可以回來(lái)? ? ? ? }? ? }}
測(cè)試:運(yùn)行
3.給Fragment添加擴(kuò)展方法Tools(封裝Fragment切換的方法)
但是切換非常頻繁不適合在Fragment里直接全寫
應(yīng)把commit封裝成對(duì)應(yīng)的方法(動(dòng)畫固定 不一定切換到哪里去 不一定入棧)
只有在Fragment里用,可以給其加拓展方法
fun Fragment.navigateTo(target: Fragment,addToStack:Boolean){? ? parentFragmentManager.commit {? ? ? ? setCustomAnimations( //設(shè)置動(dòng)畫? ? ? ? ? ? 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) //切換到那個(gè)界面? ? ? ? setReorderingAllowed(true) //允許重新排序? ? ? ? if (addToStack){? // 是否加到棧里入棧? ? ? ? ? ? addToBackStack(null)? ? ? ? }? ? }}
在PinLoginFragmen中即可這樣簡(jiǎn)寫
navigateTo(PinRegisterFragment(),true)
2023.7.19
再次完善封裝Fragment.navigateTo方法
傳入的參數(shù)addToStack默認(rèn)為true
為了方便從下往上彈的動(dòng)畫臼勉,為進(jìn)入和進(jìn)出動(dòng)畫提供默認(rèn)值(一種是左右進(jìn)出邻吭、另一種是上下進(jìn)出)
參數(shù)將必須指定的放在前面,后面提供默認(rèn)值宴霸,若不填寫則使用默認(rèn)
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)//設(shè)置動(dòng)畫? ? ? ? replace(R.id.fragmentContainerView, target) //切換到那個(gè)界面? ? ? ? setReorderingAllowed(true) //允許重新排序? ? ? ? if (addToStack){? // 是否加到棧里入棧? ? ? ? ? ? addToBackStack(null)? ? ? ? }? ? }}
到此為止Fragment添加好了囱晴,其之間的切換關(guān)系也搞好了(框架搭建完畢)
下一步:搭建界面
------------------------------------------------------------------------------------------------------------------------------------------
界面搭建
res -> values ->colors統(tǒng)一顏色管理
文字白
小主體灰
提示文字深灰
切換藍(lán)
<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全屏顯示
去掉狀態(tài)欄和
Mainfest里面有主題
android:theme="@style/Theme.LoginAnnie"
①該版本默認(rèn)NoActionBar(不顯示標(biāo)題 標(biāo)題欄(導(dǎo)航欄))
②去掉狀態(tài)欄:
<item name="android:windowFullscreen">true</item>
運(yùn)行后則全屏顯示
改Fragment背景顏色為dark_blue并刪掉textView
Bottom_sheet為alpha_blue
android:background="@color/dark_blue"
res -> values ->strings統(tǒng)一管理字符串(文本內(nèi)容)
Fragment_pin_login
中添加一個(gè)textView 頂部64 id:titleTextView
字符串內(nèi)容統(tǒng)一管理
strings里面可以更改app_name名稱
<resources>? ? <string name="welcome_title">歡迎回來(lái)</string>? ? <string name="welcome_register">歡迎注冊(cè)</string>? ? <string name="login_subtitle">讓我們一起努力學(xué)好Android開(kāi)發(fā)拿到offer</string>? ? <string name="register_subtitle">所以的結(jié)果都是從一個(gè)決定開(kāi)始的</string>? ? <string name="login_title">登錄</string>? ? <string name="register_title">注冊(cè)</string></resources>
res -> values ->styles設(shè)置字體大小顏色粗細(xì)
<resources>? ? <!--主標(biāo)題--><style name="TextTitle">? ? <item name="android:textSize">36sp</item>? ? <item name="android:textColor">@color/text_white</item>? ? <item name="android:textStyle">bold</item></style><!--副標(biāo)題--><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>
調(diào)用:在xlm對(duì)應(yīng)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用繪制資源設(shè)置圓角矩形
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>
手動(dòng)添加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文件統(tǒng)一管理//最終未使用這個(gè)
①文本 TextView -> title
②輸入框 EditText -> 輸入
③承載輸入內(nèi)容的分割線 View -> 分割線(出現(xiàn)了5次)
為了可以重復(fù)利用且更靈活,三者作為一個(gè)整體放在容器ViewGroup中
[從上至下 -> LinearLayout]
1.xml方式讓控件形成一個(gè)統(tǒng)一整體然后再進(jìn)行復(fù)用
res -> layout -> New Resource File -> UserInputView
Root element:LinearLayout
---EditText輸入框:默認(rèn)就有一條直線,這條線是貼在輸入文字上的瓢谢,而且和該控件底部存在內(nèi)間距畸写,去掉方法:background = “null”
---額外添加一條線:添加一個(gè)View,高度為1dp氓扛,再調(diào)整一下上間距即可
<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="請(qǐng)輸入用戶名"? ? ? ? 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的局限:
只能實(shí)現(xiàn)布局枯芬,不能靈活配置
不能靈活改變里面的文字,需要用代碼來(lái)操作
優(yōu)點(diǎn):布局方便->? xml中拖拽控件 快速設(shè)置對(duì)應(yīng)屬性
使用UserInputView類關(guān)聯(lián)layout_user_input.xml布局文件
使用一個(gè)類來(lái)關(guān)聯(lián)這個(gè)layout布局文件:
xml完成布局
代碼實(shí)現(xiàn)邏輯(用一個(gè)界面來(lái)管理視圖View/ViewGroup)
①建一個(gè)UserInputView類繼承于LinearLayout,重寫構(gòu)造方法
次構(gòu)造函數(shù)/**使用代碼創(chuàng)建一個(gè)控件時(shí)調(diào)用這個(gè)構(gòu)造方法*/? ? constructor(context:Context):super(context){}
/**在xml中添加一個(gè)控件采郎,并設(shè)置對(duì)應(yīng)屬性就調(diào)用這個(gè)構(gòu)造方法*/? ? constructor(context: Context, attrs: AttributeSet?):super(context,attrs){}/**在xml中添加一個(gè)控件并設(shè)置了style樣式就會(huì)調(diào)用這個(gè)構(gòu)造方法*/? ? constructor(context: Context, attrs: AttributeSet?, style:Int):super(context, attrs,style){}
提供主構(gòu)造方法
class UserInputView(context: Context,attrs:AttributeSet?) :LinearLayout(context,attrs){}
當(dāng)一個(gè)對(duì)象被創(chuàng)建時(shí):1.構(gòu)造函數(shù)2.init方法
②在init方法中實(shí)現(xiàn)View和xml中布局視圖關(guān)聯(lián)
創(chuàng)建一個(gè)對(duì)象又想做額外的事情就到init里面
將layout布局文件和當(dāng)前這個(gè)類相關(guān)聯(lián)
//ViewGroup
class UserInputView(context: Context,attrs:AttributeSet?) :LinearLayout(context,attrs){? ? //找到控件外部不能直接訪問(wèn)? ? 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)? ? ? ? //創(chuàng)建布局參數(shù) view在FrameLayout中如何顯示? ? ? ? val lp = FrameLayout.LayoutParams(? ? ? ? ? ? ViewGroup.LayoutParams.MATCH_PARENT,? ? ? ? ? ? ViewGroup.LayoutParams.WRAP_CONTENT? ? ? ? )? ? ? ? //將解析出來(lái)的View添加到當(dāng)前容器中顯示出來(lái)? ? ? ? addView(view,lp)? ? ? ? //獲取對(duì)應(yīng)控件解析出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("請(qǐng)輸入用戶名")binding.nameInputView.setPlaceholder("密碼")binding.passwordInputView.setPlaceholder("請(qǐng)輸入密碼")
使用綁定后
class UserInputView(context: Context,attrs:AttributeSet?) :LinearLayout(context,attrs){? ? //使用的時(shí)候再去解析懶加載? 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)? ? ? ? //創(chuàng)建布局參數(shù) view在FrameLayout中如何顯示? ? ? ? val lp = FrameLayout.LayoutParams(? ? ? ? ? ? ViewGroup.LayoutParams.MATCH_PARENT,? ? ? ? ? ? ViewGroup.LayoutParams.WRAP_CONTENT? ? ? ? )? ? ? ? //將解析出來(lái)的View添加到當(dāng)前容器中顯示出來(lái)? ? ? ? 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中設(shè)置對(duì)應(yīng)屬性
自定義屬性:讓這些屬性在xml中可以直接使用千所,就像使用系統(tǒng)的一樣
//不用↓以下方式
Fragment_pin_login中就不用<include了
在PinLoginFragment中配置用戶名和密碼
使用viewBinding綁定
binding.nameInputView.setTitle("用戶名")binding.passwordInputView.setTitle("請(qǐng)輸入用戶名")binding.nameInputView.setPlaceholder("密碼")binding.passwordInputView.setPlaceholder("請(qǐng)輸入密碼")
res ->valuse -> New Resource File -> attrs
1.創(chuàng)建attr.xml文件 管理自己定義的屬性res ->valuse -> New Resource File -> attrs
聲明樣式----><declare-styleable name="UserInputView">
添加屬性設(shè)置name和對(duì)應(yīng)的類型
輸入的密碼是不能看見(jiàn)的,再定義一個(gè)input_type用枚舉
<resources>? <declare-styleable name="UserInputView">? ? ? ? <!--標(biāo)題-->? ? ? <attr name="title" format="string|reference"/>? ? ? ? <!--默認(rèn)提示內(nèi)容-->? ? ? ? <attr name="placeholder" format="string|reference"/>? ? ? ? <!--設(shè)置類型:密碼輸入 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="請(qǐng)輸入密碼"app:input_type="password"
3.把自定義的屬性關(guān)聯(lián)上去蒜埋,在自定義的View中? 解析對(duì)應(yīng)的屬性//解析屬性的值
只有在init中可以直接訪問(wèn)attrs淫痰, 在init中解析對(duì)應(yīng)的屬性(拆包)
//解析xml中自定義的屬性 -> 拆包//從attrs里面解析出R.styleable.UserInputView里面自定義的對(duì)應(yīng)的屬性和值? val typedArray = context.obtainStyledAttributes(attrs,R.styleable.UserInputView)//從typedArray中解析每一個(gè)屬性的值取出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){ //設(shè)置密碼不可見(jiàn)? ? ? binding.inputEditText.inputType =
InputType.TYPE_TEXT_VARIATION_PASSWORD
or InputType.TYPE_CLASS_TEXT? }//回收? typedArray.recycle()
運(yùn)行一下木有問(wèn)題
切換到圖案解鎖:有用戶顯示,無(wú)用戶不顯示
PinLoginFragment的onViewCreated中
//設(shè)置切換到圖案解鎖的顯示與隱藏binding.switchToPatternView.visibility = if (UserManager.sharedInstance(requireContext()).hasUser()){? ? View.VISIBLE}else{? ? View.INVISIBLE}
測(cè)試:刪除userInfo文件整份,默認(rèn)無(wú)用戶狀態(tài)待错,運(yùn)行不顯示切換到圖案密碼解鎖
登錄按鈕:無(wú)用戶不能點(diǎn)擊
binding.button.isEnabled = UserManager.sharedInstance(requireContext()).hasUser()
根據(jù)狀態(tài)切換背景
res->drawable -> New Resource File -> selector_login_btnselector自動(dòng)切換drawable資源
<!--selector 選擇器給系統(tǒng)使用注意:必須給drawable資源--><selector xmlns:android="http://schemas.android.com/apk/res/android">? ? <!--當(dāng)控件的enabled狀態(tài)是false 顯示color為灰色-->? ? <item android:drawable= "@color/text_gray" android:state_enabled="false"/>? ? <!--true 顯示color為藍(lán)色-->? ? <item android:drawable= "@color/light_blue" android:state_enabled="true"/></selector>
運(yùn)行一下看一看
但是用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>
還沒(méi)注冊(cè)籽孙?現(xiàn)在注冊(cè)
前灰后黑兩個(gè)textView
defaultTextView? registerTextView
//跳轉(zhuǎn)到注冊(cè)頁(yè)面binding.registerTextView.setOnClickListener {? ? navigateTo(PinRegisterFragment())}
------------------------------------------------------------------------------------------------------------------------------------------
滑動(dòng)解鎖:九個(gè)點(diǎn)自定義繪制View實(shí)現(xiàn)
UnlockView
分析:
①自定義View有兩種情況:告訴我們大小,未告知大小所以要先確定正方形區(qū)域
②繪制圓點(diǎn)背景
③確定圓的半徑:
上中下都有間距space
(width-4*space)/6
23.8.28
Tools -> 加方法View.dp2px
View里面就有context直接給View加擴(kuò)展即可
fun View.dp2px(dp:Int):Int{? return (context.resources.displayMetrics.density*dp).toInt()}
測(cè)量尺寸(測(cè)量寬度火俄、高度):onMeasure
在onMeasure中進(jìn)行
確定寬度:else需要自己算對(duì)應(yīng)尺寸 -->需要用到默認(rèn)的尺寸
寬度由半徑和間距確定的
提供默認(rèn)半徑和默認(rèn)間距//全局的
確定高度:相同復(fù)制粘貼即可
private var mRadius = dp2px(40)//默認(rèn)半徑private var mSpace = dp2px(40)//默認(rèn)間距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)
}
確定正方形區(qū)域:onSizeChanged
寬度和半徑不一定就是我們剛給的值犯建,View不一定剛好是一個(gè)矩形區(qū)域——要確定正方形區(qū)域就是在確定繪制點(diǎn)的起始坐標(biāo)點(diǎn)x,y
起始點(diǎn)x,起始點(diǎn)y確定每個(gè)圓在哪個(gè)位置畫
定義變量:就是繪制點(diǎn)的起始點(diǎn)
當(dāng)尺寸發(fā)生變化:即從無(wú)到有那一刻瓜客,一旦確定下來(lái)适瓦,我的正方形也就確定下來(lái)了
起始點(diǎn)坐標(biāo):
mRadius+mSpace? ? 空+mRadius+mSpace
①半徑不確定:可能會(huì)變大變小
所以首先要計(jì)算半徑——以最小邊為正方形來(lái)進(jìn)行計(jì)算:(min-4*mSpace)/6
②確定起始點(diǎn)的中心點(diǎn)cx,cy
得到最小邊squareSize()
會(huì)重復(fù)多次用到,就直接提取該方法
***
private fun squareSize() = Math.min(width,height)
***
private var mStart_cx = 0f //第一個(gè)點(diǎn)的起始坐標(biāo)點(diǎn)全局變量private var mStart_cy = 0f
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {? ? super.onSizeChanged(w, h, oldw, oldh)? ? //計(jì)算半徑? ? mRadius = (squareSize()-4*mSpace)/6? ? //確定起始點(diǎn)的中心點(diǎn)cx,cy? ? mStart_cx = (width-squareSize())/2 + mSpace + mRadius.toFloat()? ? mStart_cy = (height-squareSize())/2 + mSpace + mRadius.toFloat()}
繪制:onDraw
尺寸確定完了就可以去繪制忆家,調(diào)用onDraw方法
繪制九個(gè)圓點(diǎn)drawNineDot()
先提供繪制圓點(diǎn)的畫筆——抗鋸齒、顏色(在color中統(tǒng)一管理)德迹、填充(style)
********
//默認(rèn)狀態(tài)時(shí)的背景畫筆private val mDotPaint:Paint by lazy {? ? Paint().apply {? ? ? ? isAntiAlias = true? ? ? ? color = context.resources.getColor(R.color.dot_bg_color,null)? ? ? ? style = Paint.Style.FILL? ? }}
九個(gè)圓點(diǎn)用循環(huán)就可以了芽卿,一行一行地畫,三行三列
九個(gè)點(diǎn)cx,cy不相同胳搞,cx和cy確定即可
*********
private fun drawNineDot(canvas: Canvas?){? ? for (i in 0 until 3){ //控制行數(shù)? ? ? ? for (j in 0 until 3){ //控制列數(shù)? ? ? ? ? ? val cx = mStart_cx + j*(2*mRadius + mSpace)? ? ? ? ? ? val cy = mStart_cy + i*(2*mRadius + mSpace)? ? ? ? ? ? canvas?.drawCircle(cx,cy,mRadius.toFloat(),mDotPaint)? ? ? ? }? ? }}
運(yùn)行一下:在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中默認(rèn)先不要判斷有沒(méi)有用戶(注釋掉)卸例,因?yàn)榍懊鏋榱藴y(cè)試把用戶信息刪了
先直接給”切換到圖案密碼解鎖”添加點(diǎn)擊事件直接跳轉(zhuǎn)到圖案登錄
binding.switchToPatternView.setOnClickListener {? ? navigateTo(PatternLoginFragment())}
——————
思考:如何記錄密碼:用編號(hào)
怎么知道一個(gè)點(diǎn)被點(diǎn)亮了:判斷觸摸點(diǎn)在哪兒
點(diǎn)亮:就是在上面再重新繪制圓
怎么知道圓心在哪兒?
九個(gè)點(diǎn)因?yàn)槭潜划嫵鰜?lái)的肌毅,所以在代碼層面是不存在的筷转,
但是我們要知道它們是存在的,那就進(jìn)行封裝
[但凡遇到要把多個(gè)東西集中到一個(gè)點(diǎn)/控件上去就對(duì)它進(jìn)行封裝]
DotModel() 封裝數(shù)據(jù)模型(封裝圓點(diǎn)信息)把分散的數(shù)據(jù)集中化統(tǒng)一管理
包models(放所有的模型)
沒(méi)有控件但是我們可以每繪制一個(gè)點(diǎn)就產(chǎn)生一個(gè)模型和它一一對(duì)應(yīng)就可以了
用枚舉記錄是否點(diǎn)亮(點(diǎn)亮的狀態(tài))
data class DotModel (? ? val num:Int ,//保存編號(hào)記錄密碼? ? val cx:Float,//中心點(diǎn)坐標(biāo)x,y -> 繪制點(diǎn)亮?xí)r的圓? ? val cy:Float,? ? val radius:Float,//半徑? ? var state:DotState = DotState.Normal//選中狀態(tài))//枚舉記錄點(diǎn)的狀態(tài)正常選中錯(cuò)誤enum class DotState{? ? Normal,Selected,Error}
————————————————————————
一旦繪制好了一個(gè)圓就應(yīng)該把這個(gè)圓的模型數(shù)據(jù)給存好
有圓的模型數(shù)據(jù)就要有對(duì)應(yīng)的屬性與之相關(guān)聯(lián)
*******private val mDotModels = arrayListOf<DotModel>() //存放九個(gè)點(diǎn)的模型數(shù)據(jù)
一旦繪制就封裝當(dāng)前這個(gè)點(diǎn)模型數(shù)據(jù)
需要計(jì)算點(diǎn)的編號(hào)
******
var index = 1 //記錄點(diǎn)的序號(hào)
加到封裝的模型數(shù)組里面
******mDotModels.add(? //封裝點(diǎn)的模型數(shù)據(jù)
//++在后面 -> 先拿去做事再++? ? DotModel(index++,cx,cy,mRadius.toFloat()))
onTouchEvent() 觸摸狀態(tài)
它本身就是一個(gè)View可以直接重寫onTouchEvent
直接return true 直接消費(fèi)了悬而,不需要繼續(xù)往下傳
touchBegin() 觸摸開(kāi)始 要知道觸摸點(diǎn)x,y(DOWN和MOVE)
DOWN按下去和MOVE移動(dòng)的時(shí)候都會(huì)產(chǎn)生x和y
觸摸開(kāi)始就要判斷觸摸點(diǎn)有沒(méi)有在一個(gè)圓點(diǎn)內(nèi)部
↓
isInDot() 是不是在某一個(gè)圓點(diǎn)內(nèi)
要知道:是否在某個(gè)圓點(diǎn)內(nèi)部呜舒、這個(gè)點(diǎn)有沒(méi)有點(diǎn)亮、和上一個(gè)點(diǎn)鏈接的編號(hào)值(需要知道具體內(nèi)容)
具體的信息在mDotModels里面存著笨奠,所以就直接返回DotModel模型即可(可選)
去做一個(gè)遍歷即可
需要知道圓點(diǎn)的矩形區(qū)域
↓
在DotModel模型數(shù)據(jù)內(nèi)部添加一個(gè)方法containsPoint()
讓模型自己判斷一個(gè)點(diǎn)是否在自己內(nèi)部袭蝗,外部不知道,自己是最知道的
data class DotModel (
val num:Int,val cx:Float,val cy:Float,val radius:Float,var state:DotState = DotState.Normal){? ? //判斷是否包含某個(gè)點(diǎn)? ? fun containsPoint(x:Float,y:Float):Boolean{? ? ? ? val rect = RectF(cx-radius,cy-radius,cx+radius,cy+radius)? ? ? ? return rect.contains(x,y)? ? }}
循環(huán)遍歷每一個(gè)圓點(diǎn)般婆,判斷這個(gè)圓點(diǎn)是否包含觸摸點(diǎn)x,y,如果是就把model返回出去
*****
private fun isInDot(x:Float,y:Float):DotModel?{? ? mDotModels.forEach {? ? ? ? if (it.containsPoint(x,y)){? ? ? ? ? ? return it? ? ? ? }? ? }? ? return null}
——————isInDot()全
觸摸上來(lái)了就找到是否有那個(gè)被觸摸的圓點(diǎn)
不為空則已經(jīng)進(jìn)入到某個(gè)觸摸點(diǎn)內(nèi)部了
判斷是否點(diǎn)亮稚虎,沒(méi)點(diǎn)亮就點(diǎn)亮
點(diǎn)亮->繪制一個(gè)圓
調(diào)用invalidate()重新繪制
怎么知道要畫個(gè)圓呢奸柬?畫哪個(gè)圓呢?——有一個(gè)容器專門用來(lái)保存我要繪制哪些圓
保存選中的區(qū)域(這個(gè)區(qū)域保存了那肯定要繪制它)
點(diǎn)亮:畫邊框和小圓點(diǎn)需要改變畫筆
繪制完了怎么知道是第一次繪制還是后面在繪制
1.繪制背景
2.判斷狀態(tài)
第一次繪制:繪制背景加到mDotModels
注意調(diào)用invalidate()重新繪制不能重復(fù)加到數(shù)組里面啤咽,只能加一次
怎么知道加進(jìn)數(shù)組還是不加:
根據(jù)目前數(shù)組的size長(zhǎng)度是否小于9判斷要不要加
如果index? <? mDotModels.size 就不要加進(jìn)去了
如果index? >? mDotModels.size 說(shuō)明是新的未加進(jìn)去過(guò)的點(diǎn)晋辆,加進(jìn)去
直到加到9
如果已經(jīng)加滿都不用加進(jìn)數(shù)組了,如果又重新繪制圓說(shuō)明其狀態(tài)可能改變了宇整,就要把該點(diǎn)拿過(guò)來(lái)判斷它的狀態(tài)
正常狀態(tài)啥都不用做
選中狀態(tài):就加個(gè)邊框栈拖、中間加圓
改狀態(tài)就是在繪制圓更改畫筆
****************************
//選中時(shí)空心邊框圓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() //描邊涉及邊的粗細(xì)? ? }}//選中時(shí)中心小圓private val mDotCenterPaint:Paint by lazy {? ? Paint().apply {? ? ? ? isAntiAlias = true? ? ? ? color = context.resources.getColor(R.color.light_blue,null)? ? ? ? style = Paint.Style.FILL //中心實(shí)心圓? ? }}
根據(jù)狀態(tài)繪制不同的圓
中心點(diǎn)圓的半徑不寫死,和外面大圓成一定的比例
************************************
private var mCenterDotRadius = 0f //中間小圓點(diǎn)的默認(rèn)半徑
mCenterDotRadius = mRadius.toFloat()/6
when(mDotModels[index-1].state){? ? DotState.Selected -> { //選中狀態(tài)? ? ? ? canvas?.drawCircle(cx,cy,mRadius.toFloat(),mDotSelectPaint)? ? ? ? canvas?.drawCircle(cx,cy,mCenterDotRadius,mDotCenterPaint)? ? }? ? DotState.Error -> { //錯(cuò)誤狀態(tài)? ? ? ? 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 //記錄點(diǎn)的序號(hào)? ? for (i in 0 until 3){ //控制行數(shù)? ? ? ? for (j in 0 until 3){ //控制列數(shù)? ? ? ? ? ? val cx = mStart_cx + j*(2*mRadius + mSpace)? ? ? ? ? ? val cy = mStart_cy + i*(2*mRadius + mSpace)? ? ? ? ? ? canvas?.drawCircle(cx,cy,mRadius.toFloat(),mDotPaint)
? ? ? ? ? ? //判斷dot狀態(tài)? ? ? ? ? ? if (index > mDotModels.size) {? ? ? ? ? ? ? ? //說(shuō)明還不足9個(gè)點(diǎn)第一次繪制加進(jìn)去? ? ? ? ? ? ? ? //封裝點(diǎn)的模型數(shù)據(jù)? ? ? ? ? ? ? ? mDotModels.add(? ? ? ? ? ? ? ? ? ? DotModel(index, cx, cy, mRadius.toFloat())? ? ? ? ? ? ? ? )? ? ? ? ? ? }else{ //已經(jīng)繪制過(guò)了無(wú)需再加進(jìn)數(shù)組需要判斷狀態(tài)? ? ? ? ? ? ? ? //根據(jù)狀態(tài)畫圓? ? ? ? ? ? ? ? when(mDotModels[index-1].state){? ? ? ? ? ? ? ? ? ? DotState.Selected -> { //選中狀態(tài)? ? ? ? ? ? ? ? ? ? ? ? canvas?.drawCircle(cx,cy,mRadius.toFloat(),mDotSelectPaint)? ? ? ? ? ? ? ? ? ? ? ? canvas?.drawCircle(cx,cy,mCenterDotRadius,mDotCenterPaint)? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? ? ? DotState.Error -> { //錯(cuò)誤狀態(tài)? ? ? ? ? ? ? ? ? ? ? ? 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++? ? ? ? }? ? }}
九個(gè)點(diǎn)繪制完畢
***********************
2023.8.29
記錄密碼
點(diǎn)亮記錄密碼是在up之后
繪制路徑
繪制線drawLine()
點(diǎn)的內(nèi)部直接連接線
需要用path連接:線長(zhǎng)什么樣子
滑到空白的地方無(wú)線没陡,除非已經(jīng)點(diǎn)亮一個(gè)點(diǎn)了
先畫線再畫點(diǎn)涩哟,讓點(diǎn)蓋住線
①先創(chuàng)建一條線↓
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結(jié)束
if (mPath.isEmpty) return
*****
繪制里面onTouchEvent()方法最重要,因?yàn)槲覀冊(cè)陔S時(shí)隨地在移動(dòng)
當(dāng)點(diǎn)亮第一個(gè)點(diǎn)的時(shí)候我們需要記錄一下當(dāng)前這個(gè)點(diǎn)
當(dāng)點(diǎn)亮第二個(gè)點(diǎn)就要在此之間畫一個(gè)線
記錄上一個(gè)點(diǎn)贴彼,沒(méi)有控件但是有model
記錄上一個(gè)被點(diǎn)亮的點(diǎn)的model即可
可能為空潜腻,一開(kāi)始運(yùn)行起來(lái)就是空
private var lastSelectedDotModel:DotModel? = null
就可以直接用點(diǎn)的中心點(diǎn)x,y畫線即可
在onTouchEvent()方法里面會(huì)調(diào)用touchBegin()器仗,更重要的事情都在touchBegin()里面做
點(diǎn)到某個(gè)點(diǎn)的內(nèi)部:
1.改變點(diǎn)的狀態(tài) 重新繪制
2.判斷有沒(méi)有上一個(gè)點(diǎn)
①?zèng)]有:該點(diǎn)就是起始點(diǎn)融涣,是第一個(gè)點(diǎn),記錄一下當(dāng)前點(diǎn)
把路徑的起點(diǎn)設(shè)到這個(gè)點(diǎn)
? if (lastSelectedDotModel == null){? ? //是第一個(gè)點(diǎn)記錄? ? lastSelectedDotModel = dot? ? //當(dāng)前第一個(gè)點(diǎn)就是路徑的起點(diǎn)? ? mPath.moveTo(dot.cx,dot.cy)? }
②有:拉一條線精钮,連接兩個(gè)點(diǎn)
? ? mPath.lineTo(dot.cx,dot.cy)
運(yùn)行一下:選中過(guò)的點(diǎn)無(wú)法在之間連線
只有是normal的才可以點(diǎn)亮
如果已經(jīng)選中過(guò)了威鹿,變成非normal,則已選中的兩點(diǎn)無(wú)法連線
如果想讓其之間也可以連線轨香,則應(yīng)去掉↓判斷
if(dot.state == DotState.Normal)
運(yùn)行一下:可以都連接了忽你,但是出現(xiàn)重復(fù)連接,會(huì)導(dǎo)致密碼記錄出現(xiàn)問(wèn)題(可能看到的是123 但是密碼可能是123321123321123)雖然看不到但是有在記錄
細(xì)節(jié)問(wèn)題:已經(jīng)點(diǎn)亮了某個(gè)點(diǎn)了臂容,手指繼續(xù)在點(diǎn)里面滑動(dòng)科雳,上一個(gè)點(diǎn)和當(dāng)前點(diǎn)永遠(yuǎn)是自己,在自己點(diǎn)的內(nèi)部中心點(diǎn)反復(fù)持續(xù)劃線
所以非第一個(gè)點(diǎn)時(shí)不能直接lineTo劃線脓杉,自己在自己內(nèi)部就沒(méi)有必要了
if (lastSelectedDotModel != dot) {
MyLog.v("點(diǎn)亮:${dot.num}")? ? mPath.lineTo(dot.cx, dot.cy)}
運(yùn)行一下:用MyLog.v("點(diǎn)亮:${dot.num}")打印看一下效果糟秘,出現(xiàn)一直點(diǎn)亮的問(wèn)題
這是沒(méi)有切換到上一個(gè)點(diǎn)到當(dāng)前點(diǎn),應(yīng)↓
if (lastSelectedDotModel != dot) { //只有兩個(gè)點(diǎn)不相同時(shí)再畫? MyLog.v("點(diǎn)亮:${dot.num}")? ? mPath.lineTo(dot.cx, dot.cy)? lastSelectedDotModel = dot}
運(yùn)行一下:此時(shí)已經(jīng)解決同一點(diǎn)反復(fù)點(diǎn)亮的問(wèn)題球散,但是會(huì)一條線反復(fù)來(lái)回點(diǎn)亮
思考:密碼該如何記錄
2023.9.11周一
兩點(diǎn)之間已經(jīng)連過(guò)線就不要再重復(fù)連線了
線有過(guò)就不要再連了
用一個(gè)變量記錄已經(jīng)連過(guò)的線
private var mSelectedLines = arrayListOf<Int>() //保存已經(jīng)點(diǎn)亮的線的數(shù)值
此數(shù)值就是12 56 小值的點(diǎn)的num在前 大的在后
需要兩個(gè)點(diǎn)的num值形成一個(gè)數(shù)據(jù)尿赚,到數(shù)組里面判斷是否已經(jīng)有了
獨(dú)立出一個(gè)方法
得到兩點(diǎn)之間的線lineValue()
先得到線的值,再判斷mSelectedLines數(shù)組里面是否已經(jīng)包含這個(gè)線了
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數(shù)組里面
val lineValue = lineValue(lastSelectedDotModel!!,dot)if (!mSelectedLines.contains(lineValue)) {? ? //兩點(diǎn)之間連線? ? MyLog.v("連接:${dot.num}")? ? mPath.lineTo(dot.cx, dot.cy)? ? lastSelectedDotModel = dot? ? mSelectedLines.add(lineValue)}
運(yùn)行一下: MyLog.v("連接:${dot.num}")可以解決重復(fù)連線的問(wèn)題了
實(shí)現(xiàn)線跟著手一起拽動(dòng)的效果
將上一個(gè)點(diǎn)和滑動(dòng)過(guò)程中產(chǎn)生的新的點(diǎn)連接起來(lái)就可以了
touchBegin()
else{} ——> 觸摸點(diǎn)沒(méi)有在某個(gè)圓點(diǎn)內(nèi)部
分析:先判斷有沒(méi)有上一個(gè)點(diǎn)蕉堰,沒(méi)有就在旁白處空劃吼畏,不做任何事
有上一個(gè)點(diǎn)就以該點(diǎn)中心為起始點(diǎn)連一條線
lastSelectedDotModel != null
else{ //觸摸點(diǎn)沒(méi)有在某個(gè)點(diǎn)的內(nèi)部? ? if (lastSelectedDotModel != null) {? ? ? ? //從上一個(gè)點(diǎn)和當(dāng)前點(diǎn)連成一條線? ? ? ? mPath.lineTo(x,y)? ? ? ? invalidate()? ? }}
運(yùn)行一下:出現(xiàn)畫曲線的情況
應(yīng)重新設(shè)置起點(diǎn),將起點(diǎn)改為lastSelectedDotModel
mPath.moveTo(lastSelectedDotModel!!.cx,lastSelectedDotModel!!.cy)
再mPath.lineTo(x,y)
運(yùn)行一下:出現(xiàn)扇形
因?yàn)橛肋h(yuǎn)都是從中心點(diǎn)不斷連線
但是我們先只要一條線
解決:開(kāi)一條新的從上一個(gè)點(diǎn)和在活動(dòng)點(diǎn)的Path即可
即開(kāi)一條移動(dòng)的線的路徑
private var mMoveLinePath = Path() //移動(dòng)的線的路徑
永遠(yuǎn)只需要一個(gè)從上一個(gè)點(diǎn)到現(xiàn)在的線
先重置一下嘁灯,再移動(dòng)到下一個(gè)點(diǎn)
需要再draw里面畫這個(gè)路徑
private fun drawLine(canvas: Canvas?){? ? if (mPath.isEmpty) return? ? canvas?.drawPath(mPath,mLinePaint) //繪制圓點(diǎn)之間的連接線? ? if (mMoveLinePath.isEmpty) return? ? canvas?.drawPath(mMoveLinePath,mLinePaint) //繪制移動(dòng)時(shí)跟著手移動(dòng)的線}
運(yùn)行一下:可以跟著手移動(dòng)了泻蚊,但是會(huì)出現(xiàn)同時(shí)出現(xiàn)兩條線的那一瞬間
說(shuō)明:未將移動(dòng)的線的起始點(diǎn)移到當(dāng)前點(diǎn)
兩點(diǎn)直接連接線之后要清一下mMoveLinePath,且移到當(dāng)面點(diǎn)的中心點(diǎn)
在連接連個(gè)點(diǎn)之間的線之后↓
//移動(dòng)的線mMoveLinePath.reset()mMoveLinePath.moveTo(dot.cx,dot.cy)mMoveLinePath.lineTo(x,y)
運(yùn)行一下:正常了丑婿,但是不能在圓點(diǎn)內(nèi)部實(shí)現(xiàn)線隨著手在內(nèi)部移動(dòng)
圓點(diǎn)內(nèi)部實(shí)現(xiàn)線隨著手在內(nèi)部移動(dòng)
在圓點(diǎn)內(nèi)部需要做
封裝一下
mMoveLinePath.reset()mMoveLinePath.moveTo(lastSelectedDotModel!!.cx,lastSelectedDotModel!!.cy)mMoveLinePath.lineTo(x,y)
[if !supportLists]1. [endif]直接點(diǎn)到點(diǎn)的外面性雄,沒(méi)有上一個(gè)圓點(diǎn)return
[if !supportLists]2. [endif]觸摸點(diǎn)沒(méi)有在某個(gè)圓點(diǎn)內(nèi)部,且有上一個(gè)點(diǎn)羹奉,則從上一個(gè)點(diǎn)和當(dāng)前點(diǎn)連成一條線
[if !supportLists]3. [endif]觸摸點(diǎn)在圓點(diǎn)內(nèi)部滑動(dòng)秒旋,線也要跟著手移動(dòng)
[if !supportLists]4. [endif]兩點(diǎn)之間連完線要接著確定拉伸線的路徑
到此圖案解鎖的繪制的邏輯完成
下一步完成松手的邏輯
****************
private fun touchBegin(x:Float,y:Float){? ? val dot = isInDot(x,y)? ? if (dot != null){ //在某個(gè)圓點(diǎn)內(nèi)部? ? ? ? ? ? //判斷這個(gè)點(diǎn)是不是第一個(gè)點(diǎn)? ? ? ? ? ? if (lastSelectedDotModel == null){? ? ? ? ? ? ? ? //是第一個(gè)點(diǎn)記錄? ? ? ? ? ? ? ? lastSelectedDotModel = dot? ? ? ? ? ? ? ? //當(dāng)前第一個(gè)點(diǎn)的中心點(diǎn)就是路徑的起點(diǎn)? ? ? ? ? ? ? ? mPath.moveTo(dot.cx,dot.cy)? ? ? ? ? ? }else{? ? ? ? ? ? ? ? //非第一個(gè)點(diǎn)連線 && 上一個(gè)點(diǎn)非自己在自己內(nèi)部就不要畫? ? ? ? ? ? ? ? if (lastSelectedDotModel != dot) { //只有兩個(gè)點(diǎn)不相同時(shí)再畫線? ? ? ? ? ? ? ? ? ? //判斷這條線是否已經(jīng)畫過(guò)了? ? ? ? ? ? ? ? ? ? val lineValue = lineValue(lastSelectedDotModel!!,dot)? ? ? ? ? ? ? ? ? ? if (!mSelectedLines.contains(lineValue)) {? ? ? ? ? ? ? ? ? ? ? ? //兩點(diǎn)之間連線? ? ? ? ? ? ? ? ? ? ? ? mPath.lineTo(dot.cx, dot.cy)? ? ? ? ? ? ? ? ? ? ? ? lastSelectedDotModel = dot? ? ? ? ? ? ? ? ? ? ? ? mSelectedLines.add(lineValue)? ? ? ? ? ? ? ? ? ? ? ? //移動(dòng)的線? ? ? ? ? ? ? ? ? ? ? ? addMoveLine(x,y)? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? }else{ //圓點(diǎn)內(nèi)部,線隨著手移動(dòng)? ? ? ? ? ? ? ? ? ? addMoveLine(x,y)? ? ? ? ? ? ? ? }? ? ? ? ? ? }? ? ? ? ? ? dot.state = DotState.Selected? ? ? ? ? ? //點(diǎn)亮 -> 繪制一個(gè)圓? ? ? ? ? ? invalidate()? ? }else{ //觸摸點(diǎn)沒(méi)有在某個(gè)點(diǎn)的內(nèi)部沒(méi)有點(diǎn)到某個(gè)點(diǎn)上? ? ? ? if (lastSelectedDotModel != null) {? ? ? ? ? ? //從上一個(gè)點(diǎn)和當(dāng)前點(diǎn)連成一條線? ? ? ? ? ? addMoveLine(x,y)? ? ? ? ? ? invalidate()? ? ? ? }? ? }}
2023.9.12周二
松手邏輯touchEnd()
獲取密碼
清空
注意:這個(gè)View只是完成繪制這一單一功能诀拭,密碼產(chǎn)生
至于外部拿密碼做什么(根據(jù)密碼改變顏色)那就是外部的事情了(判斷密碼邏輯)
先做密碼:
有一個(gè)變量記錄密碼
private val mPasswordBuilder = StringBuilder()
在touchBegin()里面但凡有點(diǎn)的點(diǎn)亮就把它記錄起來(lái)
①第一個(gè)點(diǎn)
②有l(wèi)ineTo()的地方
//記錄當(dāng)前點(diǎn)的值mPasswordBuilder.append(dot.num)
touchEnd()中
//獲取密碼val password = mPasswordBuilder.toString()
清空單獨(dú)寫一個(gè)方法迁筛,因?yàn)椴恢挥忻艽a記錄結(jié)束后需要清空,紅色的點(diǎn)亮完后也需要清空
清空clear()
需要清空密碼耕挨、重置線细卧、清空已經(jīng)連接的線尉桩、已經(jīng)記錄的點(diǎn),并重新繪制
*********
private fun clear(){
postDelayed({? ? //密碼? ? mPasswordBuilder.clear()? ? //重置線? ? mPath.reset()? ? mMoveLinePath.reset()
//清空上一個(gè)記錄的點(diǎn)
lastSelectedDotModel = null? ? //清空已經(jīng)連接的線? ? mSelectedLines.clear()? ? //清空已經(jīng)記錄的點(diǎn)? ? mDotModels.clear()
//清理完需要重新繪制? ? invalidate()
},500)}
不能立刻清空贪庙,需要時(shí)間延遲postDelayed()
運(yùn)行一下:可實(shí)現(xiàn)抬手清空
回調(diào)數(shù)據(jù)給外部
抬手之后清空之前蜘犁,需要把密碼回調(diào)給調(diào)用者,把當(dāng)前這個(gè)控件也傳給調(diào)用者(因?yàn)樾枰每丶锩娴姆椒?
private var mCallBack:((UnlockView,String)->Unit)? = null //回調(diào)數(shù)據(jù)
給外部提供一個(gè)公開(kāi)方法用于調(diào)用
//外部監(jiān)聽(tīng)密碼繪制結(jié)束的監(jiān)聽(tīng)器fun addDrawFinishedListener(callback:(UnlockView,String) -> Unit){? ? mCallBack = callback}
**********private fun touchEnd(){? ? //獲取密碼? ? val password = mPasswordBuilder.toString()? ? //清空? ? clear()? ? //數(shù)據(jù)回調(diào)? ? mCallBack?.let {? ? ? ? it(this,password)? ? }}
回調(diào)數(shù)據(jù)給外部:
①函數(shù)聲明:將外部傳給我的實(shí)現(xiàn)保存住
private var mCallBack:((UnlockView,String)->Unit)? = null //回調(diào)數(shù)據(jù)
②給外部一個(gè)接口(一個(gè)方法)止邮,通過(guò)這種方式傳給我的
//外部監(jiān)聽(tīng)密碼繪制結(jié)束的監(jiān)聽(tīng)器fun addDrawFinishedListener(callback:(UnlockView,String) -> Unit){? ? mCallBack = callback}
③真正回調(diào):調(diào)用這個(gè)函數(shù)
//數(shù)據(jù)回調(diào)? ? mCallBack?.let {? ? ? ? it(thiss,password) //調(diào)用這個(gè)函數(shù)这橙,password是我的參數(shù)? ? }
這樣就傳過(guò)去了
數(shù)據(jù)是回調(diào)給PatternLoginFragment的
給PatternLoginFragment添加binding
view創(chuàng)建完之后是需要獲取其事件的,PatternLoginFragment通過(guò)調(diào)用addDrawFinishedListener{}得到密碼
只有密碼還不夠导披,還要支配你屈扎,可能需要你額外做一些事情,如讓你顯示錯(cuò)誤
顯示錯(cuò)誤showError()
但是在此之前的做法是還未顯示錯(cuò)誤就已經(jīng)全部清空了
提供兩種狀態(tài)撩匕,正常狀態(tài)和錯(cuò)誤狀態(tài)
做完之后需要清一下但是又不能全部給清空了
mPath
mDotModels需要保存
矛盾:不清的話就會(huì)一直顯示
清的話就無(wú)法顯示紅色了
做一個(gè)備份鹰晨,將要的東西存著
//備份private var mBackupPath:Path? = null //備份連接線的路徑private var mBackupModels:ArrayList<DotModel>? = null //備份圓點(diǎn)的model
圓點(diǎn)不用在此處更畫筆改為紅色,因?yàn)樵诶L制圓點(diǎn)drawNineDot()的時(shí)候會(huì)根據(jù)其狀態(tài)改變顏色的
在PatternLogin里面調(diào)用showError()
**********
fun showError(){? ? //恢復(fù)數(shù)據(jù)? ? mPath = mBackupPath?:Path()? //沒(méi)有路徑就創(chuàng)建一個(gè)空的? ? mDotModels = mBackupModels?: arrayListOf() //沒(méi)有對(duì)象就創(chuàng)建空的數(shù)組? ? //顯示錯(cuò)誤顏色改變畫筆顏色? ? mLinePaint.color = resources.getColor(R.color.red,null)? ? //恢復(fù)完重新繪制一下? ? invalidate()}
測(cè)試一下:不可以
******
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {? ? super.onViewCreated(view, savedInstanceState)? ? binding.unlockView.addDrawFinishedListener {unlockView, s ->? ? ? ? unlockView.showError()? ? }}
在touchEnd()中先清空再保存
測(cè)試一下:
應(yīng)該在清理之前滑沧,一抬手就先做個(gè)備份并村,不能delay五毫秒再備份
運(yùn)行一下:出現(xiàn)紅線后又立馬消失巍实,是時(shí)機(jī)的問(wèn)題
滓技,touch()中先備份,備份完就去清空了棚潦,清空會(huì)延遲500毫秒做令漂,調(diào)用clear()
還沒(méi)開(kāi)始執(zhí)行清空的時(shí)候就去回調(diào)給外部了,外部立刻就調(diào)用showError(),showError()里面就會(huì)恢復(fù)線點(diǎn)丸边,顯示紅叠必,此刻0.5秒到了就clear()全部清空了
---------
private fun touchEnd(){? ? //獲取密碼? ? val password = mPasswordBuilder.toString()
//備份? ? mBackupPath = Path(mPath)? ? mBackupModels = ArrayList(mDotModels)? ? //清空? ? clear()? ? //數(shù)據(jù)回調(diào)? ? mCallBack?.let {? ? ? ? it(this,password)? ? }}
運(yùn)行一下:不要延遲500毫秒了,再運(yùn)行會(huì)持續(xù)留下妹窖,不會(huì)消失纬朝,為什么點(diǎn)沒(méi)有變紅?:因?yàn)槲覀儧](méi)有改變點(diǎn)的狀態(tài)值
showError()中添加
//改變點(diǎn)的狀態(tài)值使其變成紅色mDotModels.forEach {? ? if (it.state == DotState.Selected){? ? ? ? it.state = DotState.Error? ? }}
運(yùn)行一下:再運(yùn)行就是紅點(diǎn)了骄呼,但是會(huì)可以再畫共苛,應(yīng)該控制一下,任何操作都不能再做了
如果控制不能再做其他任何操作了:
給一個(gè)開(kāi)關(guān)
private var canTouch = true
但是進(jìn)入到canTouch ()中之后就不能Touch 了
同樣的在touchBegin()中也需要做判斷
if (!canTouch) return
運(yùn)行一下:這樣變紅之后就不能再畫了蜓萄,但是不能一直紅著如何恢復(fù)呢
showError()之后就將canTouch改為true隅茎,再clear()一下,且備份的東西也不要
運(yùn)行一下:可以消失并進(jìn)行下一次滑動(dòng)了嫉沽,但是下一次滑動(dòng)開(kāi)始也是紅色的
因?yàn)榫€的畫筆還是紅色的辟犀,再改一次,點(diǎn)還是紅色的是因?yàn)樯厦娈嬀艂€(gè)點(diǎn)的時(shí)候沒(méi)有改selected狀態(tài)時(shí)的畫筆顏色就直接繪制了(錯(cuò)誤的時(shí)候就一直保持錯(cuò)誤時(shí)畫筆的顏色了)
******************
fun showError(){? ? canTouch = false? ? //恢復(fù)數(shù)據(jù)? ? mPath = mBackupPath?:Path()? //沒(méi)有路徑就創(chuàng)建一個(gè)空的? ? mDotModels = mBackupModels?: arrayListOf() //沒(méi)有對(duì)象就創(chuàng)建空的數(shù)組? ? //改變點(diǎn)的狀態(tài)值使其變成紅色? ? mDotModels.forEach {? ? ? ? if (it.state == DotState.Selected){? ? ? ? ? ? it.state = DotState.Error? ? ? ? }? ? }? ? //顯示錯(cuò)誤顏色改變畫筆顏色? ? mLinePaint.color = resources.getColor(R.color.red,null)? ? //恢復(fù)完重新繪制一下? ? invalidate()? ? //變紅之后恢復(fù)? ? postDelayed({? ? ? ? mLinePaint.color = resources.getColor(R.color.light_blue,null)? ? ? ? mBackupModels = null? ? ? ? mBackupPath = null? ? ? ? canTouch = true? ? ? ? clear()? ? },500)}
自定義View部分結(jié)束
------------------------------------------------------------------------------------------------------------------------------------------
2023.9.15周五
SharedViewModel()
分析項(xiàng)目需不需要ViewModel绸硕,是否需要共享ViewModel
應(yīng)該共享users數(shù)據(jù)
在loadAllUserInfo的時(shí)候其實(shí)應(yīng)該有一個(gè)返回值堂竟,返回所有用戶信息魂毁,然后在ViewModel里面存著
ViewModel的更新更界面的監(jiān)聽(tīng)有什么關(guān)系?
需要監(jiān)聽(tīng)切換到圖案解鎖:它的顯示與隱藏的狀態(tài)是需要實(shí)時(shí)更新的
登錄按鈕:監(jiān)聽(tīng)用戶名和密碼兩個(gè)數(shù)據(jù)是否輸入完畢跃捣,只有全部輸入完畢才能點(diǎn)擊漱牵,否則不能
view -> viewmodels(包)
新建一個(gè)需要共享的ViewModel:SharedViewModel,繼承于ViewModel()
圖案解鎖的顯示與否需要監(jiān)聽(tīng):它的狀態(tài)是由數(shù)據(jù)來(lái)更改的
把它定義成一個(gè)類型疚漆,這個(gè)狀態(tài)是可以改變的酣胀,默認(rèn)值是false(注意不能私有化,需要給外部監(jiān)聽(tīng))
val showChange = MutableLiveData(false)
怎么知道是true還是false呢娶聘?
切換到圖案解鎖只有存在用戶信息的時(shí)候才會(huì)顯示
loadAllUserInfo在MyApplication時(shí)加載闻镶,此時(shí)還沒(méi)有界面,SharedViewModel無(wú)法存對(duì)應(yīng)的數(shù)據(jù)
應(yīng)對(duì)方法:寫一個(gè)init方法丸升,在初始化創(chuàng)建我這個(gè)對(duì)象的時(shí)候就確定”切換到圖案解鎖”要不要顯示
init {? ? UserManager.sharedInstance()}
這個(gè)需要context,所以我們就使用AndroidViewModel()這個(gè)需要外部傳入application的
一會(huì)可能會(huì)多次調(diào)用UserManager里面的屬性或者方法铆农,所以將其單獨(dú)摘出來(lái),方便使用狡耻,就不用每次都寫一次了
private var userManager = UserManager.sharedInstance(application.applicationContext)
程序運(yùn)行起來(lái)就要進(jìn)入這個(gè)init方法里面來(lái)墩剖,在init中調(diào)用hasUser()方法,如果有用戶showChange 的值是true夷狰,可以直接這么寫↓
init {? ? showChange .postValue(userManager.hasUser())
//如果有用戶就直接把hasUser()的true post給showChange }
此時(shí)showChange 的狀態(tài)已經(jīng)確定岭皂,下一步→監(jiān)聽(tīng)
誰(shuí)來(lái)監(jiān)聽(tīng):Fragment來(lái)監(jiān)聽(tīng)→PinLoginFragment來(lái)監(jiān)聽(tīng)
此時(shí)數(shù)據(jù)在ViewModel中,去監(jiān)聽(tīng)ViewModel
①共享數(shù)據(jù)
private val viewModel:SharedViewModel by activityViewModels()
//可以獲得當(dāng)前activity所管理的ViewModel
注意只能使用val
②監(jiān)聽(tīng)
//監(jiān)聽(tīng)是否需要顯示切換圖案解鎖的textViewviewModel.showChange.observe(viewLifecycleOwner){? ? binding.switchToPatternView.visibility = if (it) View.VISIBLE else View.INVISIBLE}
運(yùn)行一下:不能顯示切換到圖案密碼解鎖沼头,因?yàn)橹耙呀?jīng)把加載的數(shù)據(jù)刪除了爷绘,正確
按鈕是否能點(diǎn)擊的狀態(tài)是切換
根據(jù)用戶是否輸入完畢,默認(rèn)false不能點(diǎn)擊
//記錄按鈕是否可以點(diǎn)擊val loginBtnIsEnabled = MutableLiveData(false)
//監(jiān)聽(tīng)登錄按鈕是否可以點(diǎn)擊viewModel.loginBtnIsEnabled.observe(viewLifecycleOwner){? ? binding.button.isEnabled = it}
運(yùn)行一下:
寫界面:一個(gè)界面對(duì)應(yīng)一個(gè)ViewModel进倍,管理這個(gè)界面的所有數(shù)據(jù)
ViewModel可以實(shí)現(xiàn)對(duì)擁有者的生命周期的監(jiān)聽(tīng)土至,其生命周期沒(méi)有結(jié)束的時(shí)候ViewModel不會(huì)消失,好處:對(duì)一些我們需要長(zhǎng)時(shí)間存在的數(shù)據(jù)猾昆,可以把數(shù)據(jù)放在ViewModel里面陶因,如果數(shù)據(jù)的變化需要外面能感知到,那么可以使用LiveData(MutableLiveData是LiveData的實(shí)現(xiàn)子類)垂蜗,postVale設(shè)置它的值楷扬,一旦設(shè)置就會(huì)調(diào)給監(jiān)聽(tīng)者,告知其發(fā)生變化
2023.9.16周六晚
共享ViewModel
在Fragment中想和activity(或者其他的Fragment)共享ViewModel就使用by activityViewModels()么抗,就可以獲取到當(dāng)前這個(gè)activity管理的ViewModel毅否,如果這個(gè)activity上面依附了很多Fragment那么就可以確定他們創(chuàng)建的ViewModel都是一樣的
按鈕是否能點(diǎn)擊
只有用戶名和密碼都輸入了才能點(diǎn)擊
UserInputView
需要把正在輸入的這件事情回調(diào)給外部(告訴外部這個(gè)地方已經(jīng)開(kāi)始輸入了)
要知道輸入框正在輸入,要關(guān)注一下EditText
addTextChangedListener
監(jiān)聽(tīng)輸入框蝇刀,內(nèi)容改變了就把事件傳出去
2023.9.17周日
輸入狀態(tài)的監(jiān)聽(tīng)
正在發(fā)生改變螟加,要把這個(gè)事件實(shí)時(shí)往外傳
怎么回傳數(shù)據(jù):
想要外部傳遞東西就是回調(diào)數(shù)據(jù)
[if !supportLists]1. [endif]告訴外部發(fā)生變化的文本
[if !supportLists]2. [endif]外部只關(guān)心結(jié)果,不關(guān)系正在輸入或刪除的過(guò)程
[if !supportLists]3. [endif]當(dāng)前這個(gè)對(duì)象本身也要傳出去(可能需要改變顯示錯(cuò)誤的狀態(tài))
我調(diào)用你的時(shí)候不需要你返回給我,但是我要給你傳參數(shù)(對(duì)象本身捆探,文本內(nèi)容的字符串)
事件回調(diào)
外部textChangedListener通過(guò)監(jiān)聽(tīng)
//定義回調(diào)的高階函數(shù)類型
var textChangedListener:((UserInputView,String) -> Unit)? = null
一旦發(fā)生改變就立刻回調(diào)出去然爆,將當(dāng)前文本內(nèi)容傳遞給外部
textChangedListener?有沒(méi)有,如果有就立刻調(diào)用自己textChangedListener()這個(gè)函數(shù)黍图,函數(shù)的參數(shù)傳(this本身曾雕,傳當(dāng)前文本內(nèi)容text.toString)
******
binding.inputEditText.addTextChangedListener(? ? //正在發(fā)生改變? ? onTextChanged = {text: CharSequence?, start: Int, before: Int, count: Int ->? ? ? ? //將當(dāng)前文本內(nèi)容傳遞給外部? ? ? ? textChangedListener?.let {? ? ? ? ? ? it(this,text.toString())? ? ? ? }? ? })
誰(shuí)要監(jiān)聽(tīng)誰(shuí)就去設(shè)置這個(gè)監(jiān)聽(tīng)器就可以了
找到pin登錄的界面PinLoginFragment
就要去監(jiān)聽(tīng)了
外部監(jiān)聽(tīng)到了這個(gè)事件要去干什么那就是外部的事情了
nameInputView和passwordInputView兩個(gè)結(jié)合起來(lái)就要去判斷登錄按鈕是否顯示
兩個(gè)同時(shí)必須同時(shí)有內(nèi)容才可以顯示
得到對(duì)應(yīng)的輸入內(nèi)容
如果在nameInputView可以得到自己的text,只要知道password的就可以了
需要通過(guò)一個(gè)方法得到輸入的內(nèi)容
UserInputView還沒(méi)有把當(dāng)前輸入的內(nèi)容暴露給外部的方法
可以定義一個(gè)屬性text(文本內(nèi)容)
--
var text:String = “”//記錄輸入的文本內(nèi)容
用var還是val產(chǎn)生的矛盾:text屬性外部只能訪問(wèn)不能改變數(shù)據(jù)助被,內(nèi)部可以改變數(shù)據(jù)
通常用兩個(gè)屬性
private var _text:String = "" //內(nèi)部使用val text:String = "" //外部使用
當(dāng)外部通過(guò)text訪問(wèn)的時(shí)候
要經(jīng)過(guò)get方法剖张,把_text內(nèi)容給text即可
get() = _text
//但是沒(méi)有必要這么寫
可以通過(guò)binding直接找得到text(直接訪問(wèn)輸入框里面的內(nèi)容)↓
val text:String //外部使用
get() = binding.inputEditText.text.toString()
text.isNotEmpty() && binding.passwordInputView.text.isNotEmpty()
兩個(gè)條件同時(shí)成立即可顯示
那么如何改變按鈕的狀態(tài)呢?
顯示與否的狀態(tài)是SharedViewModel來(lái)做的
更改狀態(tài)的話單獨(dú)寫一個(gè)方法出來(lái)
SharedViewModel中更改登錄按鈕狀態(tài)changeBtnState()
只有兩種狀態(tài)兩個(gè)值揩环,用枚舉類
***
//給外部提供改變狀態(tài)的方法
fun changeBtnState(state: ButtonState){? ? loginBtnIsEnabled.postValue(state == ButtonState.Enabled)}
enum class ButtonState{? ? Enabled,UnEnabled}
****
PinLoginFragment
//監(jiān)聽(tīng)用戶名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)? ? }}
運(yùn)行一下:可以正常顯示
再封裝一下
initUI()
initListener()
登錄按鈕的操作
現(xiàn)在已經(jīng)實(shí)現(xiàn)輸入了
一點(diǎn)登錄就要做登錄要做的事情了
點(diǎn)了登錄就要去登錄用戶名搔弄,把用戶名和密碼拿出來(lái)做登錄操作
點(diǎn)擊:完成對(duì)應(yīng)的登錄狀態(tài)進(jìn)行登錄驗(yàn)證
注意:有了ViewModel所有的操作都是從ViewModel發(fā)出去,ViewModel就是管理數(shù)據(jù)的
想要做什么事就先在ViewModel中添加什么方法
(實(shí)際的應(yīng)用里面不是在本地登錄的丰滑,肯定是在網(wǎng)絡(luò)中登錄的顾犹,要把用戶名拿到服務(wù)器上面去,從服務(wù)器上返回一個(gè)結(jié)果出來(lái)褒墨,才能返回一個(gè)結(jié)果給調(diào)用者炫刷,才能拿去做其他事情
注意:一個(gè)線程同一時(shí)間只能做一個(gè)事情)
//記錄登錄結(jié)果是否成功的狀態(tài)(成功狀態(tài) or 失敗狀態(tài))val loginState = MutableLiveData(LoginState.Default)
枚舉三種狀態(tài):Success,Failure,Default
↓
//登錄狀態(tài)enum class LoginState{? ? Success,Failure,Default}
登錄邏輯:一旦輸入后點(diǎn)擊登錄按鈕就進(jìn)入login()方法里面來(lái),login()要去找UserManager郁妈,UserManager整完得到結(jié)果給login()浑玛,再更改loginState的狀態(tài)值,F(xiàn)ragment再監(jiān)聽(tīng)狀態(tài)值圃庭。(最終只關(guān)心實(shí)時(shí)更新的loginState的狀態(tài)值)
再PinLoginFragment中任何東西的變化锄奢、任何的操作都要去找ViewModel
↓
//登錄按鈕的點(diǎn)擊事件binding.button.setOnClickListener {? ? //登錄驗(yàn)證? ? ? viewModel.login(binding.nameInputView.text,binding.passwordInputView.text
,PasswordType.Pin)
}
要得到它的結(jié)果失晴,去監(jiān)聽(tīng)loginState
↓
//實(shí)時(shí)觀察登錄狀態(tài)
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)}
運(yùn)行一下:是登錄不了的剧腻,因?yàn)闆](méi)有文件,默認(rèn)彈出default
登錄按鈕登錄失敗后的顯示:
顯示提示文本涂屁、文字顯示紅色书在、登錄按鈕震動(dòng)
以屬性的方式顯示錯(cuò)誤后的提示文本
添加自定義屬性
1.values -> attrs
<!--錯(cuò)誤時(shí)提示內(nèi)容--><attr name="error_title" format="string|reference"/>
[if !supportLists]2. [endif]xml中配置屬性
app:error_title="用戶名不存在"
app:error_title="密碼錯(cuò)誤"
[if !supportLists]3. [endif]UserInputView中
(1)解析error_title
private lateinit var errorTitle:String
是不是空,不是空就返回拆又,如果外部沒(méi)有設(shè)置是空就顯示原來(lái)title的內(nèi)容
initUI()中↓
errorTitle = (typedArray.getString(R.styleable.UserInputView_error_title)?:binding.titleTextView.text) as String
(2)
還要給外部配置一下儒旬,顯示正常還是顯示提示錯(cuò)誤
不做任何配置,顯示的是正常狀態(tài)
給外部提供一個(gè)方法配置狀態(tài)
切換錯(cuò)誤狀態(tài)showError()
把標(biāo)題和顏色改變一下
內(nèi)部再寫一個(gè)方法
得到正常or錯(cuò)誤的狀態(tài)showState()
注意:title要存一下帖族,切換提示字的時(shí)候要用到
↓
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了
只有錯(cuò)誤狀態(tài)才切換
登錄成功就直接跳轉(zhuǎn)到主界面去了
↓
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 -> {}? ? }}
運(yùn)行一下:顯示紅色了栈源,但是無(wú)法恢復(fù)正常輸入狀態(tài)
應(yīng)該延時(shí)一會(huì)兒再變回來(lái)
①顯示正常提示狀態(tài)
②清空輸入框
↓
fun showError(){? ? showState(false)? ? postDelayed({? ? ? showState(true)? ? ? ? binding.inputEditText.setText("")? ? },1000)}
2023.9.18周一
登錄失敗后:登錄按鈕作用震動(dòng)
給控件添加晃動(dòng)的效果
tools -> AnimTools(靜態(tài)類)
左右擺動(dòng)動(dòng)畫startSwingAnim()
傳入:①View給哪個(gè)控件添加這個(gè)動(dòng)畫 ②擺動(dòng)幅度,給默認(rèn)值 ③晃動(dòng)的時(shí)間
④動(dòng)畫開(kāi)始了要做什么(開(kāi)始時(shí)事件回調(diào)) ⑤動(dòng)畫結(jié)束了要做什么 不需要參數(shù)和返回值竖般,默認(rèn)啥都不做
左右擺動(dòng)是改變x坐標(biāo)甚垦,看一下要改變的x坐標(biāo)是什么類型的
用屬性動(dòng)畫:就是給某一個(gè)控件的某一個(gè)屬性做動(dòng)畫
↓ 可以通過(guò)以下方式查看
view.translationX
所以這里的of只能用float類型
vararg理解:注意要給的是px像素值
可調(diào)用之前已經(jīng)寫過(guò)多的view.dp2px
先讓動(dòng)畫動(dòng)起來(lái),先不管onStart、onEnd
什么時(shí)候做動(dòng)畫艰亮?
登錄失敗的時(shí)候做動(dòng)畫
PinLoginFragment中的when Failure
? AnimTools.startSwingAnim(? ? ? ? binding.button,? ? ? ? time = 100)}
運(yùn)行一下:可正潮蒸妫晃動(dòng)
如果要額外做事,就監(jiān)聽(tīng)一下這個(gè)動(dòng)畫事件
addListener(onStart = {onStart()}, onEnd = {onEnd()}) //將動(dòng)畫事件傳遞給外部
注冊(cè)功能
還沒(méi)有用戶信息
先搭建Pin注冊(cè)界面
PinRegisterFragment中迄埃,binding,viewModel
↓
private lateinit var binding:FragmentPinRegisterBinding
//Pin登錄界面的數(shù)據(jù)會(huì)影響Pin注冊(cè)界面疗韵,所以單獨(dú)用一個(gè)ViewModel,不數(shù)據(jù)共享private val viewModel:SharedViewModel by activityViewModels()
監(jiān)聽(tīng)注冊(cè)按鈕的狀態(tài)
viewModel.loginBtnIsEnabled.observe(viewLifecycleOwner){? ? binding.button.isEnabled = it}
當(dāng)三個(gè)輸入框都有內(nèi)容的時(shí)候才可以點(diǎn)擊
就看這三個(gè)東西是否為空
但是三個(gè)東西同時(shí)判斷不方便判斷
用變量實(shí)時(shí)保存輸入框內(nèi)容侄非,一旦發(fā)生改變就把內(nèi)容存下來(lái)
private var name = ""private var password = ""private var confirmPassword = ""
內(nèi)容存下來(lái)就去判斷它的狀態(tài)checkEnabled()
private fun checkEnabled(){? ? val state = if (name.isNotEmpty() && password.isNotEmpty() && confirmPassword.isNotEmpty()){? ? ? ? SharedViewModel.ButtonState.Enabled? ? }else{? ? ? ? SharedViewModel.ButtonState.UnEnabled? ? }? ? viewModel.changeBtnState(state)}
↓
//監(jiān)聽(tīng)輸入框內(nèi)容改變的事件binding.nameInputView.textChangedListener = {userInputView, text ->? ? name = text? ? checkEnabled()}binding.passwordInputView.textChangedListener = {userInputView, text ->? ? password = text? ? checkEnabled()}binding.confirmPasswordInputView.textChangedListener = {userInputView, text ->? ? confirmPassword = text? ? checkEnabled()}
點(diǎn)擊注冊(cè)實(shí)現(xiàn)注冊(cè)功能
確保兩次密碼輸入一致checkPassword()
當(dāng)注冊(cè)按鈕被點(diǎn)擊的時(shí)候調(diào)用該方法or
確認(rèn)密碼輸入完之后就判斷(未采用)
想知道密碼輸入完畢
確認(rèn)密碼是否輸入完畢事件監(jiān)聽(tīng)
2023.9.19周二
點(diǎn)擊登錄就去判斷兩次密碼是否輸入一致
注冊(cè)按鈕一點(diǎn)擊就調(diào)用checkPassword()
兩個(gè)相同就注冊(cè)不同就提示錯(cuò)誤showError蕉汪、按鈕震動(dòng)
先做錯(cuò)誤時(shí)
運(yùn)行一下:正常顯示紅色,可震動(dòng)
private fun checkPassword(){? ? if (password == confirmPassword){? ? ? ? //注冊(cè)? ? }else{? ? ? ? //提示錯(cuò)誤? ? ? ? binding.passwordInputView.showError()? ? ? ? binding.confirmPasswordInputView.showError()
AnimTools.startSwingAnim(binding.button,time = 100)
? ? }}
注冊(cè)找ViewModel逞怨,這種行為動(dòng)作就去找ViewModel
在UserManager中的registerUser()要同時(shí)有兩種密碼才能注冊(cè)成功
注意:Pin注冊(cè)成功后就去進(jìn)入下一個(gè)頁(yè)面進(jìn)行Pattern的注冊(cè)
當(dāng)前界面需要保存用戶和設(shè)置的Pin密碼肤无,并進(jìn)入到下一個(gè)界面
保存Pin密碼的話需要和圖案密碼的界面用統(tǒng)一個(gè)ViewModel,因?yàn)樾枰蚕碛脩裘兔艽a
在SharedViewModel中↓
創(chuàng)建一個(gè)User對(duì)象骇钦,val user = User()宛渐,直接暴露給外部就性,要不然還要提供一個(gè)方法供外部訪問(wèn)
需要更改User()類眯搭,都改成var窥翩,給默認(rèn)值為空,isLogin默認(rèn)為false
private fun checkPassword(){? ? if (password == confirmPassword){? ? ? ? //注冊(cè):保存當(dāng)前用戶的用戶名和密碼? ? ? ? viewModel.user.name = name? ? ? ? viewModel.user.pin = password? ? ? ? //進(jìn)入到圖案密碼界面? ? ? ? navigateTo(PatternRegisterFragment(), addToStack = false)? ? }else{? ? ? ? //提示錯(cuò)誤? ? ? ? binding.passwordInputView.showError()? ? ? ? binding.confirmPasswordInputView.showError()? ? ? ? AnimTools.startSwingAnim(binding.button,time = 100)? ? }}
運(yùn)行一下:運(yùn)行正常鳞仙,可以跳轉(zhuǎn)到Pattern注冊(cè)界面
-----------------------------------------------------------
PatternRegister界面搭建 數(shù)據(jù)監(jiān)聽(tīng)
給PatternRegister添加binding
監(jiān)聽(tīng)unLockView返回的數(shù)據(jù)
binding.unlockView.addDrawFinishedListener { unlockView, password ->
unLockView中用addDrawFinishedListener()實(shí)現(xiàn)數(shù)據(jù)外傳
得到unLockView和password
第一次的時(shí)候肯定要記錄密碼
private var mPassword = ""
要判斷兩次密碼是否一致
1.先判斷mPassword是否為空寇蚊,為空就是第一次,將第一次密碼的值賦給mPassword棍好,再進(jìn)行第二次的滑動(dòng)
2.不為空就說(shuō)明是第二次了要進(jìn)行兩次密碼是否一致的判斷
提示textView顯示對(duì)應(yīng)文字
(1)①兩次密碼不一致要清空mPassword
②showError()
[if !supportLists](2)[endif]密碼一致就要保存設(shè)置的圖案密碼注冊(cè)用戶
注冊(cè)成功后就要跳轉(zhuǎn)到PinLogin界面進(jìn)行登錄操作navigateTo(PinLoginFragment()
數(shù)據(jù)在ViewModel中仗岸,添加ViewModel
PinRegister和PatternRegister需要數(shù)據(jù)的共享
但是在此之前,PinRegister用的是by viewModels
解決:
方法一:如果只有PinRegister和PatternRegister兩個(gè)之間需要數(shù)據(jù)共享借笙,不跟外部共享SharedViewModel扒怖,自己再創(chuàng)建一個(gè)SharedViewModel
方法二:二者之間需要共享的只是user的用戶名和密碼,可以把用戶名和密碼作為參數(shù)傳給Fragment
如果都共享的話业稼,loginBtnEnabled會(huì)有影響:如果在PinLogin都填入信息了盗痒,則登錄按鈕可以點(diǎn)擊,如果這個(gè)時(shí)候切換到PinRegister注冊(cè)界面低散,那么注冊(cè)按鈕就可以立即點(diǎn)擊了
干脆重新單獨(dú)弄一個(gè)registerBtnIsEnabled俯邓,changeRegisterBtnState()
此時(shí)需要修改PinRegisterFragment中就不要監(jiān)聽(tīng)loginBtnEnabled了,改成registerBtnIsEnabled
//保存注冊(cè)用戶信息viewModel.user.pattern = mPassword
↓
binding.unlockView.addDrawFinishedListener { unlockView, password ->? ? if(mPassword.isEmpty()){? ? ? ? mPassword = password? ? }else{? ? ? ? if (mPassword == password){? ? ? ? ? ? //密碼設(shè)置成功? ? ? ? ? ? binding.alertPatternView.text = "密碼設(shè)置成功"
binding.alertPatternView.text = "請(qǐng)確認(rèn)密碼圖案"? ? ? ? ? ? //保存注冊(cè)用戶信息? ? ? ? ? ? viewModel.user.pattern = mPassword? ? ? ? ? ? viewModel.register()? ? ? ? ? ? navigateTo(PinLoginFragment(), addToStack = false)? ? ? ? }else{? ? ? ? ? ? //密碼設(shè)置失敗? ? ? ? ? ? binding.alertPatternView.text = "兩次密碼不一致請(qǐng)重新設(shè)置"? ? ? ? ? ? mPassword = ""? ? ? ? ? ? unlockView.showError()? ? ? ? }? ? }}
此時(shí)user的信息已經(jīng)全了熔号,就可以去注冊(cè)稽鞭,注冊(cè)操作就要去找ViewModel
ViewModel中要提供注冊(cè)功能:
①調(diào)用UserManager里面的registerUser()
②注冊(cè)成功后,文件中就存在用戶信息了引镊,需要顯示切換到圖案解鎖朦蕴,注意重新post更新showChange的值
ViewModel注冊(cè)功能register()
fun register(){? ? userManager.registerUser(user.name,user.pin,user.pattern)? ? showChange.postValue(userManager.hasUser())}
運(yùn)行一下:正常
PinLogin登錄成功之后Success
提供一個(gè)activity
view -> New -> Activity -> Empty Views Activity -> RootActivity
登錄成功就直接進(jìn)入RootActivity
此項(xiàng)目一進(jìn)來(lái)加載的就是MainActivity主界面吃嘿,后面切換到RootActivity的,主界面不能銷毀梦重,所以只能可以返回
2023.9.20周三
PatternLogin圖案密碼解鎖登錄
解鎖密碼成功進(jìn)入主界面
PatternLoginFragment中監(jiān)聽(tīng)繪制解鎖事件
要確定登錄的用戶名是誰(shuí)
底部彈窗顯示選擇用戶
ChooseUserBottomSheetFragment()繼承于BottomSheetDialogFragment()
先不用RecyclerView處理兑燥,先彈兩個(gè)出來(lái),有請(qǐng)選擇用戶琴拧,取消彈窗就消失
選則用戶彈窗界面搭建
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
一點(diǎn)擊切換到圖案密碼解鎖,跳轉(zhuǎn)到PatternLogin界面蚓胸,立刻從底部彈出
ChooseUserBottomSheetFragment().show(parentFragmentManager,"")
運(yùn)行一下:可顯示彈窗
什么時(shí)候可以彈窗:沒(méi)有登錄用戶的信息就彈出(讓用戶選擇具體是那個(gè)用戶要登錄)
在SharedViewModel中挣饥,一旦有登錄的用戶信息就放在loginedUser
val loginedUser = MutableLiveData(User())
希望在PatternLogin界面顯示出來(lái)之后再出現(xiàn)彈窗,所以在onResume()中監(jiān)聽(tīng)loginedUser
如果不存在用戶還沒(méi)有登錄就出現(xiàn)彈窗
為了測(cè)試可以手動(dòng)添加User(name = “jack”)
override fun onResume() {? ? super.onResume()? ? viewModel.loginedUser.observe(viewLifecycleOwner){? ? ? ? if (it.name.isEmpty()){? ? ? ? ? ? ChooseUserBottomSheetFragment().show(parentFragmentManager,null)? ? ? ? }else{? ? ? ? }? ? }}
點(diǎn)擊用戶名之后就都要消失
在ChooseUserBottomSheetFragment添加點(diǎn)擊事件
點(diǎn)擊后彈窗要消失沛膳,在消失dismis()之前要將用戶信息回調(diào)出去
需要根據(jù)用戶名返回這個(gè)用戶的所有信息
在UserManager()中添加getUserInfo()方法,根據(jù)用戶名找到用戶信息
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里面就能監(jiān)聽(tīng)到用戶信息更新了短荐,馬上去登錄即可
輸完圖案密碼就去比較即可
用一個(gè)變量記錄是否有用戶信息hasUser默認(rèn)false
↓
override fun onResume() {? ? super.onResume()? ? viewModel.loginedUser.observe(viewLifecycleOwner){? ? ? ? if (it.name.isEmpty()){? //還沒(méi)有用戶信息要彈出供用戶選擇用戶名? ? ? ? ? ? ChooseUserBottomSheetFragment().show(parentFragmentManager,null)? ? ? ? }else{? ? ? ? ? ? //有用戶了去登錄? ? ? ? ? ? hasUser = true? ? ? ? }? ? }}
有了用戶信息就去調(diào)用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)? ? }}
登錄的話有成功有失敗需要監(jiān)聽(tīng)登錄是否成功的狀態(tài)loginState,登錄Success就跳轉(zhuǎn)到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 = "圖案密碼錯(cuò)誤請(qǐng)重新繪制"? ? }}
運(yùn)行一下:出現(xiàn)滑動(dòng)密碼錯(cuò)誤(不管密碼是啥)但是直接跳轉(zhuǎn)到RootActivity了风罩,錯(cuò)誤,已經(jīng)修改標(biāo)紅糠排,傳給login的應(yīng)該是unlockView傳出的password,不是正確的密碼