項(xiàng)目簡(jiǎn)介
這是一款模仿手機(jī)驗(yàn)證碼登錄的簡(jiǎn)易App酬姆,利用Bmob平臺(tái)提供的短信驗(yàn)證服務(wù)實(shí)現(xiàn)驗(yàn)證碼驗(yàn)證功能。
效果展示:
效果圖
項(xiàng)目準(zhǔn)備
注冊(cè)Bmob平臺(tái)賬號(hào)似舵,創(chuàng)建一個(gè)應(yīng)用辩涝。(溫馨提示:短信發(fā)送的次數(shù)有限,所以不要頻繁測(cè)試)
Bmob環(huán)境搭建
參考:Bmob平臺(tái)數(shù)據(jù)服務(wù)
1新蟆、配置AndroidManifest.xml
<!--允許聯(lián)網(wǎng) -->
<uses-permission android:name="android.permission.INTERNET" />
<!--獲取GSM(2g)觅赊、WCDMA(聯(lián)通3g)等網(wǎng)絡(luò)狀態(tài)的信息 -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!--獲取wifi網(wǎng)絡(luò)狀態(tài)的信息 -->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<!--保持CPU 運(yùn)轉(zhuǎn),屏幕和鍵盤燈有可能是關(guān)閉的,用于文件上傳和下載 -->
<uses-permission android:name="android.permission.WAKE_LOCK" />
<!--獲取sd卡寫的權(quán)限琼稻,用于文件上傳和下載-->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!--允許讀取手機(jī)狀態(tài) 用于創(chuàng)建BmobInstallation-->
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
2吮螺、初始化BmobSDK
class MyApplication:Application() {
override fun onCreate() {
super.onCreate()
//第一:默認(rèn)初始化
Bmob.initialize(this, "56ad195da375b130d0e9b054a9e550d0")
}
}
大致設(shè)計(jì)過(guò)程
第一個(gè)頁(yè)面設(shè)計(jì)
手機(jī)號(hào)輸入框:
輸入框的shape資源代:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="10dp"/>
<stroke android:color="@color/teal_200"
android:width="3dp"/>
<solid android:color="@color/my_blue"/>
</shape>
給輸入框添加監(jiān)聽事件:
主要涉及到輸入數(shù)字的格式化:使輸入的11位電話號(hào)碼有兩個(gè)空格
mPhoneEditText.addTextChangedListener(object :LoginTextWatcher(){
override fun afterTextChanged(s: Editable?) {
//設(shè)置登錄按鈕是否可以點(diǎn)擊
mLoginButton.isEnabled = s.toString().length==13
//如果shouldAutoSplit為false 那么整個(gè)方法就結(jié)束了 不會(huì)再執(zhí)行后面的方法了
if (!shouldAutoSplit) return
//調(diào)整號(hào)碼顯示格式 191 1206 9048
s.toString().length.also {
if (it == 3||it == 8){
s?.append(' ')
}
}
}
/**
* 通過(guò)測(cè)試打印值的變化可以知道:
* 當(dāng)count=1,before=0時(shí) 正在進(jìn)行輸入操作
* 當(dāng)count=0,before=1時(shí) 正在進(jìn)行刪除操作
* 所以可以通過(guò)count或者before的值來(lái)設(shè)置shouldAutoSplit的值
* 在afterTextChanged方法中 就通過(guò)shouldAutoSplit的值來(lái)判斷是輸入還是刪除操作
*/
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
shouldAutoSplit = count==1
}
})
登錄按鈕:
<Button
android:id="@+id/mLoginButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="25dp"
android:background="@drawable/btn_status_selector"
android:enabled="false"
android:text="獲取驗(yàn)證碼"
app:layout_constraintBottom_toTopOf="@+id/guideline4"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent" />
可選與不可選的樣式變化
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!--根據(jù)控鍵的enable值 來(lái)設(shè)定不同的顏色
注意順序:特殊在前 常規(guī)在后
enable狀態(tài) 運(yùn)行起來(lái)正常狀態(tài)enable=true 其他狀態(tài)enable=false
從上至下匹配
-->
<item android:drawable="@color/gray" android:state_enabled="false"/>
<item android:drawable="@color/botton_blue" android:state_enabled="true"/>
</selector>
//按鈕點(diǎn)擊事件
mLoginButton.setOnClickListener{
Intent().apply {
//跳轉(zhuǎn)方向
setClass(this@MainActivity,VerifyActivity::class.java)
//配置跳轉(zhuǎn)的數(shù)據(jù)
putExtra("phone",getPhoneNumber(mPhoneEditText.text))
//啟動(dòng)
startActivity(this)
}
}
至于getPhoneNumber的作用鸠补,就是將格式化的數(shù)據(jù)轉(zhuǎn)換為正常數(shù)據(jù)傳遞給下一個(gè)頁(yè)面萝风,后面將具體講解。
驗(yàn)證碼輸入頁(yè)面
主要就是驗(yàn)證碼輸入框
簡(jiǎn)單說(shuō)一下小編自己的設(shè)計(jì)思路:底部是6個(gè)小方格TextView用于顯示紫岩,表層有一個(gè)EditText用于輸入闹丐,但是其alpha的值為0.01所以看不到,給人的視覺(jué)效果就是可以直接在小方格中輸入被因。
//監(jiān)聽文本框的內(nèi)容改變事件
mOrigin.addTextChangedListener(object :LoginTextWatcher(){
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
//將輸入的內(nèi)容拆分到每一個(gè)textView中
//獲取i對(duì)應(yīng)的textView
for ((i,item) in s?.withIndex()!!){
verifyViews[i].text = item.toString()
}
//如果位數(shù)小于6個(gè) 后面的顯示的都為空
for (i in s.length..5){
verifyViews[i].text =""
}
if (s.length==6){
BmobUtil.verifySMSCode(mPhone.text.toString(),s.toString()){
if (it==BmobUtil.SUCCESS){
//模擬驗(yàn)證成功 跳轉(zhuǎn)到主頁(yè)
startActivity(Intent(this@VerifyActivity,HomeActivity::class.java))
}else{
Toast.makeText(this@VerifyActivity,"驗(yàn)證碼錯(cuò)誤",Toast.LENGTH_LONG)
mOrigin.text.clear()
}
}
}
}
})
還有一個(gè)測(cè)試主頁(yè)就不再展示卿拴,因?yàn)闆](méi)啥內(nèi)容,僅僅用來(lái)測(cè)試梨与。
Bug處理
- 1從輸入驗(yàn)證碼界面返回第一個(gè)界面堕花,在獲取號(hào)碼后,號(hào)碼不再是格式化粥鞋。
其實(shí)解決方法很簡(jiǎn)單:就是再創(chuàng)建一個(gè)對(duì)象(SpannableStringBuilder)封裝輸入的號(hào)碼對(duì)其進(jìn)行格式化轉(zhuǎn)正常數(shù)據(jù)的處理即可缘挽。
- 2 驗(yàn)證碼框可長(zhǎng)按現(xiàn)象
因?yàn)樾【幵趯?shí)現(xiàn)驗(yàn)證碼輸入框的時(shí)候是在6個(gè)正方形框(使用的TextView)的上方采用EditText(將其alpha設(shè)置為0.01),再使每次輸入的值放到TextView上顯示呻粹,所以這里有個(gè)小bug壕曼,長(zhǎng)按輸入框會(huì)出現(xiàn)選擇、復(fù)制等浊、剪切的選項(xiàng)出現(xiàn)腮郊。所以需要將EditText的longClickable設(shè)置為false。
-3 驗(yàn)證碼刪除筹燕,但顯示未變化轧飞。
出現(xiàn)此現(xiàn)象的原因和上述那個(gè)一樣,小編設(shè)計(jì)原理的過(guò)撒踪。
解決方案就是沒(méi)有數(shù)字的方格顯示空过咬。
//如果位數(shù)小于6個(gè) 后面的顯示的都為空
for (i in s.length..5){
verifyViews[i].text =""
}
兩種實(shí)現(xiàn)獲取短信的方式
1、直接使用系統(tǒng)封裝的類方法
(注:上述代碼Toast未調(diào)用show()方法制妄,所以測(cè)試的時(shí)候沒(méi)彈出提示)
可以點(diǎn)擊短信自定義模板
2掸绞、自己在系統(tǒng)提供的基礎(chǔ)上封裝自己的方法(推薦)
優(yōu)點(diǎn):可以增強(qiáng)復(fù)用性
同樣的,驗(yàn)證碼驗(yàn)證的實(shí)現(xiàn)也可采用兩種方式
主要代碼
MainActivity
package com.example.phonelogin2
import android.content.Intent
import android.os.Bundle
import android.text.Editable
import android.text.SpannableStringBuilder
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
//用于判斷是否分割
private var shouldAutoSplit = true
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mPhoneEditText.addTextChangedListener(object :LoginTextWatcher(){
override fun afterTextChanged(s: Editable?) {
//設(shè)置登錄按鈕是否可以點(diǎn)擊
mLoginButton.isEnabled = s.toString().length==13
//如果shouldAutoSplit為false 那么整個(gè)方法就結(jié)束了 不會(huì)再執(zhí)行后面的方法了
if (!shouldAutoSplit) return
//調(diào)整號(hào)碼顯示格式 191 1206 9048
s.toString().length.also {
if (it == 3||it == 8){
s?.append(' ')
}
}
}
/**
* 通過(guò)測(cè)試打印值的變化可以知道:
* 當(dāng)count=1耕捞,before=0時(shí) 正在進(jìn)行輸入操作
* 當(dāng)count=0衔掸,before=1時(shí) 正在進(jìn)行刪除操作
* 所以可以通過(guò)count或者before的值來(lái)設(shè)置shouldAutoSplit的值
* 在afterTextChanged方法中 就通過(guò)shouldAutoSplit的值來(lái)判斷是輸入還是刪除操作
*/
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
shouldAutoSplit = count==1
}
})
//按鈕點(diǎn)擊事件
mLoginButton.setOnClickListener{
Intent().apply {
//跳轉(zhuǎn)方向
setClass(this@MainActivity,VerifyActivity::class.java)
//配置跳轉(zhuǎn)的數(shù)據(jù)
putExtra("phone",getPhoneNumber(mPhoneEditText.text))
//啟動(dòng)
startActivity(this)
}
}
}
/**
* 為什么需要?jiǎng)?chuàng)建一個(gè)新的對(duì)象 來(lái)進(jìn)行操作?
* 當(dāng)輸入數(shù)據(jù)進(jìn)行格式化后 進(jìn)行跳轉(zhuǎn) 再次返回 那么數(shù)據(jù)就不再是格式化數(shù)據(jù) 不滿足跳轉(zhuǎn)條件
* 所以在將格式化轉(zhuǎn)化為正常數(shù)據(jù)的方法中 選喲創(chuàng)建一個(gè)新的對(duì)象來(lái)完成該功能
* 而SpannableStringBuilder是Editable的實(shí)現(xiàn)類砸脊,有其所有方法具篇,所以新對(duì)象就是該類型
*/
//將格式化的數(shù)據(jù)轉(zhuǎn)化為正常的數(shù)據(jù)
private fun getPhoneNumber(editable: Editable):String{
//創(chuàng)建一個(gè)新的對(duì)象 用于操作editable對(duì)象里面的內(nèi)容
SpannableStringBuilder(editable.toString()).apply {
delete(3,4)
delete(7,8)
return this.toString()
}
}
}
VerifyActivity
package com.example.phonelogin2
import android.content.Intent
import android.os.Bundle
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_verify.*
class VerifyActivity : AppCompatActivity() {
//保存所有顯示驗(yàn)證碼的textView
private val verifyViews:Array<TextView> by lazy {
arrayOf(mv1,mv2,mv3,mv4,mv5,mv6)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_verify)
//獲取數(shù)據(jù)
intent.getStringExtra("phone").also {
//顯示號(hào)碼
mPhone.text = it
}
//監(jiān)聽文本框的內(nèi)容改變事件
mOrigin.addTextChangedListener(object :LoginTextWatcher(){
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
//將輸入的內(nèi)容拆分到每一個(gè)textView中
//獲取i對(duì)應(yīng)的textView
for ((i,item) in s?.withIndex()!!){
verifyViews[i].text = item.toString()
}
//如果位數(shù)小于6個(gè) 后面的顯示的都為空
for (i in s.length..5){
verifyViews[i].text =""
}
if (s.length==6){
BmobUtil.verifySMSCode(mPhone.text.toString(),s.toString()){
if (it==BmobUtil.SUCCESS){
//模擬驗(yàn)證成功 跳轉(zhuǎn)到主頁(yè)
startActivity(Intent(this@VerifyActivity,HomeActivity::class.java))
}else{
Toast.makeText(this@VerifyActivity,"驗(yàn)證碼錯(cuò)誤",Toast.LENGTH_LONG)
mOrigin.text.clear()
}
}
}
}
})
}
override fun onResume() {
super.onResume()
BmobUtil.requestSMSCode(mPhone.text.toString()){
if (it==BmobUtil.SUCCESS){
Toast.makeText(this,"短信請(qǐng)求成功",Toast.LENGTH_LONG).show()
}else{
Toast.makeText(this,"短信請(qǐng)求失敗",Toast.LENGTH_LONG).show()
}
}
}
}
BmobUtil
package com.example.phonelogin2
import cn.bmob.v3.BmobSMS
import cn.bmob.v3.exception.BmobException
import cn.bmob.v3.listener.QueryListener
import cn.bmob.v3.listener.UpdateListener
/**
*@Description
*@Author PC
*@QQ 1578684787
*/
object BmobUtil {
const val SUCCESS = 0
const val FAILURE = 1
//向服務(wù)器請(qǐng)求...發(fā)送驗(yàn)證碼
fun requestSMSCode(phone:String,callBack:(Int)->Unit){
BmobSMS.requestSMSCode(phone,"",object :QueryListener<Int>(){
override fun done(p0: Int?, p1: BmobException?) {
if (p1 == null){
//短信發(fā)送成功
callBack(SUCCESS)
}else{
//短信發(fā)送失敗
callBack(FAILURE)
}
}
})
}
//驗(yàn)證用戶輸入的驗(yàn)證碼是否正確
fun verifySMSCode(phone: String,code:String,callBack:(Int)->Unit){
BmobSMS.verifySmsCode(phone,code,object :UpdateListener(){
override fun done(p0: BmobException?) {
if (p0==null){
//驗(yàn)證成功
callBack(SUCCESS)
}else{
//驗(yàn)證失敗
callBack(FAILURE)
}
}
})
}
}
其他可去小編github賬號(hào)上獲取
項(xiàng)目完整代碼:
https://github.com/gun-ctrl/PhoneLogin2