Pin&&Pattern開(kāi)發(fā)過(guò)程

數(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,不是正確的密碼

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末超升,一起剝皮案震驚了整個(gè)濱河市入宦,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌室琢,老刑警劉巖乾闰,帶你破解...
    沈念sama閱讀 216,544評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異研乒,居然都是意外死亡汹忠,警方通過(guò)查閱死者的電腦和手機(jī)淋硝,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門雹熬,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人谣膳,你說(shuō)我怎么就攤上這事竿报。” “怎么了继谚?”我有些...
    開(kāi)封第一講書人閱讀 162,764評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵烈菌,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng)芽世,這世上最難降的妖魔是什么挚赊? 我笑而不...
    開(kāi)封第一講書人閱讀 58,193評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮济瓢,結(jié)果婚禮上荠割,老公的妹妹穿的比我還像新娘。我一直安慰自己旺矾,他們只是感情好蔑鹦,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,216評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著箕宙,像睡著了一般嚎朽。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上柬帕,一...
    開(kāi)封第一講書人閱讀 51,182評(píng)論 1 299
  • 那天哟忍,我揣著相機(jī)與錄音,去河邊找鬼陷寝。 笑死魁索,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的盼铁。 我是一名探鬼主播粗蔚,決...
    沈念sama閱讀 40,063評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼饶火!你這毒婦竟也來(lái)了鹏控?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 38,917評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤肤寝,失蹤者是張志新(化名)和其女友劉穎当辐,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體鲤看,經(jīng)...
    沈念sama閱讀 45,329評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡缘揪,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,543評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了义桂。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片找筝。...
    茶點(diǎn)故事閱讀 39,722評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖慷吊,靈堂內(nèi)的尸體忽然破棺而出袖裕,到底是詐尸還是另有隱情,我是刑警寧澤溉瓶,帶...
    沈念sama閱讀 35,425評(píng)論 5 343
  • 正文 年R本政府宣布急鳄,位于F島的核電站谤民,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏疾宏。R本人自食惡果不足惜张足,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,019評(píng)論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望坎藐。 院中可真熱鬧兢榨,春花似錦、人聲如沸顺饮。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,671評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)捻爷。三九已至,卻和暖如春绒怨,著一層夾襖步出監(jiān)牢的瞬間赦肋,已是汗流浹背块攒。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 32,825評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留佃乘,地道東北人囱井。 一個(gè)月前我還...
    沈念sama閱讀 47,729評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像趣避,于是被迫代替她去往敵國(guó)和親庞呕。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,614評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容