之前已經(jīng)說過如何進(jìn)行登錄界面的編寫颁湖,一個好的app是需要從用戶的角度去出發(fā)的,我們登錄界面不僅能提供用戶密碼登錄雄驹,還應(yīng)該能支持用戶短信登錄甚至是短信修改密碼,這樣就需要我們用更多的精力去優(yōu)化我們的代碼.首先說一下本篇文章的重點(diǎn)
1.關(guān)于如何進(jìn)行Fragment的跳轉(zhuǎn)
2.如何進(jìn)行Fragment之前的傳參
3.如何使從當(dāng)前Fragment回退到我們需要的界面
4.如何使用mobSdk進(jìn)行短信驗(yàn)證.
5.對于用戶輸入信息的判斷.
這樣荣堰,我們一點(diǎn)一點(diǎn)來分析床未,首先,我們需要進(jìn)行Fragment的跳轉(zhuǎn)振坚。這個問題很重要薇搁,因?yàn)槲覀兊慕缑婵隙ㄊ怯邪存I需求的,當(dāng)用戶按下一個鍵后渡八,會要跳轉(zhuǎn)到我們需要的界面上啃洋,上一篇文章在最后提到可以用接口回調(diào)的方式去實(shí)現(xiàn),其實(shí)這樣太麻煩了屎鳍,我們可以用事務(wù)處理的方式去實(shí)現(xiàn)對Fragment管理.
public class FragmentTranscationUtil {
//獲取FragmentManager宏娄,開啟事務(wù),添加碎片
public static void replaceFragment(FragmentActivity activity, Fragment fragment) {
FragmentManager manager = activity.getSupportFragmentManager();
FragmentTransaction transaction = manager.beginTransaction();
transaction.setCustomAnimations(R.anim.activity_right_in, 0, 0, R.anim.activity_right_out);
transaction.replace(R.id.userloginFrame_layout, fragment);
transaction.addToBackStack(null);
transaction.commit();
}
//獲取FragmentManager逮壁,開啟事務(wù)孵坚,添加碎片
public static void replaceFragment(FragmentActivity activity, Fragment fragment, Bundle bundle) {
FragmentManager manager = activity.getSupportFragmentManager();
fragment.setArguments(bundle);
FragmentTransaction transaction = manager.beginTransaction();
transaction.setCustomAnimations(R.anim.activity_right_in, 0, 0, R.anim.activity_right_out);
transaction.replace(R.id.userloginFrame_layout, fragment);
transaction.addToBackStack(null);
transaction.commit();
}
這是一個幫助類,參數(shù)很簡單窥淆,首先卖宠,我們要獲取當(dāng)前所在Activity,這個不用多說嗎忧饭,因?yàn)镕ragment多是嵌套在Activity里面的扛伍,第二個參數(shù)就是我們需要跳轉(zhuǎn)的目標(biāo)Fragment
下面第二個構(gòu)造函數(shù)的第三個參數(shù)Bundle則是用來進(jìn)行傳參的.這個我下面會詳細(xì)講解
首先,我們要獲取到FragmentManger的實(shí)例manger,然后manager.beginTransaction();
去開啟事務(wù)词裤,將我們需要執(zhí)行的邏輯放入到事務(wù)中刺洒,在調(diào)用transaction.commit();去執(zhí)行
就行了,這里順便向大家展示一下Fragment生命周期
Fragment的生命周期和activity生命周期很像亚斋,其生命周期方法如下所示作媚。
- onAttach:綁定到activity
- onCreate:創(chuàng)建fragment
- onCreateView: 創(chuàng)建fragment的布局
- onActivityCreated: activity創(chuàng)建完成后
- onStart: 可見, 不可交互
- onResume: 可見, 可交互
- onPause: 部分可見, 不可交互
- onStop:不可見
- onDestroyView: 銷毀fragment的view對象
- onDestroy: fragment銷毀了
- onDetach: 從activity解綁了
下面給出FragmentTransaction的全部方法(API 24)轉(zhuǎn)載自http://www.reibang.com/p/5761ee2d3ea1
- add(Fragment fragment, String tag) // 調(diào)用add(int, Fragment, String),填入為0的containerViewId.
- add(int containerViewId, Fragment fragment) // 調(diào)用add(int, Fragment, String),填入為null的tag.
- add(int containerViewId, Fragment fragment, String tag) // 向Activity中添加一個Fragment.
- addSharedElement(View sharedElement, String name) // 添加共享元素
- addToBackStack(String name) // 將事務(wù)添加到回退棧
- attach(Fragment fragment) // 重新關(guān)聯(lián)Fragment(當(dāng)Fragment被detach時)
- commit() // 提交事務(wù)
- commitAllowingStateLoss() // 類似commit(),但允許在Activity狀態(tài)保存之后提交(即允許狀態(tài)丟失)帅刊。
- commitNow() // 同步提交事務(wù)
- commitNowAllowingStateLoss() // 類似commitNow(),但允許在Activity狀態(tài)保存之后提交(即允許狀態(tài)丟失)漂问。
- detach(Fragment fragment) // 將fragment保存的界面從UI中移除
- disallowAddToBackStack() // 不允許調(diào)用addToBackStack(String)操作
- hide(Fragment fragment) // 隱藏已存在的Fragment
- isAddToBackStackAllowed() // 是否允許添加到回退棧
- isEmpty() // 事務(wù)是否未包含的任何操作
- remove(Fragment fragment) // 移除一個已存在的Fragment
- replace(int containerViewId, Fragment fragment) // 調(diào)用replace(int, Fragment, String)填入為null的tag.
- replace(int containerViewId, Fragment fragment, String tag) // 替換已存在的Fragment
- setBreadCrumbShortTitle(int res) // 為事務(wù)設(shè)置一個BreadCrumb短標(biāo)題
- setBreadCrumbShortTitle(CharSequence text) // 為事務(wù)設(shè)置一個BreadCrumb短標(biāo)題赖瞒,將會被FragmentBreadCrumbs使用
- setBreadCrumbTitle(int res) // 為事務(wù)設(shè)置一個BreadCrumb全標(biāo)題,將會被FragmentBreadCrumbs使用
- setBreadCrumbTitle(CharSequence text) // 為事務(wù)設(shè)置一個BreadCrumb全標(biāo)題
- setCustomAnimations(int enter, int exit, int popEnter, int popExit) // 自定義事務(wù)進(jìn)入/退出以及入棧/出棧的動畫效果
- setCustomAnimations(int enter, int exit) // 自定義事務(wù)進(jìn)入/退出的動畫效果
- setTransition(int transit) // 為事務(wù)設(shè)置一個標(biāo)準(zhǔn)動畫
- setTransitionStyle(int styleRes) // 為事務(wù)標(biāo)準(zhǔn)動畫設(shè)置自定義樣式
- show(Fragment fragment) // 顯示一個被隱藏的Fragment
這樣我們就解決了Fragment跳轉(zhuǎn)問題蚤假,同時Fragment回退問題也給出了答案栏饮,那就是利用addToBackStack(String name)
我們知道Activity有任務(wù)棧,用戶通過startActivity將Activity加入棧磷仰,點(diǎn)擊返回按鈕將Activity出棧袍嬉。Fragment也有類似的棧,稱為回退棧(Back Stack),回退棧是由FragmentManager管理的伺通。
默認(rèn)情況下箍土,F(xiàn)ragment事務(wù)是不會加入回退棧的,如果想將Fragment加入回退棧并實(shí)現(xiàn)事物回滾罐监,首先需要在commit()方法之前調(diào)用事務(wù)的以下方法將其添加到回退棧中:
- addToBackStack(String name)
這個方法在我們跳轉(zhuǎn)Fragment時候?qū)⑹聞?wù)添加至回退棧
Fragment的回退非常簡單吴藻,然而這里又會出現(xiàn)一個新的問題,就是在修改后的案例每次只能回退到上一步操作弓柱,而并不能一次性回退到我們想要的位置沟堡,這樣才更滿足實(shí)際開發(fā)需要。這就需要我們來多了解事物回滾的相關(guān)原理矢空,其實(shí)在Fragment回退時航罗,默認(rèn)調(diào)用FragmentManager的popBackStack()方法將最上層的操作彈出回退棧。當(dāng)棧中有多層時屁药,我們可以根據(jù)id或TAG標(biāo)識來指定彈出到的操作所在層伤哺。 - popBackStack(int id, int flags):其中id表示提交變更時commit()的返回值。
- popBackStack(String name, int flags):其中name是addToBackStack(String tag)中的tag值者祖。
在上面2個方法里面立莉,都用到了flags,其實(shí)flags有兩個取值:0或FragmentManager.POP_BACK_STACK_INCLUSIVE七问。當(dāng)取值0時蜓耻,表示除了參數(shù)指定這一層之上的所有層都退出棧,指定的這一層為棧頂層械巡;當(dāng)取值POP_BACK_STACK_INCLUSIVE時刹淌,表示連著參數(shù)指定的這一層一起退出棧。
這樣Fragment的跳轉(zhuǎn)和回退就說到這里讥耗,接下來是傳參問題有勾,這個問題也很簡單,因?yàn)锽undle的底層代碼就是HasMap<key,value>組合古程,首先實(shí)例化一個bundle,然后用<key,value>儲存想要保存的數(shù)據(jù)蔼卡,然后調(diào)用 - setArguments(bundle);//添加將要傳遞的bundle
在目標(biāo)Fragment,使用getArguments();去獲取到傳入的值,比較簡單挣磨,就不在贅述了雇逞。這樣,1茁裙,2塘砸,3,問題我們都已經(jīng)解決
接下來是重點(diǎn)晤锥!如何使用mobSDK去實(shí)現(xiàn)短信驗(yàn)證.
首先給大家一張圖掉蔬,便于理解短信驗(yàn)證流程
我個人使用的是Mob開發(fā)者平臺(http://sms.mob.com)注冊廊宪,獲取到Appkey和appSecret,這個非常重要
一 配置Gradle
1女轿、打開項(xiàng)目根目錄的build.gradle箭启,在buildscrip–>dependencies 模塊下面添加 classpath ‘com.mob.sdk:MobSDK:+’,如下所示谈喳;
buildscript {
repositories {
jcenter()
}
dependencies {
...
classpath 'com.mob.sdk:MobSDK:+'
}
}
2册烈、在使用SMSSDK模塊的build.gradle中,添加MobSDK插件和擴(kuò)展婿禽,如:
// 添加插件
apply plugin: 'com.mob.sdk'
// 在MobSDK的擴(kuò)展中注冊SMSSDK的相關(guān)信息
MobSDK {
appKey "d580ad5*****"http://填寫自己獲取的appKey
appSecret "7fcae59a******7e2759e9e397c82bdd"http://填寫自己獲取的appSecret
SMSSDK {}
}
這里面的appkey和appSecret是自己注冊而獲取到的.替換一下就行了,
3赏僧、初始化MobSDK
如果您沒有在AndroidManifest中設(shè)置appliaction的類名,MobSDK會將這個設(shè)置為com.mob.MobApplication扭倾,但如果您設(shè)置了淀零,請?jiān)谀约旱腁pplication類中調(diào)用:
MobSDK.init(this);
以初始化MobSDK。
二 具體代碼
這里有兩種方式去實(shí)現(xiàn)膛壹,一個是可視化界面驾中,一個是非UI實(shí)現(xiàn),
因?yàn)槲覀€人是是使用無UI的方式模聋,所以會講述無Ui實(shí)現(xiàn)的方法肩民,這里給出可視化界面的文檔(http://wiki.mob.com/sdk-sms-android-3-0-0/)
首先介紹一下EventHandler這是一個異步操作,和Handler類似所有的業(yè)務(wù)邏輯將會在這里處理链方,它是和registerEventHandler一起使用的持痰,registerEventHandler是一個事件接收器,他是專門負(fù)責(zé)接收所有被觸發(fā)的事件祟蚀,而registerEventHandler本身可以注冊多個工窍,所有接收器也會在事件被觸發(fā)時候接收消息.一般是配套使用,否則容易產(chǎn)生內(nèi)存泄露
下面給出具體代碼
//請求短信驗(yàn)證碼
private void SendCode(String country, String number) {
EventHandler eh = new EventHandler() {
@Override
public void afterEvent(int i, int i1, Object o) {
if (i == SMSSDK.EVENT_GET_VERIFICATION_CODE) {//獲取短信驗(yàn)證碼事件
//獲取驗(yàn)證碼成功
if (i1 == SMSSDK.RESULT_COMPLETE) {
listener.getCodeSuccess();
} else if (i1 == SMSSDK.RESULT_ERROR) {
listener.getCodeFailure();
}
}
}
};
SMSSDK.registerEventHandler(eh);
SMSSDK.getVerificationCode(country, number);
}
可以看到這是我自定義的一個方法前酿,首先獲取國家患雏,中國是86開頭,沒啥好說的罢维,然后是你將要發(fā)送短信驗(yàn)證的用戶手機(jī)號碼淹仑,這也沒啥好說的,至于listener是我自定義的接口言津,會統(tǒng)一處理事務(wù)邏輯.
//提交驗(yàn)證碼
private void SubmitCode(String country, final String number, String code) {
EventHandler eh = new EventHandler() {
@Override
public void afterEvent(int i, int i1, Object o) {
if (i == SMSSDK.EVENT_SUBMIT_VERIFICATION_CODE) {//驗(yàn)證碼提交事件
if (i1 == SMSSDK.RESULT_COMPLETE) {
//回調(diào)成功
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
listener.submitCodeSuccesss(number, dateFormat.format(new Date()));
} else if (i1 == SMSSDK.RESULT_ERROR) {//提交驗(yàn)證碼失敗
listener.submitCodeFailure();
}
}
}
};
SMSSDK.registerEventHandler(eh);
SMSSDK.submitVerificationCode(country, number, code);
}
提交驗(yàn)證碼多出一個你接收短信驗(yàn)證的信息code,其他都差不多的攻人。
這里給出mob業(yè)務(wù)邏輯,可以最大限度自定義出符合你自己要求的業(yè)務(wù)邏輯悬槽。
Handler mhandle = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
int event = msg.arg1;
int result = msg.arg2;
Object data = msg.obj;
if (event == SMSSDK.EVENT_SUBMIT_VERIFICATION_CODE) {//驗(yàn)證碼提交事件
if (result == SMSSDK.RESULT_COMPLETE) {
//回調(diào)成功
Toast.makeText(context, "提交驗(yàn)證碼成功"+result, Toast.LENGTH_LONG).show();
} else if (result == SMSSDK.RESULT_ERROR) {
Toast.makeText(context, "提交驗(yàn)證碼失敗"+data, Toast.LENGTH_LONG).show();
}
} else if (event == SMSSDK.EVENT_GET_VERIFICATION_CODE) {//獲取短信驗(yàn)證碼事件
//獲取驗(yàn)證碼成功
if (result == SMSSDK.RESULT_COMPLETE) {
Toast.makeText(context, "獲取短信驗(yàn)證碼成功", Toast.LENGTH_LONG).show();
boolean mobcheck = (Boolean) data;
if (mobcheck) {
//通過智能驗(yàn)證
Toast.makeText(context, "mob云驗(yàn)證", Toast.LENGTH_LONG).show();
} else {
//依然走短信驗(yàn)證
Toast.makeText(context, "短信驗(yàn)證", Toast.LENGTH_LONG).show();
}
} else if (result == SMSSDK.RESULT_ERROR) {
Toast.makeText(context, "獲取短信驗(yàn)證碼失敗"+data, Toast.LENGTH_LONG).show();
}
} else if (event == SMSSDK.EVENT_GET_SUPPORTED_COUNTRIES) {
SMSSDK.getSupportedCountries();
} else {
try {
((Throwable) data).printStackTrace();
Throwable throwable = (Throwable) data;
JSONObject jsonObject = new JSONObject(throwable.getMessage());
String des = jsonObject.optString("detail");
int status = 0;
status = jsonObject.optInt("status");
if (TextUtils.isEmpty(des)) {
}
} catch (Exception e) {
SMSLog.getInstance().w(e);
}
}
}
};
Ps:SMSSDK已經(jīng)做了混淆處理,再次混淆會導(dǎo)致不可預(yù)期的錯誤瞬浓,請?jiān)谀幕煜_本中添加如下的配置初婆,跳過對SMSSDK的混淆操作:
-keep class com.mob.**{*;}
-keep class cn.smssdk.**{*;}
-dontwarn com.mob.**
這個時候,大家可能就有疑問了,沒有UI界面我如何去輸入收到的短信驗(yàn)證碼去驗(yàn)證磅叛,很簡單屑咳,自己寫個UI就行了啊,一般現(xiàn)在流行的界面可能就是如下圖所示了弊琴,引用一下圖片http://www.reibang.com/p/91b0b8038dd5
這位大牛的效果還是不錯的兆龙,我說下自己的思路,簡單的就是TextView+Edittext去組合實(shí)現(xiàn)的敲董,下面紫皇,附上xml資源,大家可以詳細(xì)了解一下
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorWhite">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_code1"
style="@style/codeTextView" />
<View
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<TextView
android:id="@+id/tv_code2"
style="@style/codeTextView" />
<View
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<TextView
android:id="@+id/tv_code3"
style="@style/codeTextView" />
<View
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<TextView
android:id="@+id/tv_code4"
style="@style/codeTextView" />
<View
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<TextView
android:id="@+id/tv_code5"
style="@style/codeTextView" />
<View
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<TextView
android:id="@+id/tv_code6"
style="@style/codeTextView" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center"
android:orientation="horizontal">
<View
android:id="@+id/view1"
android:layout_width="40dp"
android:layout_height="2dp"
android:layout_gravity="bottom"
android:background="@color/colorGray707061" />
<View
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<View
android:id="@+id/view2"
android:layout_width="40dp"
android:layout_height="2dp"
android:layout_gravity="bottom"
android:background="@color/colorGray707061" />
<View
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<View
android:id="@+id/view3"
android:layout_width="40dp"
android:layout_height="2dp"
android:layout_gravity="bottom"
android:background="@color/colorGray707061" />
<View
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<View
android:id="@+id/view4"
android:layout_width="40dp"
android:layout_height="2dp"
android:layout_gravity="bottom"
android:background="@color/colorGray707061" />
<View
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<View
android:id="@+id/view5"
android:layout_width="40dp"
android:layout_height="2dp"
android:layout_gravity="bottom"
android:background="@color/colorGray707061" />
<View
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<View
android:id="@+id/view6"
android:layout_width="40dp"
android:layout_height="2dp"
android:layout_gravity="bottom"
android:background="@color/colorGray707061" />
</LinearLayout>
<lf.com.android.blackfishdemo.view.CodeEditTextView
android:id="@+id/et_code_text"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="#00000000"
android:inputType="number"
android:longClickable="false"
android:maxLength="6"
android:textColor="#00000000" />
</RelativeLayout>
乍一看貌似很復(fù)雜腋寨,其實(shí)就是繪制了可以輸入6位數(shù)的Textview和對應(yīng)的下標(biāo)Item,然后上面再被我自定義的EditText所覆蓋
/**
* 驗(yàn)證碼控件聪铺,去掉傳統(tǒng)EditText雙擊選中EditText的內(nèi)容
* 和去掉光標(biāo)位置會隨點(diǎn)擊改變
*/
public class CodeEditTextView extends AppCompatEditText {
private long lastTime = 0;
public CodeEditTextView(Context context) {
super(context);
}
public CodeEditTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CodeEditTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onSelectionChanged(int selStart, int selEnd) {
super.onSelectionChanged(selStart, selEnd);
this.setSelection(this.getText().length());
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN://判斷點(diǎn)擊事件,防止用戶多次點(diǎn)擊
long currenTime = System.currentTimeMillis();
if (currenTime - lastTime < 500) {
lastTime = currenTime;
return true;
} else {
lastTime = currenTime;
}
break;
}
return super.onTouchEvent(event);
}
}
自定義的Edittext首先會通過long currenTime = System.currentTimeMillis();獲取到當(dāng)前時間點(diǎn)萄窜,判斷兩個點(diǎn)擊時間間隔铃剔,從而屏蔽雙擊事件,長按會走 onSelectionChanged這個方法查刻,所以键兜,設(shè)置光標(biāo)始終在文本后面,也就屏蔽了長按事件
然后通過對Edittext事件進(jìn)行TextWatcher監(jiān)聽(后面會詳細(xì)講解)穗泵,依次將輸入內(nèi)容分寫到Textview上普气,就完成了上面的效果.至此,如何使用短信驗(yàn)證就告一段落.
好了火欧,同通過短信驗(yàn)證就意味著棋电,用戶登錄過程已經(jīng)接近尾聲甚至已經(jīng)結(jié)束了,但是苇侵,在這里赶盔,我還是想一起說了吧,因?yàn)槎绦膨?yàn)證不僅僅用于登錄榆浓,也用于修改密碼于未,而且,用戶輸入的內(nèi)容我們要去判斷陡鹃,不可能用戶隨便輸入一個文本我們就要對其進(jìn)行一次短信驗(yàn)證吧烘浦,畢竟,喜歡找Bug的用戶也不少萍鲸,所以闷叉,我們要對用戶輸入的內(nèi)容進(jìn)行判斷。這就是我么要說的5.對于用戶輸入信息的判斷.在說這個之前脊阴,我先向大家介紹一下握侧,什么是正則表達(dá)式蚯瞧。
正則表達(dá)式是一種查找以及字符串替換操作。正則表達(dá)式在文本編輯器中廣泛使用品擎,比如正則表達(dá)式被用于:
- 檢查文本中是否含有指定的特征詞
- 找出文中匹配特征詞的位置
- 從文本中提取信息埋合,比如:字符串的子串修改文本與文本編輯器相似,幾乎所有的高級編程語言都支持正則表達(dá)式萄传。在這樣的語境下甚颂,“文本”也就是一個字符串,可以執(zhí)行的操作都是類似的秀菱。一些編程語言(比如Perl振诬,JavaScript)會檢查正則表達(dá)式的語法。
正則表達(dá)式的語法是是一種輕量級答朋、簡潔贷揽、適用于特定領(lǐng)域的編程語言。
關(guān)于詳解梦碗,建議大家去看這篇文章http://www.reibang.com/p/67af3eeb6798
這里給大家常用正則表達(dá)式的鏈接https://www.cnblogs.com/zxin/archive/2013/01/26/2877765.html
所以禽绪,當(dāng)我們使用正則表達(dá)式時候,可利用事件監(jiān)聽
private void setEditTextLitener() {
mEditText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
}
});
}
我們可以通過監(jiān)聽afterTextChanged獲取到輸入內(nèi)容洪规,從而進(jìn)行判斷印屁,然后去編寫我們所需要的邏輯,至此斩例,用戶登錄過程就完美結(jié)束了雄人,歡迎大家留言,提出問題.謝謝大家的閱讀念赶。